i151-engine 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {i151_engine-0.2.0 → i151_engine-0.2.2}/PKG-INFO +1 -1
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/protocol.py +8 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/legal_actions.py +1 -1
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/runner/match_runner.py +30 -4
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/view/builder.py +6 -3
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/view/player_view.py +10 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/PKG-INFO +1 -1
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/SOURCES.txt +1 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/pyproject.toml +1 -1
- i151_engine-0.2.2/tests/test_player_view_turn.py +68 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/ARCHITECTURE.md +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/README.md +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/random_bot.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/deck.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/errors.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/reducer.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/rules.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/scoring.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/recorder.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/round.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/round_state.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/action.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/card.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/enums.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/player.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/__init__.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/catalog.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/registry.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/schedules.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/types.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/runner/config.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/dependency_links.txt +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/requires.txt +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/top_level.txt +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/setup.cfg +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_card.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_deck.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_forfeit_scoring.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_match_runner.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_match_timeout.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_opponents.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_recorder.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_reducer.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_rules.py +0 -0
- {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_scoring.py +0 -0
|
@@ -18,3 +18,11 @@ class Bot(Protocol):
|
|
|
18
18
|
) -> Action: ...
|
|
19
19
|
|
|
20
20
|
def teardown(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
def observe(self, view: PlayerView) -> None:
|
|
23
|
+
"""Optionnel : appelé après chaque action (y compris adverses / enchaînements).
|
|
24
|
+
|
|
25
|
+
Permet au tracker de suivre le talon carte par carte sans exposer
|
|
26
|
+
l'historique complet dans ``PlayerView``.
|
|
27
|
+
"""
|
|
28
|
+
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from i151_engine.models.action import Action
|
|
3
|
+
from i151_engine.models.action import Action
|
|
4
4
|
from i151_engine.models.card import Card, Rank, Suit
|
|
5
5
|
from i151_engine.models.player import PlayerState
|
|
6
6
|
from i151_engine.game.round_state import RoundState
|
|
@@ -66,9 +66,17 @@ def play_bots_match(
|
|
|
66
66
|
opponent_id: str = "student:opponent",
|
|
67
67
|
challenger_dir: Path | None = None,
|
|
68
68
|
opponent_dir: Path | None = None,
|
|
69
|
+
manage_lifecycle: bool = True,
|
|
69
70
|
) -> MatchResult:
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
"""Joue un match entre deux bots.
|
|
72
|
+
|
|
73
|
+
Si ``manage_lifecycle`` est faux, le caller doit avoir déjà appelé
|
|
74
|
+
``setup`` et appellera ``teardown`` lui-même (ex. cache adversaire
|
|
75
|
+
pendant une collecte multi-matchs).
|
|
76
|
+
"""
|
|
77
|
+
if manage_lifecycle:
|
|
78
|
+
challenger.setup(challenger_dir or Path("."))
|
|
79
|
+
opponent.setup(opponent_dir or Path("."))
|
|
72
80
|
|
|
73
81
|
recorder = MatchRecorder(
|
|
74
82
|
seed=config.seed,
|
|
@@ -109,8 +117,9 @@ def play_bots_match(
|
|
|
109
117
|
recorder=recorder,
|
|
110
118
|
)
|
|
111
119
|
finally:
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
if manage_lifecycle:
|
|
121
|
+
challenger.teardown()
|
|
122
|
+
opponent.teardown()
|
|
114
123
|
|
|
115
124
|
|
|
116
125
|
def _run_match(
|
|
@@ -213,6 +222,7 @@ def _run_match(
|
|
|
213
222
|
)
|
|
214
223
|
|
|
215
224
|
match = replace(match, current_round=new_round)
|
|
225
|
+
_notify_observers(match, new_round, bots, specs, steps, acting_id=pid)
|
|
216
226
|
|
|
217
227
|
winner = match.winner_id
|
|
218
228
|
if winner is None and match.status == MatchStatus.IN_PROGRESS:
|
|
@@ -268,6 +278,22 @@ def _decide_with_timeout(bot: Bot, view, legal, timeout_ms: int):
|
|
|
268
278
|
signal.signal(signal.SIGALRM, previous)
|
|
269
279
|
|
|
270
280
|
|
|
281
|
+
def _notify_observers(match, round_state, bots: dict, specs, step_index: int, *, acting_id: str) -> None:
|
|
282
|
+
"""Informe les bots non-actifs (tracker) après chaque action atomique."""
|
|
283
|
+
for pid, bot in bots.items():
|
|
284
|
+
observe = getattr(bot, "observe", None)
|
|
285
|
+
if observe is None or pid == acting_id:
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
view = build_player_view(
|
|
289
|
+
match, round_state, pid, opponent_specs=specs, step_index=step_index
|
|
290
|
+
)
|
|
291
|
+
observe(view)
|
|
292
|
+
except Exception:
|
|
293
|
+
# observe est best-effort : ne doit pas forfaiter la partie
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
|
|
271
297
|
def _forfeit_result(
|
|
272
298
|
match: MatchState,
|
|
273
299
|
opponent_id: str,
|
|
@@ -57,8 +57,10 @@ def build_player_view(
|
|
|
57
57
|
opponents.sort(key=lambda o: o.seat_index)
|
|
58
58
|
next_pid = _next_player_id(round_state)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
# legal_actions = joueur courant (pas seulement la perspective « you »).
|
|
61
|
+
can_draw = any(a.type == ActionType.DRAW_CARD for a in legal)
|
|
62
|
+
can_pass = any(a.type == ActionType.PASS_TURN for a in legal)
|
|
63
|
+
current = round_state.current_player
|
|
62
64
|
|
|
63
65
|
return PlayerView(
|
|
64
66
|
you=YouState(
|
|
@@ -92,8 +94,9 @@ def build_player_view(
|
|
|
92
94
|
can_pass=can_pass,
|
|
93
95
|
draw_count_if_draw=2 if round_state.ace_effect_active else 1,
|
|
94
96
|
step_index=step_index,
|
|
95
|
-
current_player_id=
|
|
97
|
+
current_player_id=current.player_id,
|
|
96
98
|
next_player_id=next_pid,
|
|
99
|
+
is_chaining=current.is_chaining,
|
|
97
100
|
),
|
|
98
101
|
match=MatchContext(
|
|
99
102
|
round_number=match.round_number,
|
|
@@ -54,12 +54,22 @@ class TableState:
|
|
|
54
54
|
@dataclass(frozen=True)
|
|
55
55
|
class TurnState:
|
|
56
56
|
is_your_turn: bool
|
|
57
|
+
# Actions du *joueur courant* (pas seulement « you »).
|
|
58
|
+
# can_draw=False après pioche, en chaînage, ou si choose_suit — donc
|
|
59
|
+
# « a déjà pioché » se lit : can_pass and not is_chaining (pas ≡ not can_draw).
|
|
57
60
|
can_draw: bool
|
|
58
61
|
can_pass: bool
|
|
59
62
|
draw_count_if_draw: int
|
|
60
63
|
step_index: int
|
|
61
64
|
current_player_id: str
|
|
62
65
|
next_player_id: str | None
|
|
66
|
+
# État public du *joueur courant* (toi ou adversaire).
|
|
67
|
+
is_chaining: bool
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def has_drawn_this_turn(self) -> bool:
|
|
71
|
+
"""True si le joueur courant a déjà pioché ce tour (dérivé de can_pass / chaînage)."""
|
|
72
|
+
return self.can_pass and not self.is_chaining
|
|
63
73
|
|
|
64
74
|
|
|
65
75
|
@dataclass(frozen=True)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tests PlayerView / TurnState : flags du joueur courant."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from i151_engine.game.round import create_match
|
|
8
|
+
from i151_engine.view.builder import build_player_view
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_turn_exposes_current_player_chaining_and_can_draw():
|
|
12
|
+
match = create_match(("A", "B"), seed=1, target_score=1)
|
|
13
|
+
round_state = match.current_round
|
|
14
|
+
assert round_state is not None
|
|
15
|
+
|
|
16
|
+
current = round_state.current_player
|
|
17
|
+
idx = round_state.current_player_index
|
|
18
|
+
players = list(round_state.players)
|
|
19
|
+
players[idx] = replace(
|
|
20
|
+
players[idx],
|
|
21
|
+
has_drawn_this_turn=True,
|
|
22
|
+
is_chaining=False,
|
|
23
|
+
)
|
|
24
|
+
round_state = replace(round_state, players=tuple(players))
|
|
25
|
+
match = replace(match, current_round=round_state)
|
|
26
|
+
|
|
27
|
+
other = "p1" if current.player_id == "p0" else "p0"
|
|
28
|
+
view = build_player_view(
|
|
29
|
+
match,
|
|
30
|
+
round_state,
|
|
31
|
+
other,
|
|
32
|
+
opponent_specs={},
|
|
33
|
+
step_index=0,
|
|
34
|
+
)
|
|
35
|
+
assert view.turn.current_player_id == current.player_id
|
|
36
|
+
assert view.turn.is_your_turn is False
|
|
37
|
+
assert view.turn.can_draw is False
|
|
38
|
+
assert view.turn.can_pass is True
|
|
39
|
+
assert view.turn.has_drawn_this_turn is True # propriété dérivée
|
|
40
|
+
assert view.turn.is_chaining is False
|
|
41
|
+
assert view.you.has_drawn_this_turn is False
|
|
42
|
+
assert view.you.is_chaining is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_turn_flags_match_you_when_your_turn():
|
|
46
|
+
match = create_match(("A", "B"), seed=2, target_score=1)
|
|
47
|
+
round_state = match.current_round
|
|
48
|
+
assert round_state is not None
|
|
49
|
+
idx = round_state.current_player_index
|
|
50
|
+
players = list(round_state.players)
|
|
51
|
+
players[idx] = replace(players[idx], is_chaining=True)
|
|
52
|
+
round_state = replace(round_state, players=tuple(players))
|
|
53
|
+
match = replace(match, current_round=round_state)
|
|
54
|
+
|
|
55
|
+
current_id = round_state.current_player.player_id
|
|
56
|
+
view = build_player_view(
|
|
57
|
+
match,
|
|
58
|
+
round_state,
|
|
59
|
+
current_id,
|
|
60
|
+
opponent_specs={},
|
|
61
|
+
step_index=1,
|
|
62
|
+
)
|
|
63
|
+
assert view.turn.is_your_turn is True
|
|
64
|
+
assert view.turn.is_chaining is True
|
|
65
|
+
assert view.you.is_chaining is True
|
|
66
|
+
assert view.turn.can_draw is False
|
|
67
|
+
assert view.turn.can_pass is True
|
|
68
|
+
assert view.turn.has_drawn_this_turn is False # chaînage ≠ pioché
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|