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.
Files changed (50) hide show
  1. {i151_engine-0.2.0 → i151_engine-0.2.2}/PKG-INFO +1 -1
  2. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/protocol.py +8 -0
  3. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/legal_actions.py +1 -1
  4. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/runner/match_runner.py +30 -4
  5. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/view/builder.py +6 -3
  6. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/view/player_view.py +10 -0
  7. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/PKG-INFO +1 -1
  8. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/SOURCES.txt +1 -0
  9. {i151_engine-0.2.0 → i151_engine-0.2.2}/pyproject.toml +1 -1
  10. i151_engine-0.2.2/tests/test_player_view_turn.py +68 -0
  11. {i151_engine-0.2.0 → i151_engine-0.2.2}/ARCHITECTURE.md +0 -0
  12. {i151_engine-0.2.0 → i151_engine-0.2.2}/README.md +0 -0
  13. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/__init__.py +0 -0
  14. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/__init__.py +0 -0
  15. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/bots/random_bot.py +0 -0
  16. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/__init__.py +0 -0
  17. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/deck.py +0 -0
  18. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/errors.py +0 -0
  19. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/reducer.py +0 -0
  20. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/rules.py +0 -0
  21. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/core/scoring.py +0 -0
  22. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/__init__.py +0 -0
  23. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/recorder.py +0 -0
  24. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/round.py +0 -0
  25. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/game/round_state.py +0 -0
  26. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/__init__.py +0 -0
  27. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/action.py +0 -0
  28. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/card.py +0 -0
  29. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/enums.py +0 -0
  30. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/models/player.py +0 -0
  31. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/__init__.py +0 -0
  32. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/catalog.py +0 -0
  33. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/registry.py +0 -0
  34. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/schedules.py +0 -0
  35. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/opponents/types.py +0 -0
  36. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine/runner/config.py +0 -0
  37. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/dependency_links.txt +0 -0
  38. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/requires.txt +0 -0
  39. {i151_engine-0.2.0 → i151_engine-0.2.2}/i151_engine.egg-info/top_level.txt +0 -0
  40. {i151_engine-0.2.0 → i151_engine-0.2.2}/setup.cfg +0 -0
  41. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_card.py +0 -0
  42. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_deck.py +0 -0
  43. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_forfeit_scoring.py +0 -0
  44. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_match_runner.py +0 -0
  45. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_match_timeout.py +0 -0
  46. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_opponents.py +0 -0
  47. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_recorder.py +0 -0
  48. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_reducer.py +0 -0
  49. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_rules.py +0 -0
  50. {i151_engine-0.2.0 → i151_engine-0.2.2}/tests/test_scoring.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: i151-engine
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Headless game engine for the i151 student AI competition
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -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, ActionType
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
- challenger.setup(challenger_dir or Path("."))
71
- opponent.setup(opponent_dir or Path("."))
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
- challenger.teardown()
113
- opponent.teardown()
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
- can_draw = is_your_turn and any(a.type == ActionType.DRAW_CARD for a in legal)
61
- can_pass = is_your_turn and any(a.type == ActionType.PASS_TURN for a in legal)
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=round_state.current_player.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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: i151-engine
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Headless game engine for the i151 student AI competition
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -41,6 +41,7 @@ tests/test_forfeit_scoring.py
41
41
  tests/test_match_runner.py
42
42
  tests/test_match_timeout.py
43
43
  tests/test_opponents.py
44
+ tests/test_player_view_turn.py
44
45
  tests/test_recorder.py
45
46
  tests/test_reducer.py
46
47
  tests/test_rules.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "i151-engine"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Headless game engine for the i151 student AI competition"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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