i151-engine 0.2.2__tar.gz → 0.2.4__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 (53) hide show
  1. {i151_engine-0.2.2 → i151_engine-0.2.4}/ARCHITECTURE.md +2 -2
  2. {i151_engine-0.2.2 → i151_engine-0.2.4}/PKG-INFO +1 -1
  3. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/scoring.py +4 -1
  4. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/recorder.py +1 -1
  5. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/round.py +4 -1
  6. i151_engine-0.2.4/i151_engine/runner/challenge_guards.py +28 -0
  7. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/runner/config.py +3 -1
  8. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/runner/match_runner.py +4 -0
  9. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/PKG-INFO +1 -1
  10. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/SOURCES.txt +3 -0
  11. {i151_engine-0.2.2 → i151_engine-0.2.4}/pyproject.toml +1 -1
  12. i151_engine-0.2.4/tests/test_challenge_guards.py +77 -0
  13. i151_engine-0.2.4/tests/test_first_player.py +40 -0
  14. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_scoring.py +53 -0
  15. {i151_engine-0.2.2 → i151_engine-0.2.4}/README.md +0 -0
  16. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/__init__.py +0 -0
  17. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/__init__.py +0 -0
  18. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/protocol.py +0 -0
  19. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/random_bot.py +0 -0
  20. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/__init__.py +0 -0
  21. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/deck.py +0 -0
  22. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/errors.py +0 -0
  23. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/legal_actions.py +0 -0
  24. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/reducer.py +0 -0
  25. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/rules.py +0 -0
  26. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/__init__.py +0 -0
  27. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/round_state.py +0 -0
  28. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/__init__.py +0 -0
  29. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/action.py +0 -0
  30. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/card.py +0 -0
  31. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/enums.py +0 -0
  32. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/player.py +0 -0
  33. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/__init__.py +0 -0
  34. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/catalog.py +0 -0
  35. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/registry.py +0 -0
  36. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/schedules.py +0 -0
  37. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/types.py +0 -0
  38. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/view/builder.py +0 -0
  39. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/view/player_view.py +0 -0
  40. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/dependency_links.txt +0 -0
  41. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/requires.txt +0 -0
  42. {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/top_level.txt +0 -0
  43. {i151_engine-0.2.2 → i151_engine-0.2.4}/setup.cfg +0 -0
  44. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_card.py +0 -0
  45. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_deck.py +0 -0
  46. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_forfeit_scoring.py +0 -0
  47. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_match_runner.py +0 -0
  48. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_match_timeout.py +0 -0
  49. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_opponents.py +0 -0
  50. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_player_view_turn.py +0 -0
  51. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_recorder.py +0 -0
  52. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_reducer.py +0 -0
  53. {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_rules.py +0 -0
@@ -606,7 +606,7 @@ def decide(view: PlayerView, legal_actions: list[Action], time_left_ms: int) ->
606
606
  | Taille ZIP | 50 Mo |
607
607
  | Fichier modèle unique | 30 Mo |
608
608
  | Temps `setup()` | 30 s |
609
- | Temps `decide()` | `decision_timeout_ms` (2 s par défaut) |
609
+ | Temps `decide()` | `decision_timeout_ms` (1000 ms par défaut) |
610
610
  | RAM processus | 512 Mo |
611
611
 
612
612
  Le runner charge le bot une fois par match (`setup` → boucle `decide` → `teardown`).
@@ -787,7 +787,7 @@ Les parties générées localement n'alimentent pas le classement ; seules les s
787
787
  class MatchConfig:
788
788
  seed: int
789
789
  target_score: int = 151
790
- decision_timeout_ms: int = 2000
790
+ decision_timeout_ms: int = 1000
791
791
  match_timeout_ms: int = 600_000
792
792
  max_steps_per_round: int = 500 # garde-fou anti-boucle
793
793
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: i151-engine
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -108,7 +108,8 @@ def apply_round_scoring(match: MatchState, winner_id: str) -> MatchState:
108
108
  is_excluded=player.is_excluded or new_score >= match.target_score,
109
109
  has_drawn_this_turn=False,
110
110
  is_chaining=False,
111
- consecutive_round_wins=player.consecutive_round_wins,
111
+ # Aligné Flutter/TS : série cassée dès qu'un autre gagne la manche.
112
+ consecutive_round_wins=0,
112
113
  )
113
114
 
114
115
  winner, last_winner_id = _track_consecutive_wins(
@@ -130,6 +131,7 @@ def apply_round_scoring(match: MatchState, winner_id: str) -> MatchState:
130
131
  winner_id=winner_id_final,
131
132
  current_round=None,
132
133
  seed=match.seed,
134
+ cards_per_player=match.cards_per_player,
133
135
  )
134
136
 
135
137
  dealer_index, _ = _next_dealer_and_starter(players_list, match.dealer_index)
@@ -143,6 +145,7 @@ def apply_round_scoring(match: MatchState, winner_id: str) -> MatchState:
143
145
  winner_id=None,
144
146
  current_round=None,
145
147
  seed=match.seed,
148
+ cards_per_player=match.cards_per_player,
146
149
  )
147
150
 
148
151
 
@@ -66,7 +66,7 @@ class ReplayLog:
66
66
 
67
67
 
68
68
  class MatchRecorder:
69
- """Records public match state for web replay."""
69
+ """Enregistre l'état pour le replay web (après match, vue spectateur)."""
70
70
 
71
71
  def __init__(
72
72
  self,
@@ -15,8 +15,11 @@ def create_match(
15
15
  seed: int,
16
16
  target_score: int = 151,
17
17
  cards_per_player: int = 8,
18
+ initial_dealer_index: int = 0,
18
19
  ) -> MatchState:
19
20
  validate_cards_per_player(cards_per_player, player_count=len(player_names))
21
+ n = len(player_names)
22
+ dealer = int(initial_dealer_index) % n
20
23
  players = tuple(
21
24
  PlayerState(player_id=f"p{i}", name=name, hand=())
22
25
  for i, name in enumerate(player_names)
@@ -25,7 +28,7 @@ def create_match(
25
28
  status=MatchStatus.IN_PROGRESS,
26
29
  players=players,
27
30
  round_number=1,
28
- dealer_index=0,
31
+ dealer_index=dealer,
29
32
  target_score=target_score,
30
33
  last_winner_id=None,
31
34
  winner_id=None,
@@ -0,0 +1,28 @@
1
+ """Gardes compétition : aucun oracle / ground-truth pendant un match arène.
2
+
3
+ Le chemin d'entraînement MCTS (``collect_context``) peut exposer l'état moteur
4
+ complet via un ``ContextVar`` ancré dans ``sys.modules``. Les matchs officiels
5
+ n'utilisent jamais ce chemin, mais on efface tout de même le contexte avant
6
+ chaque ``decide`` pour qu'une soumission ne puisse pas lire un état résiduel.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+
13
+ # Doit rester aligné avec ``mcts_bot/collect_context.py`` (_STORE_NAME).
14
+ _GT_STORE_NAME = "_i151_mcts_collect_gt_store"
15
+
16
+
17
+ def clear_training_oracle_context() -> None:
18
+ """Efface tout ContextVar ground-truth éventuellement présent."""
19
+ store = sys.modules.get(_GT_STORE_NAME)
20
+ if store is None:
21
+ return
22
+ var = getattr(store, "var", None)
23
+ if var is None:
24
+ return
25
+ try:
26
+ var.set(None)
27
+ except Exception:
28
+ pass
@@ -9,7 +9,9 @@ class MatchConfig:
9
9
  seed: int
10
10
  target_score: int = 151
11
11
  cards_per_player: int = 8
12
- decision_timeout_ms: int = 2000
12
+ decision_timeout_ms: int = 1000
13
13
  match_timeout_ms: int = 600_000
14
14
  max_steps_per_round: int = 500
15
15
  max_rounds: int = 200
16
+ # 0 (défaut) → p1 commence la 1ʳᵉ manche ; 1 → p0 commence.
17
+ initial_dealer_index: int = 0
@@ -16,6 +16,7 @@ from i151_engine.game.round import create_match, start_round
16
16
  from i151_engine.models.enums import MatchStatus, RoundPhase
17
17
  from i151_engine.opponents.registry import OpponentRegistry, default_registry
18
18
  from i151_engine.opponents.types import OpponentSpec
19
+ from i151_engine.runner.challenge_guards import clear_training_oracle_context
19
20
  from i151_engine.runner.config import MatchConfig
20
21
  from i151_engine.view.builder import build_player_view
21
22
 
@@ -154,6 +155,7 @@ def _run_match(
154
155
  seed=config.seed,
155
156
  target_score=config.target_score,
156
157
  cards_per_player=config.cards_per_player,
158
+ initial_dealer_index=getattr(config, "initial_dealer_index", 0),
157
159
  )
158
160
  recorder.start_round(match.round_number)
159
161
  steps = 0
@@ -199,6 +201,8 @@ def _run_match(
199
201
 
200
202
  remaining_ms = config.decision_timeout_ms
201
203
  try:
204
+ # Compétition : jamais d'état moteur complet pour le bot.
205
+ clear_training_oracle_context()
202
206
  action = _decide_with_timeout(bot, view, legal, remaining_ms)
203
207
  except TimeoutError:
204
208
  return _forfeit_result(match, opponent_id, pid, steps, "timeout", recorder)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: i151-engine
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -31,12 +31,15 @@ i151_engine/opponents/catalog.py
31
31
  i151_engine/opponents/registry.py
32
32
  i151_engine/opponents/schedules.py
33
33
  i151_engine/opponents/types.py
34
+ i151_engine/runner/challenge_guards.py
34
35
  i151_engine/runner/config.py
35
36
  i151_engine/runner/match_runner.py
36
37
  i151_engine/view/builder.py
37
38
  i151_engine/view/player_view.py
38
39
  tests/test_card.py
40
+ tests/test_challenge_guards.py
39
41
  tests/test_deck.py
42
+ tests/test_first_player.py
40
43
  tests/test_forfeit_scoring.py
41
44
  tests/test_match_runner.py
42
45
  tests/test_match_timeout.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "i151-engine"
3
- version = "0.2.2"
3
+ version = "0.2.4"
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,77 @@
1
+ """Gardes compétition : oracle ground-truth effacé avant decide."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import types
7
+ from contextvars import ContextVar
8
+
9
+ from i151_engine.models.action import Action
10
+ from i151_engine.runner.challenge_guards import _GT_STORE_NAME, clear_training_oracle_context
11
+ from i151_engine.runner.config import MatchConfig
12
+ from i151_engine.runner.match_runner import play_bots_match
13
+ from i151_engine.view.player_view import PlayerView
14
+
15
+
16
+ class _OracleProbeBot:
17
+ """Laisse un résidu GT après chaque decide ; le suivant doit le trouver effacé."""
18
+
19
+ def __init__(self) -> None:
20
+ self.saw_oracle = False
21
+ self.calls = 0
22
+
23
+ def setup(self, submission_dir) -> None: # noqa: ANN001
24
+ return None
25
+
26
+ def decide(
27
+ self,
28
+ view: PlayerView,
29
+ legal_actions: list[Action],
30
+ time_left_ms: int,
31
+ ) -> Action:
32
+ self.calls += 1
33
+ store = sys.modules.get(_GT_STORE_NAME)
34
+ var = getattr(store, "var", None) if store is not None else None
35
+ if var is not None and var.get() is not None:
36
+ self.saw_oracle = True
37
+ # Résidu volontaire : le runner doit l'effacer avant le prochain decide.
38
+ if var is not None:
39
+ var.set({"leak": self.calls})
40
+ return legal_actions[0]
41
+
42
+ def teardown(self) -> None:
43
+ return None
44
+
45
+
46
+ def test_clear_training_oracle_context_resets_store():
47
+ var: ContextVar[object | None] = ContextVar("test_gt", default=None)
48
+ sys.modules[_GT_STORE_NAME] = types.SimpleNamespace(var=var)
49
+ try:
50
+ var.set(object())
51
+ assert var.get() is not None
52
+ clear_training_oracle_context()
53
+ assert var.get() is None
54
+ finally:
55
+ del sys.modules[_GT_STORE_NAME]
56
+
57
+
58
+ def test_play_bots_match_clears_oracle_before_decide():
59
+ var: ContextVar[object | None] = ContextVar("test_gt_match", default=None)
60
+ sys.modules[_GT_STORE_NAME] = types.SimpleNamespace(var=var)
61
+ try:
62
+ var.set({"pre_match_leak": True})
63
+ probe = _OracleProbeBot()
64
+ opponent = _OracleProbeBot()
65
+ result = play_bots_match(
66
+ probe,
67
+ opponent,
68
+ MatchConfig(seed=7, target_score=1, max_rounds=3, max_steps_per_round=40),
69
+ challenger_name="probe",
70
+ opponent_name="opp",
71
+ )
72
+ assert result.steps_played > 0
73
+ assert probe.calls + opponent.calls >= 2
74
+ assert not probe.saw_oracle
75
+ assert not opponent.saw_oracle
76
+ finally:
77
+ del sys.modules[_GT_STORE_NAME]
@@ -0,0 +1,40 @@
1
+ """Tests first_player_stats + initial_dealer_index."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from i151_engine.game.round import create_match
6
+ from i151_engine.bots.random_bot import RandomBot
7
+ from i151_engine.runner.config import MatchConfig
8
+ from i151_engine.runner.match_runner import play_bots_match
9
+
10
+
11
+ def test_create_match_dealer_1_makes_p0_start():
12
+ match = create_match(("A", "B"), seed=7, initial_dealer_index=1)
13
+ assert match.dealer_index == 1
14
+ assert match.current_round is not None
15
+ assert match.current_round.current_player.player_id == "p0"
16
+
17
+
18
+ def test_create_match_dealer_0_makes_p1_start():
19
+ match = create_match(("A", "B"), seed=7, initial_dealer_index=0)
20
+ assert match.dealer_index == 0
21
+ assert match.current_round is not None
22
+ assert match.current_round.current_player.player_id == "p1"
23
+
24
+
25
+ def test_match_config_initial_dealer_index():
26
+ a = play_bots_match(
27
+ RandomBot(),
28
+ RandomBot(),
29
+ MatchConfig(seed=11, target_score=1, initial_dealer_index=1),
30
+ challenger_name="c",
31
+ opponent_name="o",
32
+ )
33
+ b = play_bots_match(
34
+ RandomBot(),
35
+ RandomBot(),
36
+ MatchConfig(seed=11, target_score=1, initial_dealer_index=0),
37
+ challenger_name="c",
38
+ opponent_name="o",
39
+ )
40
+ assert a.steps_played > 0 and b.steps_played > 0
@@ -81,3 +81,56 @@ def test_consecutive_win_bonus_deducts_ten_from_winner():
81
81
  result = apply_round_scoring(match, "p0")
82
82
  assert result.player_by_id("p0").total_score == 40
83
83
  assert result.player_by_id("p0").consecutive_round_wins == 0
84
+
85
+
86
+ def test_losing_resets_consecutive_round_wins():
87
+ """Régression : sans reset, la vue exposait une série fantôme au perdant."""
88
+ end_p0 = PlayerState(player_id="p0", name="NewWinner", hand=())
89
+ end_p1 = PlayerState(player_id="p1", name="PrevStreak", hand=(Card(Rank.SEVEN, Suit.CLUBS),))
90
+
91
+ round_state = _ended_round(players=(end_p0, end_p1), winner_id="p0")
92
+ match = MatchState(
93
+ status=MatchStatus.IN_PROGRESS,
94
+ players=(
95
+ PlayerState("p0", "NewWinner", hand=(), total_score=10, consecutive_round_wins=0),
96
+ PlayerState("p1", "PrevStreak", hand=(), total_score=20, consecutive_round_wins=2),
97
+ ),
98
+ round_number=3,
99
+ dealer_index=0,
100
+ target_score=151,
101
+ last_winner_id="p1",
102
+ winner_id=None,
103
+ current_round=round_state,
104
+ seed=1,
105
+ )
106
+
107
+ result = apply_round_scoring(match, "p0")
108
+ assert result.player_by_id("p0").consecutive_round_wins == 1
109
+ assert result.player_by_id("p1").consecutive_round_wins == 0
110
+ assert result.last_winner_id == "p0"
111
+
112
+
113
+ def test_apply_round_scoring_preserves_cards_per_player():
114
+ """Régression : sans ce champ, la manche 2 repasse à 8 cartes (banque=16)."""
115
+ end_p0 = PlayerState(player_id="p0", name="A", hand=())
116
+ end_p1 = PlayerState(player_id="p1", name="B", hand=(Card(Rank.SEVEN, Suit.CLUBS),))
117
+ round_state = _ended_round(players=(end_p0, end_p1), winner_id="p0")
118
+ match = MatchState(
119
+ status=MatchStatus.IN_PROGRESS,
120
+ players=(
121
+ PlayerState("p0", "A", hand=()),
122
+ PlayerState("p1", "B", hand=()),
123
+ ),
124
+ round_number=1,
125
+ dealer_index=0,
126
+ target_score=151,
127
+ last_winner_id=None,
128
+ winner_id=None,
129
+ current_round=round_state,
130
+ seed=1,
131
+ cards_per_player=16,
132
+ )
133
+ result = apply_round_scoring(match, "p0")
134
+ assert result.status == MatchStatus.IN_PROGRESS
135
+ assert result.cards_per_player == 16
136
+ assert result.round_number == 2
File without changes
File without changes