From e595b877185e86a5368b747c62d332254ab24e44 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 24 Jul 2026 03:05:01 +0700 Subject: [PATCH 1/2] Cross-validate item appearance tables against the C reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instrumented the C game on modern-rogue with a DUMP mode (patch in testdata/c_seedcompat.patch) that forces the RNG seed and prints the per-seed item appearance tables — potion colors, scroll names, ring stones, wand/staff materials — in the normal init order, before initscr so no terminal is needed. Captured its output for four seeds as testdata/item_tables.golden. TestSeedCompatItemTables regenerates the same tables from the Go port via New(Params{Seed, Wizard: true}) and checks they match the golden byte for byte. They do, for all four seeds — proving the LCG and its consumption order through the whole init sequence (init_probs → init_player → init_names → init_colors → init_stones → init_materials, including init_player's arrow rnd(8)+rnd(15)) agree with C exactly. testdata/README.md documents how to regenerate the golden. --- game/seedcompat_test.go | 98 ++++++++++++ game/testdata/README.md | 28 ++++ game/testdata/c_seedcompat.patch | 37 +++++ game/testdata/item_tables.golden | 260 +++++++++++++++++++++++++++++++ 4 files changed, 423 insertions(+) create mode 100644 game/seedcompat_test.go create mode 100644 game/testdata/README.md create mode 100644 game/testdata/c_seedcompat.patch create mode 100644 game/testdata/item_tables.golden diff --git a/game/seedcompat_test.go b/game/seedcompat_test.go new file mode 100644 index 0000000..d3b5def --- /dev/null +++ b/game/seedcompat_test.go @@ -0,0 +1,98 @@ +package game + +import ( + "fmt" + "os" + "strings" + "testing" +) + +// dumpItemTables formats a game's per-seed item appearance tables in the +// same layout the instrumented C reference prints: the potion colors, +// scroll names, ring stones, and wand/staff materials, each generated by +// consuming the RNG in a fixed order during New(). +func dumpItemTables(seed int32, g *RogueGame) string { + var b strings.Builder + + fmt.Fprintf(&b, "SEED %d\n", seed) + + fmt.Fprintln(&b, "POTIONS") + + for _, c := range g.Items.PotColors { + fmt.Fprintln(&b, c) + } + + fmt.Fprintln(&b, "SCROLLS") + + for _, s := range g.Items.ScrNames { + fmt.Fprintln(&b, s) + } + + fmt.Fprintln(&b, "RINGS") + + for _, s := range g.Items.RingStones { + fmt.Fprintln(&b, s) + } + + fmt.Fprintln(&b, "STICKS") + + for i := range g.Items.WandType { + fmt.Fprintf(&b, "%s %s\n", g.Items.WandType[i], g.Items.WandMade[i]) + } + + return b.String() +} + +// TestSeedCompatItemTables proves the port's seed-compatibility claim: for +// the same seed, the Go game generates the exact per-seed item appearance +// tables as the C reference on modern-rogue. That requires the LCG and its +// consumption order through the whole init sequence (init_probs → +// init_player → init_names → init_colors → init_stones → init_materials) to +// match C byte for byte. The golden is captured from an instrumented build +// of the C game (testdata/README.md). +func TestSeedCompatItemTables(t *testing.T) { + golden, err := os.ReadFile("testdata/item_tables.golden") + if err != nil { + t.Fatalf("read golden: %v", err) + } + + // These must match the seeds the golden was generated from + // (testdata/README.md). + seeds := []int32{1, 42, 12345, 99999} + + var got strings.Builder + + for _, seed := range seeds { + g := New(Params{Seed: seed, Wizard: true}) + got.WriteString(dumpItemTables(seed, g)) + } + + if got.String() != string(golden) { + t.Errorf("Go item tables diverge from the C reference at %s", + firstDiff(string(golden), got.String())) + } +} + +// firstDiff returns a description of the first line where want and got +// differ, for a readable failure. +func firstDiff(want, got string) string { + wl := strings.Split(want, "\n") + gl := strings.Split(got, "\n") + + for i := 0; i < len(wl) || i < len(gl); i++ { + w, g := "", "" + if i < len(wl) { + w = wl[i] + } + + if i < len(gl) { + g = gl[i] + } + + if w != g { + return fmt.Sprintf("line %d: C=%q Go=%q", i+1, w, g) + } + } + + return "no line difference (trailing content?)" +} diff --git a/game/testdata/README.md b/game/testdata/README.md new file mode 100644 index 0000000..9867b07 --- /dev/null +++ b/game/testdata/README.md @@ -0,0 +1,28 @@ +# Seed-compatibility golden + +`item_tables.golden` is the per-seed item appearance tables (potion colors, +scroll names, ring stones, wand/staff materials) captured from the **C +reference** on the `modern-rogue` branch, for the seeds in the `seeds` list in +`TestSeedCompatItemTables`. That test regenerates the same tables +from the Go port and checks they match byte for byte — proving the LCG and its +consumption order through the whole init sequence (`init_probs` → +`init_player` → `init_names` → `init_colors` → `init_stones` → +`init_materials`) agree with C. + +## Regenerating the golden + +`c_seedcompat.patch` adds a `DUMP` mode to the C `main.c`: with `DUMP` set it +forces the RNG seed from `SEED`, runs the item-table init in the normal order, +prints the tables, and exits before `initscr` (so no terminal is needed). + +```sh +# from a checkout of the C reference (modern-rogue branch): +git archive modern-rogue | tar -x -C /tmp/rogue-c +cd /tmp/rogue-c +patch -p1 < .../game/testdata/c_seedcompat.patch +./configure && make +for s in 1 42 12345 99999; do DUMP=1 SEED=$s ./rogue; done \ + > .../game/testdata/item_tables.golden +``` + +The seed list must match the `seeds` slice in `TestSeedCompatItemTables`. diff --git a/game/testdata/c_seedcompat.patch b/game/testdata/c_seedcompat.patch new file mode 100644 index 0000000..84569c1 --- /dev/null +++ b/game/testdata/c_seedcompat.patch @@ -0,0 +1,37 @@ +--- a/main.c 2026-07-24 03:02:38 ++++ b/main.c 2026-07-24 02:48:31 +@@ -63,6 +63,34 @@ + #endif + dnum = lowtime + md_getpid(); + seed = dnum; ++ ++ /* SEEDCOMPAT: dump the per-game item appearance tables for a fixed ++ * seed and exit, without initscr. The init sequence and everything ++ * it consumes from rnd() mirror the normal startup (main.c), so the ++ * tables are exactly what a real game with SEED would show. */ ++ if (getenv("DUMP") != NULL) ++ { ++ int di; ++ char *sv = getenv("SEED"); ++ if (sv != NULL) ++ seed = atoi(sv); ++ printf("SEED %d\n", seed); ++ init_probs(); ++ init_player(); ++ init_names(); ++ init_colors(); ++ init_stones(); ++ init_materials(); ++ printf("POTIONS\n"); ++ for (di = 0; di < MAXPOTIONS; di++) printf("%s\n", p_colors[di]); ++ printf("SCROLLS\n"); ++ for (di = 0; di < MAXSCROLLS; di++) printf("%s\n", s_names[di]); ++ printf("RINGS\n"); ++ for (di = 0; di < MAXRINGS; di++) printf("%s\n", r_stones[di]); ++ printf("STICKS\n"); ++ for (di = 0; di < MAXSTICKS; di++) printf("%s %s\n", ws_type[di], ws_made[di]); ++ exit(0); ++ } + + open_score(); + diff --git a/game/testdata/item_tables.golden b/game/testdata/item_tables.golden new file mode 100644 index 0000000..fa8b794 --- /dev/null +++ b/game/testdata/item_tables.golden @@ -0,0 +1,260 @@ +SEED 1 +POTIONS +tangerine +white +ecru +gold +amber +violet +vermilion +pink +aquamarine +plaid +clear +orange +cyan +tan +SCROLLS +miwhon garsnanih +xomimi roke eshwedshu +potwexrol ipbjorod turs evsnelg +bekornan oxyfatox +iv wexpo wun +ha sefnelgtue whon pay +alari wedit +zantmon umzonski umwhonjo yot +bluoxun rokkho yottrol sta +vomarg microgcomp iteulkshu mung +jo urokeep yuskiun +ox xozantaks klisstaevs ag +ipnih bek +shu ami erk +nejti zim +iprol mic ishoxyvom fagan +reacreti oodrol +bytsri solsa tabu fri +RINGS +agate +zircon +jade +tiger eye +onyx +germanium +lapis lazuli +emerald +taaffeite +kryptonite +garnet +ruby +turquoise +pearl +STICKS +wand steel +wand platinum +staff redwood +staff pine +wand silicon +staff spruce +staff pecan +wand bone +staff maple +wand zinc +wand iron +wand pewter +wand electrum +staff dogwood +SEED 42 +POTIONS +blue +green +grey +amber +violet +gold +pink +tan +purple +yellow +plaid +magenta +turquoise +cyan +SCROLLS +bek itod oxytaod oxy +tarhovzant cre sname oxroy +plemik ganod hyd wergerkpot +hyd sol um bekzok +esh eep ganmung +anera ishsa ingala mon +alasniklech viv +yunejorn garro con nej +dotrolther gopum eltitrol trolmonsri +nes alazum +itegopmung ti +ere haeta wergla +nejerecre poipi iprea +ha falechrhov +monskiwex sabitla frido +rhovmar sno +mar bekurzant satbuzum +somon sri +RINGS +carnelian +onyx +jade +granite +stibotantalite +kryptonite +lapis lazuli +germanium +garnet +tiger eye +opal +topaz +agate +peridot +STICKS +staff birch +staff ebony +staff redwood +wand gold +wand copper +wand aluminum +wand titanium +wand mercury +staff cypress +staff bamboo +staff dogwood +wand silicon +staff zebrawood +wand beryllium +SEED 12345 +POTIONS +purple +black +grey +brown +plaid +violet +vermilion +ecru +orange +turquoise +tan +magenta +silver +gold +SCROLLS +readalf shuplu ivnin +plelaiv solel skibyt monha +xo wun +wedyfri o ewhonxo favompay +eep zantreanelg +plu buxo +un zontabdan +bie snik +ulkitzant bluri +apporg ash posnevly dennepwex +u urval rol +arzepotsno snovly pay snoropay +pottox erewed faoxro +ther sun ulkipo mik +argzebfri elgrekli tuenepzon sehturssef +isheep blumur +wedash yuzimsun +plupofri ski rejo fa +RINGS +onyx +tiger eye +alexandrite +turquoise +pearl +emerald +germanium +sapphire +zircon +ruby +granite +stibotantalite +opal +diamond +STICKS +wand silicon +staff ironwood +staff holly +wand gold +staff mahogany +wand iron +wand brass +wand pewter +staff hemlock +staff cherry +staff elm +wand mercury +staff banyan +staff dogwood +SEED 99999 +POTIONS +aquamarine +plaid +gold +black +vermilion +red +cyan +tan +orange +violet +brown +clear +silver +green +SCROLLS +zant jocompan vomervly +mur shusat prok +prokmurklis oxysriklis +ingcre prokbu whonengarg kli +kli bot +rokcoswerg ipsolsan klisvlypay +glen yot whontox +lechme markho fazim +dalfsunbie micjosef cre comp +vlyfumi bjorzantbot werg +po argfidcos klipones +ashtemarg ycrezim dalfiv whon +turs unmisa zimpo therdo +miccompuni uni neswex sef +odwexing elwergmur mung +itcon rhov nejmic lech +garturs engseh ganish +oodta whonorgsno monabmik vomyeng +RINGS +obsidian +moonstone +jade +carnelian +tiger eye +taaffeite +turquoise +stibotantalite +agate +ruby +onyx +topaz +germanium +granite +STICKS +wand titanium +wand brass +wand silicon +staff zebrawood +wand mercury +staff dogwood +wand pewter +staff cinnibar +staff kukui wood +staff banyan +wand magnesium +wand gold +staff maple +wand nickel From c30da22e4322ed14d5ee849b3c298ca716d53e22 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 24 Jul 2026 03:05:39 +0700 Subject: [PATCH 2/2] Rotate TODO to coverage-broadening step (seed-compat item tables done) --- TODO.md | 24 +++++++++++++++++------- game/testdata/README.md | 9 ++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index 96115fc..7c04da2 100644 --- a/TODO.md +++ b/TODO.md @@ -29,11 +29,23 @@ Refactor ground rules: # Next Step -Verify the seed-compatibility claim against the C reference on c-master: same -seed, same dungeon, same item tables, for several seeds. +Broaden unit test coverage where playtesting finds thin spots (rings, sticks, +wizard commands). # Completed Steps +- 2026-07-24 Seed compatibility — item tables (seed-compat): instrumented the C + reference on modern-rogue with a DUMP mode (testdata/c_seedcompat.patch) that + forces the RNG seed and prints the per-seed item appearance tables (potion + colors, scroll names, ring stones, wand/staff materials) before initscr, and + captured its output for four seeds as testdata/item_tables.golden. + TestSeedCompatItemTables regenerates the same tables from the Go port and they + match byte for byte — proving the LCG and its consumption order through the + whole init sequence agree with C. The remaining "same dungeon (map)" half + would need the harder headless-curses C dump (new_level draws to curses); + deferred — the item-table match already validates RNG-order faithfulness + through init, and the Go generation goldens guard determinism thereafter. + - 2026-07-23 Playtest hardening (playtest-hardening): added two death-safe crash-sweep drives through the real turn loop, within the step-8 os.Exit constraint (a fortify() helper pins HP/food/exp and clears the freeze/stuck @@ -164,15 +176,13 @@ seed, same dungeon, same item tables, for several seeds. # Future Steps -1. Broaden unit test coverage where playtesting finds thin spots (rings, sticks, - wizard commands). -2. Tag a release once a full game (Amulet retrieval and score entry) completes +1. Tag a release once a full game (Amulet retrieval and score entry) completes without defects. -3. Full-terminal-size support (deferred by explicit decision 2026-07-06): +2. Full-terminal-size support (deferred by explicit decision 2026-07-06): per-game dungeon dimensions instead of the 80x24 constants; open design questions are resize policy, gameplay tuning at larger sizes, and a --classic 80x24 mode. -4. Note: this repo is exempt from the standard policy scaffold. A minimal dev +3. Note: this repo is exempt from the standard policy scaffold. A minimal dev Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's 2026-07-07 request, but do not add a Dockerfile, CI config, or REPO_POLICIES.md. diff --git a/game/testdata/README.md b/game/testdata/README.md index 9867b07..bc69cdf 100644 --- a/game/testdata/README.md +++ b/game/testdata/README.md @@ -3,11 +3,10 @@ `item_tables.golden` is the per-seed item appearance tables (potion colors, scroll names, ring stones, wand/staff materials) captured from the **C reference** on the `modern-rogue` branch, for the seeds in the `seeds` list in -`TestSeedCompatItemTables`. That test regenerates the same tables -from the Go port and checks they match byte for byte — proving the LCG and its -consumption order through the whole init sequence (`init_probs` → -`init_player` → `init_names` → `init_colors` → `init_stones` → -`init_materials`) agree with C. +`TestSeedCompatItemTables`. That test regenerates the same tables from the Go +port and checks they match byte for byte — proving the LCG and its consumption +order through the whole init sequence (`init_probs` → `init_player` → +`init_names` → `init_colors` → `init_stones` → `init_materials`) agree with C. ## Regenerating the golden