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:
commit
c52f30dc59
18 files changed
+1743
No files matched your search
@@ -0,0 +1,96 @@
|
||||
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()
|
||||
Reference in new issue
Block a user