Repository navigation
ENH: Implement NFGReader and NFGWriter - #978
Merged
Merged
Conversation
Generalize the intermediate representation between a NormalFormGame and the flat payoff list of a game file to the two orders in which game files list the payoffs: `PayoffVector` stores the payoffs in a 1-dim array in the order `layout`, 'player-major' as in .gam or 'profile-major' as in the upcoming .nfg support, with the layout read in one place, `_player_block`. `to_layout` converts between the orders. This is for the upcoming reader and writer for the Gambit .nfg format. The conversions to and from a game are renamed `from_normal_form_game` and `to_normal_form_game`. Also compute the expected number of payoffs with `math.prod`, which does not overflow, and copy the payoffs in `to_normal_form_game`, so that the game does not alias the input for a one-player game. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…iters Move `from_file`, `from_url`, and `from_string` to a base class `_Reader` and `to_file` and `to_string` to a base class `_Writer`, so that a reader or writer for another format defines only `_parse` or `_dump`; move the formatting of the payoffs of `GAMWriter` to `_format_payoffs`. This is for the upcoming support of the Gambit .nfg format. No change in behavior. The inherited entry points are listed in the documentation of `GAMReader` and `GAMWriter` with `:inherited-members:`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add `NFGReader` and `from_nfg`, `from_nfg_string`, `from_nfg_url`. The text is tokenized with a regular expression, the brace structure is read into nested lists (the Lisp reader), and the lists are interpreted by position: both the payoff version and the outcome version are read, the numbers of strategies or their names are accepted, and the title, the names, and the comment are ignored. The payoffs are in profile-major order, so the reader ends in `PayoffVector(..., layout='profile-major')`. Validation is minimal: a file not starting with `NFG` is rejected, and other malformed input fails where it is used. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add `NFGWriter` and `to_nfg`, which write the payoff version with an empty title and the players named "1", ..., "N". The payoffs are those of `PayoffVector.from_normal_form_game(g, layout='profile-major')`, formatted as in `to_gam`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The width of the null outcome was taken from the array of the outcomes listed, which is empty for a file with the null outcome at every action profile. Take it from the number of players instead; as a side effect, an outcome with a wrong number of payoffs is rejected by NumPy when the array is built. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With `np.prod` the expected number of payoffs wraps around to 0, so an input of length 2 was rejected by the overflowing implementation as well; an input of length 0 is accepted by it and rejected by the current one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The comment named the action profile with the null outcome as (2, 1), which is wrong in either indexing convention; it is the fifth profile. Say so without indices, since the file is shared with GameTheory.jl. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a definition list of the eight public reader and writer functions to the module docstring, above the examples. Rename _read_tree to _read_from_tokens, after Norvig, so that "tree" stays free for the game tree of a future .efg reader, and explain the Lisp-reader design in its docstring (parse the braces only, leave the meaning to the caller) with a reference to Norvig's read_from_tokens. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop the hand-added `:inherited-members:` option, which the docs generator does not emit; the docs CI job requires docs/source to match the output of `python qe_apidoc.py`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Negative outcome indices are silently misinterpreted, and inherited converter methods would disappear from generated API documentation.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds Gambit .nfg import/export support alongside existing .gam conversion.
Changes:
- Introduces layout-aware
PayoffVectorand shared reader/writer bases. - Adds and exports NFG readers, writers, and convenience functions.
- Adds fixtures and comprehensive conversion tests.
| File | Description |
|---|---|
quantecon/game_theory/game_converters.py |
Implements shared conversion infrastructure and NFG support. |
quantecon/game_theory/__init__.py |
Exports the new NFG APIs. |
quantecon/game_theory/tests/test_game_converters.py |
Tests layouts, parsing, writing, and round trips. |
quantecon/game_theory/tests/game_files/3x2_payoff.nfg |
Adds a payoff-format fixture. |
quantecon/game_theory/tests/game_files/3x2_outcome.nfg |
Adds an outcome-format fixture. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Building the table from a nested list with a row of Python zeros let NumPy infer the dtype from the zeros as well, so a uint64 table of payoffs beyond int64 was promoted to float64 and rounded. Infer the dtype from the explicit outcomes and add the null row in that dtype. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The escape alternative `\.` of the token pattern did not match a newline, so a quoted string containing a backslash followed by a newline broke into several tokens. Compile the pattern with DOTALL. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
oyamad
added a commit
to QuantEcon/GameTheory.jl
that referenced
this pull request
Oct 3, 2026
The escape alternative `\.` of the token pattern did not match a newline, so a quoted string containing a backslash followed by a newline broke into several tokens. Compile the pattern with the `s` flag, as in QuantEcon/QuantEcon.py#978. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…backslash The token pattern paired backslashes, so that a quote after two backslashes ended the string. The format defines only `\"` as an escape, the backslash being an ordinary character otherwise: consume a run of backslashes together with the character that follows. The new pattern also covers a backslash followed by a newline, so DOTALL is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mmcky
approved these changes
Oct 7, 2026
mmcky
left a comment
Contributor
There was a problem hiding this comment.
Approved. CI is green, the branch merges cleanly onto main (also with #979 stacked on top), and the .nfg reader and writer check out against the Gambit format documentation. The internal GAMPayoffVector rename is safe, since nothing outside its own tests referenced it. Merging.
oyamad
added a commit
to QuantEcon/GameTheory.jl
that referenced
this pull request
Oct 8, 2026
* ENH: Add NFGPayoffVector via layout-parametrized PayoffVector
Generalize GAMPayoffVector into a single struct PayoffVector{L,N,T},
where the singleton type L <: PayoffLayout selects the ordering of the
flat payoff vector:
- PlayerMajor: player-major blocks, profiles in column-major order
within each block (GameTracer .gam ordering)
- ProfileMajor: profile-major blocks in column-major order, players in
order within each block (Gambit .nfg ordering)
GAMPayoffVector{N,T} and NFGPayoffVector{N,T} are aliases for the two
layouts. The layout-dependent code is confined to _player_block, which
returns a strided view of one player's payoffs; the constructors from a
NormalFormGame, the conversion back, and a new constructor converting
between layouts are each written once and make no intermediate copies.
Tests mirror the existing GAMPayoffVector testset for NFGPayoffVector,
add a hand-derived N=2 golden case covering both layouts, and check
alias identity, layout conversion, and that _player_block is a view.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ENH: Accept a PayoffVector of any layout in write_gam and gam_string
A `PayoffVector` that is not player-major is converted with
`GAMPayoffVector(p)` before writing; the method for `GAMPayoffVector`
remains the serializer. Also define `Base.convert` between
`PayoffVector` types as for `Player` and `NormalFormGame`, returning
the input itself if it already has the layout and eltype, together
with the conversion constructor `PayoffVector{L,N,T}(p)` it relies on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* FIX: Reject an empty nums_actions in the PayoffVector constructor
The check that every number of actions is positive is vacuous for an
empty tuple, so that a zero-player `PayoffVector` could be constructed
and written, which the reader then rejects.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ENH: Add a reader for the Gambit .nfg format
Add `read_nfg` and `parse_nfg`, with the optional element type as in
`read_gam`. The text is tokenized with a regular expression, the brace
structure is read into nested vectors (the Lisp reader) one item at a
time, and the items are interpreted by position: both the payoff
version and the outcome version are read, the numbers of actions or
their names are accepted, and the title, the names, and the comment are
ignored. The payoffs, in profile-major order, stay a view of the token
vector and go through the parser shared with `read_gam`, so that the
reader ends in `NormalFormGame(NFGPayoffVector(nums_actions, payoffs))`.
Validation is minimal: an input not starting with `NFG` is rejected,
and other malformed input fails where it is used.
The two test files are shared with QuantEcon.py.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ENH: Add a writer for the Gambit .nfg format
Add `write_nfg` and `nfg_string`, which write the payoff version with
an empty title and the players named "1", ..., "N", for a
`NormalFormGame` or a `PayoffVector` of any layout; the method for
`NFGPayoffVector` is the serializer, and the payoffs are written with
`_print_payoff` as in `write_gam`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* DOC: Explain the design of _read_from_tokens in its comment
Rename _read_tree to _read_from_tokens, after Norvig, so that "tree"
stays free for the game tree of a future .efg reader, and explain the
Lisp-reader design in its comment (parse the braces only, leave the
meaning to the caller) with a reference to Norvig's read_from_tokens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* FIX: read_nfg: Accept a backslash-newline inside quoted text
The escape alternative `\.` of the token pattern did not match a
newline, so a quoted string containing a backslash followed by a
newline broke into several tokens. Compile the pattern with the `s`
flag, as in QuantEcon/QuantEcon.py#978.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* FIX: Compute the expected payoff length of PayoffVector without overflow
`prod(nums_actions) * N` wraps for large numbers of actions, so that
`(2^62, 2^62)` gave an expected length of 0 and an empty payoff vector
was accepted. Compute the product in BigInt, as the Python version does
with `math.prod`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* FIX: read_nfg: Reject an outcome without exactly N payoffs
The outcome rows were flattened before being reshaped, so outcomes
with the wrong numbers of payoffs whose total fitted the table were
accepted and their values moved between outcomes. Check the length of
each outcome first, as the array construction in the Python version
does implicitly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* FIX: read_nfg: End a quoted string only at a quote not preceded by a backslash
The token pattern paired backslashes, so that a quote after two
backslashes ended the string. The format defines only `\"` as an
escape, the backslash being an ordinary character otherwise: consume a
run of backslashes together with the character that follows. The new
pattern also covers a backslash followed by a newline, so the `s` flag
is dropped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* DOC: Describe rational numbers in the docstrings of the .gam functions
`read_gam` and `parse_gam` infer `Rational{BigInt}` for an input with
rationals, and `write_gam` accepts a `Rational` type, as the .nfg
functions do through the shared parser and printer. Also say in
`write_gam` and `write_nfg` that a `Rational` is written as `n/d`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* TEST: Add large-integer, one-player ownership, and float boundary cases
- A selected null outcome with outcomes inferred as `BigInt` or
`Rational{BigInt}` keeps that element type.
- A one-player game built from a payoff vector does not share memory
with it.
- Subnormal, extreme, and adjacent floats and signed zeros round-trip
exactly through both formats.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* DOC: Mention the .nfg format in the agent instructions
The code organization list described `game_converters.jl` as the reader
and writer for the GameTracer .gam format only; it now also holds the
reader and writer for the Gambit .nfg format.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


.nfgfile format, based on its public documentation..nfginterpreter. This one is a revised version generated by Claude Code Fable 5.1.Detailed description by Claude Code follows:
Summary
Adds
from_nfg,from_nfg_string,from_nfg_url, andto_nfg, which read and write normal form games in the Gambit.nfgformat, alongside the existing.gamfunctions. The reader handles both versions of the format, the payoff version (a flat list of payoffs, player 1 varying fastest) and the outcome version (a table of named outcomes followed by one outcome index per action profile, with index 0 meaning zero payoffs). The title, the names of the players, actions, and outcomes, and the comment are read and ignored. The writer emits the payoff version with an empty title and players named"1", ...,"N".Changes, by commit
PayoffVectorreplacesGAMPayoffVectoras the intermediate representation between aNormalFormGameand a flat list of payoffs. It carries alayoutargument,'player-major'(the.gamorder: all payoffs to player 0, then player 1, ...) or'profile-major'(the.nfgorder: the N payoffs at the first action profile, then the second, ...), withfrom_normal_form_game,to_normal_form_game, andto_layoutto convert between them.GAMPayoffVectorwas never exported, so this is not a breaking change._Reader(from_file,from_url,from_string) and_Writer(to_file,to_string) base classes; each format implements only_parseand_dump. The.gamclasses now use them.NFGReader. A regular-expression tokenizer (quoted strings with\"escapes, braces, bare tokens; commas are separators), then a recursive-descent reader that turns the token stream into nested lists by the braces alone, in the manner of a Lisp reader, and finally interpretation of the nested lists by position. The version is detected from the first token after the prologue. Numbers are parsed asintwhen written as integers and asfloatotherwise; a rational such as1/3is converted to the nearest float throughfractions.Fraction.NFGWriterand the module-levelto_nfg(g, file_path=None), which writes to a file or returns the text. Floats are written withnumpy.format_float_positional, so no digits are lost.PayoffVectornow fails if the overflow check regresses; the fixture comment names the null-outcome profile in a way that is correct in both 0-based and 1-based conventions.qe_apidoc.pywithout:inherited-members:, so the reader and writer class pages show their docstrings only; the inheritedfrom_file/from_url/from_string/to_file/to_stringmethods are not rendered.Validation policy
Validation is minimal by design: these files are almost always written by programs, so the reader checks what it needs to build the game and does not try to catch every malformed input. Payoff and index counts that do not match the declared numbers of actions raise an error; names and comments are not validated.
Implementation approach
The reader is organized in the textbook way: a lexer, a parser, and an interpreter. What characterizes it is the parser, which is the classic S-expression reader, the
readof every Lisp, applied to a bracket language with braces in place of parentheses.Lexer. One regular expression,
_NFG_TOKEN, cuts the text into quoted strings (with\"escapes), braces, and bare tokens; whitespace and commas are separators and are dropped.Parser.
_read_from_tokens(tokens, pos)is a recursive-descent parser for the one-rule grammar of nested brackets,It returns the item starting at
pos, either the token itself or a nested list built by calling itself up to the matching}, together with the position after it. The grammar has a single self-referential rule, so recursive descent collapses to this one function, which knows nothing of.nfgbeyond the two braces and turns the whole file into a generic tree of nested lists. It is Norvig'sread_from_tokens, with an index in place ofpop(0), so that parsing is linear rather than quadratic in the file size, and with an outer pair of braces so that the file reads as one list.Interpreter.
NFGReader._parsegives the generic tree its meaning by position: the prologue items, the players, the action counts, an optional comment, then the body, whose first item, a{or a number, distinguishes the outcome version from the payoff version. Numbers go through_str2num, outcome indices through the outcome table, and the result is a profile-majorPayoffVector.This is Lisp's
read/evalsplit: the parser parses only the brackets, and all knowledge of the format sits in the interpreter. A parser written for.nfgdirectly would have one rule per part of the file and would catch structural errors as it met them; the Lisp-reader approach has a ten-line, format-agnostic parser and catches those errors late or, under the minimal-validation policy above, not at all. Since the parts of an.nfgfile are recognized by position, the shorter approach suffices.Tests
33 tests in
test_game_converters.py:PayoffVectorin both layouts, round trips through the layouts, the.gamtests, and for.nfgthe payoff and outcome versions, the null-only outcome file, string and file round trips, cross-format round trips with.gam, invalid inputs, and the shared fixtures.🤖 Generated with Claude Code (Claude Fable 5.1)