configWith let the day count and the Revenue floor disagree

minCombinedRevenue fell back to SOLO_CONFIG's constant — the floor for a
FIVE-Day game — whatever days said. configWith({ days: 1 }) asked a
one-Day game to clear 15, which a full five-Day game averages barely
half of; configWith({ days: 10 }) asked for that same 15. It derives
from the days it was given now.

Not a live fault: createLocalSession is the only caller and the page
always writes the floor itself, so no dealt game was ever wrong. Found
by a throwaway probe that passed only days — which is how the next
caller would reach for it. Unchanged at the default day count, since
SOLO_CONFIG's floor is this same formula at DEFAULT_DAYS.

Stays in the unshipped v0.7.9 per Jesse — no version bump for the next
several fixes. 873 tests pass, three new.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
Jesse.Markowitz
2026-08-30 01:51:11 -04:00
co-authored by Claude Sonnet 5
parent e035dda5a3
commit 45cf521a40
3 changed files with 61 additions and 3 deletions
+14 -1
View File
@@ -76,6 +76,19 @@ who reopens it with "see the full results" comes back to.
the end of a timetable. Same shape as the three attempts before it: the thing that was verified was
not the thing the player uses.
### `configWith` let the day count and the Revenue floor disagree
`minCombinedRevenue` fell back to `SOLO_CONFIG`'s constant — the floor for a *five*-Day game —
whatever `days` said. So `configWith({ days: 1 })` asked a one-Day game to clear **15**, a figure a
full five-Day game averages barely half of, and `configWith({ days: 10 })` asked for the same 15 a
five-Day game does. It derives from the days it was actually given now.
Not a live fault: `createLocalSession` is the only caller, and the page always writes the floor
itself, so no dealt game was ever wrong. It was found by a throwaway probe written to reproduce the
extension bug above, which passed only `days` — which is exactly how the next caller would reach for
it. At the default day count the answer is unchanged, since `SOLO_CONFIG`'s own floor is this same
formula at `DEFAULT_DAYS`; the three new tests pin that as well as the derivation.
### Three wording and layout fixes
- **The collision entries** on all three screens now read "The game ends immediately and results in
@@ -90,7 +103,7 @@ not the thing the player uses.
cost, not a danger, and red would outrank the actual rules above it. The buttons say **Continue
Existing Saved Game** and **Deal New Game** rather than "Continue saved game" and "Deal".
870 tests pass, three of them new; one existing test asserted the opposite of the collision ruling
873 tests pass, six of them new; one existing test asserted the opposite of the collision ruling
above and says so where it was reversed.
---
+17 -2
View File
@@ -134,10 +134,25 @@ export type NewGameOptions = {
/** The same config with the New Game dialog's answers in it. */
export function configWith(opts: NewGameOptions): GameConfig {
const days = opts.days ?? SOLO_CONFIG.days;
return {
...SOLO_CONFIG,
days: opts.days ?? SOLO_CONFIG.days,
minCombinedRevenue: opts.minCombinedRevenue ?? SOLO_CONFIG.minCombinedRevenue,
days,
/**
* DERIVED FROM THE DAYS ACTUALLY IN PLAY, not from `SOLO_CONFIG`'s five-Day constant.
*
* It fell back to the constant until 2026-08-30, so `configWith({ days: 1 })` asked a one-Day
* game to clear **15** — a floor a five-Day game averages barely half of — and
* `configWith({ days: 10 })` asked for the same 15 a five-Day game does. The two fields silently
* disagreed, which is the one thing a "build me a config" helper must not let happen.
*
* Not a live fault when it was found: `createLocalSession` is the only caller, and the page
* always writes `minCombinedRevenue` itself (`solitaireDefaults` re-derives it from the preset).
* Found by a throwaway probe that passed only `days` — which is exactly how the next caller
* would use this. At the default day count the answer is unchanged, since `SOLO_CONFIG`'s own
* floor is this same formula at `DEFAULT_DAYS`.
*/
minCombinedRevenue: opts.minCombinedRevenue ?? collectiveRevenueFloor(1, days),
maxCollisionsPerDay: opts.maxCollisionsPerDay ?? SOLO_CONFIG.maxCollisionsPerDay,
maxCollisionsTotal: opts.maxCollisionsTotal ?? SOLO_CONFIG.maxCollisionsTotal,
optionalRules: { ...SOLO_CONFIG.optionalRules, ...(opts.optionalRules ?? {}) },
+30
View File
@@ -32,6 +32,8 @@ import { createGame as createEngineGame } from '../src/engine/setup.ts';
import { advance as advanceEngine } from '../src/engine/advance.ts';
import type { GameConfig } from '../src/engine/state.ts';
import {
configWith,
SOLO_CONFIG,
overHandLimit,
actionGroups,
actionMenu,
@@ -3339,6 +3341,34 @@ describe('the Day rolling over says so (Gitea#10)', () => {
// ---------------------------------------------------------------------------
describe('configWith', () => {
/**
* The Revenue floor is a function of how long the game is, so a helper that takes `days` and
* defaults the floor has to derive it. It fell back to `SOLO_CONFIG`'s five-Day constant until
* 2026-08-30, which let the two fields disagree: a one-Day game was asked to clear 15, a floor a
* five-Day game averages barely half of. Found by a probe that passed only `days` — which is how
* the next caller would reach for it.
*/
it('derives the Revenue floor from the days it was given', () => {
assert.equal(configWith({ days: 1 }).minCombinedRevenue, 3, 'a one-Day game kept the five-Day floor');
assert.equal(configWith({ days: 10 }).minCombinedRevenue, 30, 'a ten-Day game kept the five-Day floor');
});
it('leaves the default day count exactly where it was', () => {
// The fix must not move the floor anybody is actually playing against: SOLO_CONFIG's own floor
// is this same formula at DEFAULT_DAYS, so the default path is unchanged.
assert.equal(configWith({}).minCombinedRevenue, SOLO_CONFIG.minCombinedRevenue);
assert.equal(configWith({ days: SOLO_CONFIG.days }).minCombinedRevenue, SOLO_CONFIG.minCombinedRevenue);
});
it('still lets a caller name a floor that has nothing to do with the length', () => {
// Deriving is the DEFAULT, not a rule — the New Game dialog and the lobby both let a player set
// a floor directly, and that has to survive being passed through here.
assert.equal(configWith({ days: 10, minCombinedRevenue: 4 }).minCombinedRevenue, 4);
assert.equal(configWith({ days: 3, minCombinedRevenue: 0 }).minCombinedRevenue, 0, 'an off floor was re-derived');
});
});
describe('the end-of-game results screen (Gitea#16)', () => {
/**
* A finished game, built by running the clock off the end of a real one rather than by hand — the