게임 엔진을 game 모듈로 통합
This commit is contained in:
@@ -43,8 +43,6 @@ include = ["coolrl_lost_cities*"]
|
|||||||
"assets/*.json",
|
"assets/*.json",
|
||||||
"fixtures/*.json",
|
"fixtures/*.json",
|
||||||
"docs/*.md",
|
"docs/*.md",
|
||||||
]
|
|
||||||
"coolrl_lost_cities.games.classic.engines" = [
|
|
||||||
"*.pxd",
|
"*.pxd",
|
||||||
"*.pyx",
|
"*.pyx",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ extensions = cythonize(
|
|||||||
"coolrl_lost_cities.games.classic.game",
|
"coolrl_lost_cities.games.classic.game",
|
||||||
["src/coolrl_lost_cities/games/classic/game.pyx"],
|
["src/coolrl_lost_cities/games/classic/game.pyx"],
|
||||||
),
|
),
|
||||||
Extension(
|
|
||||||
"coolrl_lost_cities.games.classic.engines.fast",
|
|
||||||
["src/coolrl_lost_cities/games/classic/engines/fast.pyx"],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
language_level=3,
|
language_level=3,
|
||||||
compiler_directives={
|
compiler_directives={
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from .bots import (
|
|||||||
available_bot_names,
|
available_bot_names,
|
||||||
build_bot,
|
build_bot,
|
||||||
)
|
)
|
||||||
from .engines import FastGameState as GameState
|
|
||||||
from .env import LostCitiesEnv
|
from .env import LostCitiesEnv
|
||||||
from .evaluation import (
|
from .evaluation import (
|
||||||
GameResult,
|
GameResult,
|
||||||
@@ -18,6 +17,7 @@ from .evaluation import (
|
|||||||
play_match,
|
play_match,
|
||||||
)
|
)
|
||||||
from .game import (
|
from .game import (
|
||||||
|
GameState,
|
||||||
IllegalMoveError,
|
IllegalMoveError,
|
||||||
LostCitiesConfig,
|
LostCitiesConfig,
|
||||||
classic_config,
|
classic_config,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ..engines import FastGameState as GameState
|
from ..game import GameState
|
||||||
from ..interfaces import BotInput, Snapshot
|
from ..interfaces import BotInput, Snapshot
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import logging
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from ..engines import FastGameState as GameState
|
from ..game import Card, GameState, LostCitiesConfig
|
||||||
from ..game import Card, LostCitiesConfig
|
|
||||||
from ..interfaces import BotInput, LostCitiesBot
|
from ..interfaces import BotInput, LostCitiesBot
|
||||||
from .base import first_legal, legal_from_obs
|
from .base import first_legal, legal_from_obs
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ..engines import FastGameState as GameState
|
from ..game import GameState
|
||||||
from ..interfaces import BotInput, Snapshot
|
from ..interfaces import BotInput, Snapshot
|
||||||
from .base import first_legal, legal_from_obs
|
from .base import first_legal, legal_from_obs
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .fast import FastGameState
|
|
||||||
|
|
||||||
__all__ = ["FastGameState"]
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def encode_card(color: int, rank: int, n_ranks: int) -> int:
|
|
||||||
return int(color) * (int(n_ranks) + 1) + int(rank)
|
|
||||||
|
|
||||||
|
|
||||||
def decode_card(card: int, n_ranks: int) -> tuple[int, int]:
|
|
||||||
stride = int(n_ranks) + 1
|
|
||||||
return int(card) // stride, int(card) % stride
|
|
||||||
|
|
||||||
|
|
||||||
def card_to_snapshot(card: int, n_ranks: int) -> dict[str, int]:
|
|
||||||
color, rank = decode_card(card, n_ranks)
|
|
||||||
return {"color": color, "rank": rank}
|
|
||||||
|
|
||||||
|
|
||||||
def encode_card_snapshot(data: Any, n_ranks: int) -> int:
|
|
||||||
if isinstance(data, int):
|
|
||||||
return data
|
|
||||||
if isinstance(data, dict):
|
|
||||||
return encode_card(int(data["color"]), int(data["rank"]), n_ranks)
|
|
||||||
if isinstance(data, (list, tuple)) and len(data) == 2:
|
|
||||||
return encode_card(int(data[0]), int(data[1]), n_ranks)
|
|
||||||
color = getattr(data, "color", None)
|
|
||||||
rank = getattr(data, "rank", None)
|
|
||||||
if color is not None and rank is not None:
|
|
||||||
return encode_card(int(color), int(rank), n_ranks)
|
|
||||||
raise ValueError(f"invalid card snapshot: {data!r}")
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .engines import FastGameState as GameState
|
from .game import GameState, IllegalMoveError, LostCitiesConfig
|
||||||
from .game import IllegalMoveError, LostCitiesConfig
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from typing import Any
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .bots import available_bot_names, build_bot
|
from .bots import available_bot_names, build_bot
|
||||||
from .engines import FastGameState as GameState
|
from .game import GameState, LostCitiesConfig, classic_config
|
||||||
from .game import LostCitiesConfig, classic_config
|
|
||||||
from .interfaces import LostCitiesBot
|
from .interfaces import LostCitiesBot
|
||||||
|
|
||||||
BotFactory = Callable[[int | None], LostCitiesBot]
|
BotFactory = Callable[[int | None], LostCitiesBot]
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ ctypedef struct UndoRecord:
|
|||||||
int total_score_before
|
int total_score_before
|
||||||
|
|
||||||
|
|
||||||
cdef class FastGameState:
|
cdef class GameState:
|
||||||
cdef public object config
|
cdef public object config
|
||||||
cdef int n_colors
|
cdef int n_colors
|
||||||
cdef int n_ranks
|
cdef int n_ranks
|
||||||
@@ -56,7 +56,7 @@ cdef class FastGameState:
|
|||||||
cdef void _configure(self, object config) except *
|
cdef void _configure(self, object config) except *
|
||||||
cdef void _clear(self) noexcept
|
cdef void _clear(self) noexcept
|
||||||
|
|
||||||
cpdef FastGameState clone(self)
|
cpdef GameState clone(self)
|
||||||
cpdef list legal_card_mask(self)
|
cpdef list legal_card_mask(self)
|
||||||
cpdef list legal_draw_mask(self)
|
cpdef list legal_draw_mask(self)
|
||||||
cpdef list legal_mask(self)
|
cpdef list legal_mask(self)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Protocol, TypeAlias, runtime_checkable
|
from typing import Protocol, TypeAlias, runtime_checkable
|
||||||
|
|
||||||
from .engines import FastGameState as GameState
|
from .game import GameState
|
||||||
from .snapshots import Snapshot
|
from .snapshots import Snapshot
|
||||||
|
|
||||||
BotInput: TypeAlias = dict | GameState | Snapshot
|
BotInput: TypeAlias = dict | GameState | Snapshot
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from .bots import DEFAULT_BOT, LostCitiesBot, available_bot_names, build_bot
|
from .bots import DEFAULT_BOT, LostCitiesBot, available_bot_names, build_bot
|
||||||
from .engines import FastGameState as GameState
|
from .game import Card, GameState, LostCitiesConfig, classic_config
|
||||||
from .game import Card, LostCitiesConfig, classic_config
|
|
||||||
from .resources import theme_path
|
from .resources import theme_path
|
||||||
from .snapshots import Snapshot, snapshot_from_state, snapshot_summary
|
from .snapshots import Snapshot, snapshot_from_state, snapshot_summary
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from .engines import FastGameState as GameState
|
from .game import Card, GameState, LostCitiesConfig, score_expedition
|
||||||
from .game import Card, LostCitiesConfig, score_expedition
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
+86
-88
@@ -6,7 +6,6 @@ import pytest
|
|||||||
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
|
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
|
||||||
|
|
||||||
from coolrl_lost_cities.games.classic.bots import RandomBot
|
from coolrl_lost_cities.games.classic.bots import RandomBot
|
||||||
from coolrl_lost_cities.games.classic.engines import FastGameState
|
|
||||||
|
|
||||||
|
|
||||||
def _card(color: int, rank: int) -> dict[str, int]:
|
def _card(color: int, rank: int) -> dict[str, int]:
|
||||||
@@ -59,58 +58,57 @@ def _snapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_public_game_state_alias_matches_fast_new_game_from_deck_snapshot() -> None:
|
def test_public_game_state_alias_matches_game_state_new_game_from_deck_snapshot() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
deck = build_deck(config)
|
deck = build_deck(config)
|
||||||
|
|
||||||
assert GameState is FastGameState
|
|
||||||
left = GameState.new_game_from_deck(deck, config)
|
left = GameState.new_game_from_deck(deck, config)
|
||||||
right = FastGameState.new_game_from_deck(deck, config)
|
other = GameState.new_game_from_deck(deck, config)
|
||||||
|
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
right.validate_invariants()
|
other.validate_invariants()
|
||||||
|
|
||||||
|
|
||||||
def test_fast_snapshot_roundtrip_preserves_snapshot() -> None:
|
def test_game_state_snapshot_roundtrip_preserves_snapshot() -> None:
|
||||||
config = LostCitiesConfig(seed=11)
|
config = LostCitiesConfig(seed=11)
|
||||||
left = GameState.new_game(config)
|
left = GameState.new_game(config)
|
||||||
right = FastGameState.from_snapshot(left.to_snapshot())
|
other = GameState.from_snapshot(left.to_snapshot())
|
||||||
|
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
restored = FastGameState.from_snapshot(right.to_snapshot())
|
restored = GameState.from_snapshot(other.to_snapshot())
|
||||||
assert restored.to_snapshot() == right.to_snapshot()
|
assert restored.to_snapshot() == other.to_snapshot()
|
||||||
|
|
||||||
|
|
||||||
def test_fast_from_snapshot_rejects_oversized_regions_before_write() -> None:
|
def test_game_state_from_snapshot_rejects_oversized_regions_before_write() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
state = GameState.new_game(config, seed=3)
|
state = GameState.new_game(config, seed=3)
|
||||||
|
|
||||||
deck_snapshot = state.to_snapshot()
|
deck_snapshot = state.to_snapshot()
|
||||||
deck_snapshot["deck"] = [_card(0, 1)] * (config.deck_size + 1)
|
deck_snapshot["deck"] = [_card(0, 1)] * (config.deck_size + 1)
|
||||||
with pytest.raises(ValueError, match="deck snapshot exceeds capacity"):
|
with pytest.raises(ValueError, match="deck snapshot exceeds capacity"):
|
||||||
FastGameState.from_snapshot(deck_snapshot)
|
GameState.from_snapshot(deck_snapshot)
|
||||||
|
|
||||||
hand_snapshot = state.to_snapshot()
|
hand_snapshot = state.to_snapshot()
|
||||||
hand_snapshot["hands"][0] = [_card(0, 1)] * (config.hand_size + 1)
|
hand_snapshot["hands"][0] = [_card(0, 1)] * (config.hand_size + 1)
|
||||||
with pytest.raises(ValueError, match="hand 0 snapshot exceeds hand_size"):
|
with pytest.raises(ValueError, match="hand 0 snapshot exceeds hand_size"):
|
||||||
FastGameState.from_snapshot(hand_snapshot)
|
GameState.from_snapshot(hand_snapshot)
|
||||||
|
|
||||||
expedition_snapshot = state.to_snapshot()
|
expedition_snapshot = state.to_snapshot()
|
||||||
expedition_snapshot["expeditions"][0][0] = [_card(0, 1)] * (
|
expedition_snapshot["expeditions"][0][0] = [_card(0, 1)] * (
|
||||||
config.n_ranks + config.n_handshakes + 1
|
config.n_ranks + config.n_handshakes + 1
|
||||||
)
|
)
|
||||||
with pytest.raises(ValueError, match="expedition 0/0 snapshot exceeds capacity"):
|
with pytest.raises(ValueError, match="expedition 0/0 snapshot exceeds capacity"):
|
||||||
FastGameState.from_snapshot(expedition_snapshot)
|
GameState.from_snapshot(expedition_snapshot)
|
||||||
|
|
||||||
discard_snapshot = state.to_snapshot()
|
discard_snapshot = state.to_snapshot()
|
||||||
discard_snapshot["discards"][0] = [_card(0, 1)] * (config.n_ranks + config.n_handshakes + 1)
|
discard_snapshot["discards"][0] = [_card(0, 1)] * (config.n_ranks + config.n_handshakes + 1)
|
||||||
with pytest.raises(ValueError, match="discard 0 snapshot exceeds capacity"):
|
with pytest.raises(ValueError, match="discard 0 snapshot exceeds capacity"):
|
||||||
FastGameState.from_snapshot(discard_snapshot)
|
GameState.from_snapshot(discard_snapshot)
|
||||||
|
|
||||||
|
|
||||||
def test_fast_validate_invariants_rejects_bad_expedition_order() -> None:
|
def test_game_state_validate_invariants_rejects_bad_expedition_order() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
snapshot = FastGameState.new_game(config, seed=4).to_snapshot()
|
snapshot = GameState.new_game(config, seed=4).to_snapshot()
|
||||||
snapshot["deck"].extend(
|
snapshot["deck"].extend(
|
||||||
[
|
[
|
||||||
_card(0, 2),
|
_card(0, 2),
|
||||||
@@ -123,10 +121,10 @@ def test_fast_validate_invariants_rejects_bad_expedition_order() -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="expedition is not strictly increasing"):
|
with pytest.raises(ValueError, match="expedition is not strictly increasing"):
|
||||||
FastGameState.from_snapshot(snapshot)
|
GameState.from_snapshot(snapshot)
|
||||||
|
|
||||||
|
|
||||||
def test_fast_pending_discard_sequence_is_deterministic() -> None:
|
def test_game_state_pending_discard_sequence_is_deterministic() -> None:
|
||||||
snapshot = _snapshot(
|
snapshot = _snapshot(
|
||||||
hands=[
|
hands=[
|
||||||
[_card(0, 1)],
|
[_card(0, 1)],
|
||||||
@@ -135,29 +133,29 @@ def test_fast_pending_discard_sequence_is_deterministic() -> None:
|
|||||||
deck=[_card(2, 1), _card(3, 1)],
|
deck=[_card(2, 1), _card(3, 1)],
|
||||||
)
|
)
|
||||||
left = GameState.from_snapshot(snapshot)
|
left = GameState.from_snapshot(snapshot)
|
||||||
right = FastGameState.from_snapshot(snapshot)
|
other = GameState.from_snapshot(snapshot)
|
||||||
|
|
||||||
left.apply_action(1)
|
left.apply_action(1)
|
||||||
right.apply_action(1)
|
other.apply_action(1)
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.legal_draw_mask() == left.legal_draw_mask()
|
assert other.legal_draw_mask() == left.legal_draw_mask()
|
||||||
assert right.legal_draw_mask()[1] is False
|
assert other.legal_draw_mask()[1] is False
|
||||||
|
|
||||||
left.apply_action(0)
|
left.apply_action(0)
|
||||||
right.apply_action(0)
|
other.apply_action(0)
|
||||||
left.apply_action(1)
|
left.apply_action(1)
|
||||||
right.apply_action(1)
|
other.apply_action(1)
|
||||||
left.apply_action(0)
|
left.apply_action(0)
|
||||||
right.apply_action(0)
|
other.apply_action(0)
|
||||||
left.apply_action(1)
|
left.apply_action(1)
|
||||||
right.apply_action(1)
|
other.apply_action(1)
|
||||||
|
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.legal_draw_mask() == left.legal_draw_mask()
|
assert other.legal_draw_mask() == left.legal_draw_mask()
|
||||||
assert right.legal_draw_mask()[1] is True
|
assert other.legal_draw_mask()[1] is True
|
||||||
|
|
||||||
|
|
||||||
def test_fast_terminal_edges_are_deterministic() -> None:
|
def test_game_state_terminal_edges_are_deterministic() -> None:
|
||||||
last_draw_snapshot = _snapshot(
|
last_draw_snapshot = _snapshot(
|
||||||
deck=[_card(1, 1)],
|
deck=[_card(1, 1)],
|
||||||
hands=[
|
hands=[
|
||||||
@@ -170,15 +168,15 @@ def test_fast_terminal_edges_are_deterministic() -> None:
|
|||||||
for card in remaining_deck:
|
for card in remaining_deck:
|
||||||
last_draw_snapshot["discards"][card["color"]].append(card)
|
last_draw_snapshot["discards"][card["color"]].append(card)
|
||||||
left = GameState.from_snapshot(last_draw_snapshot)
|
left = GameState.from_snapshot(last_draw_snapshot)
|
||||||
right = FastGameState.from_snapshot(last_draw_snapshot)
|
other = GameState.from_snapshot(last_draw_snapshot)
|
||||||
|
|
||||||
left.apply_action(1)
|
left.apply_action(1)
|
||||||
right.apply_action(1)
|
other.apply_action(1)
|
||||||
left.apply_action(0)
|
left.apply_action(0)
|
||||||
right.apply_action(0)
|
other.apply_action(0)
|
||||||
|
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.terminal is True
|
assert other.terminal is True
|
||||||
|
|
||||||
defensive_snapshot = {
|
defensive_snapshot = {
|
||||||
"config": LostCitiesConfig().to_snapshot(),
|
"config": LostCitiesConfig().to_snapshot(),
|
||||||
@@ -193,16 +191,16 @@ def test_fast_terminal_edges_are_deterministic() -> None:
|
|||||||
"terminal": False,
|
"terminal": False,
|
||||||
}
|
}
|
||||||
left = GameState.from_snapshot(defensive_snapshot, validate=False)
|
left = GameState.from_snapshot(defensive_snapshot, validate=False)
|
||||||
right = FastGameState.from_snapshot(defensive_snapshot, validate=False)
|
other = GameState.from_snapshot(defensive_snapshot, validate=False)
|
||||||
|
|
||||||
left.apply_action(1)
|
left.apply_action(1)
|
||||||
right.apply_action(1)
|
other.apply_action(1)
|
||||||
|
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.terminal is True
|
assert other.terminal is True
|
||||||
|
|
||||||
|
|
||||||
def test_fast_last_numeric_legality_edges() -> None:
|
def test_game_state_last_numeric_legality_edges() -> None:
|
||||||
handshake_snapshot = _snapshot(
|
handshake_snapshot = _snapshot(
|
||||||
hands=[
|
hands=[
|
||||||
[_card(0, 1)],
|
[_card(0, 1)],
|
||||||
@@ -214,9 +212,9 @@ def test_fast_last_numeric_legality_edges() -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
left = GameState.from_snapshot(handshake_snapshot)
|
left = GameState.from_snapshot(handshake_snapshot)
|
||||||
right = FastGameState.from_snapshot(handshake_snapshot)
|
other = GameState.from_snapshot(handshake_snapshot)
|
||||||
assert right.legal_card_mask() == left.legal_card_mask()
|
assert other.legal_card_mask() == left.legal_card_mask()
|
||||||
assert right.legal_card_mask()[0] is True
|
assert other.legal_card_mask()[0] is True
|
||||||
|
|
||||||
numeric_snapshot = _snapshot(
|
numeric_snapshot = _snapshot(
|
||||||
hands=[
|
hands=[
|
||||||
@@ -229,14 +227,14 @@ def test_fast_last_numeric_legality_edges() -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
left = GameState.from_snapshot(numeric_snapshot)
|
left = GameState.from_snapshot(numeric_snapshot)
|
||||||
right = FastGameState.from_snapshot(numeric_snapshot)
|
other = GameState.from_snapshot(numeric_snapshot)
|
||||||
assert right.legal_card_mask() == left.legal_card_mask()
|
assert other.legal_card_mask() == left.legal_card_mask()
|
||||||
assert right.legal_card_mask()[0] is False
|
assert other.legal_card_mask()[0] is False
|
||||||
assert right.legal_card_mask()[2] is False
|
assert other.legal_card_mask()[2] is False
|
||||||
assert right.legal_card_mask()[4] is True
|
assert other.legal_card_mask()[4] is True
|
||||||
|
|
||||||
|
|
||||||
def test_fast_score_cache_and_undo_restore_snapshot() -> None:
|
def test_game_state_score_cache_and_undo_restore_snapshot() -> None:
|
||||||
snapshot = _snapshot(
|
snapshot = _snapshot(
|
||||||
hands=[
|
hands=[
|
||||||
[_card(0, 7)],
|
[_card(0, 7)],
|
||||||
@@ -263,30 +261,30 @@ def test_fast_score_cache_and_undo_restore_snapshot() -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
left = GameState.from_snapshot(snapshot)
|
left = GameState.from_snapshot(snapshot)
|
||||||
right = FastGameState.from_snapshot(snapshot)
|
other = GameState.from_snapshot(snapshot)
|
||||||
before = right.to_snapshot()
|
before = other.to_snapshot()
|
||||||
|
|
||||||
assert right.expedition_score(0, 0) == left.expedition_score(0, 0)
|
assert other.expedition_score(0, 0) == left.expedition_score(0, 0)
|
||||||
assert right.total_score(0) == left.total_score(0)
|
assert other.total_score(0) == left.total_score(0)
|
||||||
|
|
||||||
undo = right.apply_action_with_undo(0)
|
undo = other.apply_action_with_undo(0)
|
||||||
left.apply_action(0)
|
left.apply_action(0)
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.expedition_score(0, 0) == left.expedition_score(0, 0)
|
assert other.expedition_score(0, 0) == left.expedition_score(0, 0)
|
||||||
assert right.total_score(0) == left.total_score(0)
|
assert other.total_score(0) == left.total_score(0)
|
||||||
|
|
||||||
right.undo_action(undo)
|
other.undo_action(undo)
|
||||||
assert right.to_snapshot() == before
|
assert other.to_snapshot() == before
|
||||||
assert right.total_score(0) == GameState.from_snapshot(before).total_score(0)
|
assert other.total_score(0) == GameState.from_snapshot(before).total_score(0)
|
||||||
|
|
||||||
|
|
||||||
def test_fast_discard_draw_push_pop_restores_snapshot() -> None:
|
def test_game_state_discard_draw_push_pop_restores_snapshot() -> None:
|
||||||
snapshot = _snapshot(
|
snapshot = _snapshot(
|
||||||
hands=[[], [_card(1, 1)]],
|
hands=[[], [_card(1, 1)]],
|
||||||
discards=[[_card(0, 1)], [], [], [], []],
|
discards=[[_card(0, 1)], [], [], [], []],
|
||||||
phase="draw",
|
phase="draw",
|
||||||
)
|
)
|
||||||
state = FastGameState.from_snapshot(snapshot)
|
state = GameState.from_snapshot(snapshot)
|
||||||
before = state.to_snapshot()
|
before = state.to_snapshot()
|
||||||
|
|
||||||
assert state.push_action(1) == 1
|
assert state.push_action(1) == 1
|
||||||
@@ -295,66 +293,66 @@ def test_fast_discard_draw_push_pop_restores_snapshot() -> None:
|
|||||||
assert state.to_snapshot() == before
|
assert state.to_snapshot() == before
|
||||||
|
|
||||||
|
|
||||||
def test_fast_random_action_sequence_is_deterministic() -> None:
|
def test_game_state_random_action_sequence_is_deterministic() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
for seed in range(48):
|
for seed in range(48):
|
||||||
left = GameState.new_game(config, seed=seed)
|
left = GameState.new_game(config, seed=seed)
|
||||||
right = FastGameState.new_game(config, seed=seed)
|
other = GameState.new_game(config, seed=seed)
|
||||||
rng = random.Random(seed ^ 0xF457)
|
rng = random.Random(seed ^ 0xF457)
|
||||||
steps = 0
|
steps = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
assert right.unified_legal_mask() == left.unified_legal_mask()
|
assert other.unified_legal_mask() == left.unified_legal_mask()
|
||||||
assert right.unified_legal_actions() == [
|
assert other.unified_legal_actions() == [
|
||||||
index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal
|
index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal
|
||||||
]
|
]
|
||||||
assert right.score_diff(0) == left.score_diff(0)
|
assert other.score_diff(0) == left.score_diff(0)
|
||||||
if left.terminal:
|
if left.terminal:
|
||||||
break
|
break
|
||||||
|
|
||||||
legal = [index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal]
|
legal = [index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal]
|
||||||
action = rng.choice(legal)
|
action = rng.choice(legal)
|
||||||
left.apply_unified_action(action)
|
left.apply_unified_action(action)
|
||||||
right.apply_unified_action(action)
|
other.apply_unified_action(action)
|
||||||
steps += 1
|
steps += 1
|
||||||
assert steps < 1000
|
assert steps < 1000
|
||||||
|
|
||||||
|
|
||||||
def test_fast_random_bot_self_play_is_deterministic() -> None:
|
def test_game_state_random_bot_self_play_is_deterministic() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
for seed in range(32):
|
for seed in range(32):
|
||||||
left = GameState.new_game(config, seed=seed)
|
left = GameState.new_game(config, seed=seed)
|
||||||
right = FastGameState.new_game(config, seed=seed)
|
other = GameState.new_game(config, seed=seed)
|
||||||
left_bots = [RandomBot(seed=seed * 2), RandomBot(seed=seed * 2 + 1)]
|
left_bots = [RandomBot(seed=seed * 2), RandomBot(seed=seed * 2 + 1)]
|
||||||
right_bots = [RandomBot(seed=seed * 2), RandomBot(seed=seed * 2 + 1)]
|
other_bots = [RandomBot(seed=seed * 2), RandomBot(seed=seed * 2 + 1)]
|
||||||
steps = 0
|
steps = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
assert right.to_snapshot() == left.to_snapshot()
|
assert other.to_snapshot() == left.to_snapshot()
|
||||||
if left.terminal:
|
if left.terminal:
|
||||||
break
|
break
|
||||||
|
|
||||||
player = left.current_player
|
player = left.current_player
|
||||||
assert right.current_player == player
|
assert other.current_player == player
|
||||||
left_action = left_bots[player].act(left)
|
left_action = left_bots[player].act(left)
|
||||||
right_action = right_bots[player].act({"legal_mask": right.legal_mask()})
|
other_action = other_bots[player].act({"legal_mask": other.legal_mask()})
|
||||||
assert right_action == left_action
|
assert other_action == left_action
|
||||||
|
|
||||||
left.apply_action(left_action)
|
left.apply_action(left_action)
|
||||||
right.apply_action(right_action)
|
other.apply_action(other_action)
|
||||||
steps += 1
|
steps += 1
|
||||||
assert steps < 1000
|
assert steps < 1000
|
||||||
|
|
||||||
assert right.total_score(0) == left.total_score(0)
|
assert other.total_score(0) == left.total_score(0)
|
||||||
assert right.total_score(1) == left.total_score(1)
|
assert other.total_score(1) == left.total_score(1)
|
||||||
assert right.score_diff(0) == left.score_diff(0)
|
assert other.score_diff(0) == left.score_diff(0)
|
||||||
|
|
||||||
|
|
||||||
def test_fast_apply_undo_restores_every_legal_action() -> None:
|
def test_game_state_apply_undo_restores_every_legal_action() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
for seed in range(32):
|
for seed in range(32):
|
||||||
state = FastGameState.new_game(config, seed=seed)
|
state = GameState.new_game(config, seed=seed)
|
||||||
rng = random.Random(seed ^ 0xFA57A11)
|
rng = random.Random(seed ^ 0xFA57A11)
|
||||||
steps = 0
|
steps = 0
|
||||||
|
|
||||||
@@ -373,10 +371,10 @@ def test_fast_apply_undo_restores_every_legal_action() -> None:
|
|||||||
assert steps < 1000
|
assert steps < 1000
|
||||||
|
|
||||||
|
|
||||||
def test_fast_push_pop_action_restores_nested_sequence() -> None:
|
def test_game_state_push_pop_action_restores_nested_sequence() -> None:
|
||||||
config = LostCitiesConfig()
|
config = LostCitiesConfig()
|
||||||
for seed in range(32):
|
for seed in range(32):
|
||||||
state = FastGameState.new_game(config, seed=seed)
|
state = GameState.new_game(config, seed=seed)
|
||||||
rng = random.Random(seed ^ 0x517ACC)
|
rng = random.Random(seed ^ 0x517ACC)
|
||||||
before = state.to_snapshot()
|
before = state.to_snapshot()
|
||||||
actions: list[int] = []
|
actions: list[int] = []
|
||||||
@@ -397,4 +395,4 @@ def test_fast_push_pop_action_restores_nested_sequence() -> None:
|
|||||||
assert state.to_snapshot() == before
|
assert state.to_snapshot() == before
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="undo stack is empty"):
|
with pytest.raises(ValueError, match="undo stack is empty"):
|
||||||
FastGameState.new_game(config, seed=1).pop_action()
|
GameState.new_game(config, seed=1).pop_action()
|
||||||
Reference in New Issue
Block a user