Skip to content

Draft of histories operator concept - #1111

Closed
tturocy wants to merge 13 commits into
masterfrom
dev_histories
Closed

Draft of histories operator concept#1111
tturocy wants to merge 13 commits into
masterfrom
dev_histories

Conversation

@tturocy

@tturocy tturocy commented Sep 2, 2026

Copy link
Copy Markdown
Member

No description provided.

tturocy and others added 8 commits September 2, 2026 18:05
First working slice of the H expression-engine design: a game-neutral
Selector built by H.path(*steps)/H.plays, evaluated only when handed to
Game.get_nodes (internal, returns Node) or Game.get_histories (public,
materializes plain History tuples). Reuses Node's existing navigation
(.children, .plays) rather than new C++ traversal code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seed form searches the whole game (via game.nodes) for any node whose
own trailing labels match; chained form is a pure filter over whatever
is already selected. Evaluator now tracks whether a selection has been
seeded yet, since .after's seed candidates differ from .path/.plays's
(root-anchored) default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GroupedSelector wraps a base Selector plus a key function, game-neutral
until Game.get_groups evaluates it into dict[key, list[History]].
HistoryView is what the key callable actually receives: plain sequence
indexing/slicing like a History tuple, plus .last_action(player) --
built by walking Node.parent/.player/.prior_action -- but never exposes
the Node or game it's privately backed by (verified via hasattr checks
inside a probing callback). get_histories refactored to share the new
_history_of helper with get_groups instead of duplicating the walk.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chained-only (no bare H.filter(...) seed -- unlike .after(...), a
predicate has no natural whole-game starting domain). Keeps elements
where predicate(HistoryView) is truthy; complements .after(...)'s
label-pattern matching for anything needing richer navigation like
.last_action(player).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_nodes now resolves a Selector via get_nodes before its usual
node resolution, so both append_move and append_event accept an H
expression directly wherever they took Node/NodeReferenceSet before --
no changes needed to either method's own body for the flat case.
append_move additionally accepts a GroupedSelector (from .by(...)),
dispatching to one append_move call per group via the new
Game._group_nodes helper (shares get_groups's logic, keeping Node
objects instead of materializing Histories, avoiding a round trip).

Verified end-to-end: building Kuhn poker's deal via append_event(H.path(...))
and Alice's three per-card infosets via one
append_move(H.path(...).plays.by(lambda h: h[0]), ...) call, confirming
is_perfect_recall and each infoset's membership.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_node now also accepts a Selector (must resolve to exactly one
node) and a bare History tuple (resolved via Selector().path(*history),
the always-available manual fallback), and _resolve_nodes now excludes
tuple from its "treat as a collection" check the same way it already
excluded str -- so a single History isn't misread as several
single-label node references. append_infoset gets Selector support in
both its nodes and infoset arguments for free through this; make_outcome
gets it through location's existing _resolve_nodes path.

Verified end-to-end: the Absent-Minded Driver (append_move(H.path(),...),
append_infoset(H.path("S"), H.path()), make_outcome at H.path("S","S")
etc.) reproduces is_perfect_recall == False and the expected two-member
root infoset; and passing a get_groups(...) group (a list[tuple]) straight
to make_outcome works, matching the checkpointed Kuhn poker outcomes
pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GroupedSelector gains .plays/.after(...) (per-group expand/filter,
key untouched) and .with_recall(player), which -- from that point on
-- makes every subsequent .plays also refine each group's key by
folding in player's last action at that point (via the new shared
_last_action helper). Scoped to .plays specifically for now, not every
expand-style op. Game._group_nodes applies a GroupedSelector's post_ops
in order, per-group, doing the recall refinement when with_recall set a
player.

Surfaced and fixed a real edge case along the way: filtering after a
recall-refined .plays can produce empty groups (e.g. a card's "bet
first" branch has no "...,Check,Bet" suffix) -- append_move's
per-group dispatch now skips empty groups rather than erroring,
settling an earlier open design question in favor of "dropped".

Verified end-to-end: the full Kuhn poker betting tree, including
Alice's second decision reusing a with_recall-tagged partition,
reproduces is_perfect_recall == True and the correct 3-infosets-of-2
structure -- the same result the tuple-pivot phase needed a manual
get_last_action+get_infoset dance to achieve, now automatic on reuse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
doc/tutorials/advanced_tutorials/h_selector_prototype.ipynb -- a design
prototype, not a released feature, demonstrating the H selector algebra
sketched on this branch: Selten's Horse, Kuhn poker (construction +
outcomes), bayes2a (a regular two-stage Bayesian game needing neither
with_recall nor append_infoset), a new minimal example of imperfect
recall via forgetting a past observation (distinct in shape from
absent-mindedness and untimeability), the Jakobsen et al. (2016)
untimeable game, and an extended Absent-Minded Driver showing
append_infoset composes normally with further construction.

Every cell actually executed (via jupyter nbconvert --execute) against
the real built module, not hand-traced -- outputs are real, including
cross-checks against the original .efg/catalog files for Selten's
Horse, bayes2a, and the AM-driver-subgame fixture (infoset partitions
and outcome maps verified to match exactly before being folded into the
notebook's own diagnostics).

Also: pip install -e . to make src/pygambit importable directly by the
notebook's Jupyter kernel, which doesn't inherit PYTHONPATH the way a
shell subprocess does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

tturocy and others added 5 commits September 2, 2026 19:47
test_execute_notebook builds nbclient.NotebookClient with
kernel_name=nb.metadata["kernelspec"]["name"] -- our notebook never
had that key set (built via raw nbformat.v4.new_notebook(), which
doesn't populate it the way Jupyter's own UI or an already-kernelspec'd
source notebook would), so kernel_name came through as None, which
traitlets rejects outright before any cell runs.

Verified against the actual CI entry point, not just nbconvert
--execute succeeding: ran pytest tests/test_tutorials.py -k
h_selector_prototype -m tutorials directly, matching CI's own
NotebookClient construction. Passes, and the rest of the tutorials
suite (all 10 notebooks) still passes too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…me_null

_resolve_outcome_location already delegated to _resolve_nodes for tree
games, which handles Node, History (tuple), Selector, and iterables of
these -- so make_outcome/make_outcome_null already worked with H-built
selectors and materialized histories, just undocumented and untested.
Updates the docstrings and adds coverage for both call sites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
….members

Mirrors Node.members but returns each member's History (a plain tuple)
rather than a Node, so a .by(...)/.filter(...) callable can reason about
infoset/event membership -- e.g. build a grouping key from it -- without
ever touching Node, Infoset, or Event.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Game.root and Game.nodes are removed from the public API -- H selectors
and materialized History tuples are now the only entry points into a
tree. Game.nodes's only real job was letting calling code build ad hoc
selections by hand, which H does directly, so it has no replacement;
Game.root is superseded by H.path()/(). Internally, get_nodes()'s
H.after(...) whole-game seed and _resolve_node()'s label lookup now
route through new private Game._root_node()/_all_nodes() helpers
instead of the public properties. GameNodes (which only ever backed
Game.nodes) is deleted outright. layout_tree()'s own Node-keyed dict
return type is untouched, so its known external consumer (gtdraw) is
not affected by this specific change -- though see below.

MixedBehaviorProfile.__getitem__/__setitem__/set_mixed_action and
MixedBehavior.__getitem__ now take a History (tuple of action labels)
instead of a Node, like-for-like -- same resolution semantics, routed
through Game._resolve_infoset (already History-capable). Node gains a
new `.history` property as the bridge for code that still holds a Node
(e.g. from Game.get_infosets/get_events) and needs to index a profile
with it. BehaviorSupportProfile's indexing already accepted a History
tuple-shaped resolution path once _resolve_infoset_arg was widened, so
it picked this up the same way.

Fixed three production call sites that broke under their own change:
Game._fill_behavior_profile, Game.random_behavior_profile, and
src/pygambit/cli/common.py's starting-profile reader all still indexed
a MixedBehaviorProfile by Node; now use node.history. Added
Game._num_nodes() (an O(1) count via the C++ layer) so catalog.py's
n_nodes filter didn't regress into materializing every node just to
count them.

Updated every test file that constructed or indexed via game.root/
game.nodes (games.py gained root_node()/all_nodes()/history_of() test
helpers backing this across the suite) or indexed a MixedBehaviorProfile
by Node. Deleted test_strategic_game_root/test_strategic_game_nodes
(tested the removed properties' error behavior on strategic games, no
longer applicable) and test_nodes_iteration_order (tested Game.nodes's
own DFS ordering guarantee, which no longer exists). Full suite green
(1898 passed).

Not yet done, flagged rather than silently skipped: several tutorial
notebooks (02_extensive_form, 03_stripped_down_poker, h_selector_prototype,
agent_versus_non_agent_regret, openspiel) call game.root/game.nodes
directly and now fail to execute; 04_creating_images additionally fails
via gtdraw (an external package), which calls game.root directly in its
own layout code -- a real break for that consumer, not just a
theoretical risk, and needs a decision on how to handle before touching
notebooks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
02_extensive_form.ipynb, 03_stripped_down_poker.ipynb, and
interoperability_tutorials/openspiel.ipynb all built games via
game.root/game.root.children[...] chains, and the poker notebook
also indexed a MixedBehaviorProfile by Node (eqm[node]/eqm[bob_node]).
Switched construction to H.path(...)/H.path(...)-with-wildcards and
profile indexing to node.history, matching the API changes landed in
07146c3.

Verified two ways: (1) built each fixed game programmatically and
diffed its outcome/infoset structure against a reconstruction using
the old Node-based navigation (via a get_nodes(H.path())[0] stand-in
for the removed game.root) -- identical in every case; (2) executed
full copies of the notebooks end-to-end (--allow-errors, outside the
repo) and confirmed every remaining error is a `draw(...)` cell
failing inside gtdraw itself (an external package -- flagged
separately, not fixed here), not a pygambit-API cell.

Three other notebooks (h_selector_prototype, agent_versus_non_agent_
regret, 04_creating_images) have no in-repo API usage to fix -- they
fail solely through gtdraw's own game.root call in its layout code,
so there's nothing to change here; they'll pass once gtdraw is fixed
upstream.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tturocy

tturocy commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

This has been superseded by a different line of organising this work.

@tturocy tturocy closed this Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant