Files
noitu/web/e2e/fixture-dictionary.js
T
tiennm99 80f216d56b feat(dict): build the corpus and word meanings from the Wikimedia viwiktionary dump
reader for both wikitext dialects; `meanings(word, ord, pos, gloss)` table; `meaning_count`/`words_with_meaning`/`source_pages` in meta, `source_rows` gone, builder_version 5; `--dump`/`--min-pages` replace `--kaikki`; attribution names the dump and the definition excerpts; 36,200 words, 96.9% with a meaning, every kaikki word kept.
2026-09-08 22:48:26 +07:00

93 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`
);
}