Finish go

This commit is contained in:
s3lph 2022-10-02 07:13:46 +02:00
parent 5351850fa1
commit 6125ac5eab
3 changed files with 179 additions and 82 deletions

View file

@ -1,14 +1,62 @@
let stones = document.getElementsByClassName('go-stone-empty'); function setupEventHandler() {
let inputRow = document.getElementById('go-input-row'); let stones = document.getElementsByClassName('go-stone-empty');
let inputCol = document.getElementById('go-input-col'); let inputRow = document.getElementById('go-input-row');
let inputPlace = document.getElementById('go-input-place'); let inputCol = document.getElementById('go-input-col');
let inputPlace = document.getElementById('go-input-place');
for (let i = 0; i < stones.length; ++i) { for (let i = 0; i < stones.length; ++i) {
stones[i].onclick = (e) => { stones[i].onclick = (e) => {
console.log('place'); console.log('place');
inputCol.value = stones[i].attributes['data-col'].value; inputCol.value = stones[i].attributes['data-col'].value;
inputRow.value = stones[i].attributes['data-row'].value; inputRow.value = stones[i].attributes['data-row'].value;
inputPlace.click(); inputPlace.click();
}; };
}
} }
let updateInterval = null;
if (document.getElementsByClassName('go-territory').length == 0) {
setInterval(update, 500);
}
function update() {
var req = new XMLHttpRequest();
req.addEventListener('load', (ev) => {
let parser = new DOMParser();
let doc = parser.parseFromString(req.responseText, 'text/html');
let currentField = document.querySelector('svg');
let newField = doc.querySelector('svg');
let currentStones = currentField.getElementsByClassName('go-stone');
let newStones = newField.getElementsByClassName('go-stone');
let newTerritory = doc.getElementsByClassName('go-territory');
let currentInfo = document.getElementById('gameinfo');
let newInfo = doc.getElementById('gameinfo');
if (newTerritory.length > 0) {
window.location.reload();
return;
}
for (let i = 0; i < currentStones.length; ++i) {
if (doc.getElementById(currentStones[i].id) === null) {
currentStones[i].remove();
}
}
if (currentInfo.classList[0] != newInfo.classList[0]) {
console.log(newInfo.classList[0]);
currentInfo.innerHTML = newInfo.innerHTML;
currentInfo.classList.remove(currentInfo.classList[0]);
currentInfo.classList.add(newInfo.classList[0]);
}
for (let i = 0; i < newStones.length; ++i) {
if (document.getElementById(newStones[i].id) === null) {
currentField.append(newStones[i]);
}
}
setupEventHandler();
});
req.open('GET', window.location.href);
req.send();
}
setupEventHandler();

View file

@ -3,10 +3,10 @@
{%- block body %} {%- block body %}
<h1>Go: {{ game.human_id }}</h1> <h1>Go: {{ game.human_id }}</h1>
<form method="POST"> <form method="POST" id="gogame">
<div class="field"> <div class="field">
<svg xmlns="http://www.w3.org/2000/svg" width="{{ 50 * boardsize }}" height="{{ 50 * boardsize }}" version="2.0"> <svg xmlns="http://www.w3.org/2000/svg" id="gogamesvg "width="{{ 50 * boardsize }}" height="{{ 50 * boardsize }}" version="2.0">
<defs> <defs>
<radialGradient id="white-stone" fx="40%" fy="25%"> <radialGradient id="white-stone" fx="40%" fy="25%">
<stop offset="0%" stop-color="#ffffff" /> <stop offset="0%" stop-color="#ffffff" />
@ -45,15 +45,15 @@
{% for col in range(boardsize) %} {% for col in range(boardsize) %}
<!-- {{ row }} {{ col }} {{ field[row][col] }} --> <!-- {{ row }} {{ col }} {{ field[row][col] }} -->
{% if field[row][col] == 0 and current_player == colormap[player.uuid] and not abandoned and score is none %} {% if field[row][col] == 0 and current_player == colormap[player.uuid] and not abandoned and score is none %}
<circle class="go-stone go-stone-empty go-stone-{{ colormap[player.uuid].name.lower() }}" data-row="{{ row }}" data-col="{{ col }}" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" /> <circle id="go-stone-{{ row }}-{{ col }}-empty" class="go-stone go-stone-empty go-stone-{{ colormap[player.uuid].name.lower() }}" data-row="{{ row }}" data-col="{{ col }}" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% elif field[row][col] == 1 %} {% elif field[row][col] == 1 %}
<circle class="go-stone go-stone-white" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" /> <circle id="go-stone-{{ row }}-{{ col }}-white" class="go-stone go-stone-white" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% elif field[row][col] == 2 %} {% elif field[row][col] == 2 %}
<circle class="go-stone go-stone-black" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" /> <circle id="go-stone-{{ row }}-{{ col }}-black" class="go-stone go-stone-black" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% elif field[row][col] == 5 %} {% elif field[row][col] == 5 %}
<circle class="go-stone go-stone-dead go-stone-white" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" /> <circle id="go-stone-{{ row }}-{{ col }}-white-dead" class="go-stone go-stone-dead go-stone-white" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% elif field[row][col] == 6 %} {% elif field[row][col] == 6 %}
<circle class="go-stone go-stone-dead go-stone-black" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" /> <circle id="go-stone-{{ row }}-{{ col }}-black-dead" class="go-stone go-stone-dead go-stone-black" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
@ -61,65 +61,71 @@
{% for col in range(boardsize) %} {% for col in range(boardsize) %}
<!-- {{ territory[row][col] }} --> <!-- {{ territory[row][col] }} -->
{% if territory[row][col] == 3 %} {% if territory[row][col] == 3 %}
<rect class="go-territory go-territory-white" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" /> <rect id="go-territory-{{ row }}-{{ col }}-white" class="go-territory go-territory-white" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" />
{% elif territory[row][col] == 4 %} {% elif territory[row][col] == 4 %}
<rect class="go-territory go-territory-black" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" /> <rect id="go-territory-{{ row }}-{{ col }}-white" class="go-territory go-territory-black" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" />
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</svg> </svg>
</div> </div>
{% if abandoned %} <div id="gameinfo" class="{% if abandoned or score is not none %}over{% elif current_player == colormap[player.uuid] %}active{% else %}passive{% endif %}">
<b>Game abandoned</b> {% if abandoned %}
{% elif score is not none %} <b>Game abandoned</b>
<b> {% elif score is not none %}
{% if score > 0 %} <b>
Black won with {{ score }} points. {% if score > 0 %}
{% else %} Black won with {{ score }} points.
White won with {{ -score }} points. {% else %}
{% endif %} White won with {{ -score }} points.
</b> {% endif %}
{% elif current_player == colormap[player.uuid] %} </b>
<input id="go-input-pass" name="go-input-submit" type="submit" value="Pass" /> {% elif current_player == colormap[player.uuid] %}
<input id="go-input-place" name="go-input-submit" type="submit" value="Place" /> {% if passed[0] %}White passed{% endif %}
<input id="go-input-row" name="go-input-row" type="hidden" value="-1" /> {% if passed[1] %}Black passed{% endif %}
<input id="go-input-col" name="go-input-col" type="hidden" value="-1" /> <input id="go-input-pass" name="go-input-submit" type="submit" value="Pass" />
{% endif %} <input id="go-input-place" name="go-input-submit" type="submit" value="Place" />
<input id="go-input-row" name="go-input-row" type="hidden" value="-1" />
<input id="go-input-col" name="go-input-col" type="hidden" value="-1" />
{% endif %}
</div>
<a href="{{ baseurl }}/{{ player.uuid }}/{{ game.uuid }}/play">Refresh</a>
<ul>
{% for p in players.keys() %}
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" version="2.0">
<defs>
<radialGradient id="white-stone" fx="40%" fy="25%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="70%" stop-color="#dddddd" />
<stop offset="100%" stop-color="#bbbbbb" />
</radialGradient>
<radialGradient id="black-stone" fx="40%" fy="25%">
<stop offset="0%" stop-color="#444444" />
<stop offset="70%" stop-color="#222222" />
<stop offset="100%" stop-color="#000000" />
</radialGradient>
</defs>
<circle class="go-stone go-stone-{{ colormap[p].name.lower() }}" cx="10" cy="10" r="9" />
</svg>
{% if p == player.uuid %}
<b>{{ game.players[p].name }}</b>
{% elif aiplayer is not none and p == aiplayer.uuid %}
GNU Go AI
{% else %}
{{ game.players[p].name }}
{% endif %}
: {{ captures[colormap[p].value-1] }}
{% if colormap[p].value == 1 %}
+ {{ komi }}
{% endif %}
{% endfor %}
</ul>
</form> </form>
<a href="{{ baseurl }}/{{ player.uuid }}/{{ game.uuid }}/play">Refresh</a>
<ul>
{% for p in players.keys() %}
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" version="2.0">
<defs>
<radialGradient id="white-stone" fx="40%" fy="25%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="70%" stop-color="#dddddd" />
<stop offset="100%" stop-color="#bbbbbb" />
</radialGradient>
<radialGradient id="black-stone" fx="40%" fy="25%">
<stop offset="0%" stop-color="#444444" />
<stop offset="70%" stop-color="#222222" />
<stop offset="100%" stop-color="#000000" />
</radialGradient>
</defs>
<circle class="go-stone go-stone-{{ colormap[p].name.lower() }}" cx="10" cy="10" r="9" />
</svg>
{% if p == player.uuid %}
<b>{{ game.players[p].name }}</b>
{% else %}
{{ game.players[p].name }}
{% endif %}
: {{ captures[colormap[p].value-1] }}
{% if colormap[p].value == 1 %}
+ {{ komi }}
{% endif %}
{% endfor %}
</ul>
<script src="{{ baseurl }}/static/gogame.js"></script> <script src="{{ baseurl }}/static/gogame.js"></script>
{%- endblock %} {%- endblock %}

View file

@ -11,6 +11,7 @@ import re
from threading import Timer, Lock from threading import Timer, Lock
from webgames.puzzle import Puzzle from webgames.puzzle import Puzzle
from webgames.player import Player
class BoardSize(enum.Enum): class BoardSize(enum.Enum):
SMALL = 9 SMALL = 9
@ -48,6 +49,7 @@ class GoPuzzle(Puzzle):
self._field = None self._field = None
self._territory = None self._territory = None
self._abandoned = False self._abandoned = False
self.ai = None
self._timer = None self._timer = None
self._tlock = Lock() self._tlock = Lock()
self._captures = [0, 0] self._captures = [0, 0]
@ -76,12 +78,16 @@ class GoPuzzle(Puzzle):
with self._tlock: with self._tlock:
self._timer = Timer(ABANDON_TIMEOUT, self._shutdown) self._timer = Timer(ABANDON_TIMEOUT, self._shutdown)
self._timer.start() self._timer.start()
ailevel = int(options.get('go-input-ailevel', 10))
self.size = BoardSize(int(options.get('go-input-boardsize', BoardSize.MEDIUM.value))) self.size = BoardSize(int(options.get('go-input-boardsize', BoardSize.MEDIUM.value)))
self.gnugo = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.gnugo = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.gnugo.connect('/run/gnugo.sock') self.gnugo.connect('/run/gnugo.sock')
self._gtp(f'boardsize {self.size.value}') self._gtp(f'boardsize {self.size.value}')
self._gtp('clear_board') self._gtp('clear_board')
self._gtp(f'komi {self.komi}') self._gtp(f'komi {self.komi}')
if len(self._players) == 1:
self.ai = Player('GnuGO AI')
self._players[self.ai.uuid] = self.ai.uuid
players = list(self._players.keys()) players = list(self._players.keys())
random.shuffle(players) random.shuffle(players)
self.colormap[players[0]] = FieldState.BLACK self.colormap[players[0]] = FieldState.BLACK
@ -95,16 +101,33 @@ class GoPuzzle(Puzzle):
self._field[i].append(0) self._field[i].append(0)
self._territory[i].append(0) self._territory[i].append(0)
self._urlbase = urlbase self._urlbase = urlbase
# First move if GnuGO AI is black
if self.ai is not None:
self._gtp(f'level {ailevel}')
if self.colormap[self.ai.uuid] == FieldState.BLACK:
self._gtp('genmove_black')
self.current_player = FieldState(3 - self.current_player.value)
self.update_field()
def get_extra_options_html(self) -> str: def get_extra_options_html(self) -> str:
return ''' options = '''
<label for="go-input-boardsize">Board Size: </label> <label for="go-input-boardsize">Board Size: </label>
<select id="go-input-boardsize" name="go-input-boardize"> <select id="go-input-boardsize" name="go-input-boardsize">
<option value="9">9x9</option> <option value="9">9x9</option>
<option value="13">13x13</option> <option value="13" selected="selected">13x13</option>
<option value="19">19x19</option> <option value="19">19x19</option>
</select> </select>
''' '''
if len(self._players) == 1:
options += '''
<p>
Press Play! to play against GNU Go or invite a second player.
<label for="go-input-ailevel">GNU Go Level: </label>
<input id="go-input-ailevel" name="go-input-ailevel" type="number" min="0" max="10" value="10" />
</p>
'''
return options
def add_player(self, uuid: UUID) -> None: def add_player(self, uuid: UUID) -> None:
if len(self._players) >= 2: if len(self._players) >= 2:
@ -128,14 +151,16 @@ class GoPuzzle(Puzzle):
for vertex in vertices: for vertex in vertices:
if len(vertex.strip()) != 2: if len(vertex.strip()) != 2:
continue continue
y, x = vertex.strip() x, y = vertex.strip()
col = int(x) - 1 row = int(y) - 1
row = self.colnames.index(y.upper()) col = self.colnames.index(x.upper())
if state == 'dead': if state == 'dead':
if self._field[row][col] == FieldState.BLACK.value: if self._field[row][col] == FieldState.BLACK.value:
self._field[row][col] = FieldState.BLACK_DEAD.value self._field[row][col] = FieldState.BLACK_DEAD.value
self._territory[row][col] = FieldState.WHITE_TERRITORY.value
elif self._field[row][col] == FieldState.WHITE.value: elif self._field[row][col] == FieldState.WHITE.value:
self._field[row][col] = FieldState.WHITE_DEAD.value self._field[row][col] = FieldState.WHITE_DEAD.value
self._territory[row][col] = FieldState.BLACK_TERRITORY.value
elif state == 'black_territory': elif state == 'black_territory':
self._territory[row][col] = FieldState.BLACK_TERRITORY.value self._territory[row][col] = FieldState.BLACK_TERRITORY.value
elif state == 'white_territory': elif state == 'white_territory':
@ -145,11 +170,10 @@ class GoPuzzle(Puzzle):
def process_action(self, player, action) -> None: def process_action(self, player, action) -> None:
if self.colormap[player] != self.current_player: if self.colormap[player] != self.current_player or self.gnugo is None:
return return
if 'go-input-submit' not in action: if 'go-input-submit' not in action:
return return
self.current_player = FieldState(3 - self.current_player.value)
with self._tlock: with self._tlock:
if self._abandoned: if self._abandoned:
return return
@ -173,17 +197,34 @@ class GoPuzzle(Puzzle):
try: try:
ret = self._gtp(f'play {player} {self.colnames[col]}{row+1}') ret = self._gtp(f'play {player} {self.colnames[col]}{row+1}')
except RuntimeError: except RuntimeError:
self.current_player = FieldState[3 - self.current_player.value] return
self.update_field() if self.ai is None:
self.current_player = FieldState(3 - self.current_player.value)
elif self.gnugo is not None:
ai_color = self.colormap[self.ai.uuid].name.lower()
ai_resp = self._gtp(f'genmove_{ai_color}')
if 'pass' in ai_resp.lower():
if self.colormap[self.ai.uuid] == FieldState.BLACK:
self.black_passed = True
else:
self.white_passed = True
if self.black_passed and self.white_passed:
self._score_game()
else:
self.black_passed = False
self.white_passed = False
self.update_field()
def update_field(self): def update_field(self):
if self.gnugo is None:
return
ret = self._gtp('showboard') ret = self._gtp('showboard')
for line in ret.splitlines(): for line in ret.splitlines():
cm = re.match('.*(BLACK|WHITE) ... has captured (\\d+) stones.*', line) cm = re.match('.*(BLACK|WHITE) ... has captured (\\d+) stones.*', line)
if cm is not None: if cm is not None:
color = FieldState[cm[1]] color = FieldState[cm[1]]
self._captures[2-color.value] = int(cm[2]) self._captures[color.value-1] = int(cm[2])
line = line.strip() line = line.strip()
if len(line) > 0 and line[0] in [str(i) for i in range(10)]: if len(line) > 0 and line[0] in [str(i) for i in range(10)]:
@ -194,9 +235,9 @@ class GoPuzzle(Puzzle):
if token in ['.', '+']: if token in ['.', '+']:
self._field[row][col] = FieldState.EMPTY.value self._field[row][col] = FieldState.EMPTY.value
elif token == 'X': elif token == 'X':
self._field[row][col] = FieldState.WHITE.value
elif token == 'O':
self._field[row][col] = FieldState.BLACK.value self._field[row][col] = FieldState.BLACK.value
elif token == 'O':
self._field[row][col] = FieldState.WHITE.value
def serialize(self, player): def serialize(self, player):
return { return {
@ -214,6 +255,7 @@ class GoPuzzle(Puzzle):
ser = self.serialize(player) ser = self.serialize(player)
return tmpl.render(player=player, return tmpl.render(player=player,
players=self._players, players=self._players,
aiplayer=self.ai,
komi=self.komi, komi=self.komi,
score=self._score, score=self._score,
game=game, game=game,
@ -222,6 +264,7 @@ class GoPuzzle(Puzzle):
field=ser['field'], field=ser['field'],
territory=ser['territory'], territory=ser['territory'],
captures=ser['captures'], captures=ser['captures'],
passed=[self.white_passed, self.black_passed],
boardsize=self.size.value, boardsize=self.size.value,
colormap=self.colormap, colormap=self.colormap,
current_player=self.current_player, current_player=self.current_player,