Skip to content
Closed
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
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [16.5.0] - unreleased

### Added
- Implement `GameTreeRep::GetOwnPriorActions` (C++) and `Game.get_own_prior_actions` (Python)
to compute, for a given information set, the set of last actions taken by the player acting
in the information set before reaching it. (#582)

### Changed
- In the graphical interface, removed option to configure information set link drawing; information sets
are always drawn and indicators are always drawn if an information set spans multiple levels.
Expand Down
1 change: 1 addition & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Information about the game
Game.infosets
Game.nodes
Game.contingencies
Game.get_own_prior_actions

.. autosummary::
:toctree: api/
Expand Down
2 changes: 2 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,8 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
/// Returns the set of terminal nodes which are descendants of members of an action
virtual std::vector<GameNode> GetPlays(GameAction action) const { throw UndefinedException(); }

/// Returns, for a given infoset, the set of the most recent action(s) of the player active in it
virtual std::vector<GameAction> GetOwnPriorActions(GameInfoset infoset) const = 0;
/// Returns true if the game is perfect recall
virtual bool IsPerfectRecall() const = 0;
//@}
Expand Down
4 changes: 4 additions & 0 deletions src/games/gameagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ class GameAGGRep : public GameRep {
//@{
bool IsTree() const override { return false; }
bool IsAgg() const override { return true; }
std::vector<GameAction> GetOwnPriorActions(GameInfoset infoset) const override
{
throw UndefinedException();
}
bool IsPerfectRecall() const override { return true; }
bool IsConstSum() const override;
/// Returns the smallest payoff to any player in any outcome of the game
Expand Down
4 changes: 4 additions & 0 deletions src/games/gamebagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class GameBAGGRep : public GameRep {
//@{
bool IsTree() const override { return false; }
virtual bool IsBagg() const { return true; }
std::vector<GameAction> GetOwnPriorActions(GameInfoset infoset) const override
{
throw UndefinedException();
}
bool IsPerfectRecall() const override { return true; }
bool IsConstSum() const override { throw UndefinedException(); }
/// Returns the smallest payoff to any player in any outcome of the game
Expand Down
5 changes: 5 additions & 0 deletions src/games/gametable.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class GameTableRep : public GameExplicitRep {
//@{
bool IsTree() const override { return false; }
bool IsConstSum() const override;
std::vector<GameAction> GetOwnPriorActions(GameInfoset infoset) const override
{
throw UndefinedException();
}

bool IsPerfectRecall() const override { return true; }
//@}

Expand Down
20 changes: 20 additions & 0 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,26 @@ bool GameTreeRep::IsConstSum() const
}
}

std::vector<GameAction> GameTreeRep::GetOwnPriorActions(GameInfoset infoset) const
{
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
}

auto it = m_infosetParents.find(infoset.get());

// If the infoset is unreachable, return an empty vector.
if (it == m_infosetParents.end()) {
return {};
}

std::vector<GameAction> own_prior_actions;
for (auto action_ptr : it->second) {
own_prior_actions.emplace_back((action_ptr) ? action_ptr->shared_from_this() : nullptr);
}
return own_prior_actions;
}

bool GameTreeRep::IsPerfectRecall() const
{
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
Expand Down
1 change: 1 addition & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class GameTreeRep : public GameExplicitRep {
//@{
bool IsTree() const override { return true; }
bool IsConstSum() const override;
std::vector<GameAction> GetOwnPriorActions(GameInfoset infoset) const override;
bool IsPerfectRecall() const override;
//@}

Expand Down
1 change: 1 addition & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ cdef extern from "games/game.h":
stdvector[c_GameNode] GetPlays(c_GameNode) except +
stdvector[c_GameNode] GetPlays(c_GameInfoset) except +
stdvector[c_GameNode] GetPlays(c_GameAction) except +
stdvector[c_GameAction] GetOwnPriorActions(c_GameInfoset) except +
bool IsPerfectRecall() except +

c_GameInfoset AppendMove(c_GameNode, c_GamePlayer, int) except +ValueError
Expand Down
31 changes: 31 additions & 0 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,37 @@ class Game:
"""Whether the game is constant sum."""
return self.game.deref().IsConstSum()

def get_own_prior_actions(
self,
infoset: typing.Union[Infoset, str]
) -> typing.List[typing.Optional[Action]]:
"""Return the list of actions which immediately precede `infoset` in the graph of
the player's information set.

An "own prior action" is an action such that, for a given member node of the information
set, it is the action most recently played by the player on the path to that node.
If there is a member node where there is no such action, that is, the player has not
yet played prior to reaching that node, the own prior action is null, which is represented
by `None` in the list of actions returned.

If a member node is not reachable due to the path to the node passing through an
absent-minded information set, that node has no own prior action.

.. versionadded:: 16.5

Returns
-------
list of {Action, None}
The list of the prior actions.
"""
infoset = self._resolve_infoset(infoset, "get_own_prior_actions")
return [
None if not action else Action.wrap(action)
for action in self.game.deref().GetOwnPriorActions(
cython.cast(Infoset, infoset).infoset
)
]

@property
def is_perfect_recall(self) -> bool:
"""Whether the game is perfect recall.
Expand Down
82 changes: 82 additions & 0 deletions tests/test_extensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,88 @@ def test_game_add_players_nolabel():
game.add_player()


@pytest.mark.parametrize(
"game_file, infoset_specification, expected_actions",
[
# ======================================================================
# Game 1: binary_3_levels_generic_payoffs.efg (Perfect Recall)
# ======================================================================
# Player 1's root infoset. Reachable
("binary_3_levels_generic_payoffs.efg", ("Player 1", 0), [None]),
# Player 1's second infoset. Reached after P1's own action 0 ("1").
(
"binary_3_levels_generic_payoffs.efg",
("Player 1", 1),
[("Player 1", 0, 0)],
),
# Player 1's third infoset. Reached after P1's own action 1 ("2").
(
"binary_3_levels_generic_payoffs.efg",
("Player 1", 2),
[("Player 1", 0, 1)],
),
# Player 2's only infoset. Reachable
("binary_3_levels_generic_payoffs.efg", ("Player 2", 0), [None]),
# ======================================================================
# Game 2: wichardt.efg (Imperfect Recall)
# ======================================================================
# The root infoset for Player 1. Reachable
("wichardt.efg", ("Player 1", 0), [None]),
# Player 1's second infoset. It can be reached after either action 0 ("L") or 1 ("R").
(
"wichardt.efg",
("Player 1", 1),
[("Player 1", 0, 0), ("Player 1", 0, 1)],
),
# Player 2's only infoset. Reachable.
("wichardt.efg", ("Player 2", 0), [None]),
# ======================================================================
# Game 3: noPR-action-AM-two-hops.efg (Absent-Mindedness)
# ======================================================================
# Player 1's infoset 0. Has the property of Absent-Mindedness:
# Contains the root vertex and can be further reached via two different prior actions.
(
"noPR-action-AM-two-hops.efg",
("Player 1", 0),
[None, ("Player 1", 0, 0), ("Player 1", 1, 1)],
),
# Player 1's infoset 1. Reached via a single prior action.
("noPR-action-AM-two-hops.efg", ("Player 1", 1), [("Player 1", 0, 0)]),
# Player 2's infoset 0. Reached via a single prior action.
("noPR-action-AM-two-hops.efg", ("Player 2", 0), [None, ("Player 2", 0, 0)]),
# Player 2's infoset 1. This infoset is unreachable.
("noPR-action-AM-two-hops.efg", ("Player 2", 1), []),
],
)
def test_get_own_prior_actions(
game_file: str,
infoset_specification: tuple[str, int],
expected_actions: list,
):
"""
Verifies get_own_prior_actions returns correct sets of actions for various infosets:
root, perfect recall, imperfect recall, absent-minded, and unreachable cases.
"""
game = games.read_from_file(game_file)
player_label, infoset_num = infoset_specification

player = game.players[player_label]
infoset = player.infosets[infoset_num]

result_actions = game.get_own_prior_actions(infoset)

results = [
None if action is None else (
action.infoset.player.label,
action.infoset.number,
action.number,
)
for action in result_actions
]

assert sorted(results, key=str) == sorted(expected_actions, key=str)


@pytest.mark.parametrize("game_input,expected_result", [
# Games with perfect recall from files (game_input is a string)
("e01.efg", True),
Expand Down
Loading