Say who the game is waiting on, and move the Fedora; replay in words
Three items folded into the unshipped v0.7.9. WAITING ON (reported by Jesse from play). The status line said "nobody — the Division is running itself" while the game was stopped on the Superintendent. Frame.actor carried clock.currentActor, which is null for the whole Mainline Phase, so all three interruptions — §8.1's clearance ruling, Gitea#5's Yard Office offer, Gitea#19's Red Flag prompt — reported that nobody was holding it up. actingPlayer had the answer since the Gitea#5 refactor; the Frame threw it away. It carries actingPlayer now, plus a new `awaiting` field naming the question and the train: "waiting on Bob · a clearance ruling · Train 4". Naming the person alone is not enough when three different things can be pending. THE FEDORA (TODO #29) rides at the right-hand end of the phase row instead of a line of its own, and wraps under rather than squeezing the chips. THE DEVELOPER REPLAY (TODO #34) printed "loss — revenueFloor", the same defect Gitea#16 was filed about, still alive because nothing player-facing pointed at it. panels.ts's reasonSentence is exported and shared rather than reimplemented, fed the last recorded frame and stripped of markup. The drift test maps win/loss to won/lost so it still checks the two AGREE rather than that they are spelled alike. Also carries the previous, unsigned commit's work: the two setup screens worded the same section by section. 877 tests pass, ten new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
131538dc7c
commit
a70b7f88f3
+59
-10
@@ -2549,6 +2549,47 @@ describe('the static build', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('names who the game is waiting on when it stops to ask them something', () => {
|
||||
/**
|
||||
* REPORTED BY JESSE 2026-08-30: "waiting on shows 'nobody — the Division is running itself' BUT
|
||||
* the system is actually waiting on the Superintendent."
|
||||
*
|
||||
* `Frame.actor` carried `clock.currentActor`, which is null for the whole Mainline Phase — so a
|
||||
* game stopped dead on a §8.1 clearance ruling said nobody was holding it up, while it waited on
|
||||
* a named person to click. `actingPlayer` had the answer the whole time; the Frame threw it
|
||||
* away. Naming them is only half of it: three different interruptions can be pending, and
|
||||
* "waiting on Bob" alone is a game that looks stuck to everyone except Bob.
|
||||
*/
|
||||
const base = { day: 2, stage: 5, clock: '2:20', phase: 'Mainline', phaseKey: 'mainline' };
|
||||
|
||||
const idle = turnChartHtml({ ...base, actor: null, awaiting: null }, null, 'Bob');
|
||||
assert.match(idle, /nobody — the Division is running itself/, 'an automatic phase should say so');
|
||||
|
||||
for (const [asks, train] of [
|
||||
['a clearance ruling', 'Train 4'],
|
||||
['the Yard Office offer', 'Train 7'],
|
||||
['a Red Flag', 'Train X18'],
|
||||
] as [string, string][]) {
|
||||
const html = turnChartHtml({ ...base, actor: 1, awaiting: { asks, train } }, 'Bob', 'Bob');
|
||||
assert.doesNotMatch(html, /running itself/, `"${asks}" still reported nobody`);
|
||||
assert.match(html, /waiting on <b>Bob<\/b>/, `"${asks}" did not name who it waits on`);
|
||||
assert.match(html, new RegExp(asks.replace(/ /g, ' ')), `"${asks}" did not say what is being asked`);
|
||||
assert.match(html, new RegExp(train), `"${asks}" did not name the train it is about`);
|
||||
}
|
||||
});
|
||||
|
||||
it('puts the Fedora at the right-hand end of the phase row (TODO #29)', () => {
|
||||
// It sat on a line of its own between the phases and everything above them, which put a thing
|
||||
// that moves every third Stage in among the things that move every Stage. The row it belongs
|
||||
// beside is the one whose last chip is Supervisor Shift — the phase that passes it.
|
||||
const frame = { day: 1, stage: 4, clock: '2:00', phase: 'New Train', phaseKey: 'newTrain', actor: 1 };
|
||||
const html = turnChartHtml(frame, 'Bob', 'Bob');
|
||||
const row = /<div class="tc-row">([\s\S]*?)<\/div>\s*$/.exec(html)?.[1] ?? '';
|
||||
assert.match(row, /<ol class="tc-phases">/, 'the phase row is not in the row wrapper');
|
||||
assert.ok(row.indexOf('tc-super') > row.indexOf('tc-phases'), 'the Fedora is not after the phases');
|
||||
assert.match(turnChartHtml(frame, 'Bob', null), /tc-row/, 'solitaire lost the row wrapper with the Fedora');
|
||||
});
|
||||
|
||||
it('names the Superintendent at a table, and stays quiet about it in solitaire', () => {
|
||||
/**
|
||||
* REPORTED BY JESSE 2026-08-23, playing two-player on StartOS: seat 1 played a train card and
|
||||
@@ -3865,8 +3906,13 @@ describe('the lobby screen', () => {
|
||||
els.get('lb-freight')!['value'] = '3';
|
||||
(els.get('lb-freight')!['oninput'] as () => void)();
|
||||
assert.equal(chosen(groups, 'lb-type'), 'custom', 'changing a RULE should have selected Custom');
|
||||
assert.match(String(els.get('lb-type-note')!['textContent']), /scored as Co-op/);
|
||||
assert.match(String(els.get('lb-type-note')!['textContent']), /1 setting differs from Co-op/);
|
||||
// The summary sentence under the radios is gone (2026-08-30). What says HOW a Custom game
|
||||
// differs is the hint on the row that differs, which is where it can be acted on.
|
||||
assert.match(
|
||||
String(els.get('lb-freight-hint')!['textContent']),
|
||||
/Co-op default/,
|
||||
'the changed row does not say what it was changed from',
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking a type again resets every rule, and leaves the parameters alone', async () => {
|
||||
@@ -4229,10 +4275,11 @@ describe('the solitaire setup screen', () => {
|
||||
const tuned = await load('?seed=430&transit=4');
|
||||
(tuned.els.get('newgame')!['onclick'] as () => void)();
|
||||
assert.equal(readForm(tuned.els, tuned.groups).type, 'custom', 'a game paying for transits still read as Solitaire');
|
||||
// As in the lobby: the row that differs carries the difference, not a sentence under the radios.
|
||||
assert.match(
|
||||
String(tuned.els.get('ss-type-note')!['textContent']),
|
||||
/1 setting differs from Solitaire/,
|
||||
'the note did not say what differs',
|
||||
String(tuned.els.get('ss-transit-hint')!['textContent']),
|
||||
/Solitaire default/,
|
||||
'the changed row does not say what it was changed from',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4245,11 +4292,13 @@ describe('the solitaire setup screen', () => {
|
||||
assert.deepEqual(disabled, ['coop', 'competitive', 'cutthroat'], 'the wrong game types are dealable here');
|
||||
// Deal is never disabled here any more: the only types this screen can SELECT are the two it can
|
||||
// deal, so a disabled button would be answering a question the radios no longer ask.
|
||||
assert.match(
|
||||
readFileSync(join(dist, 'play.html'), 'utf8'),
|
||||
/use the Multiplayer door instead/,
|
||||
'nothing on the setup screen says where a multiplayer game comes from',
|
||||
);
|
||||
// No reason is printed beside the dimmed rows any more (Jesse, 2026-08-30 — "grayed out with no
|
||||
// additional explanation"). The heading is what says which game the screen deals, once, and it
|
||||
// is the same shape on the lobby, which is what lets one list serve both.
|
||||
const page = readFileSync(join(dist, 'play.html'), 'utf8');
|
||||
assert.match(page, /Game type \(solitaire\)/, 'the setup screen does not name the game it deals');
|
||||
assert.match(page, /Game type \(multi-player\)/, 'the lobby does not name the game it deals');
|
||||
assert.ok(!page.includes('lb-why'), 'the per-row reason is back, on the page or in its stylesheet');
|
||||
});
|
||||
|
||||
it('clicking a game type resets every rule below to it, and leaves the parameters alone', async () => {
|
||||
|
||||
Reference in New Issue
Block a user