1+ import dataclasses
2+ import functools
3+ import typing
4+
15import pytest
26
37import 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