This commit is contained in:
s3lph 2022-09-30 22:25:07 +02:00
parent 87a6220d68
commit 5351850fa1
7 changed files with 451 additions and 3 deletions

View file

@ -17,11 +17,12 @@ setup(
long_description='', long_description='',
python_requires='>=3.6', python_requires='>=3.6',
install_requires=[ install_requires=[
'jinja2==2.11.1', 'bottle==0.12.23',
'bottle==0.12.18', 'Jinja2==3.1.2',
'MarkupSafe==2.1.1',
'namesgenerator==0.3',
'sudoku-manager==1.0.6', 'sudoku-manager==1.0.6',
'sudokugen==0.2.1', 'sudokugen==0.2.1',
'namesgenerator==0.3'
], ],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [

14
static/gogame.js Normal file
View file

@ -0,0 +1,14 @@
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();
};
}

View file

@ -146,3 +146,65 @@ input {
.playercardlist .playercard-2 { .playercardlist .playercard-2 {
transform: rotate(10deg) scale(0.5) translate(0px, 10px); transform: rotate(10deg) scale(0.5) translate(0px, 10px);
} }
/*
* Go
*/
.go-background {
fill: #ffdd88;
}
.go-line {
stroke: black;
stroke-width: 3;
stroke-opacity: 1;
stroke-linecap: round;
}
.go-hoshi, .go-territory-black {
fill: black;
}
.go-territory-white {
fill: white;
}
.go-stone-black {
fill: url(#black-stone);
}
.go-stone-white {
fill: url(#white-stone);
}
.go-stone-empty {
cursor: pointer;
opacity: 0;
}
.go-stone-empty:hover, .go-stone-dead {
opacity: 0.5;
}
input.go-field-input {
display: none;
}
label.go-field-label {
padding: 0;
margin: 0;
}
label.go-field-label > svg {
padding: 0;
margin: 0;
}
div#field {
padding: 0;
}
input#go-input-place {
display: none;
}

View file

@ -0,0 +1,125 @@
{% extends 'cwa/base.html.j2' %}
{%- block body %}
<h1>Go: {{ game.human_id }}</h1>
<form method="POST">
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" width="{{ 50 * boardsize }}" height="{{ 50 * boardsize }}" 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>
<rect class="go-background" x="0" y="0" width="{{ 50 * boardsize }}" height="{{ 50 * boardsize }}" />
{% for row in range(boardsize) %}
<line class="go-line" x1="25" y1="{{ 25 + row * 50 }}" x2="{{ 25 + (boardsize-1) * 50 }}" y2="{{ 25 + row * 50 }}" />
{% endfor %}
{% for col in range(boardsize) %}
<line class="go-line" x1="{{ 25 + col * 50 }}" y1="25" x2="{{ 25 + col * 50 }}" y2="{{ 25 + (boardsize-1) * 50 }}" />
{% endfor %}
{% if boardsize == 19 %}
{% for y in [3, 9, 15] %}
{% for x in [3, 9, 15] %}
<circle class="go-hoshi" cx="{{ x * 50 + 25 }}" cy="{{ y * 50 + 25 }}" r="7" />
{% endfor %}
{% endfor %}
{% elif boardsize == 13 %}
{% for y, x in [[3, 3], [3, 9], [9, 3], [9, 9], [6, 6]] %}
<circle class="go-hoshi" cx="{{ x * 50 + 25 }}" cy="{{ y * 50 + 25 }}" r="7" />
{% endfor %}
{% elif boardsize == 9 %}
{% for y, x in [[2, 2], [2, 6], [6, 2], [6, 6], [4, 4]] %}
<circle class="go-hoshi" cx="{{ x * 50 + 25 }}" cy="{{ y * 50 + 25 }}" r="7" />
{% endfor %}
{% endif %}
{% for row in range(boardsize) %}
{% for col in range(boardsize) %}
<!-- {{ row }} {{ col }} {{ field[row][col] }} -->
{% 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" />
{% elif field[row][col] == 1 %}
<circle class="go-stone go-stone-white" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% elif field[row][col] == 2 %}
<circle class="go-stone go-stone-black" cx="{{ col * 50 + 25 }}" cy="{{ row * 50 + 25 }}" r="20" />
{% 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" />
{% 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" />
{% endif %}
{% endfor %}
{% endfor %}
{% for row in range(boardsize) %}
{% for col in range(boardsize) %}
<!-- {{ territory[row][col] }} -->
{% if territory[row][col] == 3 %}
<rect class="go-territory go-territory-white" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" />
{% elif territory[row][col] == 4 %}
<rect class="go-territory go-territory-black" x="{{ col * 50 + 20 }}" y="{{ row * 50 + 20 }}" width="10" height="10" />
{% endif %}
{% endfor %}
{% endfor %}
</svg>
</div>
{% if abandoned %}
<b>Game abandoned</b>
{% elif score is not none %}
<b>
{% if score > 0 %}
Black won with {{ score }} points.
{% else %}
White won with {{ -score }} points.
{% endif %}
</b>
{% elif current_player == colormap[player.uuid] %}
<input id="go-input-pass" name="go-input-submit" type="submit" value="Pass" />
<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 %}
</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>
{%- endblock %}

View file

@ -11,6 +11,7 @@
<option selected="selected" value="sudoku">Sudoku</option> <option selected="selected" value="sudoku">Sudoku</option>
<option value="set">Set</option> <option value="set">Set</option>
<option value="carcassonne">Carcassonne</option> <option value="carcassonne">Carcassonne</option>
<option value="go">Go</option>
</select> </select>
<input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li> <input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li>
<li>Invite other players</li> <li>Invite other players</li>

View file

@ -9,6 +9,7 @@ from webgames.puzzle import Puzzle
from webgames.sudoku_puzzle import Sudoku from webgames.sudoku_puzzle import Sudoku
from webgames.set_puzzle import SetPuzzle from webgames.set_puzzle import SetPuzzle
from webgames.carcassonne import Carcassonne from webgames.carcassonne import Carcassonne
from webgames.go import GoPuzzle
from webgames.player import Player from webgames.player import Player
from webgames.human import HumanID from webgames.human import HumanID

244
webgames/go.py Normal file
View file

@ -0,0 +1,244 @@
from typing import Any, Dict
import enum
from uuid import UUID
import string
import subprocess
import random
import socket
import re
from threading import Timer, Lock
from webgames.puzzle import Puzzle
class BoardSize(enum.Enum):
SMALL = 9
MEDIUM = 13
LARGE = 19
class FieldState(enum.Enum):
EMPTY = 0
WHITE = 1
BLACK = 2
WHITE_TERRITORY = 3
BLACK_TERRITORY = 4
WHITE_DEAD = 5
BLACK_DEAD = 6
ABANDON_TIMEOUT = 3600
class GoPuzzle(Puzzle):
def __init__(self, pargs = None):
super().__init__('go', self.__class__)
self._players = {}
self.current_player = FieldState.BLACK
self.black_passed = False
self.white_passed = False
self.colormap = {}
self._score = None
self.komi = 6.5
self.score = None
self.colnames = list(string.ascii_uppercase)
self.colnames.remove('I')
self._field = None
self._territory = None
self._abandoned = False
self._timer = None
self._tlock = Lock()
self._captures = [0, 0]
self._urlbase = '/'
def _gtp(self, command):
self.gnugo.send(f'{command}\n'.encode())
ret = self.gnugo.recv(1024)
while not ret.startswith(b'=') and not ret.startswith(b'?'):
ret = self.gnugo.recv(1024)
while not ret.endswith(b'\n\n'):
ret += self.gnugo.recv(1024)
ret = ret.decode()
if ret.startswith('?'):
raise RuntimeError(ret)
return ret
def _shutdown(self):
with self._tlock:
if self.gnugo is not None:
self.gnugo.close()
self.gnugo = None
self._abandoned = True
def begin(self, options: Dict[str, Any], urlbase):
with self._tlock:
self._timer = Timer(ABANDON_TIMEOUT, self._shutdown)
self._timer.start()
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}')
players = list(self._players.keys())
random.shuffle(players)
self.colormap[players[0]] = FieldState.BLACK
self.colormap[players[1]] = FieldState.WHITE
self._field = []
self._territory = []
for i in range(self.size.value):
self._field.append([])
self._territory.append([])
for j in range(self.size.value):
self._field[i].append(0)
self._territory[i].append(0)
self._urlbase = urlbase
def get_extra_options_html(self) -> str:
return '''
<label for="go-input-boardsize">Board Size: </label>
<select id="go-input-boardsize" name="go-input-boardize">
<option value="9">9x9</option>
<option value="13">13x13</option>
<option value="19">19x19</option>
</select>
'''
def add_player(self, uuid: UUID) -> None:
if len(self._players) >= 2:
return
self._players[uuid] = uuid
def _score_game(self):
self._timer.cancel()
# Compute final sore
ret = self._gtp('final_score')
sgf_score = ret.splitlines()[0].split(' ', 1)[1].strip()
winner, score = sgf_score.split('+', 1)
if winner == 'B':
self._score = float(score)
else:
self._score = -float(score)
# Update board with scoring result
for state in ['dead', 'black_territory', 'white_territory']:
ret = self._gtp(f'final_status_list {state}')
vertices = ret.replace('\n', ' ').split(' ')
for vertex in vertices:
if len(vertex.strip()) != 2:
continue
y, x = vertex.strip()
col = int(x) - 1
row = self.colnames.index(y.upper())
if state == 'dead':
if self._field[row][col] == FieldState.BLACK.value:
self._field[row][col] = FieldState.BLACK_DEAD.value
elif self._field[row][col] == FieldState.WHITE.value:
self._field[row][col] = FieldState.WHITE_DEAD.value
elif state == 'black_territory':
self._territory[row][col] = FieldState.BLACK_TERRITORY.value
elif state == 'white_territory':
self._territory[row][col] = FieldState.WHITE_TERRITORY.value
self.gnugo.close()
self.gnugo = None
def process_action(self, player, action) -> None:
if self.colormap[player] != self.current_player:
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
self._timer.cancel()
self._timer = Timer(ABANDON_TIMEOUT, self._shutdown)
self._timer.start()
move = action['go-input-submit']
if move == 'Pass':
if self.current_player == FieldState.BLACK:
self.black_passed = True
else:
self.white_passed = True
if self.black_passed and self.white_passed:
self._score_game()
elif move == 'Place':
self.black_passed = False
self.white_passed = False
row = int(action['go-input-row'])
col = int(action['go-input-col'])
player = self.current_player.name.lower()
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()
def update_field(self):
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])
line = line.strip()
if len(line) > 0 and line[0] in [str(i) for i in range(10)]:
cols = line.split()
row = int(cols[0]) - 1
for col in range(self.size.value):
token = cols[col+1]
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
def serialize(self, player):
return {
'field': self._field,
'territory': self._territory,
'captures': self._captures,
'current_player': self.current_player,
'colormap': self.colormap
}
def render(self, env, game, player, duration):
with self._tlock:
tmpl = env.get_template('cwa/gogame.html.j2')
ser = self.serialize(player)
return tmpl.render(player=player,
players=self._players,
komi=self.komi,
score=self._score,
game=game,
abandoned=self._abandoned,
duration=duration,
field=ser['field'],
territory=ser['territory'],
captures=ser['captures'],
boardsize=self.size.value,
colormap=self.colormap,
current_player=self.current_player,
baseurl=self._urlbase)
@property
def human_name(self) -> str:
return 'Go'
@property
def scores(self):
return {}
@property
def is_completed(self) -> bool:
return self.black_passed and self.white_passed
GoPuzzle()