Skip to content
Open
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 `IsAbsentMinded()` (C++) and `is_absent_minded` (Python) to detect
if a game has absent-mindedness (a player, moving along a single path of play,
can re-enter an information set visited previously).

### 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 @@ -98,6 +98,7 @@ Information about the game
Game.comment
Game.is_const_sum
Game.is_tree
Game.is_absent_minded
Game.is_perfect_recall
Game.players
Game.outcomes
Expand Down
2 changes: 2 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,8 @@ 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 game has at least one absent-minded infoset
virtual bool IsAbsentMinded() const = 0;
//@}

/// @name Writing data files
Expand Down
1 change: 1 addition & 0 deletions src/games/gameagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class GameAGGRep : public GameRep {
bool IsTree() const override { return false; }
bool IsAgg() const override { return true; }
bool IsPerfectRecall() const override { return true; }
bool IsAbsentMinded() const override { return false; }
bool IsConstSum() const override;
/// Returns the smallest payoff to any player in any outcome of the game
Rational GetMinPayoff() const override { return Rational(aggPtr->getMinPayoff()); }
Expand Down
1 change: 1 addition & 0 deletions src/games/gamebagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class GameBAGGRep : public GameRep {
bool IsTree() const override { return false; }
virtual bool IsBagg() const { return true; }
bool IsPerfectRecall() const override { return true; }
bool IsAbsentMinded() const override { return false; }
bool IsConstSum() const override { throw UndefinedException(); }
/// Returns the smallest payoff to any player in any outcome of the game
Rational GetMinPayoff() const override { return Rational(baggPtr->getMinPayoff()); }
Expand Down
1 change: 1 addition & 0 deletions src/games/gametable.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class GameTableRep : public GameExplicitRep {
bool IsTree() const override { return false; }
bool IsConstSum() const override;
bool IsPerfectRecall() const override { return true; }
bool IsAbsentMinded() const override { return false; }
//@}

/// @name Dimensions of the game
Expand Down
15 changes: 15 additions & 0 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,19 @@ bool GameTreeRep::IsConstSum() const
}
}

bool GameTreeRep::IsAbsentMinded() const
{
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
}

if (GetRoot()->IsTerminal()) {
return true;
}

return !m_absentMindedInfosets.empty();
}

bool GameTreeRep::IsPerfectRecall() const
{
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
Expand Down Expand Up @@ -800,6 +813,7 @@ void GameTreeRep::ClearComputedValues() const
}
const_cast<GameTreeRep *>(this)->m_nodePlays.clear();
const_cast<GameTreeRep *>(this)->m_infosetParents.clear();
const_cast<GameTreeRep *>(this)->m_absentMindedInfosets.clear();
m_computedValues = false;
}

Expand Down Expand Up @@ -900,6 +914,7 @@ void GameTreeRep::BuildInfosetParents()
m_infosetParents[child->m_infoset].insert(prior_action ? prior_action.get() : nullptr);

if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
m_absentMindedInfosets.insert(child->m_infoset);
const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this());
position.emplace(AbsentMindedEdge{replay_action, child});
}
Expand Down
2 changes: 2 additions & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class GameTreeRep : public GameExplicitRep {
std::size_t m_numNonterminalNodes = 0;
std::map<GameNodeRep *, std::vector<GameNodeRep *>> m_nodePlays;
std::map<GameInfosetRep *, std::set<GameActionRep *>> m_infosetParents;
std::set<GameInfosetRep *> m_absentMindedInfosets;

/// @name Private auxiliary functions
//@{
Expand Down Expand Up @@ -74,6 +75,7 @@ class GameTreeRep : public GameExplicitRep {
bool IsTree() const override { return true; }
bool IsConstSum() const override;
bool IsPerfectRecall() const override;
bool IsAbsentMinded() const override;
//@}

/// @name Players
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 +
bool IsAbsentMinded() except +
bool IsPerfectRecall() except +

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

@property
def is_absent_minded(self) -> bool:
"""Whether the game has absent-mindedness.

By convention, games with a strategic representation have perfect recall as they
are treated as simultaneous-move games, thus, they do not have absent-mindedness.
"""
return self.game.deref().IsAbsentMinded()

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


@pytest.mark.parametrize("game_input,expected_result", [
# Games without absent-mindedness (includes all games with perfect recall)
("e01.efg", False),
("e02.efg", False),
("cent3.efg", False),
("poker.efg", False),
("basic_extensive_game.efg", False),
("wichardt.efg", False), # forgetting past action; Wichardt (GEB, 2008)
("noPR-action-selten-horse.efg", False), # forgetting past action
("noPR-information-no-deflate.efg", False), # forgetting past information
("gilboa_two_am_agents.efg", False), # forgetting past information; Gilboa (GEB, 1997)

# Generated games without absent-mindedness
# - Centipede games
(games.Centipede.get_test_data(N=3, m0=2, m1=7)[0], False),
(games.Centipede.get_test_data(N=4, m0=2, m1=7)[0], False),
# - Two-player binary tree games
(games.BinEfgTwoPlayer.get_test_data(level=3)[0], False),
(games.BinEfgTwoPlayer.get_test_data(level=4)[0], False),
# - Three-player binary tree games
(games.BinEfgThreePlayer.get_test_data(level=3)[0], False),
# - One-player imperfect recall binary tree games
(games.BinEfgOnePlayerIR.get_test_data(level=3)[0], False),
(games.BinEfgOnePlayerIR.get_test_data(level=4)[0], False),


# Games with absent-mindedness (a subset of games with imperfect recall)
("noPR-AM-driver-one-player.efg", True), # 1 players, one infoset unreached
("noPR-AM-driver-two-players.efg", True), # 2 players, one infoset unreached
("noPR-action-AM.efg", True), # 2 players + forgetting past action; P1 has one infoset
("noPR-action-AM2.efg", True), # 2 players + forgetting past action; P1 has >1 infoset
("noPR-action-AM-two-hops.efg", True), # 2 players, one AM-infoset each
])
def test_is_absent_minded(game_input, expected_result: bool):
"""
Verify the IsAbsentMinded implementation against a suite of games
with and without the absent-mindedness property.
"""
game = None
if isinstance(game_input, str):
game = games.read_from_file(game_input)
elif isinstance(game_input, gbt.Game):
game = game_input
else:
pytest.fail(f"Unknown type for game_input: {type(game_input)}")

assert game.is_absent_minded == expected_result


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