Refresh web UI and make ML imports optional
All checks were successful
ci / tests (push) Successful in 21s
All checks were successful
ci / tests (push) Successful in 21s
This commit is contained in:
152
tests/test_rulebook.py
Normal file
152
tests/test_rulebook.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from catan.data import Resource
|
||||
from catan.game import Game, GameConfig, Phase
|
||||
|
||||
|
||||
def _prepare_main_phase(game: Game) -> None:
|
||||
game.phase = Phase.MAIN
|
||||
game.has_rolled = True
|
||||
game.pending_discards.clear()
|
||||
game.robber_move_required = False
|
||||
|
||||
|
||||
def _assign_settlement(game: Game, player, hex_id: int, corner_index: int = 0) -> None:
|
||||
corner_id = game.board.hexes[hex_id].corners[corner_index]
|
||||
corner = game.board.corners[corner_id]
|
||||
corner.owner = player.name
|
||||
corner.building = "settlement"
|
||||
player.settlements.add(corner_id)
|
||||
|
||||
|
||||
def _find_resource_hex(game: Game) -> Tuple[int, Resource, int]:
|
||||
for tile in game.board.hexes.values():
|
||||
if tile.resource != Resource.DESERT and tile.number_token:
|
||||
return tile.id, tile.resource, tile.number_token
|
||||
raise RuntimeError("No producible hex found")
|
||||
|
||||
|
||||
def _find_edge_path(game: Game, length: int) -> Tuple[List[Tuple[int, int, int]], List[Tuple[int, int, int]]]:
|
||||
board = game.board
|
||||
|
||||
def dfs(corner_id, path_edges, path_corners, visited_edges):
|
||||
if len(path_edges) == length:
|
||||
return list(path_edges), list(path_corners)
|
||||
for edge_id in board.corners[corner_id].edges:
|
||||
if edge_id in visited_edges:
|
||||
continue
|
||||
visited_edges.add(edge_id)
|
||||
edge = board.edges[edge_id]
|
||||
next_corner = next(c for c in edge.corners if c != corner_id)
|
||||
path_edges.append(edge_id)
|
||||
path_corners.append(next_corner)
|
||||
result = dfs(next_corner, path_edges, path_corners, visited_edges)
|
||||
if result:
|
||||
return result
|
||||
path_edges.pop()
|
||||
path_corners.pop()
|
||||
visited_edges.remove(edge_id)
|
||||
return None
|
||||
|
||||
for start in board.corners:
|
||||
result = dfs(start, [], [start], set())
|
||||
if result:
|
||||
return result
|
||||
raise RuntimeError("Unable to find connected edge path")
|
||||
|
||||
|
||||
def test_resource_shortage_blocks_multiple_players() -> None:
|
||||
game = Game(GameConfig(player_names=["A", "B", "C"]))
|
||||
hex_id, resource, token = _find_resource_hex(game)
|
||||
_assign_settlement(game, game.players[0], hex_id, 0)
|
||||
_assign_settlement(game, game.players[1], hex_id, 1)
|
||||
game.bank[resource] = 1
|
||||
|
||||
game._distribute_resources(token) # noqa: SLF001 - internal rule helper
|
||||
|
||||
assert game.players[0].resources[resource] == 0
|
||||
assert game.players[1].resources[resource] == 0
|
||||
assert game.bank[resource] == 1
|
||||
|
||||
|
||||
def test_resource_shortage_single_player_gets_remaining() -> None:
|
||||
game = Game(GameConfig(player_names=["A", "B", "C"]))
|
||||
hex_id, resource, token = _find_resource_hex(game)
|
||||
_assign_settlement(game, game.players[0], hex_id, 0)
|
||||
game.bank[resource] = 1
|
||||
|
||||
game._distribute_resources(token) # noqa: SLF001
|
||||
|
||||
assert game.players[0].resources[resource] == 1
|
||||
assert game.bank[resource] == 0
|
||||
|
||||
|
||||
def test_robber_requires_valid_victim() -> None:
|
||||
game = Game(GameConfig(player_names=["A", "B", "C"]))
|
||||
_prepare_main_phase(game)
|
||||
other = game.players[1]
|
||||
target_hex = next(h for h in game.board.hexes if h != game.board.robber_hex)
|
||||
_assign_settlement(game, other, target_hex, 0)
|
||||
other.resources[Resource.BRICK] = 1
|
||||
game.robber_move_required = True
|
||||
|
||||
with pytest.raises(ValueError, match="victim"):
|
||||
game.move_robber(target_hex)
|
||||
with pytest.raises(ValueError, match="adjacent"):
|
||||
game.move_robber(target_hex, victim=game.players[2].name)
|
||||
|
||||
empty_hex = next(
|
||||
hid
|
||||
for hid in game.board.hexes
|
||||
if hid not in {target_hex, game.board.robber_hex}
|
||||
and all(game.board.corners[c].owner is None for c in game.board.hexes[hid].corners)
|
||||
)
|
||||
game.robber_move_required = True
|
||||
with pytest.raises(ValueError, match="No valid victim"):
|
||||
game.move_robber(empty_hex, victim=other.name)
|
||||
|
||||
game.robber_move_required = True
|
||||
game.move_robber(target_hex, victim=other.name)
|
||||
assert not game.robber_move_required
|
||||
|
||||
|
||||
def test_trade_restrictions_enforced() -> None:
|
||||
game = Game(GameConfig(player_names=["A", "B", "C"]))
|
||||
_prepare_main_phase(game)
|
||||
player = game.current_player
|
||||
opponent = game.players[1]
|
||||
player.resources[Resource.BRICK] = 2
|
||||
opponent.resources[Resource.WOOL] = 2
|
||||
|
||||
with pytest.raises(ValueError, match="yourself"):
|
||||
game.trade_with_player(player.name, {Resource.BRICK: 1}, {Resource.WOOL: 1})
|
||||
with pytest.raises(ValueError, match="both ways"):
|
||||
game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {})
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
game.trade_with_player(opponent.name, {Resource.BRICK: 0}, {Resource.WOOL: 1})
|
||||
with pytest.raises(ValueError, match="matching resources"):
|
||||
game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.BRICK: 1})
|
||||
|
||||
game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.WOOL: 1})
|
||||
assert player.resources[Resource.WOOL] == 1
|
||||
assert opponent.resources[Resource.BRICK] == 1
|
||||
|
||||
|
||||
def test_settlement_breaks_longest_road() -> None:
|
||||
game = Game(GameConfig(player_names=["A", "B", "C"]))
|
||||
player = game.players[0]
|
||||
rival = game.players[1]
|
||||
edges, corners = _find_edge_path(game, 5)
|
||||
for edge_id in edges:
|
||||
game.board.edges[edge_id].owner = player.name
|
||||
player.roads.add(edge_id)
|
||||
game._update_longest_road() # noqa: SLF001
|
||||
assert player.longest_road
|
||||
|
||||
block_corner = corners[2]
|
||||
game._build_settlement(rival, block_corner, require_connection=False) # noqa: SLF001
|
||||
assert not player.longest_road
|
||||
Reference in New Issue
Block a user