Skip to content

Commit b31be34

Browse files
committed
Expose access to action map for a strategy
This provides an interface to the mapping from strategy to actions in an extensive game: * `GameStrategyRep::GetAction(GameInfoset)` in C++ * `Strategy.action(Infoset)` in Python`
1 parent 7df3d34 commit b31be34

7 files changed

Lines changed: 164 additions & 4 deletions

File tree

ChangeLog

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
been removed as planned. (#357)
1111

1212
### Added
13-
- Implement `GetPlays()` (C++) and `get_plays` (Python) to compute the set of terminal nodes consistent
14-
with a node, information set, or action (#517)
13+
- Implement `GetPlays()` (C++) and `get_plays` (Python) to compute the set of terminal nodes
14+
consistent with a node, information set, or action (#517)
15+
- Implement `GameStrategyRep::GetAction` (C++) and `Strategy.action` (Python) retrieving the action
16+
prescribed by a strategy at an information set
1517

1618

1719
## [16.3.1] - unreleased

doc/pygambit.api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ Information about the game
178178
Strategy.game
179179
Strategy.player
180180
Strategy.number
181+
Strategy.action
181182

182183

183184
Player behavior

src/games/game.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ GameOutcomeRep::GameOutcomeRep(GameRep *p_game, int p_number) : m_game(p_game),
4646
}
4747
}
4848

49+
//========================================================================
50+
// class GameStrategyRep
51+
//========================================================================
52+
53+
GameAction GameStrategyRep::GetAction(const GameInfoset &p_infoset) const
54+
{
55+
if (p_infoset->GetPlayer() != m_player) {
56+
throw MismatchException();
57+
}
58+
const int action = m_behav[p_infoset->GetNumber()];
59+
return (action) ? *std::next(p_infoset->GetActions().cbegin(), action - 1) : nullptr;
60+
}
61+
4962
//========================================================================
5063
// class GamePlayerRep
5164
//========================================================================

src/games/game.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ class GameStrategyRep : public GameObject {
358358
GamePlayer GetPlayer() const;
359359
/// Returns the index of the strategy for its player
360360
int GetNumber() const { return m_number; }
361+
362+
/// Returns the action specified by the strategy at the information set
363+
GameAction GetAction(const GameInfoset &) const;
361364
//@}
362365
};
363366

src/pygambit/gambit.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ cdef extern from "games/game.h":
7373
c_GameNodeRep *deref "operator->"() except +RuntimeError
7474

7575
cdef cppclass c_GameAction "GameObjectPtr<GameActionRep>":
76+
bool operator !() except +
7677
bool operator !=(c_GameAction) except +
7778
c_GameActionRep *deref "operator->"() except +RuntimeError
7879

@@ -97,6 +98,7 @@ cdef extern from "games/game.h":
9798
c_GamePlayer GetPlayer() except +
9899
string GetLabel() except +
99100
void SetLabel(string) except +
101+
c_GameAction GetAction(c_GameInfoset) except +
100102

101103
cdef cppclass c_GameActionRep "GameActionRep":
102104
int GetNumber() except +

src/pygambit/strategy.pxi

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,46 @@ class Strategy:
7373
def number(self) -> int:
7474
"""The number of the strategy."""
7575
return self.strategy.deref().GetNumber() - 1
76+
77+
def action(self, infoset: typing.Union[Infoset, str]) -> typing.Optional[Action]:
78+
"""Get the action prescribed by a strategy for a given information set.
79+
80+
.. versionadded:: 16.4.0
81+
82+
Parameters
83+
----------
84+
infoset
85+
The information set for which to find the prescribed action.
86+
Can be an Infoset object or its string label.
87+
88+
Returns
89+
-------
90+
Action or None
91+
The prescribed action or None if the strategy is not defined for this
92+
information set, that is, the information set is unreachable under this strategy.
93+
94+
Raises
95+
------
96+
UndefinedOperationError
97+
If the game is not an extensive-form (tree) game.
98+
ValueError
99+
If the information set belongs to a different player than the strategy.
100+
"""
101+
if not self.game.is_tree:
102+
raise UndefinedOperationError(
103+
"Strategy.action is only defined for strategies in extensive-form games."
104+
)
105+
106+
resolved_infoset: Infoset = self.game._resolve_infoset(infoset, "Strategy.action")
107+
108+
if resolved_infoset.player != self.player:
109+
raise ValueError(
110+
f"Information set {resolved_infoset} belongs to player "
111+
f"'{resolved_infoset.player.label}', but this strategy "
112+
f"belongs to player '{self.player.label}'."
113+
)
114+
115+
action: c_GameAction = self.strategy.deref().GetAction(resolved_infoset.infoset)
116+
if not action:
117+
return None
118+
return Action.wrap(action)

tests/test_actions.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,103 @@ def test_action_plays():
139139
test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1]
140140

141141
expected_set_of_plays = {
142-
list_nodes[4], list_nodes[7]
143-
} # paths=[0, 1, 0], [0, 1]
142+
list_nodes[4], list_nodes[7] # paths=[0, 1, 0], [0, 1]
143+
}
144144

145145
assert set(test_action.plays) == expected_set_of_plays
146+
147+
148+
@pytest.mark.parametrize(
149+
"game, player_ind, str_ind, infoset_ind, expected_action_ind",
150+
[
151+
(games.read_from_file("e01.efg"), 0, 0, 0, 0),
152+
(games.read_from_file("e01.efg"), 0, 1, 0, 1),
153+
(games.read_from_file("e01.efg"), 1, 0, 1, 0),
154+
(games.read_from_file("e01.efg"), 1, 1, 1, 1),
155+
(games.read_from_file("e01.efg"), 2, 0, 2, 0),
156+
(games.read_from_file("e01.efg"), 2, 1, 2, 1),
157+
(games.read_from_file("e02.efg"), 0, 0, 0, 0),
158+
(games.read_from_file("e02.efg"), 0, 1, 0, 1),
159+
(games.read_from_file("e02.efg"), 1, 0, 2, 0),
160+
(games.read_from_file("e02.efg"), 1, 1, 2, 1),
161+
(games.read_from_file("basic_extensive_game.efg"), 0, 0, 0, 0),
162+
(games.read_from_file("basic_extensive_game.efg"), 0, 1, 0, 1),
163+
(games.read_from_file("basic_extensive_game.efg"), 1, 0, 1, 0),
164+
(games.read_from_file("basic_extensive_game.efg"), 1, 1, 1, 1),
165+
(games.read_from_file("basic_extensive_game.efg"), 2, 0, 2, 0),
166+
(games.read_from_file("basic_extensive_game.efg"), 2, 1, 2, 1),
167+
],
168+
)
169+
def test_strategy_action_defined(game, player_ind, str_ind, infoset_ind, expected_action_ind):
170+
"""Verify `Strategy.action` retrieves the correct action for defined actions.
171+
"""
172+
player = game.players[player_ind]
173+
strategy = player.strategies[str_ind]
174+
infoset = game.infosets[infoset_ind]
175+
expected_action = infoset.actions[expected_action_ind]
176+
177+
prescribed_action = strategy.action(infoset)
178+
179+
assert prescribed_action == expected_action
180+
181+
182+
@pytest.mark.parametrize(
183+
"game, player_ind, str_ind, infoset_ind",
184+
[
185+
(games.read_from_file("e02.efg"), 0, 0, 1),
186+
(games.read_from_file("cent3.efg"), 0, 0, 1),
187+
(games.read_from_file("cent3.efg"), 0, 0, 2),
188+
(games.read_from_file("cent3.efg"), 0, 1, 2),
189+
(games.read_from_file("cent3.efg"), 1, 0, 7),
190+
(games.read_from_file("cent3.efg"), 1, 0, 7),
191+
(games.read_from_file("cent3.efg"), 1, 1, 8),
192+
],
193+
)
194+
def test_strategy_action_undefined_returns_none(game, player_ind, str_ind, infoset_ind):
195+
"""Verify `Strategy.action` returns None when called on an unreached player's infoset
196+
"""
197+
player = game.players[player_ind]
198+
strategy = player.strategies[str_ind]
199+
infoset = game.infosets[infoset_ind]
200+
201+
prescribed_action = strategy.action(infoset)
202+
203+
assert prescribed_action is None
204+
205+
206+
@pytest.mark.parametrize(
207+
"game, player_ind, infoset_ind",
208+
[
209+
(games.read_from_file("e01.efg"), 0, 1),
210+
(games.read_from_file("e01.efg"), 1, 0),
211+
(games.read_from_file("e02.efg"), 0, 2),
212+
(games.read_from_file("e02.efg"), 1, 0),
213+
(games.read_from_file("basic_extensive_game.efg"), 0, 1),
214+
(games.read_from_file("basic_extensive_game.efg"), 1, 2),
215+
(games.read_from_file("basic_extensive_game.efg"), 2, 0),
216+
],
217+
)
218+
def test_strategy_action_raises_value_error_for_wrong_player(game, player_ind, infoset_ind):
219+
"""
220+
Verify `Strategy.action` raises ValueError when the infoset belongs
221+
to a different player than the strategy.
222+
"""
223+
player = game.players[player_ind]
224+
strategy = player.strategies[0]
225+
other_players_infoset = game.infosets[infoset_ind]
226+
227+
with pytest.raises(ValueError):
228+
strategy.action(other_players_infoset)
229+
230+
231+
def test_strategy_action_raises_error_for_strategic_game():
232+
"""Verify `Strategy.action` retrieves the action prescribed by the strategy
233+
"""
234+
game_efg = games.read_from_file("e02.efg")
235+
game_nfg = game_efg.from_arrays(game_efg.to_arrays()[0], game_efg.to_arrays()[1])
236+
alice = game_nfg.players[0]
237+
strategy = alice.strategies[0]
238+
test_infoset = game_efg.infosets[0]
239+
240+
with pytest.raises(gbt.UndefinedOperationError):
241+
strategy.action(test_infoset)

0 commit comments

Comments
 (0)