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: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [17.0.0-beta.1] - unreleased

### Added
- Added `Game.has_perfect_recall(player)`, and `GameRep::HasPerfectRecall(const GamePlayer &)`
in C++, reporting whether an individual player has perfect recall. (#1107)

## [17.0.0-alpha.3] - 2026-08-31

### Added
Expand Down
1 change: 1 addition & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Information about the game
Game.is_const_sum
Game.is_tree
Game.is_perfect_recall
Game.has_perfect_recall
Game.players
Game.outcomes
Game.min_payoff
Expand Down
8 changes: 8 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,14 @@ class GameRep : public std::enable_shared_from_this<GameRep> {

/// Returns true if the game is perfect recall
virtual bool IsPerfectRecall() const = 0;
/// Returns true if the player has perfect recall
virtual bool HasPerfectRecall(const GamePlayer &p_player) const
{
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
return true;
}
/// Returns true if the information set is absent-minded
virtual bool IsAbsentMinded(const GameInfoset &p_infoset) const
{
Expand Down
15 changes: 15 additions & 0 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,21 @@ bool GameTreeRep::IsPerfectRecall() const
[](const auto &pair) { return pair.second.size() <= 1; });
}

bool GameTreeRep::HasPerfectRecall(const GamePlayer &p_player) const
{
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
EnsureOwnPriorActions();

// Restriction of the check in IsPerfectRecall() to the information sets belonging to p_player.
return std::all_of(m_ownPriorActionInfo->infoset_map.cbegin(),
m_ownPriorActionInfo->infoset_map.cend(),
[player = p_player.get()](const auto &pair) {
return pair.first->m_player != player || pair.second.size() <= 1;
});
}

bool GameTreeRep::IsAbsentMinded(const GameInfoset &p_infoset) const
{
if (p_infoset->GetGame().get() != this) {
Expand Down
1 change: 1 addition & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class GameTreeRep final : public GameExplicitRep {
bool IsTree() const override { return true; }
bool IsConstSum() const override;
bool IsPerfectRecall() const override;
bool HasPerfectRecall(const GamePlayer &p_player) const override;

/// Returns the smallest payoff to the player in any play of the game
Rational GetPlayerMinPayoff(const GamePlayer &) 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 @@ -324,6 +324,7 @@ cdef extern from "games/game.h":
stdvector[c_GameNode] GetPlays(c_GameInfoset) except +
stdvector[c_GameNode] GetPlays(c_GameAction) except +
bool IsPerfectRecall() except +
bool HasPerfectRecall(c_GamePlayer) except +
bool IsAbsentMinded(c_GameInfoset) except +

c_GameInfoset AppendMove(c_GameNode, c_GamePlayer, stdvector[string]) except +ValueError
Expand Down
36 changes: 36 additions & 0 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,45 @@ class Game:

By convention, games with a strategic representation have perfect recall as they
are treated as simultaneous-move games.

See Also
--------
Game.has_perfect_recall
"""
return self.game.deref().IsPerfectRecall()

def has_perfect_recall(self, player: str) -> bool:
"""Returns whether `player` has perfect recall.

A player has perfect recall if, at each of the player's information sets, every
member node is reached by the same sequence of the player's own prior actions;
that is, the player never forgets an action they took previously, nor information
they previously knew. A game has perfect recall if and only if every player does.

By convention, in games with a strategic representation every player has perfect
recall as such games are treated as simultaneous-move games.

.. versionadded:: 17.0.0

Parameters
----------
player : str
The label of the player.

Raises
------
KeyError
If no player in the game has label `player`.
ValueError
If `player` is an empty string or all whitespace.

See Also
--------
Game.is_perfect_recall
"""
resolved_player = self._resolve_player(player, "has_perfect_recall")
return self.game.deref().HasPerfectRecall(resolved_player)

@property
def min_payoff(self) -> decimal.Decimal | Rational:
"""The minimum payoff to any player in any play of the game.
Expand Down
25 changes: 25 additions & 0 deletions tests/test_extensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ def test_is_perfect_recall(game_input, expected_result: bool):
assert game.is_perfect_recall == expected_result


@pytest.mark.parametrize("game_input,expected_result", [
(gbt.catalog.load("journals/geb/wichardt2008"), {"Player 1": False, "Player 2": True}),
("noPR-information-no-deflate.efg", {"Player 1": True, "Player 2": False}),
("noPR-action-AM.efg", {"Player 1": False, "Player 2": True}),
("stripped_down_poker.efg", {"Alice": True, "Bob": True}),
("gilboa_two_am_agents.efg", {"Player 1": False, "Player 2": True}),
("2x2.agg", {"1": True, "2": True}),
])
def test_has_perfect_recall(game_input, expected_result: dict):
"""
Verify the HasPerfectRecall implementation, for individual players, against games
with and without perfect recall, and in each representation.
"""
game = (games.read_from_file(game_input) if isinstance(game_input, str) else game_input)
assert set(game.players) == set(expected_result)
for player, expected in expected_result.items():
assert game.has_perfect_recall(player) == expected


def test_has_perfect_recall_trivial_game():
game = gbt.Game.new_tree(players=["Alice", "Bob"])
assert game.has_perfect_recall("Alice")
assert game.has_perfect_recall("Bob")


def test_getting_payoff_by_label_string():
game = games.read_from_file("sample_extensive_game.efg")
s1 = game.get_strategies("Player 1")
Expand Down
Loading