Skip to content

Interaction case · Rule-level replay

When the Rulebook and the Engine Disagree ​

Fail·《罪恶摇摆:白垩与黑铁》v2.0.0·Evidence cutoff 2026-10-21

TL;DR ​

A four-player social deduction game can be internally consistent at the engine layer and still be unshippable. In this v2.0.0 audit, the engine regression was green — 11/11 assertions passed — but the player-facing rulebook, the action controls and the engine described three different games. Two mismatches were player-visible blockers; one was a precision defect; one was an unresolved wording problem. The interesting failure is not a crash. It is a silent information gap: every layer is individually plausible, so no exception is thrown when the player plans around a number the engine will never pay.

Context ​

《罪恶摇摆:白垩与黑铁》 is an H5 social deduction game: one human and three AI players, four masks dealt without replacement, five rounds of public statements followed by secret actions, and a black/white pendulum that moves with every choice. Suspicion is clamped to [0, 100]; a player at 100 is executed; only survivors are ranked.

The release package contains four documents:

DocumentSource of truth for
01_角色与规则设定.mdWhat the player reads in the role reveal, tutorial and store page
02_核心数值与结算引擎.mdWhat the engine actually computes
03_三剧本回归测试记录.mdWhat the automated regression covers — and what it does not
04_发行说明.mdWhat changed in v2.0.0 and what is declared ready

There is also an inline copy of the engine and player-facing role copy inside src/App.jsx. The regression record explicitly says the inline copy is not compared with src/gameEngine.js. That omission matters later.

The information gap ​

The player and the engine hold different models of the same four rules. None of the differences produces an error message.

RulePlayer-facing copyEngine / sourceConsequence
Whisper Nun hidden victory+500 assassination scoreassassinationBonus = 50; regression asserts 50The player values one execution 10× too highly
Faceless Duke disguiseOptional checkbox; unchecked means no disguiseFirst EXECUTE_BLACK auto-disguises; context.disguiseRequests is never readThe player cannot choose “do not disguise”
Regent betrayal bonus“Betrayal action +75%”+75% is bound to EXECUTE_BLACK, not SABOTAGE_BOTH; the term table defines “betrayal” as SABOTAGE_BOTHThe literal reading gives 0 × 1.75 = 0; intent needs design confirmation
Duke balance bonus“Difference ≤10 reaches the +50 cap”max(0, round(50 − diff)); diff 10 → 40A 10-point expectation gap at the advertised cap boundary

The first two are blockers because the player can see or operate the wrong thing. The third is unresolved because two readings are possible and the material does not say which one is intended. The fourth is a precision defect: it changes the displayed payoff, but not the “perfect balance” rating or the winner in the audited scenarios.

Multi-round replay ​

This is a rule-level replay, not a recorded player session. Every number below is reproducible from the cited documents; the point is to show how the gaps compound across rounds.

RoundWhat the player plansWhat the engine doesWhat the player observes
1Draws the Whisper Nun; forces an execution because the hidden condition says +500Awards +50A plausible but wrong score breakdown; no warning
2Draws the Faceless Duke; selects EXECUTE_BLACK and leaves the disguise box uncheckedAuto-disguises on the first black action, marks dukeDisguiseUsed = trueThe table is told the action was white; the player’s “no disguise” plan is silently overridden
3Draws the Regent; chooses “betrayal” expecting +75%Applies the bonus only to EXECUTE_BLACK; SABOTAGE_BOTH has zero credit to amplifyEither no bonus or an unexpected bonus, depending on which action was chosen
4Plays the Duke at a credit difference of 10, expecting the +50 capComputes max(0, round(50 − 10)) = 40A 10-point gap at the exact boundary the copy advertises
5Reconstructs the final ranking from the rulebookRanks only survivors using engine valuesThe story and the scoreboard disagree, but both are “valid” outputs

The compounding effect is the real interaction failure. A single mismatch can be dismissed as a display bug. Four mismatches across five rounds produce a player who cannot build a reliable mental model of the game — while every individual subsystem reports success.

Why 11/11 did not catch it ​

The regression suite is honest about its own boundary:

This regression covers only src/gameEngine.js engine values and dialogue generation. It does not cover player-facing copy, UI-to-engine parameter passing, or consistency between the two engine copies.

That is exactly where the release failures live. Assertion #5 checks assassinationBonus = 50 — the engine constant — and therefore passes while the player-facing hidden condition still says +500. The suite never opens the role modal, never clicks the disguise checkbox, and never diffs the inline App.jsx copy against gameEngine.js.

A rule-by-rule audit of the release package produced 20 checks:

DispositionCountExamples
Pass15Four action tables, clamp range, tide threshold, execution order, survivor ranking
Blocker2Nun +500 vs +50; disguise checkbox not wired
Non-blocking1Duke +50 cap vs +40 at diff 10
Unresolved1Regent “betrayal” wording vs implementation
Pending1Contract tests, copy snapshots and dual-copy diff not written

Test design ​

  1. Freeze an evidence cutoff and list every source by audience: player-visible, engine, test evidence, release note, UI source.
  2. Build a rule-by-rule matrix before reading any conclusion. For each row, record the copy value, the implementation value, the test evidence and the player-visible consequence.
  3. Recompute boundary values independently. Do not trust a green assertion to validate the copy that the player reads.
  4. Classify each mismatch by reachability and exposure:
    • player can see or operate it → blocker;
    • unreachable in the current build → pending;
    • wording ambiguity → unresolved, not silently “passed”.
  5. Add the three missing contract tests:
    • disguise checked → disguise;
    • disguise unchecked → no disguise;
    • second request in the same game → no effect.
  6. Add a player-copy snapshot test for every role reveal and rule line.
  7. Diff the inline App.jsx engine against src/gameEngine.js in CI. A duplicated engine is a second source of truth until it is proven identical.

Evidence ​

CheckResult
npm run sim re-run on 2026-10-2211/11 PASS
Release-package checks20
Player-visible blockers2
Non-blocking precision defects1
Unresolved wording issues1
Contract tests for the disguise checkbox0
Copy snapshot tests0
Dual-engine diff tests0
Findings unsupported by the source package0

The inline UI copy is not a hypothetical: App.jsx contains its own HIDDEN_WIN_CONDITIONS.NUN string with +500, and its processTurn auto-sets dukeDisguiseUsed without reading the useDisguise state. The engine file and the UI file can both be “correct” relative to their own tests and still disagree with each other.

Verdict ​

Fail. The engine is healthy enough for its own regression, but the release is not. Two player-visible blockers remain open: the Nun hidden-condition copy pays the wrong number, and the Duke disguise control does not control anything. The Regent wording still needs a design decision, and the Duke cap needs a copy fix.

A green regression is evidence about the layer it covers, not a release certificate. The reusable rule is: test the contract between layers, not only the layer that happens to have tests.

Remediation ​

ItemOwnerClose when
Nun hidden condition +500 → +50 (or engine to 500 with rebalance)Gameplay/logic engineerRole-modal screenshot shows +50; regression still passes
Wire checkbox state into context.disguiseRequests; engine only disguises on requestClient + gameplay engineerChecked / unchecked / repeat-request contract tests pass
Clarify Regent “betrayal” or rebind the bonusDesign + gameplay engineerSigned design note and final copy
Fix Duke cap copy (diff 0 → +50, decreasing by 1 per point)Gameplay engineerCopy snapshot passes
Add copy snapshots and dual-engine diffQACI job fails on any future drift

Crystaria (with Paw and Kyle)