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
21 changes: 19 additions & 2 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,7 @@ void GameTreeRep::BuildSubgameRoots() const

// Phase 2: Reachability and detection
struct BridgeVisitor {
const std::unordered_map<GameNodeRep *, Range> &m_disc;
std::unordered_map<GameNodeRep *, Range> &m_disc;
const std::unordered_map<GameInfosetRep *, Range> &m_hull;
std::vector<GameNodeRep *> &m_subgames;
std::unordered_map<GameNodeRep *, Range> m_low;
Expand All @@ -1210,10 +1210,27 @@ void GameTreeRep::BuildSubgameRoots() const

for (const auto &child : p_node->GetChildren()) {
low.Merge(m_low.at(child.get()));
m_low.erase(child.get());
}

if (low == m_disc.at(node)) {
m_subgames.push_back(node);
// The `low == disc` test is exact only with distinct terminal spans. A single-action
// chain above a candidate node collapses the spans and can create false positives.
// Reject a node if some single-action ancestor's infoset (possibly the node's own)
// has a member in the node's subtree (possibly the node itself).
// Note that such an infoset is necessarily absent-minded.
bool spurious = false;
for (auto *anc = node->m_parent; anc && anc->m_children.size() == 1 && !spurious;
anc = anc->m_parent) {
const auto &members = anc->m_infoset->m_members;
spurious = members.size() >= 2 &&
std::any_of(members.begin(), members.end(), [&](const auto &member) {
return member.get() != anc && member->IsSuccessorOf(p_node);
});
}
if (!spurious) {
m_subgames.push_back(node);
}
}

return DFSCallbackResult::Continue;
Expand Down
17 changes: 17 additions & 0 deletions tests/test_games/AM-unary-branches.efg
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
EFG 2 R "Unary chain with AM-infosets: Example 2" { "Player 1" "Player 2" }
"A tailored edge case for the interval Tarjan subgame detector:
low == disc is an exact bridge test only when nodes have distinct terminal spans.
On this single-action chain an absent-minded infoset collapses the spans, so the
Player 1 branching node passes the low == disc test but is not a subgame root."

p "" 2 1 "" { "T" "B" } 0
p "" 1 1 "" { "1" } 0
p "" 1 1 "" { "1" } 0
p "" 1 2 "" { "1" } 0
p "" 2 2 "" { "1" } 0
p "" 1 3 "" { "t" "b" } 0
p "" 2 2 "" { "1" } 0
t "" 1 "Outcome 1" { 1, -1 }
t "" 2 "Outcome 2" { 2, -2 }
p "" 1 2 "" { "1" } 0
t "" 3 "Outcome 3" { 3, -3 }
19 changes: 19 additions & 0 deletions tests/test_games/AM-unary-hops.efg
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
EFG 2 R "Unary chain with AM-infosets: Example 1" { "Player 1" "Player 2" }
"A tailored edge case for the interval Tarjan subgame detector:
low == disc is an exact bridge test only when nodes have distinct terminal spans.
This single-action chain carries several absent-minded infosets that collapse the spans,
so several nodes pass the low == disc test without being subgame roots: among them an
interior chain node straddled by one absent-minded set while it is the upper member of
another, and the Player 1 branching node straddled by a member nested below it.
The only true subgame roots are the game root and the two Player 2 nodes."

p "" 1 1 "" { "1" } 0
p "" 1 1 "" { "1" } 0
p "" 2 1 "" { "1" } 0
p "" 1 2 "" { "1" } 0
p "" 2 1 "" { "1" } 0
p "" 1 3 "" { "T" "B" } 0
p "" 2 2 "" { "1" } 0
t "" 1 "Outcome 1" { 1, -1 }
p "" 1 2 "" { "1" } 0
t "" 2 "Outcome 2" { 2, -2 }
14 changes: 14 additions & 0 deletions tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ class SubgameRootsTestCase:
),
id="Absent-minded-game-with-paths-intersecting-infoset-three-times"
),
pytest.param(
SubgameRootsTestCase(
factory=functools.partial(games.read_from_file, "AM-unary-hops.efg"),
expected_paths=[[], ["1", "1"], ["T", "1", "1", "1", "1", "1"]]
),
id="Absent-minded-game-with-paths-intersecting-infoset-two-times"
),
pytest.param(
SubgameRootsTestCase(
factory=functools.partial(games.read_from_file, "AM-unary-branches.efg"),
expected_paths=[[], ["1", "1", "1", "T"]]
),
id="Absent-minded-game-with-paths-intersecting-infoset-two-times"
),
]


Expand Down
Loading