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()