Files
coorl-lost-cities/tests/games/classic/test_safe_heuristic_equivalence.py
T
coolguyandClaude Opus 4.7 05de0e2a81 Port safe-heuristic bots to Cython with Python reference fallback
Cython implementation in heuristic_cy.pyx achieves ~2.55× speedup on
opponent_act_seconds (200-game eval: 59.20s → 23.24s). Original Python
implementation preserved verbatim in heuristic_py.py as the equivalence
reference. Action-sequence equivalence is verified by
test_safe_heuristic_equivalence.py against seeded game corpora.

Key implementation notes:
- File-local wraparound=True override required for negative discard
  indexing; Cython global wraparound=False would segfault.
- annotation_typing=False preserves verbatim Python semantics.
- _CachedState materializes hands/expeditions/discards/deck once per
  act() call — this is the dominant performance win.

Further C-array optimization of _card_value_for_me /
_card_value_for_opponent / _color_commitment / _bonus_potential is
deferred. The current 2.55× delivers most of the dense-eval future
benefit; further work is gated on actually adopting denser eval
schedules (eval_every=5, games=1000).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:03:53 +09:00

57 lines
1.8 KiB
Python

from __future__ import annotations
import pytest
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots.heuristic import SafeHeuristicBot
from coolrl_lost_cities.games.classic.bots.heuristic_py import (
SafeHeuristicBot as PythonSafeHeuristicBot,
)
from coolrl_lost_cities.games.classic.bots.registry import (
LOOSE_SAFE_HEURISTIC_PARAMS,
STRICT_SAFE_HEURISTIC_PARAMS,
)
VARIANTS = (
("default", None),
("loose", LOOSE_SAFE_HEURISTIC_PARAMS),
("strict", STRICT_SAFE_HEURISTIC_PARAMS),
)
CONFIGS = (
LostCitiesConfig(),
LostCitiesConfig(n_colors=2, n_ranks=8, hand_size=3),
LostCitiesConfig(n_colors=3, n_ranks=5, n_handshakes=0, hand_size=5),
)
@pytest.mark.parametrize(("variant_name", "params"), VARIANTS)
@pytest.mark.parametrize("config", CONFIGS)
@pytest.mark.parametrize("seed", range(2))
def test_cython_safe_heuristic_matches_python_action_sequence(
variant_name: str,
params,
config: LostCitiesConfig,
seed: int,
) -> None:
py_bot = PythonSafeHeuristicBot(params)
cy_bot = SafeHeuristicBot(params)
py_state = GameState.new_game(config, seed=seed)
cy_state = GameState.new_game(config, seed=seed)
turn = 0
while not py_state.terminal:
assert turn < 500, f"variant={variant_name} seed={seed} did not terminate"
py_action = py_bot.act(py_state)
cy_action = cy_bot.act(cy_state)
assert cy_action == py_action, (
f"variant={variant_name} seed={seed} turn={turn} "
f"phase={py_state.phase} player={py_state.current_player} "
f"python={py_action} cython={cy_action}"
)
py_state.apply_action(py_action)
cy_state.apply_action(cy_action)
turn += 1
assert cy_state.terminal is True