Preserve PGN tags on load -> save - #6
Merged
Conversation
Loading a PGN, saving it back, and re-loading the saved file lost
everything except the moves — Event / Site / Date / Round / White /
Black / Result all reset to the engine's defaults ('Casual Game' /
'Chess.py vX.Y.Z' / today / '?' / '?' / '?' / derived). For anyone
loading a famous game and saving it back that's a surprise.
- Adds a public ``pgn_tags: Dict[str, str]`` field on Game (and a
matching copy in the from-existing-game ctor branch). Empty by
default.
- PGNFile.game() now populates it from the pgnparser PGNGame's
attributes for the seven-tag roster plus Annotator, PlyCount,
TimeControl, Time, Termination, and Mode. Only non-empty values
are stored.
- Game.save_pgn() layers tags as: built-in defaults < self.pgn_tags <
caller's ``headers`` argument. So a load -> save now reproduces
the original metadata; a fresh game still uses the defaults; and
a caller can still override anything by passing headers=.
- The PGN-recorded Result wins over the derived one — important for
games that ended by resignation or agreement (and never reached an
on-board mate), where game.result() would otherwise say '*'.
JSON save/load is intentionally untouched; pgn_tags lives on the
Game object but isn't serialized, so a PGN -> JSON -> PGN cycle
still loses the tags. Adding a JSON field is the natural follow-up.
Tests: empty default on fresh game; PGNFile.game() captures all
seven tags from test1.pgn; save_pgn round-trip preserves them and
prefers the PGN Result over derived; caller headers= still wins;
brand-new games still emit defaults.
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.
Summary
Loading a PGN, saving it back, and re-loading the saved file used to lose everything except the moves — Event / Site / Date / Round / White / Black / Result all reset to the engine's defaults. For anyone loading a famous game and saving it back, that's a surprise. This PR preserves the metadata across a load → save round-trip.
Changes
Game.pgn_tags: Dict[str, str]— new public field, defaults to{}on a fresh game; copy semantics in the from-existing-game ctor branch.PGNFile.game()— populatespgn_tagsfrom thepgnparser.PGNGameattributes for the Seven Tag Roster plus Annotator, PlyCount, TimeControl, Time, Termination, and Mode. Only non-empty values are stored.Game.save_pgn()— tags now layer as built-in defaults <self.pgn_tags< callerheaders=. So:Casual Game/Chess.py vX.Y.Z/ today /?/?/?/ derivedResult/ countedPlyCount).headers=can still override anything (used by callers, never invoked from the UI today, but documented behavior).The PGN-recorded
Resultwins over the derived one when both are present — important for games that ended by resignation or agreement (and never reached an on-board mate), wheregame.result()would otherwise say*. For a brand-new game played to mate, the derivedResultstill gets used (nopgn_tagsentry to override it).Scope notes
JSON save/load is intentionally untouched.
pgn_tagslives on the Game object but isn't serialized to JSON, so a PGN → JSON → PGN cycle still loses the tags. Threadingpgn_tagsthroughserializable()/parse_json_file()is the natural follow-up but adds a JSON format change and tests on the JSON path, so it didn't make sense to bundle here.Tests
Five new tests in
tests/test_game.py:Game()haspgn_tags == {}.PGNFile.game(0)againsttest1.pgnpopulates all seven STR tags correctly.Result "1/2-1/2"(the Fischer–Spassky draw — notegame.result()would say*since no mate occurred).headers=overrides both defaults and loaded tags.Casual Game/?/?defaults.Test plan
make test— fast suite (213 passed, 2 deselected) in ~1.4 s.make test-all— full sweep including Carlsen/Nakamura passes in ~7:42. Verified locally.coverage reportaftermake testshows TOTAL 100%.tests/test1.pgn, save as PGN to a new path, diff the two — Event/Site/Date/White/Black/Result/Round all match the input.Casual Game, White/Black are?.https://claude.ai/code/session_01RnQvxtXfn9JFTN8jpNyriL
Generated by Claude Code