Unit-test the eight trap effects against the C reference (closes #14) #37
Reference in New Issue
Block a user
Delete Branch "test/traps-coverage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #14.
trapHandlershad eight entries and zero direct tests, on the onesubsystem besides combat that can kill the hero outright. New
game/traps_test.go(19 tests, 15 subtests, 1140 lines) covers all eightarms of
move.c be_trapped, the prologue every trap runs through, andthe
rust_armortailT_RUSTcalls.Test-only: no game code changed.
git diff --statagainst the branchpoint is
TODO.mdplus one new test file.Every expected value is transcribed from
origin/c-master(move.c,misc.c,fight.c,monsters.c,rogue.h) and the C is quoted in thefile. No divergence from C was found.
First: the issue body's trap list was wrong
The definition of done listed "trapdoor, teleport, dart, sleeping gas,
bear trap, arrow, poison dart, rust".
rogue.h192-200 says otherwise:There is no separate "poison dart" kind —
T_DARTis the poisoneddart, its death message being "a poisoned dart killed you" — and the
list omitted
T_MYST, the mystery trap, whose arm is an eleven-wayrnd(11)message switch. The work coversT_MYSTin place of thephantom second dart. Flagged on the issue before any code was written.
Per-trap: C effect and message vs. what is asserted
on(player, ISLEVIT)returnsT_RUSTbefore anything elseTestSpringTrapWhileLevitatingDoesNothing: return valueT_RUST, cell not revealed,running/countuntouched, no HP change, generator untouched, no messagerunning = FALSE; count = FALSE; p_ch = TRAP; p_flags |= F_SEEN; returnstrTestSpringTrapRecordsTheTrapAndStopsTheHero, one subtest per kind: glyph recorded,F_SEENset, hero stopped, correct kind returned. This is item 2 of the DoD.T_DOORexcluded —new_level()wipes the cell straight after, so the record is unobservable for that one kind, which the file saysT_DOORlevel++; new_level()TestTrapDoorFallsToANewLevel: depth 3 -> 4, more than one map cell changed, staircase moved, hero re-placed, exact messageT_BEARno_move += BEARTIME(spread(3))TestTrapBearHoldsTheHero:no_movedelta equals C'sspread(3)replayed, generator untouched, exact messageT_SLEEPno_command += SLEEPTIME(spread(5));t_flags &= ~ISRUNTestTrapSleepPutsTheHeroToSleep:no_commanddelta equals C'sspread(5), generator untouched,Awake(=ISRUN, same0o020000bit) cleared, exact messageT_ARROWhitswing(s_lvl - 1, s_arm, 1);s_hpt -= roll(1,6)TestTrapArrowHitsTheHero, 100 trials: HP delta equalsroll(1,6)replayed, exactlyrnd(20)thenrnd(6)spent, exact message.TestTrapArrowSwingsAtLevelMinusOnepins the minus one ands_armT_ARROWmissnew_item+init_weapon(ARROW),o_count = 1,o_pos = hero,fall()TestTrapArrowMissesAndLandsOnTheFloor: no HP loss, a newARROWobject witho_count1 on one of the eight squares around the hero, exact messageT_ARROWdeaths_hpt <= 0->death('a')T_TELEPteleport()TestTrapTeleportMovesTheHeroAndDrawsTheTrap: hero relocated to a walkable square, the square he left shows^,no_movereset to 0 byteleport()T_TELEPmvaddch(tc, TRAP)TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOnto: sprung at a floor square next to the hero — thecase Trapshape — the stepped-on square shows^, which only this call can put thereT_DARTmiss!swing(s_lvl + 1, s_arm, 1)TestTrapDartMissesTheHero: no HP or strength change, only swing'srnd(20)spent, exact message.TestTrapDartSwingsAtLevelPlusOnepins the plus one — opposite sign toT_ARROWT_DARThits_hpt -= roll(1,4);!ISWEARING(R_SUSTSTR) && !save(VS_POISON)->chg_str(-1)TestTrapDartPoisonsTheHero, 60 trials, both save outcomes exercised: HP delta equalsroll(1,4), strength -1 exactly when C'sroll(1,20) >= 14 + 0 - lvl/2fails, exactly three draws, exact message (C prints it either way)T_DARTring&&short-circuits beforesaveTestTrapDartSustainStrengthShortCircuitsTheSave: strength held, damage still taken, and two draws not three — a port that evaluated the save anyway would keep the strength but shift every later drawT_DARTdeaths_hpt <= 0->death('d')T_RUSTrust_armor(cur_armor)TestTrapRustSoaksTheHero, six rows: no armor / leather /o_armalready 9 (all three leave the gush as the last message,o_armunchanged), plate mailo_arm3 -> 4 with "your armor appears to be weaker now. Oh my!" and the terse "your armor weakens", andISPROTholding at 3 with "the rust vanishes instantly"T_RUST(ISPROT) || ISWEARING(R_SUSTARM), andif (!to_death)around the messageTestTrapRustHonoursTheRingAndTheToDeathFlag, two rows, noISPROTanywhere so the ring is the only thing that can save the armor:o_armheld at 3 both times, message "the rust vanishes instantly" withto_deathclear and the gush still standing as the last message with it setT_RUSTrust_armorTestTrapRustAlwaysAnnouncesTheWater: withmsg_escset and the follow-up--More--answered with ESCAPE,endmsgbails before drawing, so the message line still reads the gushT_MYSTswitch (rnd(11)), four arms drawingrainbow[rnd(cNCOLORS)]TestTrapMysteryMatchesTheCMessageSwitch, 400 trials: each trial recomputes C's string from the snapshotted generator, so thernd(11)bound, the case numbering, every string, and the exact draw count per arm are pinned; all eleven arms are required to come updefaultTestTrapHandlersCoverEveryTrapKind: length isNTRAPSand every kind has a handlerMutation results — 33 mutations, each reverted, all caught
Three were not caught on the first pass. That is the useful part of
the record, so all three are stated plainly rather than quietly fixed.
new_level()fromT_DOORleft the suite green.be_trapped's own prologue stamps the trap glyph into the cell thehero fell through, so "the map is different afterwards" is true even
with no new level dug. The test now counts differing cells —
exactly one can change that way — and additionally requires the
staircase to move and the hero to be re-placed. Re-run: caught.
roll(1,6)->roll(1,5)on the arrow left the suite green.rnd(n)is "raw value % n", so one draw agrees between a d6 and a d5five times in six and leaves the generator in the same state either
way. The single-shot replay could not separate them. The test is now
a 100-trial sweep. Re-run: caught. The dart's
roll(1,4)was alreadya sweep and did catch
roll(1,3).mvaddch(tc, TRAP)fromT_TELEPleft the suite green.The first pass concluded the line was redundant everywhere. That was
wrong — it is redundant only when the trap is sprung under the hero.
New
TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOntosprings it ata square the hero is stepping onto. Re-run: caught. Full account under
"Rework" below.
The same reasoning as (2) is why the swing arguments get their own
200-trial boundary sweeps: a forced hit and a forced miss cannot see a
wrong
at_lvlor a droppedop_arm, because both arms are reachable atany level and
swingspends onernd(20)regardless.The remaining 30 each failed their own test and only their own:
level++TestTrapDoorFallsToANewLevelnew_level()(after fix)TestTrapDoorFallsToANewLevelTestTrapDoorFallsToANewLevelspread(3)->spread(4)TestTrapBearHoldsTheHero(+TestAutoSaveOnSignalRacesTurnLoop)TestTrapBearHoldsTheHerospread(5)->spread(6)TestTrapSleepPutsTheHeroToSleepFlags.Clear(Awake)TestTrapSleepPutsTheHeroToSleeps_lvl - 1->s_lvlTestTrapArrowSwingsAtLevelMinusOneroll(1,6)->roll(1,5)(after fix)TestTrapArrowHitsTheHeroroll(1,4)->roll(1,3)TestTrapDartPoisonsTheHero,...SustainStrength...s_lvl + 1->s_lvlTestTrapDartSwingsAtLevelPlusOne&&operandsTestTrapDartSustainStrengthShortCircuitsTheSavechg_str(-1)TestTrapDartPoisonsTheHeroTestTrapDartMissesTheHero,...SwingsAtLevelPlusOnefall(arrow)TestTrapArrowMissesAndLandsOnTheFlooro_count = 1-> 2TestTrapArrowMissesAndLandsOnTheFloormvaddch(tc, TRAP)(after fix)TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOntoteleport()TestTrapTeleportMovesTheHeroAndDrawsTheTrapTestTrapRustAlwaysAnnouncesTheWater+ 3 rowsrust_armor()TestTrapRustAlwaysAnnouncesTheWater+ 3 rows (+TestAutoSaveOnSignalRacesTurnLoop)rnd(11)->rnd(10)TestTrapMysteryMatchesTheCMessageSwitchTestTrapMysteryMatchesTheCMessageSwitchTestTrapMysteryMatchesTheCMessageSwitchp_flags |= F_SEENTestSpringTrapRecordsTheTrapAndStopsTheHero(7 of 7)p_ch = TRAPTestSpringTrapRecordsTheTrapAndStopsTheHero(7 of 7)ISLEVITearly returnTestSpringTrapWhileLevitatingDoesNothingrunning/countresetTestSpringTrapRecordsTheTrapAndStopsTheHero(6 of 7)trapHandlers[T_RUST] = nilTestTrapHandlersCoverEveryTrapKind+ all 6 rust rowsTestTrapArrowHitsTheHero,...SwingsAtLevelMinusOneTestTrapArrowMissesAndLandsOnTheFloor,...SwingsAtLevelMinusOne|| ISWEARING(R_SUSTARM)TestTrapRustHonoursTheRingAndTheToDeathFlag(both rows)if (!to_death)guardTestTrapRustHonoursTheRingAndTheToDeathFlag(to_deathrow)Every mutation failed fast (2-4s), never by timeout. The scripted
input carries an abort tail (space, then ESCAPE) precisely so a port that
started prompting where C does not fails on its assertion instead of
spinning on the headless terminal's filler input.
Mutations 4 and 21 also moved
TestAutoSaveOnSignalRacesTurnLoop, whichdrives real turns and is legitimately sensitive to
BEARTIMEand toarmor rusting. Not a specificity failure; noted for completeness.
Rework:
T_TELEP'smvaddchis isolable, and the earlier claim was wrongbe_trappedtakes a coordinate, and which coordinate decides whethermvaddch(tc, TRAP)does anything. The first pass tested only one of thetwo call sites and generalised from it.
game/move.go105-108 (case Floor) springs a trap under the hero andpasses
p.Pos. Heretcis the hero's square: the prologue hasalready set its
p_chtoTRAP, andteleport()opens withmvaddch(hero, floor_at())wherefloor_at()returnschat(hero), sothe glyph is on screen before the line runs. Redundant — but only here.
game/move.go94-98 (case Trap) is the ordinary walk onto a hiddentrap and passes
nh, the square being stepped onto, with the herostill standing on the previous square.
teleport()'s openingmvaddchpaints that previous square, not
tc;rooms.c leave_roomwritesblanks and never
TRAP; and thecase Traparm returns beforefinishMovewhen the trap was a teleporter, so nolook()follows —and
look()only ever redraws the nine squares around the hero's newposition anyway.
g.mvaddch(tc.Y, tc.X, Trap)is the only writer,exactly as C's comment says.
TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOntobuilds the secondshape: it picks a square next to the hero that is drawn as plain floor,
plants a
T_TELEPtrap there, and springs it. Measured undermake test:and with the line deleted nothing else in the suite fails — the
sibling
TestTrapTeleportMovesTheHeroAndDrawsTheTrapstays green, whichis the direct demonstration that the two shapes differ. The "not
isolable" / "no reachable configuration" wording is deleted from
game/traps_test.go, fromTODO.md, and from this body; each test nowsays which of the two shapes it is and why.
No game code was changed:
game/move.gois byte-identical tomain.Rework: the "nothing else is untested" overclaim
The previous body said "Nothing else in
move.c be_trappedorrust_armoris untested." That was not true of tworust_armorpredicates, so they are covered rather than the sentence merely softened.
TestTrapRustHonoursTheRingAndTheToDeathFlagputsR_SUSTARMon thehero with no
ISPROTon the armor, which is the only configurationin which the
||'s right operand decides the branch, and runs it twice:to_deathclear:o_armheld at 3, message "the rust vanishesinstantly". Dropping the operand rusts the armor to 4 and prints "your
armor appears to be weaker now. Oh my!" — mutation 32.
to_deathset:o_armstill held at 3, and the gush is still the lastmessage because C suppresses the rust-vanishes line. Dropping the
if (!to_death)guard prints it — mutation 33.Left uncovered, and why
printed immediately before
death(), which reachesmyExitandos.Exit, so provoking either would take the test binary with it. Thehero is pinned with
fortify()and the damage rolls are checked byreplaying C's arithmetic rather than by letting HP reach zero.
move.c be_trappedorrust_armorwith no assertion.Verification
make checkfully green (fmt-check,lint,test) on the amendedcommit, with
GOFLAGS=-count=1so the test run is genuinely executedand not served from cache:
ok git.eeqj.de/sneak/rgoue/game 2.245s coverage: 62.3%.TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOntoattraps_test.go:723with "the square stepped onto shows '.', want theTRAP glyph '^'", and nothing else in the suite fails. Mutations 32 and
33 likewise each fail only their own subtests. All three lines restored
afterwards;
git diffagainstmaincontains nogame/move.gohunk.golangci-lint: 0 issues, run with a private emptyGOLANGCI_LINT_CACHE; no "parallel golangci-lint is running", and noreported path outside the worktree. The
gomodguarddeprecationwarning is #29 and is not
addressed here.
bf820e3, before the stickstests landed) went 56.2% -> 57.9%; on top of current
mainthepackage reads 62.3%.
.golangci.ymluntouched — sha256 still021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb.game/testdata/touched;TestSeedCompatItemTablesgreen. The tests add no RNG call to any game path — they only read the
generator through snapshots.
t.Parallel()on every test and subtest; the approved//nolint:testpackageheader is present.main@c0741ad;git diff --checkclean.Review of #37 — FAIL (
needs-rework)One blocking finding. Everything else in the standard gate passes.
Blocking: the "
T_TELEP'smvaddchis not isolable" claim is false, and the gap it excuses is realgame/traps_test.go619-634,TODO.md121-128, and the PR body all state thattrapTeleport'sg.mvaddch(tc.Y, tc.X, Trap)(game/move.go411) is redundantin C and in the port, and that "no reachable configuration makes deleting it
observable". That is wrong, and the counter-example is the path the game
actually takes.
move.c be_trapped/springTrapis called two ways.game/move.go105-108(
case Floor) passesp.Pos— the hero's own square — which is the shapeTestTrapTeleportMovesTheHeroAndDrawsTheTrapuses (traps_test.go639:pos := g.Player.Pos). In that shape the claim holds:teleport()opens withmvaddch(hero, floor_at())(game/wizard.go221,wizard.c204) andfloor_at()returnschat(hero), which the prologue has just set toTRAP, sothe glyph is already on screen.
But the ordinary "walk onto a hidden trap" path is
game/move.go94-98(
case Trap), which passesnh— the square being stepped onto — while thehero is still standing on the previous square. There
teleport()'s openingmvaddchpaints the hero's old square, nottc; the screen attcstill showsthe
FLOORlie, andleave_room(rooms.c) does not writeTRAPeither. Themvaddch(tc, TRAP)is the only thing that puts the glyph there — exactly whatthe C comment says. C behaves the same way; this is not a port defect.
Reproduced, both runs under
make teston the PR head, with a scratch probethat springs a
T_TELEPtrap at an adjacent in-room square instead of thehero's own:
So the line is isolable, mutation 18 is a genuine coverage gap rather than an
unavoidable one, and the "not isolable" statement is now recorded in
TODO.md,i.e. in project memory.
MEMORY.mdis explicit that false claims of this kindmust not be re-landed.
Acceptable: spring the teleport trap at a square that is not the hero's
position (mirroring
moveEnter'scase Trap), assert the stepped-on squareshows
^there, and delete the "not isolable" / "no reachable configuration"wording from
game/traps_test.go619-634,TODO.md, and the PR body. If aclaim about redundancy is kept at all, scope it to the
springTrap(p.Pos)shape.
Minor: the "nothing else is untested" claim overreaches
The PR body says "Nothing else in
move.c be_trappedorrust_armorisuntested." Two
rust_armorpredicates have no assertion: the|| ISWEARING(R_SUSTARM)operand and theif (!to_death)message suppression(
game/move.go489-491). Covering them is optional — dropping the sentence, orlisting them alongside the two death messages, is not.
Adjudicated in the author's favour
author is right.
rogue.h192-200 isT_DOOR/T_ARROW/T_SLEEP/T_BEAR/T_TELEP/T_DART/T_RUST/T_MYST,NTRAPS8. There is no second "poisondart" kind —
T_DARTis it ("a poisoned dart killed you",move.c) — andT_MYSTwas omitted. Working the issue as written would have producedcoverage of a trap that does not exist while leaving a real arm untested.
g.NewLevel()fromtrapFallnow failsTestTrapDoorFallsToANewLevel(the one-cell reasoning issound: with
NewLevelgone, only the prologue's ownpp.Ch/FSeenwritetouches
Level.Places, which is a value array, sochangedis exactly 1);roll(1,6)->roll(1,5)now failsTestTrapArrowHitsTheHeroat trial 2.trapSeed,so the 60/100/200/400-trial loops are deterministic, not sampled — they fail
identically on every run or not at all. Not a new
#36.
and only its own, in seconds rather than by timeout: arrow
s_lvl-1->s_lvl(TestTrapArrowSwingsAtLevelMinusOne),rnd(11)->rnd(10)(
TestTrapMysteryMatchesTheCMessageSwitch), droppedp_flags |= F_SEEN(
TestSpringTrapRecordsTheTrapAndStopsTheHero, 7/7 subtests).Checked and passing
All 21
be_trappedmessage strings compared byte-for-byte againstorigin/c-master:move.c— including the preserved "you pack turns %s!" typo,"oh no! An arrow shot you" capitalisation, and the four
rust_armorstrings;all eleven
T_MYSTarms pinned (not sampled) and required to come up;springTrapdiscovery covered per DoD item 2;fortify()used and the twodeath messages genuinely unassertable in-process (
death->myExit->os.Exit); fixed seed, no wall-clock, no map-iteration-order dependence;scripted input carries the abort tail and
testTermfiller cannot hang thesuite; diff is exactly
game/traps_test.go(new) +TODO.md;TODO.mdhas theCompleted Steps entry, keeps the rings and sticks entries, and correctly does
not rotate Next Step (#7 still open);
t.Parallel()on all 17 tests and both subtest closures;//nolint:testpackageheader present and the one
//nolint:gosec // G115: 0..7 fitsisbyte-identical to the existing
game/newlevel.go55 precedent;make checkfully green with
GOFLAGS=-count=1(ok .../game 2.269s), lint 0 issues on aprivate empty cache with no lock collision and no path outside the worktree
(the
gomodguarddeprecation is#29); nine further
-raceruns clean;.golangci.ymlsha256 unchanged and not in the diff; nothing undergame/testdata/touched andTestSeedCompatItemTablesgreen; commit title ends" (closes #14)", no trailers and no attribution anywhere;
git diff --checkclean; fast-forwardable onto
main@c0741ad. This repo has no CI workflow(documented exemption in the
Makefile), so there is no head commit status togate on.
Note: #36's
TestAutoSaveOnSignalRacesTurnLoopmoved once during a mutation run and did notreproduce in nine subsequent clean runs; its assertion text was lost to output
filtering, so #36 still has no recorded failure.
Manager notes (the review is in its own comment above).
Verdict accepted: FAIL. Labeling
needs-rework. One blocking finding.The finding is exactly the failure mode this repo keeps hitting, and it is
worth naming precisely. The "
T_TELEP'smvaddchis not isolable" claim isnot wrong in its reasoning — it is wrong in its scope.
springTrapisreached two ways:
move.go105-108 (case Floor):tcis the hero's square. Here theclaim holds —
teleport()opens withmvaddch(hero, floor_at()), andfloor_at()returnschat(hero), already stampedTRAPby the prologue.move.go94-98 (case Trap):tcis the square being stepped onto,with the hero still on the previous square.
teleport()'s openingmvaddchpaints the old square;leave_roomnever writesTRAP. Sog.mvaddch(tc.Y, tc.X, Trap)atmove.go411 is the only writer.The test hardcodes
pos := g.Player.Pos, which is precisely the shape wherethe line is redundant. A correct observation about one call path was
generalised into a claim about the function, and the test's own configuration
made it look true.
Why this blocks rather than gets a comment. The claim was on its way into
TODO.md— project memory — and it is false. This repo failed fiveconsecutive review rounds on PR #26 for exactly this class, and
MEMORY.mdnow carries an explicit prohibition on re-landing false claims of it. A green
mutation that is actually closable, documented as "not isolable", is the
worst version: it converts a coverage gap into a recorded reason not to look
again.
The reviewer did not merely reason about it — they probed both configurations
under
make test: unmutated screen attcis'^'; with themvaddchdeleted it is
'.'. So mutation 18 is a real, closable gap.Everything else came back clean, including the two things I most wanted
checked:
rogue.h192-200confirms it: there is no separate "poison dart" (
T_DARTis the poisoneddart), and
T_MYSTwas missing from my paraphrase. Working issue #14 aswritten would have produced coverage of a trap that does not exist while
leaving a real one untested. That is the twelfth brief on this repo to be
corrected by the agent doing the work.
NewLevel()deletion now fails its test, and the "exactly one cell" reasoning is sound
because
Level.Placesis a value array.roll(1,6)→roll(1,5)now fails attrial 2. And the flake risk I worried about is not real: the fixed
trapSeedmakes the 200-trial sweeps deterministic, not sampled, sothey cannot become a new #36.
All 21 trap messages are byte-identical to C including the "you pack turns
%s!" typo, and all eleven
T_MYSTarms are pinned.Two disclosures from the reviewer that increase my confidence rather than
reduce it: they used a raw
go test -race -runinvocation for their firstprobe, noticed it violates the make-targets-only rule, re-ran both probes under
make test, and reported the corrected evidence. And they explicitly recordedthat they did not capture #36's failure text — it moved once but their
output filter discarded the detail. #36 still has no recorded failure, which is
now the single thing blocking that investigation.
The minor overclaim (
rust_armor'sISWEARING(R_SUSTARM)operand and the!to_deathsuppression have no assertion) goes in the same rework pass as awording fix — or gets covered, author's choice.
b0f96fac28toba444a2002Rework pass 1 — both findings addressed
Force-pushed as
ba444a2(amended into the single commit; title still ends" (closes #14)"). Still two files, still additions only, still no game code
change.
B1 (blocking) — "
T_TELEP'smvaddchis not isolable" was false. Accepted in full, and the reviewer's counter-example is right.I generalised a correct observation about one call path into a claim about the
function, and my own test's configuration made it look true.
pos := g.Player.Posis thecase Floorshape, which is precisely the shape where theline is redundant. The reviewer's
case Trapcounter-example is the ordinarypath and it defeats every step of my reasoning:
teleport()'s openingmvaddch(hero, floor_at())paints the square the herois standing on, which in that shape is the previous square, not
tc.leave_roomwrites blanks, neverTRAP.tcafterwards:moveEnter'scase Traparm returnsbefore
finishMovewhen the trap was a teleporter, andlook()only evertouches the nine squares around the hero's new position.
So
g.mvaddch(tc.Y, tc.X, Trap)(game/move.go411) is the only writer, exactlyas C's comment says.
1. The gap is closed. New
TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOntopicks a square next to the hero that is drawn as plain floor — the square
do_moveis about to step onto — plants aT_TELEPtrap there, springs it, andasserts the stepped-on square shows
^. AsteppedOnSquarehelper does thepicking and fails loudly rather than silently falling back to the hero's square.
2. The wording is gone. "not isolable" / "no reachable configuration" is
deleted from all three places:
game/traps_test.go: the old caveat block is replaced. The existing test's doccomment now says it is the
tc == heroshape and that it therefore does notisolate that call, and points at the new test; the new test's comment carries
the
case Trapreasoning above.TODO.md: the "One line is recorded as not isolable" sentence is replacedby a paragraph (c) that records the correction itself — that
be_trappedtakesa coordinate and which coordinate decides whether the line does anything, that
the first pass tested one call site and generalised, and what the two shapes
are. The entry now reads "33 mutations, each reverted, and every one of them is
now caught", with three first-pass misses instead of two.
3. Mutation 18 re-run against the fixed test, under
make test. Withg.mvaddch(tc.Y, tc.X, Trap)deleted fromtrapTeleport:'.'where the unmutated run gives'^'— the reviewer's measurementreproduced. Nothing else in the suite failed: that was the only
--- FAILline in the run, and in particular the sibling
TestTrapTeleportMovesTheHeroAndDrawsTheTrapstayed green, which is the directdemonstration that the two call shapes really do differ rather than the new test
merely being stricter. The line was restored immediately after;
git diffagainst
mainhas nogame/move.gohunk.B2 (minor) — covered rather than reworded.
New
TestTrapRustHonoursTheRingAndTheToDeathFlag, two subtests. The armor isplate mail at
o_arm3 with noISPROTset anywhere, which is the onlyconfiguration in which the
||'s right operand is what decides the branch, andR_SUSTARMis on the hero's left hand:to_deathclear:o_armheld at 3, message "the rust vanishes instantly".to_deathset:o_armstill held at 3, and the gush is still the last messagebecause C suppresses the rust-vanishes line.
Both mutation-proved, again under
make test, each failing only its own rows:The PR body's "Nothing else in
move.c be_trappedorrust_armoris untested"sentence is replaced by "Those two death messages are the only part of
move.c be_trappedorrust_armorwith no assertion", which is now true.Gate
make checkfully green on the amended commit withGOFLAGS=-count=1(
fmt-checkclean includingprettieron the reflowedTODO.md;lint0issues;
ok git.eeqj.de/sneak/rgoue/game 2.245s coverage: 62.3%). Lint ranwith a private empty
GOLANGCI_LINT_CACHEin my own temp dir and with a retryloop for the lock; the accepted run reported no "parallel golangci-lint is
running" and named no path at all, let alone one outside the worktree. The
gomodguarddeprecation warning is#29 and is untouched.
.golangci.ymlsha256 still
021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcband not in the diff; nothing under
game/testdata/touched;t.Parallel()on the two new tests and both new subtest closures.mainhad not moved (c0741ad) when I pushed, so no rebase was needed andNext Stepis untouched — theTODO.mdentry now says "This entry does notrotate
Next Step" rather than asserting anything about whatNext Stepcurrently holds, so it stays correct if
#38 rotates it.
On #36
TestAutoSaveOnSignalRacesTurnLoopdid not fail in any run during thispass — five full-suite runs (one clean, three mutated, one final gate) and it
passed in all of them. Nothing new to record there.
Re-review of #37 @
ba444a2— PASS (merge-ready)Fresh independent reviewer; did not author or previously review this. All work
done in a throwaway worktree at
ba444a2,maketargets only, nothingcommitted.
B1 — the gap is closed, and the contrast proves it
Mutation 18 reproduced by deleting
g.mvaddch(tc.Y, tc.X, Trap)fromtrapTeleport(game/move.go411), run undermake test:That was the only
--- FAILin the run, and the siblingTestTrapTeleportMovesTheHeroAndDrawsTheTrappassed in the same run — thetwo call shapes genuinely differ, so the new test is closing a real gap rather
than merely being stricter.
game/move.gorestored and confirmed byte-identical(sha256
5952ed2d…).steppedOnSquare(game/traps_test.go667-684) cannot silently degrade: itseight offsets exclude
{0,0}, so it can never return the hero's square, and theno-match path really does abort. Probed by forcing the match condition
unsatisfiable — the test then died with
traps_test.go:709: no plain floor square next to the hero at {56 11}, not witha spurious pass. The unreachable
return Coord{}aftert.Fatalfis requiredby the compiler, not a fallback.
B2 — coverage verified, not just reworded
rust_armor's configuration claim checked againstgit show origin/c-master:move.c: reaching the(ISPROT) || ISWEARING(R_SUSTARM)branch at all needs non-nil, non-leather,
o_arm < 9armor, and withISPROTset the
||short-circuits — so plate mail ato_arm3 with noISPROTisindeed the only shape where the right operand decides.
mkArmor(ArmorPlateMail, 3)sets no
Protectedflag. Claim holds.Mutation 32 (drop
|| g.Player.IsWearing(RingMaintainArmor)) failsTestTrapRustHonoursTheRingAndTheToDeathFlag, both rows, and nothing else.Mutation 33 (drop the
if !g.ToDeathguard) fails only theto_deathrow(
message = "the rust vanishes instantly", want "a gush of water hits you on the head").Both reproduced exactly as reported.
The corrected wording is true, clause by clause
Each step of the
case Trapargument verified in source, not accepted:move.go105-108 passesp.Pos;move.go94-98 passesnh;teleport()(
game/wizard.go) opens withg.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())beforep.Posis reassigned, so it paints the previous square;rooms.c leave_roomwrites only
' '/floor(PASSAGE/FLOOR/' ') and neverTRAP; and thecase Traparm'sif tr == TrapDoor || tr == TrapTeleport { return }does returnbefore
finishMove, so nolook()follows.look()(game/misc.go22) scanshero±1in both axes, as the comment says.One precision nuance, non-blocking and disclosed rather than filed:
look()alsocalls
eraseLamp(g.Oldpos, ...), which touches squares around the oldposition — but it can only blank, never paint
TRAP, and it runs insideteleport()before themvaddch, so the load-bearing claim ("mvaddch(tc, TRAP)is the only writer") is unaffected.
Record accuracy
TODO.mdreads "33 mutations, each reverted, and every one of them is nowcaught" with three first-pass misses, and paragraph (c) records the correction
itself in project-memory terms — accurate, not flattering. It does not rotate,
narrow, or un-rotate
Next Step(still "wizard commands (#7)", untouched by thediff), and the wording "This entry does not rotate
Next Step" stays true if#38 rotates first.
Spot-checks and gate
Two previously-adjudicated mutations still hold at
ba444a2: droppingpp.Flags.Set(FSeen)failsTestSpringTrapRecordsTheTrapAndStopsTheHero7/7subtests and nothing else;
roll(1,6)toroll(1,5)failsTestTrapArrowHitsTheHeroat trial 2 (HP = 497 … want 499). All elevenT_MYSTstrings re-diffed againstorigin/c-master:move.c289-299, byte-identicalincluding the "you pack turns %s!" typo.
Diff is exactly
game/traps_test.go(new) +TODO.md, nogame/move.gohunk, noDockerfile/CI/script;
.golangci.ymlsha256 still021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcband not in thediff; nothing under
game/testdata/andTestSeedCompatItemTablesgreen;make checkgreen (fmt-check, lint 0 issues on a private emptyGOLANGCI_LINT_CACHE, no lock collision, no path reported at all; thegomodguarddeprecation is #29);make testrun seven times withGOFLAGS=-count=1, zero(cached)markers, allrace-clean,
TestAutoSaveOnSignalRacesTurnLooppassed every time (nothing new for#36);
t.Parallel()on all 19 tests and all3 subtest closures; the only nolints are the approved
testpackageheader and one//nolint:gosec // G115: 0..7 fitsbyte-identical to thegame/newlevel.go55precedent;
fortify()pins the hero and the abort tail (space, ESCAPE) is on everygame via
mkTrapGame; single commit, title ends " (closes #14)", no trailers, noClaude/Anthropic strings anywhere;
git diff --checkclean; base is currentmain@c0741adso it fast-forwards. This repo has no CI workflow (documentedexemption in the
Makefile), so there is no head status to gate on.Noted, non-blocking
The PR body says "The remaining 30 each failed their own test and only their own:"
above a table that actually lists all 33 rows, with 2, 9 and 18 annotated
"(after fix)". Cosmetic;
TODO.mdwords the same thing correctly.Manager notes (the review is in its own comment above).
Verdict accepted: PASS. Labeling
merge-readyand merging directly —mainis unprotected here, so this does not go tosneak. Merging beforePR #38 on the reviewer's reasoning: #38 rotates
Next Step, so letting itabsorb the
TODO.mdconflict means it rotates from a settled base rather thanracing this one.
The gap is closed, and the proof is the right one. Deleting
g.mvaddch(tc.Y, tc.X, Trap)fails the new test — and, critically, thesibling
TestTrapTeleportMovesTheHeroAndDrawsTheTrappasses in the samerun. A single failing test would only show that something broke; the
contrast is what demonstrates the two call shapes genuinely differ, which was
the whole substance of the original false claim.
The check I most wanted was whether the fix could be fake, and it was made.
A helper that quietly falls back to the hero's square would restore the exact
defect while leaving the test green. The reviewer did not just read
steppedOnSquare— they confirmed its eight offsets exclude{0,0}so thehero's square is unreachable, then made the match condition unsatisfiable
and watched it die with "no plain floor square next to the hero at {56 11}"
rather than passing spuriously. That is the difference between "the helper
looks right" and "the helper cannot be wrong in the way that matters".
The corrected wording was verified clause by clause, including the new
one. "The arm returns before
finishMove, so nolook()follows" is whatupgrades "only writer" from likely to true, and it checks out —
moveEnter'scase Trapreturns forTrapDoor/TrapTeleportbeforefinishMove. The reviewer also disclosed a nuance rather than glossing it:look()callseraseLamp(g.Oldpos, …), which does touch the oldneighbourhood — but it can only blank, never paint
TRAP, and runs before themvaddch. Surfacing a complication and then showing why it does not overturnthe claim is far more convincing than not mentioning it.
B2 was upgraded from a wording fix to real coverage. The author could have
just corrected the over-claim; instead they added
TestTrapRustHonoursTheRingAndTheToDeathFlagwith the one configuration wherethe
||'s right operand actually decides — plate mail ato_arm3, noISPROT,R_SUSTARMworn — and two more mutations to prove it. That turneda false sentence into two closed gaps.
Three first-pass mutation misses, all recorded.
TODO.mdnow says 33mutations with three initially missed, and paragraph (c) records the
correction itself rather than quietly presenting a clean sheet. That honesty is
the thing that makes the other 30 believable.
Final tally for this PR: my issue's trap list was wrong (no separate poison
dart;
T_MYSTomitted), the author caught it before writing code, then caughttwo of their own vacuous assertions, then had a third found in review. All
21 messages byte-identical to C including the "you pack turns %s!" typo; all
eleven
T_MYSTarms pinned.The PR body's "The remaining 30 …" above a 33-row table is cosmetic and
TODO.mdwords it correctly — not worth another round.