Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
been removed as planned. (#357)

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


## [16.3.1] - unreleased
Expand Down
1 change: 1 addition & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ Information about the game
Strategy.game
Strategy.player
Strategy.number
Strategy.action


Player behavior
Expand Down
13 changes: 13 additions & 0 deletions src/games/game.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ GameOutcomeRep::GameOutcomeRep(GameRep *p_game, int p_number) : m_game(p_game),
}
}

//========================================================================
// class GameStrategyRep
//========================================================================

GameAction GameStrategyRep::GetAction(const GameInfoset &p_infoset) const
{
if (p_infoset->GetPlayer() != m_player) {
throw MismatchException();
}
const int action = m_behav[p_infoset->GetNumber()];
return (action) ? *std::next(p_infoset->GetActions().cbegin(), action - 1) : nullptr;
}

//========================================================================
// class GamePlayerRep
//========================================================================
Expand Down
3 changes: 3 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ class GameStrategyRep : public GameObject {
GamePlayer GetPlayer() const;
/// Returns the index of the strategy for its player
int GetNumber() const { return m_number; }

/// Returns the action specified by the strategy at the information set
GameAction GetAction(const GameInfoset &) const;
//@}
};

Expand Down
2 changes: 2 additions & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ cdef extern from "games/game.h":
c_GameNodeRep *deref "operator->"() except +RuntimeError

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

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

cdef cppclass c_GameActionRep "GameActionRep":
int GetNumber() except +
Expand Down
43 changes: 43 additions & 0 deletions src/pygambit/strategy.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,46 @@ class Strategy:
def number(self) -> int:
"""The number of the strategy."""
return self.strategy.deref().GetNumber() - 1

def action(self, infoset: typing.Union[Infoset, str]) -> typing.Optional[Action]:
"""Get the action prescribed by a strategy for a given information set.

.. versionadded:: 16.4.0

Parameters
----------
infoset
The information set for which to find the prescribed action.
Can be an Infoset object or its string label.

Returns
-------
Action or None
The prescribed action or None if the strategy is not defined for this
information set, that is, the information set is unreachable under this strategy.

Raises
------
UndefinedOperationError
If the game is not an extensive-form (tree) game.
ValueError
If the information set belongs to a different player than the strategy.
"""
if not self.game.is_tree:
raise UndefinedOperationError(
"Strategy.action is only defined for strategies in extensive-form games."
)

resolved_infoset: Infoset = self.game._resolve_infoset(infoset, "Strategy.action")

if resolved_infoset.player != self.player:
raise ValueError(
f"Information set {resolved_infoset} belongs to player "
f"'{resolved_infoset.player.label}', but this strategy "
f"belongs to player '{self.player.label}'."
)

action: c_GameAction = self.strategy.deref().GetAction(resolved_infoset.infoset)
if not action:
return None
return Action.wrap(action)
100 changes: 98 additions & 2 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,103 @@ def test_action_plays():
test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1]

expected_set_of_plays = {
list_nodes[4], list_nodes[7]
} # paths=[0, 1, 0], [0, 1]
list_nodes[4], list_nodes[7] # paths=[0, 1, 0], [0, 1]
}

assert set(test_action.plays) == expected_set_of_plays


@pytest.mark.parametrize(
"game, player_ind, str_ind, infoset_ind, expected_action_ind",
[
(games.read_from_file("e01.efg"), 0, 0, 0, 0),
(games.read_from_file("e01.efg"), 0, 1, 0, 1),
(games.read_from_file("e01.efg"), 1, 0, 1, 0),
(games.read_from_file("e01.efg"), 1, 1, 1, 1),
(games.read_from_file("e01.efg"), 2, 0, 2, 0),
(games.read_from_file("e01.efg"), 2, 1, 2, 1),
(games.read_from_file("e02.efg"), 0, 0, 0, 0),
(games.read_from_file("e02.efg"), 0, 1, 0, 1),
(games.read_from_file("e02.efg"), 1, 0, 2, 0),
(games.read_from_file("e02.efg"), 1, 1, 2, 1),
(games.read_from_file("basic_extensive_game.efg"), 0, 0, 0, 0),
(games.read_from_file("basic_extensive_game.efg"), 0, 1, 0, 1),
(games.read_from_file("basic_extensive_game.efg"), 1, 0, 1, 0),
(games.read_from_file("basic_extensive_game.efg"), 1, 1, 1, 1),
(games.read_from_file("basic_extensive_game.efg"), 2, 0, 2, 0),
(games.read_from_file("basic_extensive_game.efg"), 2, 1, 2, 1),
],
)
def test_strategy_action_defined(game, player_ind, str_ind, infoset_ind, expected_action_ind):
"""Verify `Strategy.action` retrieves the correct action for defined actions.
"""
player = game.players[player_ind]
strategy = player.strategies[str_ind]
infoset = game.infosets[infoset_ind]
expected_action = infoset.actions[expected_action_ind]

prescribed_action = strategy.action(infoset)

assert prescribed_action == expected_action


@pytest.mark.parametrize(
"game, player_ind, str_ind, infoset_ind",
[
(games.read_from_file("e02.efg"), 0, 0, 1),
(games.read_from_file("cent3.efg"), 0, 0, 1),
(games.read_from_file("cent3.efg"), 0, 0, 2),
(games.read_from_file("cent3.efg"), 0, 1, 2),
(games.read_from_file("cent3.efg"), 1, 0, 7),
(games.read_from_file("cent3.efg"), 1, 0, 7),
(games.read_from_file("cent3.efg"), 1, 1, 8),
],
)
def test_strategy_action_undefined_returns_none(game, player_ind, str_ind, infoset_ind):
"""Verify `Strategy.action` returns None when called on an unreached player's infoset
"""
player = game.players[player_ind]
strategy = player.strategies[str_ind]
infoset = game.infosets[infoset_ind]

prescribed_action = strategy.action(infoset)

assert prescribed_action is None


@pytest.mark.parametrize(
"game, player_ind, infoset_ind",
[
(games.read_from_file("e01.efg"), 0, 1),
(games.read_from_file("e01.efg"), 1, 0),
(games.read_from_file("e02.efg"), 0, 2),
(games.read_from_file("e02.efg"), 1, 0),
(games.read_from_file("basic_extensive_game.efg"), 0, 1),
(games.read_from_file("basic_extensive_game.efg"), 1, 2),
(games.read_from_file("basic_extensive_game.efg"), 2, 0),
],
)
def test_strategy_action_raises_value_error_for_wrong_player(game, player_ind, infoset_ind):
"""
Verify `Strategy.action` raises ValueError when the infoset belongs
to a different player than the strategy.
"""
player = game.players[player_ind]
strategy = player.strategies[0]
other_players_infoset = game.infosets[infoset_ind]

with pytest.raises(ValueError):
strategy.action(other_players_infoset)


def test_strategy_action_raises_error_for_strategic_game():
"""Verify `Strategy.action` retrieves the action prescribed by the strategy
"""
game_efg = games.read_from_file("e02.efg")
game_nfg = game_efg.from_arrays(game_efg.to_arrays()[0], game_efg.to_arrays()[1])
alice = game_nfg.players[0]
strategy = alice.strategies[0]
test_infoset = game_efg.infosets[0]

with pytest.raises(gbt.UndefinedOperationError):
strategy.action(test_infoset)
Loading