Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion apps/client/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,20 @@ button:disabled {
position: relative;
z-index: 1;
margin: 0 auto 1rem;
max-width: 44rem;
text-align: center;
font-weight: 700;
font-size: clamp(1rem, 2vw, 1.4rem);
}
.display-results > h1 {
max-width: 44rem;
}
.display-results__quip {
width: fit-content;
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
white-space: normal;
}
.display-results__fireworks {
position: absolute;
inset: 0;
Expand Down
15 changes: 15 additions & 0 deletions apps/client/src/styles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ describe('tile typography', () => {
expect(styles).not.toMatch(
/\.display-results\s*\{[^}]*justify-content: center;/s,
);
expect(styles).toMatch(
/\.display-results > h1\s*\{[^}]*max-width: 44rem;/s,
);
expect(styles).toMatch(
/\.display-results__quip\s*\{[^}]*width: fit-content;[^}]*max-width: 100%;[^}]*min-width: 0;[^}]*overflow-wrap: anywhere;[^}]*white-space: normal;/s,
);
expect(styles).not.toMatch(
/\.display-results__quip\s*\{[^}]*width: max-content;/s,
);
expect(styles).not.toMatch(
/\.display-results__quip\s*\{[^}]*max-width: 44rem;/s,
);
expect(styles).not.toMatch(
/\.display-results__quip\s*\{[^}]*white-space: nowrap;/s,
);
expect(styles).toMatch(
/\.display-results__cards\s*\{[^}]*width: fit-content;[^}]*max-width: 96vw;[^}]*grid-template-columns: repeat\(2, minmax\(16rem, max-content\)\);/s,
);
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
generateDefaultBoard,
loadProductionDictionary,
PRODUCTION_DICTIONARY_IDENTITY,
selectMedianBoard,
selectStrongBoard,
type ProductionDictionaryLoadResult,
} from '@words/game-data';
import {
Expand Down Expand Up @@ -243,7 +243,7 @@ export function createWordsServer(
attempts: 0,
};
}
return selectMedianBoard({
return selectStrongBoard({
size,
random: randomSource,
dictionary: gameDataRuntime.dictionary,
Expand Down
4 changes: 2 additions & 2 deletions apps/server/tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1978,7 +1978,7 @@ describe('Words Stage 4B server', () => {
});
});

it('uses the median selector with eight production-valid candidates', async () => {
it('uses the strong selector with 32 production-valid candidates', async () => {
await server.stop();
const randomSource = { next: vi.fn(() => 0.5) };
const boardGenerator = vi.fn(({ size, random }) => {
Expand Down Expand Up @@ -2013,7 +2013,7 @@ describe('Words Stage 4B server', () => {
ok: true,
room: { round: { generationAttempts: 1 } },
});
expect(boardGenerator).toHaveBeenCalledTimes(8);
expect(boardGenerator).toHaveBeenCalledTimes(32);
});

it('broadcasts one count-only progress snapshot while keeping accepted words private', async () => {
Expand Down
15 changes: 15 additions & 0 deletions docs/DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Development log

## 2026-08-20 — Strong high-density board candidate (draft)

- Added a separate server-only strong selector that chooses the highest
playable-word count from 32 production-valid candidates, breaking ties by
cell coverage and then generation order. The median selector remains intact.
- Audited 500 deterministic selections per size twice with seed
`strong-board-selection-v1`; outputs were byte-identical and selected medians
exceeded the retained original corpus P95 for 4 × 4, 5 × 5, and 6 × 6.
- Let the display result quip prefer its intrinsic width with `fit-content`
while shrinking safely on narrow portrait screens; normal wrapping and the
heading's 44rem bound remain intact.
- Kept generation profiles, dictionary, `QU`, network contracts, lifecycle,
result content, and production publication unchanged. This branch is only a
physical gameplay and TV test candidate.

## 2026-08-14 — Post-round player board review (draft)

- Player phones now retain the exact authoritative finished board as a frozen,
Expand Down
51 changes: 51 additions & 0 deletions docs/board-quality-research/STRONG_SELECTION_EXPERIMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Strong board selection experiment

This physical-test candidate keeps the production generator, dictionary, board
acceptance profiles, `QU` policy, and generation-attempt bounds unchanged. For
each round it solves 32 accepted production-valid candidates, then selects by:

1. highest playable-word count;
2. highest cell coverage;
3. earliest candidate generation index.

Long-word count and vowel spread are measured but are not ranking criteria.
Production's existing median selector and targets remain available unchanged.

## Deterministic audit

`npm run research:strong-selection` ran 500 selections per board size with seed
`strong-board-selection-v1`, covering 48,000 requested candidates. A second run
produced a byte-identical
`generated/strong-selection-summary.json`. Runtime values are intentionally not
stored in that deterministic artifact because they vary by machine.

| Size | Playable words P5 / P50 / P95 | Long words P5 / P50 / P95 | Cell coverage P5 / P50 / P95 | Mean successful candidates | Mean generation attempts |
| ---: | ----------------------------: | ------------------------: | ---------------------------: | -------------------------: | -----------------------: |
| 4×4 | 125.95 / 164 / 226.05 | 38 / 67 / 107.05 | 0.9375 / 1 / 1 | 32 | 39.316 |
| 5×5 | 250 / 307 / 398 | 98 / 147 / 220.1 | 0.92 / 1 / 1 | 32 | 38.832 |
| 6×6 | 404.95 / 479 / 603.05 | 179.95 / 244 / 357.05 | 0.9444444444444444 / 1 / 1 | 31.998 | 43.086 |

One representative local run measured these complete selector runtimes:

| Size | Mean ms | P50 ms | P95 ms |
| ---: | ------: | -----: | -----: |
| 4×4 | 9.537 | 9.402 | 10.562 |
| 5×5 | 20.046 | 19.994 | 22.020 |
| 6×6 | 36.022 | 35.722 | 38.966 |

## Original-corpus comparison

The retained original corpus contains 5,000 independently generated boards per
size. The strong selector's median is above that corpus's P95 for every size:

| Size | Original P5 / P50 / P95 | Selected P5 / P50 / P95 | Selected P50 minus original P95 |
| ---: | ----------------------: | ----------------------: | ------------------------------: |
| 4×4 | 31 / 72 / 144 | 125.95 / 164 / 226.05 | +20 |
| 5×5 | 77 / 153 / 267 | 250 / 307 / 398 | +40 |
| 6×6 | 148 / 262 / 428.05 | 404.95 / 479 / 603.05 | +50.95 |

Selected P5 remains slightly below the original P95, by 18.05, 17, and 23.10
words respectively. The complete selected distributions nevertheless occupy
the intended upper tail rather than clustering around the median targets. This
audit establishes selection behavior, not whether denser boards are more fun;
that remains the purpose of physical testing.
149 changes: 149 additions & 0 deletions docs/board-quality-research/generated/strong-selection-summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{
"schemaVersion": 1,
"seed": "strong-board-selection-v1",
"selectionsPerSize": 500,
"candidatePoolSize": 32,
"ranking": [
"highest playable-word count",
"highest cell coverage",
"earliest candidate generation index"
],
"sizes": {
"4": {
"originalFiveThousandBoardCorpus": {
"playableWordCount": {
"min": 6,
"p5": 31,
"p50": 72,
"p95": 144,
"p99": 180,
"max": 276
},
"longPlayableWordCount": {
"min": 0,
"p5": 3,
"p50": 18,
"p95": 58,
"p99": 81.01000000000022,
"max": 155
},
"cellCoverage": {
"min": 0.5625,
"p5": 0.875,
"p50": 1,
"p95": 1,
"p99": 1,
"max": 1
}
},
"selected": {
"playableWordCount": {
"p5": 125.95,
"p50": 164,
"p95": 226.04999999999995
},
"longPlayableWordCount": {
"p5": 38,
"p50": 67,
"p95": 107.04999999999995
},
"cellCoverage": { "p5": 0.9375, "p50": 1, "p95": 1 }
},
"upperTailComparison": {
"selectedP5MinusOriginalP95": -18.049999999999997,
"selectedP50MinusOriginalP95": 20,
"selectedP5AtOrAboveOriginalP95": false
},
"meanSuccessfulCandidateCount": 32,
"meanUnderlyingGenerationAttempts": 39.316
},
"5": {
"originalFiveThousandBoardCorpus": {
"playableWordCount": {
"min": 23,
"p5": 77,
"p50": 153,
"p95": 267,
"p99": 330.0100000000002,
"max": 436
},
"longPlayableWordCount": {
"min": 2,
"p5": 15,
"p50": 51,
"p95": 126,
"p99": 172,
"max": 256
},
"cellCoverage": {
"min": 0.6,
"p5": 0.88,
"p50": 1,
"p95": 1,
"p99": 1,
"max": 1
}
},
"selected": {
"playableWordCount": { "p5": 250, "p50": 307, "p95": 398 },
"longPlayableWordCount": {
"p5": 98,
"p50": 147,
"p95": 220.0999999999999
},
"cellCoverage": { "p5": 0.92, "p50": 1, "p95": 1 }
},
"upperTailComparison": {
"selectedP5MinusOriginalP95": -17,
"selectedP50MinusOriginalP95": 40,
"selectedP5AtOrAboveOriginalP95": false
},
"meanSuccessfulCandidateCount": 32,
"meanUnderlyingGenerationAttempts": 38.832
},
"6": {
"originalFiveThousandBoardCorpus": {
"playableWordCount": {
"min": 51,
"p5": 148,
"p50": 262,
"p95": 428.0500000000002,
"p99": 514.0100000000002,
"max": 672
},
"longPlayableWordCount": {
"min": 2,
"p5": 38,
"p50": 101,
"p95": 215,
"p99": 282,
"max": 423
},
"cellCoverage": {
"min": 0.75,
"p5": 0.9166666666666666,
"p50": 1,
"p95": 1,
"p99": 1,
"max": 1
}
},
"selected": {
"playableWordCount": { "p5": 404.95, "p50": 479, "p95": 603.05 },
"longPlayableWordCount": {
"p5": 179.95,
"p50": 244,
"p95": 357.04999999999995
},
"cellCoverage": { "p5": 0.9444444444444444, "p50": 1, "p95": 1 }
},
"upperTailComparison": {
"selectedP5MinusOriginalP95": -23.100000000000193,
"selectedP50MinusOriginalP95": 50.94999999999982,
"selectedP5AtOrAboveOriginalP95": false
},
"meanSuccessfulCandidateCount": 31.998,
"meanUnderlyingGenerationAttempts": 43.086
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"data:boards:audit": "tsx packages/game-data/scripts/audit-boards.mjs",
"research:boards": "tsx scripts/research-board-quality.ts",
"research:median-selection": "tsx scripts/audit-median-board-selection.ts",
"research:strong-selection": "tsx scripts/audit-strong-board-selection.ts",
"privacy:check": "node scripts/check-public-privacy.mjs",
"build:production": "npm run build --workspace @words/client && node scripts/build-production.mjs",
"start:production": "node dist/production/server/index.mjs",
Expand Down
Loading