Add Go
This commit is contained in:
parent
87a6220d68
commit
5351850fa1
7 changed files with 451 additions and 3 deletions
7
setup.py
7
setup.py
|
@ -17,11 +17,12 @@ setup(
|
|||
long_description='',
|
||||
python_requires='>=3.6',
|
||||
install_requires=[
|
||||
'jinja2==2.11.1',
|
||||
'bottle==0.12.18',
|
||||
'bottle==0.12.23',
|
||||
'Jinja2==3.1.2',
|
||||
'MarkupSafe==2.1.1',
|
||||
'namesgenerator==0.3',
|
||||
'sudoku-manager==1.0.6',
|
||||
'sudokugen==0.2.1',
|
||||
'namesgenerator==0.3'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
|
14
static/gogame.js
Normal file
14
static/gogame.js
Normal 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();
|
||||
};
|
||||
}
|
|
@ -146,3 +146,65 @@ input {
|
|||
.playercardlist .playercard-2 {
|
||||
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;
|
||||
}
|
||||
|
|
125
templates/cwa/gogame.html.j2
Normal file
125
templates/cwa/gogame.html.j2
Normal 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 %}
|
|
@ -11,6 +11,7 @@
|
|||
<option selected="selected" value="sudoku">Sudoku</option>
|
||||
<option value="set">Set</option>
|
||||
<option value="carcassonne">Carcassonne</option>
|
||||
<option value="go">Go</option>
|
||||
</select>
|
||||
<input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li>
|
||||
<li>Invite other players</li>
|
||||
|
|
|
@ -9,6 +9,7 @@ from webgames.puzzle import Puzzle
|
|||
from webgames.sudoku_puzzle import Sudoku
|
||||
from webgames.set_puzzle import SetPuzzle
|
||||
from webgames.carcassonne import Carcassonne
|
||||
from webgames.go import GoPuzzle
|
||||
from webgames.player import Player
|
||||
from webgames.human import HumanID
|
||||
|
||||
|
|
244
webgames/go.py
Normal file
244
webgames/go.py
Normal 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()
|
Loading…
Reference in a new issue