Skip to content

Commit ab077f1

Browse files
authored
Refactor infoset tests to use factory pattern and dataclasses. (#737)
Update `test_infoset_own_prior_actions` and `test_infoset_is_absent_minded` to match the new test suite architecture.
1 parent 6dbc0d0 commit ab077f1

1 file changed

Lines changed: 159 additions & 104 deletions

File tree

tests/test_infosets.py

Lines changed: 159 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import dataclasses
2+
import functools
3+
import typing
4+
15
import pytest
26

37
import pygambit as gbt
@@ -71,86 +75,160 @@ def test_infoset_plays():
7175
assert set(test_infoset.plays) == expected_set_of_plays
7276

7377

74-
@pytest.mark.parametrize("game_file, expected_results", [
75-
# Perfect recall game
76-
(
77-
"binary_3_levels_generic_payoffs.efg",
78-
[
79-
# Player 1, Infoset 0 (Root):
80-
# No prior history.
81-
("Player 1", 0, {None}),
82-
83-
# Player 1, Infoset 1:
84-
# Reached via "Left" from Infoset 0.
85-
("Player 1", 1, {("Player 1", 0, "Left")}),
86-
87-
# Player 1, Infoset 2:
88-
# Reached via "Right" from Infoset 0.
89-
("Player 1", 2, {("Player 1", 0, "Right")}),
78+
@dataclasses.dataclass
79+
class PriorActionsTestCase:
80+
"""TestCase for testing own_prior_actions."""
81+
factory: typing.Callable[[], gbt.Game]
82+
expected_results: list[tuple]
83+
84+
85+
@dataclasses.dataclass
86+
class AbsentMindednessTestCase:
87+
"""TestCase for testing is_absent_minded."""
88+
factory: typing.Callable[[], gbt.Game]
89+
expected_am_paths: list[list[str]]
90+
91+
92+
PRIOR_ACTIONS_CASES = [
93+
pytest.param(
94+
PriorActionsTestCase(
95+
factory=functools.partial(games.read_from_file, "binary_3_levels_generic_payoffs.efg"),
96+
expected_results=[
97+
("Player 1", 0, {None}),
98+
("Player 1", 1, {("Player 1", 0, "Left")}),
99+
("Player 1", 2, {("Player 1", 0, "Right")}),
100+
("Player 2", 0, {None}),
101+
]
102+
),
103+
id="perfect_recall"
104+
),
105+
pytest.param(
106+
PriorActionsTestCase(
107+
factory=functools.partial(games.read_from_file, "wichardt.efg"),
108+
expected_results=[
109+
("Player 1", 0, {None}),
110+
("Player 1", 1, {("Player 1", 0, "L"), ("Player 1", 0, "R")}),
111+
("Player 2", 0, {None}),
112+
]
113+
),
114+
id="wichardt_forgetting_action"
115+
),
116+
pytest.param(
117+
PriorActionsTestCase(
118+
factory=functools.partial(games.read_from_file, "subgames.efg"),
119+
expected_results=[
120+
("Player 1", 0, {None}),
121+
("Player 1", 1, {None}),
122+
("Player 1", 2, {("Player 1", 1, "1")}),
123+
("Player 1", 3, {("Player 1", 5, "1"), ("Player 1", 1, "2")}),
124+
("Player 1", 4, {("Player 1", 1, "2")}),
125+
("Player 1", 5, {("Player 1", 4, "2")}),
126+
("Player 1", 6, {("Player 1", 1, "2")}),
127+
("Player 2", 0, {None}),
128+
("Player 2", 1, {("Player 2", 0, "2")}),
129+
("Player 2", 2, {("Player 2", 1, "1")}),
130+
("Player 2", 3, {("Player 2", 2, "1")}),
131+
("Player 2", 4, {("Player 2", 2, "2")}),
132+
("Player 2", 5, {("Player 2", 4, "1")}),
133+
]
134+
),
135+
id="four_subgames"
136+
),
137+
pytest.param(
138+
PriorActionsTestCase(
139+
factory=functools.partial(games.read_from_file, "AM-driver-subgame.efg"),
140+
expected_results=[
141+
("Player 1", 0, {None, ("Player 1", 0, "S")}),
142+
("Player 2", 0, {None}),
143+
]
144+
),
145+
id="AM_driver"
146+
),
147+
]
90148

91-
# Player 2, Infoset 0:
92-
# No prior history.
93-
("Player 2", 0, {None}),
94-
]
149+
ABSENT_MINDEDNESS_CASES = [
150+
# Games without absent-mindedness
151+
pytest.param(
152+
AbsentMindednessTestCase(
153+
factory=functools.partial(games.read_from_file, "e02.efg"),
154+
expected_am_paths=[]
155+
),
156+
id="short_centipede_perfect_info"
157+
),
158+
pytest.param(
159+
AbsentMindednessTestCase(
160+
factory=functools.partial(games.read_from_file, "stripped_down_poker.efg"),
161+
expected_am_paths=[]
162+
),
163+
id="poker_stripped"
95164
),
96-
# Imperfect recall games, no absent-mindedness
97-
(
98-
"wichardt.efg",
99-
[
100-
# Player 1, Infoset 0 (Root):
101-
# No prior history.
102-
("Player 1", 0, {None}),
103-
104-
# Player 1, Infoset 1:
105-
# Reachable via "L" or "R" from Infoset 0.
106-
("Player 1", 1, {("Player 1", 0, "L"), ("Player 1", 0, "R")}),
107-
108-
# Player 2, Infoset 0:
109-
# No prior history.
110-
("Player 2", 0, {None}),
111-
]
165+
pytest.param(
166+
AbsentMindednessTestCase(
167+
factory=functools.partial(games.read_from_file, "basic_extensive_game.efg"),
168+
expected_am_paths=[]
169+
),
170+
id="basic_extensive"
112171
),
113-
(
114-
"subgames.efg",
115-
[
116-
("Player 1", 0, {None}),
117-
("Player 1", 1, {None}),
118-
("Player 1", 2, {("Player 1", 1, "1")}),
119-
("Player 1", 3, {("Player 1", 5, "1"), ("Player 1", 1, "2")}),
120-
("Player 1", 4, {("Player 1", 1, "2")}),
121-
("Player 1", 5, {("Player 1", 4, "2")}),
122-
("Player 1", 6, {("Player 1", 1, "2")}),
123-
("Player 2", 0, {None}),
124-
("Player 2", 1, {("Player 2", 0, "2")}),
125-
("Player 2", 2, {("Player 2", 1, "1")}),
126-
("Player 2", 3, {("Player 2", 2, "1")}),
127-
("Player 2", 4, {("Player 2", 2, "2")}),
128-
("Player 2", 5, {("Player 2", 4, "1")}),
129-
]
172+
pytest.param(
173+
AbsentMindednessTestCase(
174+
factory=functools.partial(games.read_from_file, "gilboa_two_am_agents.efg"),
175+
expected_am_paths=[]
176+
),
177+
id="gilboa_forgetting_info"
130178
),
131-
# An absent-minded driver game
132-
(
133-
"AM-driver-subgame.efg",
134-
[
135-
# Player 1, Infoset 0:
136-
# One member is the root (no prior history),
137-
# the other is reached via "S" from this same infoset.
138-
("Player 1", 0, {None, ("Player 1", 0, "S")}),
139-
140-
# Player 2, Infoset 0:
141-
# No prior history.
142-
("Player 2", 0, {None}),
143-
]
179+
pytest.param(
180+
AbsentMindednessTestCase(
181+
factory=functools.partial(games.read_from_file, "wichardt.efg"),
182+
expected_am_paths=[]
183+
),
184+
id="wichardt_forgetting_action"
185+
),
186+
# Games with absent-mindedness
187+
pytest.param(
188+
AbsentMindednessTestCase(
189+
factory=functools.partial(games.read_from_file, "noPR-AM-driver-two-players.efg"),
190+
expected_am_paths=[[]]
191+
),
192+
id="AM_driver_two_players"
144193
),
145-
])
146-
def test_infoset_own_prior_actions(game_file, expected_results):
194+
pytest.param(
195+
AbsentMindednessTestCase(
196+
factory=functools.partial(games.read_from_file, "noPR-action-AM.efg"),
197+
expected_am_paths=[[]]
198+
),
199+
id="AM_forgetting_action"
200+
),
201+
pytest.param(
202+
AbsentMindednessTestCase(
203+
factory=functools.partial(games.read_from_file, "noPR-action-AM-two-hops.efg"),
204+
expected_am_paths=[["2", "1", "1", "1", "1"], ["1", "1", "1"]]
205+
),
206+
id="AM_infoset_takes_two_hops"
207+
),
208+
]
209+
210+
211+
def _get_node_by_path(game, path: list[str]) -> gbt.Node:
147212
"""
148-
Tests `infoset.own_prior_actions` by collecting the action details
149-
(player label, infoset num, label) and comparing against expected sets.
213+
Helper to find a node by following a sequence of action labels.
150214
"""
151-
game = games.read_from_file(game_file)
215+
node = game.root
216+
for action_label in reversed(path):
217+
node = node.children[action_label]
218+
return node
152219

153-
for player_label, infoset_num, expected_set in expected_results:
220+
221+
@pytest.mark.parametrize("test_case", PRIOR_ACTIONS_CASES)
222+
def test_infoset_own_prior_actions(test_case: PriorActionsTestCase):
223+
"""
224+
Test `infoset.own_prior_actions`.
225+
226+
Verifies that the set of prior actions (as player-infoset-label tuples)
227+
matches the expected results.
228+
"""
229+
game = test_case.factory()
230+
231+
for player_label, infoset_num, expected_set in test_case.expected_results:
154232
player = game.players[player_label]
155233
infoset = player.infosets[infoset_num]
156234

@@ -164,42 +242,19 @@ def test_infoset_own_prior_actions(game_file, expected_results):
164242
assert actual_details == expected_set
165243

166244

167-
def _get_node_by_path(game, path: list[str]) -> gbt.Node:
245+
@pytest.mark.parametrize("test_case", ABSENT_MINDEDNESS_CASES)
246+
def test_infoset_is_absent_minded(test_case: AbsentMindednessTestCase):
168247
"""
169-
Helper to find a node by following a sequence of action labels.
170-
171-
Parameters
172-
----------
173-
path : list[str]
174-
A list of action labels in Node->Root order.
175-
"""
176-
node = game.root
177-
for action_label in reversed(path):
178-
node = node.children[action_label]
179-
180-
return node
181-
248+
Test `infoset.is_absent_minded`.
182249
183-
@pytest.mark.parametrize("game_input, expected_am_paths", [
184-
# Games without absent-mindedness
185-
("e02.efg", []),
186-
("stripped_down_poker.efg", []),
187-
("basic_extensive_game.efg", []),
188-
("gilboa_two_am_agents.efg", []), # forgetting past information; Gilboa (GEB, 1997)
189-
("wichardt.efg", []), # forgetting past action; Wichardt (GEB, 2008)
190-
191-
# Games with absent-mindedness
192-
("noPR-AM-driver-two-players.efg", [[]]),
193-
("noPR-action-AM.efg", [[]]),
194-
("noPR-action-AM-two-hops.efg", [["2", "1", "1", "1", "1"], ["1", "1", "1"]]),
195-
])
196-
def test_infoset_is_absent_minded(game_input, expected_am_paths):
250+
Verifies that the set of infosets marked as absent-minded matches the
251+
expected set derived from action paths.
197252
"""
198-
Verify the is_absent_minded property of information sets.
199-
"""
200-
game = games.read_from_file(game_input)
253+
game = test_case.factory()
201254

202-
expected_infosets = {_get_node_by_path(game, path).infoset for path in expected_am_paths}
255+
expected_infosets = {
256+
_get_node_by_path(game, path).infoset for path in test_case.expected_am_paths
257+
}
203258
actual_infosets = {infoset for infoset in game.infosets if infoset.is_absent_minded}
204259

205260
assert actual_infosets == expected_infosets

0 commit comments

Comments
 (0)