mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-11 12:28:54 +00:00
A /rules page says, in one place, what nothing in the app said before: the chain rule, the clock, what a dead end costs, elimination and the last player standing, how a word is scored and the reconnect window. Linked from the landing page and from the board and lobby headers. The chat control moves above the chain as a pill with the unread count, where it can be reached on a phone mid-game, and the lobby's folded chat now carries an unread badge too. ESLint with the Svelte and JSDoc plugins, run in CI; the real findings it turned up (missing each keys, untyped timer handles) are fixed.
91 lines
3.3 KiB
JavaScript
91 lines
3.3 KiB
JavaScript
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
/**
|
|
* The same word list the server is playing against, read so a test can answer
|
|
* whatever syllable it is shown.
|
|
*
|
|
* Scripting a fixed sequence of words would not survive the bot choosing a
|
|
* different reply, and the bot is allowed to. Reading the graph instead lets a
|
|
* test assert what matters — that a legal word is accepted and an illegal one
|
|
* is refused — without depending on which legal word the opponent picked.
|
|
*/
|
|
const listPath = fileURLToPath(new URL('../../testdata/fixture-words.txt', import.meta.url));
|
|
|
|
// A line is the word, then optional tab-separated meanings; only the word is
|
|
// part of the graph, and the meanings are kept so a test can say what the
|
|
// chain should show for whichever word was drawn.
|
|
const lines = readFileSync(listPath, 'utf8')
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter((line) => line && !line.startsWith('#'));
|
|
|
|
const words = lines.map((line) => line.split('\t')[0].trim());
|
|
|
|
/** @type {Map<string, string>} word → its first sense as the chain renders it */
|
|
const firstSense = new Map();
|
|
for (const line of lines) {
|
|
const [word, sense] = line.split('\t').map((cell) => cell.trim());
|
|
if (!sense) continue;
|
|
const [pos, gloss] = sense.includes('|') ? sense.split('|') : ['', sense];
|
|
firstSense.set(word, pos ? `(${pos}) ${gloss}` : gloss);
|
|
}
|
|
|
|
/**
|
|
* The first sense of a fixture word, rendered as the chain renders it:
|
|
* `(pos) gloss`, or the gloss alone. Undefined for a word without one.
|
|
* @param {string} word
|
|
*/
|
|
export function renderedSense(word) {
|
|
return firstSense.get(word);
|
|
}
|
|
|
|
/** @type {Map<string, string[]>} */
|
|
const byFirstSyllable = new Map();
|
|
for (const word of words) {
|
|
const first = word.split(/\s+/)[0];
|
|
const list = byFirstSyllable.get(first) ?? [];
|
|
list.push(word);
|
|
byFirstSyllable.set(first, list);
|
|
}
|
|
|
|
/**
|
|
* A legal continuation of `syllable` that has not been played yet.
|
|
* @param {string} syllable
|
|
* @param {Set<string>} used
|
|
* @returns {string | undefined}
|
|
*/
|
|
export function continuation(syllable, used) {
|
|
return (byFirstSyllable.get(syllable) ?? []).find((word) => !used.has(word));
|
|
}
|
|
|
|
/**
|
|
* A word that is not in the dictionary at all. Vietnamese-shaped so it passes
|
|
* the syllable-count rule and is refused for the reason the test is about.
|
|
*/
|
|
export const UNKNOWN_WORD = 'xoăn xoét';
|
|
|
|
/** A single-syllable input, which the rules refuse before any lookup. */
|
|
export const ONE_SYLLABLE_WORD = 'sinh';
|
|
|
|
/**
|
|
* The syllable every game opens on, and how many words continue it.
|
|
*
|
|
* The server will only open on a word whose last syllable has at least 20
|
|
* continuations (minOpeningOutDegree in room.go). Exactly one syllable in this
|
|
* fixture clears that, which is what makes a scripted game deterministic — and
|
|
* what makes deleting a few words from the list break every browser test with
|
|
* an unhelpful "game_start_failed". Asserting it here turns that into a
|
|
* sentence a reader can act on.
|
|
*/
|
|
const HUB_SYLLABLE = 'sinh';
|
|
const MIN_OPENING_OUT_DEGREE = 20;
|
|
|
|
const hubContinuations = (byFirstSyllable.get(HUB_SYLLABLE) ?? []).length;
|
|
if (hubContinuations < MIN_OPENING_OUT_DEGREE) {
|
|
throw new Error(
|
|
`testdata/fixture-words.txt has ${hubContinuations} words starting with "${HUB_SYLLABLE}"; ` +
|
|
`the server needs ${MIN_OPENING_OUT_DEGREE} before it will open a game on that syllable`
|
|
);
|
|
}
|