Files
eisterman 0ca905eb71 Speed up the simulator 5-7x with identical games (6.10)
Precompute board adjacency and square sets, look up opponents once per
move, and cache walks that exclude only the start square. Every game is
unchanged for the same seed, checked with agents/scripts/fingerprint_games.py.
Plan: skip catch-up 6.9 (D44), stop running the random lineup (D45).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 17:02:53 +02:00

203 lines
6.4 KiB
Python

import pytest
from gridedit import (
cell_column,
cell_line,
edit,
h_segment_column,
wall_column_left_of,
wall_line_above,
)
from dojo.board import (
BOARD_SIZE,
Board,
BoardError,
Pos,
build_board,
edge,
load_board,
parse_legend,
quadrant_of,
rotate,
)
from dojo.grid import parse_grid, render_grid
def build(text: str, offsets: dict[str, int] | None = None) -> Board:
return build_board(parse_grid(text), offsets)
def build_problems(text: str, offsets: dict[str, int] | None = None) -> list[str]:
with pytest.raises(BoardError) as raised:
build(text, offsets)
return raised.value.problems
def test_rotation_is_clockwise_around_the_centre() -> None:
corner = Pos(0, 0)
assert [rotate(corner, k) for k in range(4)] == [(0, 0), (0, 15), (15, 15), (15, 0)]
assert [quadrant_of(rotate(Pos(3, 5), k)) for k in range(4)] == [0, 1, 2, 3]
assert rotate(Pos(3, 5), 4) == Pos(3, 5)
def test_board_is_sixteen_by_sixteen_with_central_dojo(board: Board) -> None:
assert board.size == BOARD_SIZE
assert board.dojo == {Pos(r, c) for r in range(6, 10) for c in range(6, 10)}
def test_every_cell_matches_its_rotated_source(board: Board, quadrant_text: str) -> None:
quadrant = parse_grid(quadrant_text)
for r in range(8):
for c in range(8):
for k in range(4):
assert board.code(rotate(Pos(r, c), k)) == quadrant.cells[r][c]
def test_walls_are_rotated(board: Board) -> None:
safe_room_east_wall = edge(Pos(0, 2), Pos(0, 3))
for k in range(4):
a, b = safe_room_east_wall
assert edge(rotate(a, k), rotate(b, k)) in board.walls
assert edge(Pos(1, 2), Pos(1, 3)) not in board.walls
def test_neighbours_respect_walls_and_border(board: Board) -> None:
assert set(board.neighbours(Pos(0, 0))) == {Pos(0, 1), Pos(1, 0)}
assert Pos(0, 3) not in set(board.neighbours(Pos(0, 2)))
assert Pos(1, 3) in set(board.neighbours(Pos(1, 2)))
def test_neighbours_come_north_east_south_west(board: Board) -> None:
square = Pos(1, 2)
compass = [Pos(0, 2), Pos(1, 3), Pos(2, 2), Pos(1, 1)]
assert list(board.neighbours(square)) == [q for q in compass if q in board.neighbours(square)]
def test_square_lookups_without_matches_are_empty(board: Board) -> None:
assert board.cells_with("unknown") == frozenset()
assert board.safe_room(4) == frozenset()
def test_seams_join_neighbouring_quadrants(board: Board) -> None:
assert Pos(0, 8) in set(board.neighbours(Pos(0, 7)))
assert Pos(8, 0) in set(board.neighbours(Pos(7, 0)))
def test_to_grid_round_trips_through_render(board: Board) -> None:
grid = board.to_grid()
assert parse_grid(render_grid(grid)) == grid
def test_tunnels_pair_within_the_quadrant_by_default(board: Board) -> None:
a, b = Pos(1, 6), Pos(3, 4)
for k in range(4):
assert board.tunnels[rotate(a, k)] == rotate(b, k)
assert board.tunnels[rotate(b, k)] == rotate(a, k)
assert len(board.tunnels) == 8
@pytest.mark.parametrize("offset", [1, 2, 3])
def test_tunnel_offset_links_the_first_cell_to_a_later_quadrant(
quadrant_text: str, offset: int
) -> None:
board = build(quadrant_text, {"1": offset})
a, b = Pos(1, 6), Pos(3, 4)
for k in range(4):
assert board.tunnels[rotate(a, k)] == rotate(b, k + offset)
assert board.tunnels[rotate(b, k + offset)] == rotate(a, k)
assert all(board.tunnels[board.tunnels[p]] == p for p in board.tunnels)
def test_quadrant_must_be_eight_by_eight() -> None:
small = "+---+\n| D |\n+---+\n"
assert build_problems(small) == ["quadrant must be 8x8, got 1x1"]
def test_unknown_cell_code(quadrant_text: str) -> None:
text = edit(quadrant_text, cell_line(4), cell_column(4), "X")
assert "cell (4, 4): unknown code 'X'" in build_problems(text)
def test_outer_border_must_be_walled(quadrant_text: str) -> None:
open_top = edit(quadrant_text, wall_line_above(0), h_segment_column(4), " ")
open_left = edit(quadrant_text, cell_line(4), wall_column_left_of(0), " ")
assert any("top border" in p for p in build_problems(open_top))
assert any("left border" in p for p in build_problems(open_left))
def test_seams_must_match(quadrant_text: str) -> None:
text = edit(quadrant_text, cell_line(2), wall_column_left_of(8), "|")
assert any("seam mismatch: right border at row 2" in p for p in build_problems(text))
def test_matching_seam_walls_are_accepted(quadrant_text: str) -> None:
text = edit(quadrant_text, cell_line(2), wall_column_left_of(8), "|")
text = edit(text, wall_line_above(8), h_segment_column(2), "---")
board = build(text)
assert edge(Pos(2, 7), Pos(2, 8)) in board.walls
assert edge(Pos(7, 2), Pos(8, 2)) in board.walls
def test_dojo_cells_are_fixed(quadrant_text: str) -> None:
text = edit(quadrant_text, cell_line(5), cell_column(7), "D")
assert any("Dojo cells must be exactly" in p for p in build_problems(text))
def test_tunnel_needs_exactly_two_cells(quadrant_text: str) -> None:
text = edit(quadrant_text, cell_line(4), cell_column(4), "1")
assert "tunnel '1' must appear on exactly 2 cells, found 3" in build_problems(text)
def test_legend_offset_must_be_valid(quadrant_text: str) -> None:
problems = build_problems(quadrant_text, {"1": 4, "2": 0})
assert "tunnel '1': quadrant_offset must be 0..3, got 4" in problems
assert "legend links tunnel '2', which is not on the quadrant" in problems
def test_parse_legend() -> None:
assert parse_legend({"tunnels": {"1": {"quadrant_offset": 2}}}) == {"1": 2}
assert parse_legend({}) == {}
@pytest.mark.parametrize(
("data", "message"),
[
({"extra": {}}, "unknown sections"),
({"tunnels": 3}, "must be a table"),
({"tunnels": {"a": {"quadrant_offset": 1}}}, "must be a digit"),
({"tunnels": {"1": {"offset": 1}}}, "only quadrant_offset"),
],
)
def test_parse_legend_errors(data: dict[str, object], message: str) -> None:
with pytest.raises(BoardError) as raised:
parse_legend(data)
assert any(message in p for p in raised.value.problems)
def test_load_board_reads_legend(tmp_path, quadrant_text: str) -> None:
quadrant = tmp_path / "quadrant.txt"
legend = tmp_path / "legend.toml"
quadrant.write_text(quadrant_text, encoding="utf-8")
legend.write_text("[tunnels.1]\nquadrant_offset = 1\n", encoding="utf-8")
board = load_board(quadrant, legend)
assert board.tunnels[Pos(1, 6)] == rotate(Pos(3, 4), 1)