Moonchess: vanishing tic-tac-toe, online and practice modes

Online play over WebSockets with any-string game codes, seats and spectators,
and a 1-second heartbeat. Practice mode against Columbina, backed by a full
solver, with an accuracy slider, spoilers, undo and piece freshness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
tukaunuandClaude Opus 5.5 committed 2026-09-28 03:36:46 +02:00
commit c52f30dc59
18 files changed
+1743

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
"""Moonchess practice server: play against the computer.
Stateless: the page sends the current position with every request and gets the new
position back, so there's no session to lose. The page decides whose turn belongs to
the computer; this server only applies moves and picks them.
"""
import argparse
import logging
from aiohttp import web
from game import Game
from server import STATIC_DIR, no_cache
from solver import Solver
solver = Solver() # solves every position up front; takes about a second
async def index(request):
return web.FileResponse(STATIC_DIR / "practice.html")
async def play(request):
"""Body: {history, turn, cell} to play `cell`, or {history, turn, accuracy} for a
computer move, where accuracy (0-1) is the chance it plays a perfect move. A computer
move's reply also says how it was chosen, as computerMove: {cell, how}."""
try:
body = await request.json()
game = Game.from_history(body["history"], body["turn"])
cell = body.get("cell")
accuracy = float(body.get("accuracy", 1))
except (ValueError, KeyError, TypeError, AttributeError):
return web.json_response({"error": "Invalid request"}, status=400)
computer_move = None
if cell is None and game.winner is None:
cell, how = solver.choose(game, accuracy)
computer_move = {"cell": cell, "how": how}
error = game.play(game.turn, cell)
if error:
return web.json_response({"error": error}, status=400)
return web.json_response({**game.to_dict(), "computerMove": computer_move})
def make_app():
app = web.Application(middlewares=[no_cache])
app.router.add_get("/", index)
app.router.add_post("/api/play", play)
app.router.add_static("/static", STATIC_DIR)
return app
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Moonchess practice server")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8081)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
web.run_app(make_app(), host=args.host, port=args.port)