Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e8ce584f |
@@ -19,6 +19,57 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.8.0.6 — 2026-09-10
|
||||
|
||||
Playing v0.8.0.5: *"saw bot's office area now — much better."* Three things still wrong, and one of
|
||||
them was mine hiding inside the fix for another.
|
||||
|
||||
### Your move is put away while the board is catching up
|
||||
|
||||
*"Your actions should be hidden while catching up."* Two reasons, and the second is the one that
|
||||
changed my mind about a decision taken early in v0.8.0 ("never block input"). The board on screen is
|
||||
behind the game, so a move offered there is a move against a position that has already moved on — the
|
||||
menu is computed from the CURRENT state and would be acted on while looking at an older one. And the
|
||||
screen had grown to four things competing at once: the district, the history, the catching-up row,
|
||||
and now a lit pile. Taking the action list out of that competition, while there is nothing to decide
|
||||
anyway, is the cheapest way to quieten it.
|
||||
|
||||
Not a block: Skip is one click away at the left of the row, so the wait stays voluntary. The buttons
|
||||
are replaced by the reason they are gone.
|
||||
|
||||
### …which could have locked a player out of their own game
|
||||
|
||||
Hiding actions behind `busy()` makes that flag the thing standing between a player and their turn —
|
||||
and **without `requestAnimationFrame` nothing ever advances the queue, so `busy()` would never
|
||||
clear.** The action list would have been hidden permanently, with Skip the only way to play.
|
||||
|
||||
Caught by `test/web.test.ts`, whose DOM stub has no `rAF` — the same stub that has been proving this
|
||||
page still starts since long before any of this existed. Two fallbacks now: no `rAF` means draw
|
||||
everything at once (exactly what `pace = 0` does deliberately), and a queue that throws empties
|
||||
itself rather than stranding the player. `test/step-queue.test.ts` pins that a never-advanced queue
|
||||
is still skippable.
|
||||
|
||||
### The lit pile was never brief — it was too quiet
|
||||
|
||||
*"Never saw decks lighting up… caught one flash deck light up for just a very brief moment, but
|
||||
couldn't see that with what bot was doing in office area and history and catch up area all at same
|
||||
time."*
|
||||
|
||||
Measured before changing anything: at 10× a pile stays lit for **6997ms**, just under seven seconds.
|
||||
So the highlight was not brief at all. It was a single 0.45s flash-in over a dark green fill, easy to
|
||||
miss entirely while looking at the district — a state that settles stops asking to be looked at. It
|
||||
pulses now for as long as the move is up, with a ring and a glow. The reduced-motion fallback is loud
|
||||
in a different way rather than simply still, since motion is the whole point here.
|
||||
|
||||
### The ceiling was not theoretical
|
||||
|
||||
*"At 10× — still a bit fast, but followable."* 10× was the top of the ladder, so the control's
|
||||
slowest setting was not slow enough for the person using it. `PACE_LEVELS` now runs to 20 and
|
||||
`MAX_PACE` with it. A control whose limit is reached in ordinary use has the wrong limit, not the
|
||||
right one held firmly.
|
||||
|
||||
---
|
||||
|
||||
## 0.8.0.5 — 2026-09-10
|
||||
|
||||
**Somewhere to look.** Jesse, playing v0.8.0.4 at 10×: *"many operations still occurred too fast for
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "station-master",
|
||||
"version": "0.8.0.5",
|
||||
"version": "0.8.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Station Master — a railroad operations game",
|
||||
|
||||
+9
-4
@@ -143,10 +143,15 @@ export function kindOf(cause: StepCause): StepKind {
|
||||
*
|
||||
* `pace` has no lower surprise — 0 means off — but an unbounded upper one does: `?pace=300` from
|
||||
* somebody typing 3.00, or a corrupt `localStorage` value, would give a switching move a five-minute
|
||||
* dwell and look exactly like a frozen board. Ten is far beyond any speed anyone would choose (2 and
|
||||
* 3 are the ones actually asked for) and well short of unusable.
|
||||
* dwell and look exactly like a frozen board. Twenty is far past any speed anyone would choose and
|
||||
* well short of unusable.
|
||||
*
|
||||
* RAISED FROM TEN 2026-09-10, because the ceiling turned out not to be theoretical: Jesse played at
|
||||
* 10× — the top of the ladder — and reported it *"still a bit fast, but followable"*. A control whose
|
||||
* slowest setting is not slow enough for the person using it has the wrong ceiling, not the right one
|
||||
* held firmly.
|
||||
*/
|
||||
export const MAX_PACE = 10;
|
||||
export const MAX_PACE = 20;
|
||||
|
||||
/**
|
||||
* The speeds the on-screen control offers, slowest last.
|
||||
@@ -158,7 +163,7 @@ export const MAX_PACE = 10;
|
||||
* was real. Watching a bot shunt cars is the point of this feature, and it is worth as long as it
|
||||
* takes.
|
||||
*/
|
||||
export const PACE_LEVELS = [0, 0.5, 1, 2, 3, 5, 7, 10] as const;
|
||||
export const PACE_LEVELS = [0, 0.5, 1, 2, 3, 5, 7, 10, 15, 20] as const;
|
||||
|
||||
/**
|
||||
* How long to show one step, in ms, at a given speed.
|
||||
|
||||
+52
-2
@@ -184,6 +184,20 @@ function drainIntoQueue(): void {
|
||||
const reset = session.takeDisplayReset();
|
||||
if (reset) stepQueue.reset(reset);
|
||||
stepQueue.push(session.takeDisplaySteps());
|
||||
/**
|
||||
* NOWHERE TO ANIMATE MEANS DO NOT QUEUE AT ALL.
|
||||
*
|
||||
* Without `requestAnimationFrame` nothing ever advances the queue, so `busy()` would stay true for
|
||||
* good — and since "Your Move" is now put away while the board is catching up, that would hide a
|
||||
* player's own actions permanently, leaving Skip as the only way to play the game. Drawing
|
||||
* everything at once is exactly what `pace = 0` does deliberately, so that is the honest fallback
|
||||
* rather than a broken page. Caught by `test/web.test.ts`, whose DOM stub has no `rAF` — the same
|
||||
* stub that has been proving this page still starts since long before any of this existed.
|
||||
*/
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
stepQueue.skip();
|
||||
return;
|
||||
}
|
||||
if (stepQueue.busy()) startAnimationLoop();
|
||||
}
|
||||
|
||||
@@ -316,12 +330,25 @@ function startAnimationLoop(): void {
|
||||
*/
|
||||
console.error('display queue stopped:', err);
|
||||
animating = false;
|
||||
// Do not strand the player behind a queue that can no longer advance: jump the board to the
|
||||
// live position, which brings "Your Move" back with it.
|
||||
try {
|
||||
stepQueue.skip();
|
||||
} catch {
|
||||
/* nothing further to try — the authoritative Frame is still what the rest of the page draws */
|
||||
}
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (!stepQueue.busy()) {
|
||||
animating = false;
|
||||
// One last render so the "N behind" row collapses the moment the board is level.
|
||||
renderWatching();
|
||||
/**
|
||||
* A FULL RENDER, not just the row. The board being level again is what brings "Your Move"
|
||||
* back and clears the last lit pile, so redrawing only the catching-up row would leave the
|
||||
* action list hidden until something else happened to trigger a render — which, when the game
|
||||
* is waiting on this player, is nothing at all.
|
||||
*/
|
||||
render();
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
@@ -1987,6 +2014,29 @@ function renderActions(
|
||||
renderEnding(el, f);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* YOUR MOVE IS PUT AWAY WHILE THE BOARD IS CATCHING UP — Jesse, 2026-09-10: *"your actions should
|
||||
* be hidden while catching up."*
|
||||
*
|
||||
* Two reasons, and the second is the one that changed my mind about it. The board on screen is
|
||||
* behind the game, so a move offered here is a move against a position that has already moved on —
|
||||
* the menu is computed from the CURRENT state and would be acted on while looking at an older one.
|
||||
* And the display had grown to four things demanding attention at once — the district, the history,
|
||||
* the catching-up row and now a lit pile — which is what made the pile highlight so easy to miss.
|
||||
* Taking the action list out of that competition while there is nothing to decide anyway is the
|
||||
* cheapest way to quieten it.
|
||||
*
|
||||
* NOT A BLOCK. Skip is one click away and sits at the left of the row, so the wait is always
|
||||
* voluntary; this replaces the buttons with the reason they are gone, rather than leaving a live
|
||||
* menu over a stale board.
|
||||
*/
|
||||
if (stepQueue.busy()) {
|
||||
el.innerHTML =
|
||||
'<div class="dim">Catching up on what everyone else did — your move is here when the board is ' +
|
||||
'level with the game. <b>Skip</b> jumps straight to it.</div>';
|
||||
return;
|
||||
}
|
||||
// The game is running, so the next ending — an extended Day's, or a fresh game's — is entitled to
|
||||
// put its results up unasked again (Gitea#11).
|
||||
resultsShown = false;
|
||||
|
||||
+14
-4
@@ -723,10 +723,20 @@ h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3;ma
|
||||
the player would be back to staring at an unchanged board. The flash-in marks the moment; the lit
|
||||
border and background stay for exactly as long as the step is up, because the class is on the
|
||||
element only while that step is the one being shown. */
|
||||
.handcard.pilelit{border-color:#8fd6a0;background:#1d3327;animation:pileflash .45s ease-out 1}
|
||||
@keyframes pileflash{0%{background:#2f6b47;border-color:#c7f0d4;transform:scale(1.1)}
|
||||
100%{background:#1d3327;border-color:#8fd6a0;transform:scale(1)}}
|
||||
@media(prefers-reduced-motion:reduce){.handcard.pilelit{animation:none}}
|
||||
.handcard.pilelit{border-color:#8fd6a0;background:#1d3327;box-shadow:0 0 0 2px #2f6b47,0 0 14px rgba(143,214,160,.55);
|
||||
animation:pilepulse 1.15s ease-in-out infinite}
|
||||
/* A PULSE FOR THE WHOLE DWELL, not one flash at the start. Measured: at 10x a pile stays lit for
|
||||
just under seven seconds, so the highlight was never brief — but a single 0.45s flash-in and a
|
||||
dark green fill were easy to miss entirely while watching the district. Jesse: "caught one flash
|
||||
deck light up for just a very brief moment, but couldn't see that with what bot was doing in
|
||||
office area and history and catch up area all at same time." Something still moving keeps drawing
|
||||
the eye for as long as the move is up; a state that settles stops asking to be looked at. */
|
||||
@keyframes pilepulse{0%,100%{background:#1d3327;box-shadow:0 0 0 2px #2f6b47,0 0 14px rgba(143,214,160,.45)}
|
||||
50%{background:#2f6b47;box-shadow:0 0 0 3px #8fd6a0,0 0 22px rgba(143,214,160,.85)}}
|
||||
/* Motion is the point here, so the reduced-motion fallback has to be loud in a different way rather
|
||||
than simply not moving: a solid ring and a brighter fill, held. */
|
||||
@media(prefers-reduced-motion:reduce){
|
||||
.handcard.pilelit{animation:none;background:#2f6b47;box-shadow:0 0 0 3px #8fd6a0}}
|
||||
.handcard.unplayable::after{content:"";position:absolute;inset:0;border-radius:5px;pointer-events:none;
|
||||
background:repeating-linear-gradient(45deg,transparent 0 5px,rgba(150,160,175,.20) 5px 6px)}
|
||||
/* THE CARD JUST DRAWN. It sits first in the row, and this says which one that is — three cards that
|
||||
|
||||
@@ -131,6 +131,12 @@ describe('pacing — dwell by kind', () => {
|
||||
assert.equal(PACE_LEVELS[0], 0, 'off must be the first rung — #18 wants it turned off');
|
||||
assert.ok(PACE_LEVELS.includes(1), 'the default must be on the ladder');
|
||||
assert.ok(PACE_LEVELS.includes(7), '7x was asked for by name');
|
||||
/**
|
||||
* The ceiling is not theoretical. Jesse played at 10× — the top of the ladder as it then was —
|
||||
* and called it "still a bit fast, but followable", so the ladder has to go past the speed
|
||||
* somebody actually reached for and found insufficient.
|
||||
*/
|
||||
assert.ok(PACE_LEVELS.some((p) => p > 10), 'the ladder must go beyond the speed that was too fast');
|
||||
for (const p of PACE_LEVELS) {
|
||||
assert.ok(p <= MAX_PACE, `${p}x is past MAX_PACE, so the control would lie about it`);
|
||||
assert.equal(dwellFor('switch.move', p), Math.round(DWELL.switching * p));
|
||||
|
||||
@@ -251,6 +251,25 @@ describe('the step queue', () => {
|
||||
assert.ok(q.showing() !== null, 'the caption should survive a reset');
|
||||
});
|
||||
|
||||
it('can always be emptied, so a player is never stranded behind it', () => {
|
||||
/**
|
||||
* "Your Move" is put away while the board is catching up (v0.8.0.6), which makes `busy()` the
|
||||
* thing standing between a player and their own turn. So the ways it can be cleared matter more
|
||||
* than they did: `skip()` must always work, from any state, including one where the clock has
|
||||
* never advanced — which is exactly the situation a page with no `requestAnimationFrame` is in,
|
||||
* and how this was found.
|
||||
*/
|
||||
const { steps, final } = realSteps(1917398, 400);
|
||||
const q = createStepQueue();
|
||||
q.reset(baseline(1917398));
|
||||
q.push(steps);
|
||||
// Never advanced at all: no frame has been shown, and the queue is full.
|
||||
assert.equal(q.busy(), true);
|
||||
assert.equal(q.skip(), true, 'a never-advanced queue must still be skippable');
|
||||
assert.equal(q.busy(), false, 'and must be idle afterwards, or the player stays locked out');
|
||||
assert.deepEqual(q.current(), final);
|
||||
});
|
||||
|
||||
it('draws nothing before a reset has arrived', () => {
|
||||
const q = createStepQueue();
|
||||
assert.equal(q.current(), null);
|
||||
|
||||
Reference in New Issue
Block a user