Files
tukaunuandClaude Opus 5.5 c52f30dc59 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]>
2026-09-28 03:36:46 +02:00

97 lines
3.7 KiB
Python

import random
import unittest
from game import Game
from solver import DRAW, LOSS, WIN, Solver, _moves
solver = Solver()
def game_at(mine, theirs):
"""The Game for a solver position, with `mine` as the player to move (seat 0)."""
return Game.from_history([list(mine), list(theirs)], 0)
class SolverTest(unittest.TestCase):
def test_perfect_play_is_a_draw(self):
self.assertEqual(solver.value[((), ())], DRAW)
self.assertEqual(sorted(solver.best_moves((), ())), list(range(9)))
def test_nobody_can_be_forced_to_lose(self):
# With only two of your pieces down while placing, you can make one threat at a time.
self.assertNotIn(LOSS, solver.value.values())
def test_blocks_a_threat(self):
# X: 0, 1 threatens 2
self.assertEqual(solver.best_moves((4,), (0, 1)), [2])
def test_blocks_where_own_piece_just_vanished(self):
# X's newest two (0, 1) threaten 2, where O's oldest piece was until O's turn started
self.assertEqual(solver.best_moves((5, 6), (7, 0, 1)), [2])
def test_takes_an_immediate_win(self):
# X: 0, 1 with 2 open -> 2 wins now
self.assertEqual(solver.best_moves((0, 1), (4, 8)), [2])
def test_moves_match_game_rules(self):
rng = random.Random(1)
positions = rng.sample(sorted(solver.value), 3000)
for mine, theirs in positions:
for cell, nxt in _moves(mine, theirs):
game = game_at(mine, theirs)
self.assertIsNone(game.play(0, cell))
if nxt is None:
self.assertEqual(game.winner, 0)
else:
self.assertIsNone(game.winner)
self.assertEqual(nxt, (tuple(game.history[1]), tuple(game.history[0])))
def test_values_are_consistent(self):
rng = random.Random(2)
for pos in rng.sample(sorted(solver.value), 3000):
children = [solver.value[n] if n else "immediate" for _, n in _moves(*pos)]
value = solver.value[pos]
if value == WIN:
self.assertTrue("immediate" in children or LOSS in children)
elif value == LOSS:
self.assertTrue(all(c == WIN for c in children))
else:
self.assertNotIn(LOSS, children)
self.assertIn(DRAW, children)
def test_accuracy_extremes(self):
game = game_at((0, 1), (4, 8))
rng = random.Random(3)
self.assertTrue(all(solver.choose(game, 1.0, rng) == (2, "perfect") for _ in range(50)))
picks = {solver.choose(game, 0.0, rng) for _ in range(200)}
self.assertEqual(picks, {(2, "lucky"), (3, "random"), (5, "random"),
(6, "random"), (7, "random")})
class FromHistoryTest(unittest.TestCase):
def test_rebuilds_board_and_winner(self):
g = Game.from_history([[0, 1, 2], [4, 5]], 1)
self.assertEqual(g.board[:3], [0, 0, 0])
self.assertEqual(g.winner, 0)
self.assertEqual(g.win_line, [0, 1, 2])
def test_rejects_invalid(self):
for history, turn in [
([[0, 0], []], 0), # duplicate cell
([[0], [0]], 0), # overlap
([[9], []], 0), # out of range
([[0, 1, 2, 3], []], 0), # too many pieces
([[0, 1, 5], [3, 4, 8]], 0), # mover's oldest should have vanished
([["a"], []], 0),
([[True], []], 0),
([[0]], 0), # one seat only
([[], []], 2), # bad turn
(None, 0),
]:
with self.assertRaises(ValueError, msg=(history, turn)):
Game.from_history(history, turn)
if __name__ == "__main__":
unittest.main()