File size: 3,476 Bytes
4be6a52 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | from dataclasses import FrozenInstanceError, asdict, replace
import pytest
from stackcraft.engine import new_game
from stackcraft.players import (
BoardFeatures,
Decision,
HeuristicPlayer,
RandomPlayer,
board_features,
observe,
validate_decision,
)
def test_observation_has_no_hidden_future_or_seed() -> None:
state = new_game(12)
observation = observe(state)
assert set(asdict(observation)) == {
"board",
"current",
"next_piece",
"legal_actions",
"rules_version",
}
assert observe(replace(state, seed=555, piece_index=77)) == observation
with pytest.raises(FrozenInstanceError):
observation.current = "I" # ty: ignore[invalid-assignment]
def test_player_choices_do_not_depend_on_hidden_seed() -> None:
state = new_game(9)
other = replace(state, seed=271, piece_index=9)
assert HeuristicPlayer().choose(observe(state)) == HeuristicPlayer().choose(observe(other))
assert RandomPlayer(81).choose(observe(state)) == RandomPlayer(81).choose(observe(other))
def test_random_has_independent_reproducible_rng() -> None:
import random
before = random.getstate()
observation = observe(new_game(7))
first, second = RandomPlayer(33), RandomPlayer(33)
for _ in range(12):
decision = first.choose(observation)
assert decision == second.choose(observation)
validate_decision(decision, observation)
assert random.getstate() == before
def test_features_count_buried_holes_and_adjacent_height_differences() -> None:
rows = [[0] * 10 for _ in range(20)]
rows[17][0] = rows[19][0] = rows[19][1] = 1
board = tuple(tuple(row) for row in rows)
assert board_features(board) == BoardFeatures(4, 1, 3)
def test_heuristic_clears_line_instead_of_building_high_stack() -> None:
state = new_game(0)
board = state.board[:-1] + ((0, 0, 0, 0, 1, 1, 1, 1, 1, 1),)
observation = observe(replace(state, board=board, current="I"))
decision = HeuristicPlayer().choose(observation)
assert decision.action_id == "r0x0"
assert decision.probabilities is None
@pytest.mark.parametrize("bad", [float("nan"), float("inf"), -0.1, 1.1, True, "0.1"])
def test_probability_validation_rejects_invalid_values(bad: object) -> None:
observation = observe(new_game(0))
probabilities = {
action.id: 1 / len(observation.legal_actions) for action in observation.legal_actions
}
probabilities[observation.legal_actions[0].id] = bad # ty: ignore[invalid-assignment]
with pytest.raises(ValueError, match="finite numbers"):
validate_decision(Decision(observation.legal_actions[0].id, probabilities), observation)
def test_decision_validation_rejects_illegal_ids_and_incomplete_distributions() -> None:
observation = observe(new_game(0))
action_id = observation.legal_actions[0].id
with pytest.raises(ValueError, match="illegal action"):
validate_decision(Decision("not-legal"), observation)
with pytest.raises(ValueError, match="cover every"):
validate_decision(Decision(action_id, {action_id: 1.0}), observation)
with pytest.raises(ValueError, match="sum to one"):
validate_decision(
Decision(action_id, {a.id: 0.0 for a in observation.legal_actions}), observation
)
with pytest.raises(ValueError, match="return a Decision"):
validate_decision(None, observation) # ty: ignore[invalid-argument-type]
|