From 6125ac5eab4f3369b3de9d0ccef8bb7572dd360c Mon Sep 17 00:00:00 2001 From: s3lph Date: Sun, 2 Oct 2022 07:13:46 +0200 Subject: [PATCH] Finish go --- static/gogame.js | 72 +++++++++++++++++---- templates/cwa/gogame.html.j2 | 120 ++++++++++++++++++----------------- webgames/go.py | 69 ++++++++++++++++---- 3 files changed, 179 insertions(+), 82 deletions(-) diff --git a/static/gogame.js b/static/gogame.js index 5c0032e..cf33d99 100644 --- a/static/gogame.js +++ b/static/gogame.js @@ -1,14 +1,62 @@ -let stones = document.getElementsByClassName('go-stone-empty'); -let inputRow = document.getElementById('go-input-row'); -let inputCol = document.getElementById('go-input-col'); -let inputPlace = document.getElementById('go-input-place'); - -for (let i = 0; i < stones.length; ++i) { - stones[i].onclick = (e) => { - console.log('place'); - inputCol.value = stones[i].attributes['data-col'].value; - inputRow.value = stones[i].attributes['data-row'].value; - inputPlace.click(); - }; +function setupEventHandler() { + let stones = document.getElementsByClassName('go-stone-empty'); + let inputRow = document.getElementById('go-input-row'); + let inputCol = document.getElementById('go-input-col'); + let inputPlace = document.getElementById('go-input-place'); + + for (let i = 0; i < stones.length; ++i) { + stones[i].onclick = (e) => { + console.log('place'); + inputCol.value = stones[i].attributes['data-col'].value; + inputRow.value = stones[i].attributes['data-row'].value; + 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(); diff --git a/templates/cwa/gogame.html.j2 b/templates/cwa/gogame.html.j2 index 4e62c19..4704f42 100644 --- a/templates/cwa/gogame.html.j2 +++ b/templates/cwa/gogame.html.j2 @@ -3,10 +3,10 @@ {%- block body %}

Go: {{ game.human_id }}

-
+
- + @@ -45,15 +45,15 @@ {% for col in range(boardsize) %} {% if field[row][col] == 0 and current_player == colormap[player.uuid] and not abandoned and score is none %} - + {% elif field[row][col] == 1 %} - + {% elif field[row][col] == 2 %} - + {% elif field[row][col] == 5 %} - + {% elif field[row][col] == 6 %} - + {% endif %} {% endfor %} {% endfor %} @@ -61,65 +61,71 @@ {% for col in range(boardsize) %} {% if territory[row][col] == 3 %} - + {% elif territory[row][col] == 4 %} - + {% endif %} {% endfor %} {% endfor %}
- {% if abandoned %} - Game abandoned - {% elif score is not none %} - - {% if score > 0 %} - Black won with {{ score }} points. - {% else %} - White won with {{ -score }} points. - {% endif %} - - {% elif current_player == colormap[player.uuid] %} - - - - - {% endif %} +
+ {% if abandoned %} + Game abandoned + {% elif score is not none %} + + {% if score > 0 %} + Black won with {{ score }} points. + {% else %} + White won with {{ -score }} points. + {% endif %} + + {% elif current_player == colormap[player.uuid] %} + {% if passed[0] %}White passed{% endif %} + {% if passed[1] %}Black passed{% endif %} + + + + + {% endif %} +
+ Refresh + + +
- Refresh - - - {%- endblock %} diff --git a/webgames/go.py b/webgames/go.py index 7eab858..ccda7b5 100644 --- a/webgames/go.py +++ b/webgames/go.py @@ -11,6 +11,7 @@ import re from threading import Timer, Lock from webgames.puzzle import Puzzle +from webgames.player import Player class BoardSize(enum.Enum): SMALL = 9 @@ -48,6 +49,7 @@ class GoPuzzle(Puzzle): self._field = None self._territory = None self._abandoned = False + self.ai = None self._timer = None self._tlock = Lock() self._captures = [0, 0] @@ -76,12 +78,16 @@ class GoPuzzle(Puzzle): with self._tlock: self._timer = Timer(ABANDON_TIMEOUT, self._shutdown) self._timer.start() + ailevel = int(options.get('go-input-ailevel', 10)) self.size = BoardSize(int(options.get('go-input-boardsize', BoardSize.MEDIUM.value))) self.gnugo = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.gnugo.connect('/run/gnugo.sock') self._gtp(f'boardsize {self.size.value}') self._gtp('clear_board') 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()) random.shuffle(players) self.colormap[players[0]] = FieldState.BLACK @@ -95,16 +101,33 @@ class GoPuzzle(Puzzle): self._field[i].append(0) self._territory[i].append(0) 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: - return ''' + options = ''' - - + ''' + if len(self._players) == 1: + options += ''' +

+Press Play! to play against GNU Go or invite a second player. + + +

+''' + return options + def add_player(self, uuid: UUID) -> None: if len(self._players) >= 2: @@ -128,14 +151,16 @@ class GoPuzzle(Puzzle): for vertex in vertices: if len(vertex.strip()) != 2: continue - y, x = vertex.strip() - col = int(x) - 1 - row = self.colnames.index(y.upper()) + x, y = vertex.strip() + row = int(y) - 1 + col = self.colnames.index(x.upper()) if state == 'dead': if self._field[row][col] == FieldState.BLACK.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: self._field[row][col] = FieldState.WHITE_DEAD.value + self._territory[row][col] = FieldState.BLACK_TERRITORY.value elif state == 'black_territory': self._territory[row][col] = FieldState.BLACK_TERRITORY.value elif state == 'white_territory': @@ -145,11 +170,10 @@ class GoPuzzle(Puzzle): 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 if 'go-input-submit' not in action: return - self.current_player = FieldState(3 - self.current_player.value) with self._tlock: if self._abandoned: return @@ -173,17 +197,34 @@ class GoPuzzle(Puzzle): try: ret = self._gtp(f'play {player} {self.colnames[col]}{row+1}') except RuntimeError: - self.current_player = FieldState[3 - self.current_player.value] - self.update_field() + return + 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): + if self.gnugo is None: + return ret = self._gtp('showboard') for line in ret.splitlines(): cm = re.match('.*(BLACK|WHITE) ... has captured (\\d+) stones.*', line) if cm is not None: color = FieldState[cm[1]] - self._captures[2-color.value] = int(cm[2]) + self._captures[color.value-1] = int(cm[2]) line = line.strip() 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 ['.', '+']: self._field[row][col] = FieldState.EMPTY.value elif token == 'X': - self._field[row][col] = FieldState.WHITE.value - elif token == 'O': self._field[row][col] = FieldState.BLACK.value + elif token == 'O': + self._field[row][col] = FieldState.WHITE.value def serialize(self, player): return { @@ -214,6 +255,7 @@ class GoPuzzle(Puzzle): ser = self.serialize(player) return tmpl.render(player=player, players=self._players, + aiplayer=self.ai, komi=self.komi, score=self._score, game=game, @@ -222,6 +264,7 @@ class GoPuzzle(Puzzle): field=ser['field'], territory=ser['territory'], captures=ser['captures'], + passed=[self.white_passed, self.black_passed], boardsize=self.size.value, colormap=self.colormap, current_player=self.current_player,