When a browser game has 128 choice sequences, test them all

2026-11-05来源 dev.toEnglish

Disclosure: This article was generated with AI from our project's existing source code and tests. The examples and reported model results were checked with automated tests. All shop figures below belong to a fictional simulation.

A small shop simulation has an awkward-looking result: it earns $7,450 in profit, then stops on Day 28 because it cannot pay wages. It still has $2,858 in cash. The bill is $2,960.

That $102 gap is intentional. But the result alone cannot tell you whether the simulation is correct. A duplicated expense, an invoice collected twice, or a failed payment deducted too early could produce a similarly convincing ending.

The useful constraint in this project is its size: seven decisions, with two choices at each decision. There are only 2⁷ = 128 complete input sequences per scenario. That makes exhaustive enumeration practical at the model layer.

Make the simulation independent of the screen

The core operation is a deterministic replay:

simulate(choices, throughDay, scenario) 

It starts from the scenario's initial state and returns daily snapshots, the current state, the next pending decision, and any failed bill. It does not read the DOM, local storage, or the wall clock.

The UI can show a day slowly, display a receipt, or skip an animation without owning the financial calculation. Replaying the same input recomputes a result; it does not apply another payment to a shared mutable balance.

That separation makes the model small enough to test without opening a browser.

Enumerate inputs, then check more than the ending

This TypeScript helper builds the input sequences:

type Choice = 'a' | 'b'; function allChoiceSequences(count: number): Choice[][] { let sequences: Choice[][] = [[]]; for (let i = 0; i < count; i++) { sequences = sequences.flatMap((prefix): Choice[][] => [ [...prefix, 'a'], [...prefix, 'b'], ]); } return sequences; } 

For seven decisions this produces 128 distinct sequences. The helper assumes a small, non-negative integer count; it is not intended to accept arbitrary user input.

For each sequence, the tests inspect every recorded daily state, not just the final cash balance. In this simplified model, the accounting identity is:

cash + inventory at cost + receivables − tax owed − wages owed − rent owed − materials owed − customer deposits = starting cash + cumulative profit 

The equation assumes this model's limited set of assets and liabilities, with no owner withdrawals or extra capital injections. It is not a complete accounting system.

The implementation stores amounts in integer cents and uses big.js for monetary arithmetic. The test compares the two sides exactly and also checks that monetary snapshots are safe integers. A rounded display value is not the test oracle.

These checks catch different mistakes from an ending assertion. For example, receiving a customer deposit increases cash and a liability. Treating it as immediate profit would break the model's intended timing, even if the final cash happened to look reasonable. There is a separate event-specific test for that transition.

Define what a failed payment means

When a bill cannot be paid, this model stops before deducting it. The available cash and the unpaid obligation both remain in the snapshot.

Here is a shortened test against the replay API; it assumes the project's simulate function is in scope:

import assert from 'node:assert/strict'; import Big from 'big.js'; const run = simulate(['b', 'a', 'a', 'a', 'a', 'a', 'b']); assert.equal(run.outcome, 'closed'); assert.equal(run.state.day, 28); assert.equal(run.state.cash, 285_800); assert.equal(run.state.profit, 745_000); assert.ok(run.failedBill); assert.equal(run.failedBill.amount, 296_000); assert.equal( new Big(run.failedBill.amount).minus(run.state.cash).toString(), '10200', ); 

Those are cents, so the shortfall is $102. Subtracting the whole unpaid bill would instead show negative cash and misrepresent the state where the game stopped.

This detail matters when the result screen and a teaching worksheet use the same simulation. Both should explain the same failed payment, rather than invent separate descriptions of the ending.

Test a partially played game too

Full enumeration alone does not cover the player's pause points.

Before an unanswered decision, replay pauses before that day's trade. After a choice, the UI can request the state through that decision's day. A useful check compares that partial replay with the matching snapshot from a complete run:

const route: Choice[] = ['b', 'a', 'a', 'b', 'a', 'a', 'a']; const complete = simulate(route); for (const [index, event] of scenario.events.entries()) { const prefix = route.slice(0, index + 1); const partial = simulate(prefix, event.day); const expected = complete.history.find((s) => s.day === event.day); assert.ok(expected); assert.deepEqual(partial.state, expected); assert.deepEqual(partial, simulate(prefix, event.day)); } 

Here scenario is the configuration used by simulate. The snippet is a test pattern, not a standalone game engine.

The replay save contains the scenario version, chosen options, and a presentation phase such as decision, feedback, or result. On restoration, the parser validates the data and replays it. It does not trust a saved cash total as the authoritative result.

There is a consequence: the rules associated with a scenario version must remain stable. Changing those rules while retaining the version can silently change an old save's result. Versioned inputs only reproduce history when the corresponding rules remain available or an explicit migration handles the change.

Treat outcome counts as a regression signal

For the current first scenario, enumeration produces:

Model outcome Input sequences
Robust finish 90
Tight finish 15
Closed 23

These are uniformly enumerated inputs, not measured player success rates. Players do not necessarily choose each option with equal probability. In other games, early termination may also make several complete input sequences collapse onto the same played path.

The counts are useful as a baseline: an unexpected change prompts investigation. They are not a reason to reject an intentional balance change or a substitute for invariant checks. Before preparing this article, the existing engine and scenario suites were rerun: 15 tests passed.

Enumeration also has limits. It does not test touch targets, image loading, animation timing, or real browser storage failures. Those need separate browser checks. And with 30 binary decisions, the input space grows to more than a billion sequences; this approach stops being a cheap default.

For a small deterministic game, however, it is worth checking the size of the state space before reaching for sampling. Sometimes every possible choice sequence fits comfortably inside a test run.

原文:https://www.a8q9.cn/en/article/when-a-browser-game-has-128-choice-sequences-test-them-all-139e31.html

相关文章