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.
- {i151_engine-0.2.2 → i151_engine-0.2.4}/ARCHITECTURE.md +2 -2
- {i151_engine-0.2.2 → i151_engine-0.2.4}/PKG-INFO +1 -1
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/scoring.py +4 -1
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/recorder.py +1 -1
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/round.py +4 -1
- i151_engine-0.2.4/i151_engine/runner/challenge_guards.py +28 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/runner/config.py +3 -1
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/runner/match_runner.py +4 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/PKG-INFO +1 -1
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/SOURCES.txt +3 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/pyproject.toml +1 -1
- i151_engine-0.2.4/tests/test_challenge_guards.py +77 -0
- i151_engine-0.2.4/tests/test_first_player.py +40 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_scoring.py +53 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/README.md +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/protocol.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/bots/random_bot.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/deck.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/errors.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/legal_actions.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/reducer.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/core/rules.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/game/round_state.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/action.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/card.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/enums.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/models/player.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/__init__.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/catalog.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/registry.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/schedules.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/opponents/types.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/view/builder.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine/view/player_view.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/dependency_links.txt +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/requires.txt +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/i151_engine.egg-info/top_level.txt +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/setup.cfg +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_card.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_deck.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_forfeit_scoring.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_match_runner.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_match_timeout.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_opponents.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_player_view_turn.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_recorder.py +0 -0
- {i151_engine-0.2.2 → i151_engine-0.2.4}/tests/test_reducer.py +0 -0
- {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` (
|
|
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 =
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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=
|
|
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 =
|
|
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)
|
|
@@ -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
|
|
@@ -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
|
|
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
|