fix(sang-eratosthenes): keep the grid usable for primes above 50

rippling was raised before the stagger loop but lowered only inside the
final timeout, so a prime with no multiple left on the 100-cell grid left
the flag raised and rejected every later click. Affected 10 of the 25
primes on the grid.
This commit is contained in:
2026-09-15 10:06:46 +07:00
parent 0f7da4cf35
commit d558159173
2 changed files with 73 additions and 0 deletions
@@ -51,6 +51,14 @@ export function createGridState(buildAnnouncement) {
markedPrimes = [...markedPrimes, n];
const multiples = multiplesOf(n, 100);
// Every prime above 50 has no multiple left on a 100-cell grid. There is
// nothing to stagger, and entering ripple mode would lock the grid for
// good, because only the final timeout clears the flag.
if (multiples.length === 0) {
announcement = buildAnnouncement(n, multiples);
return;
}
if (reducedMotion) {
const next = new Map(crossings);
for (const cell of multiples) {
@@ -0,0 +1,65 @@
import { describe, it, expect } from 'vitest';
import { createGridState } from './grid-interaction.svelte.js';
/** Minimal announcement builder; the tests only care that it was called. */
const build = (/** @type {number} */ p, /** @type {number[]} */ mults) =>
`${p}: ${mults.length}`;
describe('createGridState — marking primes', () => {
it('a prime with no multiple left on the grid does not lock it', () => {
const grid = createGridState(build);
// 53 is prime and 2×53 = 106 is off the 100-cell grid, so there is
// nothing to cross out.
grid.handleCellActivate(53);
expect(grid.rippling).toBe(false);
expect(grid.markedPrimes).toContain(53);
expect(grid.announcement).toBe('53: 0');
// The grid must still accept the next prime.
grid.handleCellActivate(59);
expect(grid.markedPrimes).toContain(59);
});
it('every prime above 50 leaves the grid usable', () => {
const grid = createGridState(build);
for (const p of [53, 59, 61, 67, 71, 73, 79, 83, 89, 97]) {
grid.handleCellActivate(p);
expect(grid.rippling).toBe(false);
}
expect(grid.markedPrimes).toHaveLength(10);
});
it('a prime with multiples enters ripple mode and blocks further clicks', () => {
const grid = createGridState(build);
grid.handleCellActivate(7); // 14, 21, … are on the grid
expect(grid.rippling).toBe(true);
grid.handleCellActivate(11);
expect(grid.markedPrimes).not.toContain(11);
});
it('a composite is rejected and marks no prime', () => {
const grid = createGridState(build);
grid.handleCellActivate(9);
expect(grid.markedPrimes).toHaveLength(0);
expect(grid.shakeIndex).toBe(8);
});
it('the same prime cannot be marked twice', () => {
const grid = createGridState(build);
grid.handleCellActivate(53);
grid.handleCellActivate(53);
expect(grid.markedPrimes).toEqual([53]);
});
it('reset clears marks and unlocks the grid', () => {
const grid = createGridState(build);
grid.handleCellActivate(7);
expect(grid.rippling).toBe(true);
grid.handleReset();
expect(grid.rippling).toBe(false);
expect(grid.markedPrimes).toHaveLength(0);
expect(grid.announcement).toBe('');
});
});