117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
"""Deterministic dice and agents for engine tests."""
|
|
|
|
from collections import deque
|
|
from collections.abc import Iterable, Sequence
|
|
from dataclasses import dataclass, field, replace
|
|
from pathlib import Path
|
|
|
|
from dojo.board import Board, Pos, build_board
|
|
from dojo.engine import FightView, Game, GameView
|
|
from dojo.grid import parse_grid
|
|
from dojo.movement import Path as Walk
|
|
from dojo.rules import VARIANT_A, Card, RulesConfig
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "quadrant_valid.txt"
|
|
NO_CARDS: tuple[tuple[Card, int], ...] = ()
|
|
|
|
|
|
class ScriptedDice:
|
|
"""Rolls come from a fixed list; running out means the engine rolled more than expected."""
|
|
|
|
def __init__(self, rolls: Iterable[int] = ()) -> None:
|
|
self.rolls = deque(rolls)
|
|
|
|
def d6(self) -> int:
|
|
if not self.rolls:
|
|
raise AssertionError("unexpected die roll")
|
|
return self.rolls.popleft()
|
|
|
|
def shuffle(self, cards: list[Card]) -> None:
|
|
"""Keep the given order, so tests control the deck."""
|
|
|
|
|
|
@dataclass
|
|
class ScriptedAgent:
|
|
# Each entry picks the offered path ending on that square, or exactly that path.
|
|
ends: deque[Pos | Walk] = field(default_factory=deque)
|
|
start_square: Pos | None = None
|
|
use_tunnels: bool = False
|
|
tunnel_destination: Pos | None = None
|
|
sfida_target: int | None = None
|
|
irimi_target: int | None = None
|
|
exam_defender: int | None = None
|
|
keep_drawn: bool = False
|
|
plays: frozenset[Card] = frozenset()
|
|
fight_views: list[FightView] = field(default_factory=list)
|
|
offered_paths: list[Sequence[Walk]] = field(default_factory=list)
|
|
path_views: list[GameView] = field(default_factory=list)
|
|
|
|
def place_pawn(self, view: GameView, squares: Sequence[Pos]) -> Pos:
|
|
if self.start_square is not None and self.start_square in squares:
|
|
return self.start_square
|
|
return squares[0]
|
|
|
|
def choose_path(self, view: GameView, paths: Sequence[Walk]) -> Walk:
|
|
self.offered_paths.append(paths)
|
|
self.path_views.append(view)
|
|
if not self.ends:
|
|
return paths[0]
|
|
wanted = self.ends.popleft()
|
|
for path in paths:
|
|
if path == wanted or (isinstance(wanted, Pos) and path[-1] == wanted):
|
|
return path
|
|
raise AssertionError(f"no offered path matches {wanted}")
|
|
|
|
def use_tunnel(self, view: GameView, exit: Pos) -> bool:
|
|
return self.use_tunnels
|
|
|
|
def choose_tunnel_destination(self, view: GameView, tunnels: Sequence[Pos]) -> Pos:
|
|
return self.tunnel_destination if self.tunnel_destination is not None else tunnels[0]
|
|
|
|
def choose_sfida_target(self, view: GameView, targets: Sequence[int]) -> int:
|
|
return self.sfida_target if self.sfida_target is not None else targets[0]
|
|
|
|
def choose_irimi_target(self, view: GameView, targets: Sequence[int]) -> int:
|
|
return self.irimi_target if self.irimi_target is not None else targets[0]
|
|
|
|
def choose_exam_defender(self, view: GameView, candidates: Sequence[int]) -> int:
|
|
return self.exam_defender if self.exam_defender is not None else candidates[0]
|
|
|
|
def keep_tecnica(self, view: GameView, held: Card, drawn: Card) -> Card:
|
|
return drawn if self.keep_drawn else held
|
|
|
|
def play_tecnica(self, view: GameView, fight: FightView) -> bool:
|
|
self.fight_views.append(fight)
|
|
return view.tecnica in self.plays
|
|
|
|
|
|
def fixture_board() -> Board:
|
|
return build_board(parse_grid(FIXTURE.read_text(encoding="utf-8")))
|
|
|
|
|
|
def make_game(
|
|
players: int = 2,
|
|
config: RulesConfig = VARIANT_A,
|
|
dice: Iterable[int] = (),
|
|
deck: tuple[tuple[Card, int], ...] = NO_CARDS,
|
|
board: Board | None = None,
|
|
) -> tuple[Game, list[ScriptedAgent], ScriptedDice]:
|
|
"""A game on the fixture board with an empty deck unless ``deck`` is given."""
|
|
agents = [ScriptedAgent() for _ in range(players)]
|
|
randomness = ScriptedDice(dice)
|
|
game = Game(board or fixture_board(), replace(config, deck=deck), agents, randomness)
|
|
return game, agents, randomness
|
|
|
|
|
|
def put(game: Game, player: int, position: Pos, track: int | None = None) -> None:
|
|
game.players[player].position = position
|
|
if track is not None:
|
|
game.players[player].track = track
|
|
|
|
|
|
def turn(game: Game, agents: list[ScriptedAgent], player: int, *ends: Pos | Walk) -> None:
|
|
"""Play ``player``'s turn, choosing paths by ``ends`` in order (see ``ScriptedAgent``)."""
|
|
game.current = player
|
|
agents[player].ends.extend(ends)
|
|
game.play_turn()
|