Adjustments to Word Games
This commit is contained in:
parent
1e153b34ef
commit
2a22bdbcde
|
|
@ -612,24 +612,30 @@ export default class KiitosGame extends Phaser.Scene {
|
||||||
promptNameWord(newBuilt) {
|
promptNameWord(newBuilt) {
|
||||||
(this.humanBtns ?? []).forEach((b) => b.destroy());
|
(this.humanBtns ?? []).forEach((b) => b.destroy());
|
||||||
this.promptTxt.setText(`Name your word — it must start with "${newBuilt}".`);
|
this.promptTxt.setText(`Name your word — it must start with "${newBuilt}".`);
|
||||||
const cy = CENTER_Y + 200;
|
|
||||||
this.humanInput = new TextInput(this, GAME_WIDTH / 2 - 130, cy, {
|
// Two-row layout: input centered, then Cancel | Announce below.
|
||||||
width: 360, height: 56, value: newBuilt.toLowerCase(),
|
// All three share the same outer extents (cx ± 230) for clean alignment.
|
||||||
|
const cx = GAME_WIDTH / 2;
|
||||||
|
const inputY = CENTER_Y + 196;
|
||||||
|
const btnY = inputY + 72;
|
||||||
|
|
||||||
|
this.humanInput = new TextInput(this, cx, inputY, {
|
||||||
|
width: 460, height: 56, value: newBuilt.toLowerCase(),
|
||||||
placeholder: `word starting "${newBuilt}"…`, maxLength: 12, autocomplete: 'off',
|
placeholder: `word starting "${newBuilt}"…`, maxLength: 12, autocomplete: 'off',
|
||||||
});
|
});
|
||||||
this.humanInput.focus();
|
this.humanInput.focus();
|
||||||
this.humanInput.on('keydown', (e) => { if (e.key === 'Enter') this.confirmTentativeWord(); });
|
this.humanInput.on('keydown', (e) => { if (e.key === 'Enter') this.confirmTentativeWord(); });
|
||||||
|
|
||||||
this.feedbackTxt?.destroy();
|
this.feedbackTxt?.destroy();
|
||||||
this.feedbackTxt = this.add.text(GAME_WIDTH / 2, cy + 60, '', {
|
this.feedbackTxt = this.add.text(cx, btnY + 62, '', {
|
||||||
fontFamily: 'Righteous', fontSize: '24px', color: THEME.negativeHex,
|
fontFamily: 'Righteous', fontSize: '24px', color: THEME.negativeHex,
|
||||||
}).setOrigin(0.5).setDepth(D.ui);
|
}).setOrigin(0.5).setDepth(D.ui);
|
||||||
|
|
||||||
this.humanBtns = [
|
this.humanBtns = [
|
||||||
new Button(this, GAME_WIDTH / 2 + 130, cy, 'Announce', () => this.confirmTentativeWord(),
|
new Button(this, cx + 130, btnY, 'Announce', () => this.confirmTentativeWord(),
|
||||||
{ width: 200, height: 56, fontSize: 24, bg: THEME.accent }).setDepth(D.ui),
|
{ width: 200, height: 56, fontSize: 24, bg: THEME.accent }).setDepth(D.ui),
|
||||||
new Button(this, GAME_WIDTH / 2 - 360, cy, 'Cancel', () => this.cancelTentative(),
|
new Button(this, cx - 130, btnY, 'Cancel', () => this.cancelTentative(),
|
||||||
{ variant: 'ghost', width: 150, height: 56, fontSize: 22 }).setDepth(D.ui),
|
{ variant: 'ghost', width: 200, height: 56, fontSize: 22 }).setDepth(D.ui),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,9 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
||||||
this.letters = data.letters ?? [];
|
this.letters = data.letters ?? [];
|
||||||
this.validWords = new Set((data.validWords ?? []).map((w) => w.toUpperCase()));
|
this.validWords = new Set((data.validWords ?? []).map((w) => w.toUpperCase()));
|
||||||
this.pangramSet = new Set((data.pangrams ?? []).map((w) => w.toUpperCase()));
|
this.pangramSet = new Set((data.pangrams ?? []).map((w) => w.toUpperCase()));
|
||||||
|
this.difficulty = difficulty;
|
||||||
this.maxScore = data.maxScore ?? 0;
|
this.maxScore = data.maxScore ?? 0;
|
||||||
this.tiers = buildTiers(this.maxScore);
|
this.tiers = buildTiers(this.maxScore, difficulty);
|
||||||
|
|
||||||
this.buildBoard();
|
this.buildBoard();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
// Rank tiers as a fraction of the puzzle's maximum possible score (NYT scale).
|
// Rank tiers as a fraction of the puzzle's maximum possible score (NYT scale).
|
||||||
// Queen Bee (100%) is intentionally omitted — Genius is the achievable goal.
|
// Queen Bee (100%) is intentionally omitted — Genius is the achievable goal.
|
||||||
|
// Genius threshold scales down on easier difficulties so it's actually reachable.
|
||||||
const TIER_TABLE = [
|
const TIER_TABLE = [
|
||||||
{ name: 'Beginner', pct: 0.00 },
|
{ name: 'Beginner', pct: 0.00 },
|
||||||
{ name: 'Good Start', pct: 0.02 },
|
{ name: 'Good Start', pct: 0.02 },
|
||||||
|
|
@ -14,6 +15,9 @@ const TIER_TABLE = [
|
||||||
{ name: 'Genius', pct: 0.70 },
|
{ name: 'Genius', pct: 0.70 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Fraction of maxScore required to reach Genius per difficulty.
|
||||||
|
const GENIUS_PCT = { easy: 0.40, normal: 0.55, hard: 0.70 };
|
||||||
|
|
||||||
// Number of distinct letters in a word.
|
// Number of distinct letters in a word.
|
||||||
function distinctCount(word) {
|
function distinctCount(word) {
|
||||||
return new Set(word.toUpperCase()).size;
|
return new Set(word.toUpperCase()).size;
|
||||||
|
|
@ -33,10 +37,12 @@ export function scoreWord(word) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concrete tier thresholds (rounded score needed to reach each tier).
|
// Concrete tier thresholds (rounded score needed to reach each tier).
|
||||||
export function buildTiers(maxScore) {
|
export function buildTiers(maxScore, difficulty = 'normal') {
|
||||||
|
const geniusPct = GENIUS_PCT[difficulty] ?? GENIUS_PCT.normal;
|
||||||
|
const scale = geniusPct / TIER_TABLE[TIER_TABLE.length - 1].pct;
|
||||||
return TIER_TABLE.map((t) => ({
|
return TIER_TABLE.map((t) => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
threshold: Math.round(t.pct * maxScore),
|
threshold: Math.round(t.pct * scale * maxScore),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@
|
||||||
const ABS_MIN = 4; // shortest legal Kiitos word (round 1 minimum)
|
const ABS_MIN = 4; // shortest legal Kiitos word (round 1 minimum)
|
||||||
const MAX_LEN = 12; // cap word length so play stays snappy
|
const MAX_LEN = 12; // cap word length so play stays snappy
|
||||||
|
|
||||||
let root = null; // trie root: { word: boolean, kids: Map<char, node> }
|
let root = null; // trie root: { word: boolean, kids: Map<char, node> }
|
||||||
let wordSet = null; // Set<string> of every legal Kiitos word
|
let wordSet = null; // Set<string> of every legal Kiitos word
|
||||||
|
let commonWordSet = null; // Set<string> of recognizable common words (AI prefers these)
|
||||||
|
|
||||||
const completionCache = new Map(); // `${prefix}|${minLen}` -> boolean
|
const completionCache = new Map(); // `${prefix}|${minLen}` -> boolean
|
||||||
const wordCache = new Map(); // `${prefix}|${minLen}` -> string|null
|
const wordCache = new Map(); // `${prefix}|${minLen}` -> string|null
|
||||||
|
const commonWordCache = new Map(); // `${prefix}|${minLen}` -> string|null
|
||||||
|
|
||||||
function makeNode() { return { word: false, kids: new Map() }; }
|
function makeNode() { return { word: false, kids: new Map() }; }
|
||||||
|
|
||||||
|
|
@ -25,6 +27,7 @@ export function initKiitosDictionary(words) {
|
||||||
wordSet = new Set();
|
wordSet = new Set();
|
||||||
completionCache.clear();
|
completionCache.clear();
|
||||||
wordCache.clear();
|
wordCache.clear();
|
||||||
|
commonWordCache.clear();
|
||||||
for (const raw of words) {
|
for (const raw of words) {
|
||||||
const w = String(raw).toUpperCase();
|
const w = String(raw).toUpperCase();
|
||||||
if (w.length < ABS_MIN || w.length > MAX_LEN || !/^[A-Z]+$/.test(w)) continue;
|
if (w.length < ABS_MIN || w.length > MAX_LEN || !/^[A-Z]+$/.test(w)) continue;
|
||||||
|
|
@ -40,6 +43,18 @@ export function initKiitosDictionary(words) {
|
||||||
return { words: wordSet.size };
|
return { words: wordSet.size };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called after initKiitosDictionary with words from a frequency-filtered common list.
|
||||||
|
// Only words already in wordSet are kept (common list may include short words not in trie).
|
||||||
|
export function initKiitosCommonWords(words) {
|
||||||
|
commonWordSet = new Set();
|
||||||
|
commonWordCache.clear();
|
||||||
|
for (const raw of words) {
|
||||||
|
const w = String(raw).toUpperCase();
|
||||||
|
if (wordSet && wordSet.has(w)) commonWordSet.add(w);
|
||||||
|
}
|
||||||
|
return { common: commonWordSet.size };
|
||||||
|
}
|
||||||
|
|
||||||
export const MIN_WORD_LEN = ABS_MIN;
|
export const MIN_WORD_LEN = ABS_MIN;
|
||||||
|
|
||||||
// ── Lookups ─────────────────────────────────────────────────────────────────────
|
// ── Lookups ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -108,6 +123,30 @@ export function findWord(prefix, minLen = ABS_MIN) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Like findWord but only returns words in the common word set (AI prefers these).
|
||||||
|
// Falls back to null if no common word extends `prefix`.
|
||||||
|
function findCommonWord(prefix, minLen = ABS_MIN) {
|
||||||
|
if (!commonWordSet) return null;
|
||||||
|
const key = `${prefix}|${minLen}`;
|
||||||
|
const cached = commonWordCache.get(key);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
const start = nodeFor(prefix);
|
||||||
|
let result = null;
|
||||||
|
if (start) {
|
||||||
|
let frontier = [[start, prefix]];
|
||||||
|
while (frontier.length && !result) {
|
||||||
|
const next = [];
|
||||||
|
for (const [n, s] of frontier) {
|
||||||
|
if (n.word && s.length >= minLen && commonWordSet.has(s)) { result = s; break; }
|
||||||
|
if (s.length < MAX_LEN) for (const [ch, c] of n.kids) next.push([c, s + ch]);
|
||||||
|
}
|
||||||
|
frontier = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonWordCache.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ── AI move selection ─────────────────────────────────────────────────────────
|
// ── AI move selection ─────────────────────────────────────────────────────────
|
||||||
// Returns one of:
|
// Returns one of:
|
||||||
// { type: 'play', letter, forced: true } — mandatory next letter
|
// { type: 'play', letter, forced: true } — mandatory next letter
|
||||||
|
|
@ -154,7 +193,7 @@ export function chooseMove({
|
||||||
if (seen.has(prefix)) return;
|
if (seen.has(prefix)) return;
|
||||||
seen.add(prefix);
|
seen.add(prefix);
|
||||||
if (!hasCompletion(prefix, minLen)) return;
|
if (!hasCompletion(prefix, minLen)) return;
|
||||||
const word = findWord(prefix, minLen);
|
const word = findCommonWord(prefix, minLen) ?? findWord(prefix, minLen);
|
||||||
if (word) cands.push({ letter, prefix, word });
|
if (word) cands.push({ letter, prefix, word });
|
||||||
};
|
};
|
||||||
for (const L of new Set(H)) {
|
for (const L of new Set(H)) {
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,26 @@ const SKILL = {
|
||||||
|
|
||||||
const wordSets = {}; // length → Set<string>
|
const wordSets = {}; // length → Set<string>
|
||||||
const wordArrays = {}; // length → string[]
|
const wordArrays = {}; // length → string[]
|
||||||
|
const commonSets = {}; // length → Set<string> (frequency-filtered subset)
|
||||||
|
|
||||||
export function initWordLadderDictionary(words3, words4) {
|
export function initWordLadderDictionary(words3, words4) {
|
||||||
setFor(3, words3);
|
setFor(3, words3);
|
||||||
setFor(4, words4);
|
setFor(4, words4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called after the common word list is loaded so puzzle generation can prefer
|
||||||
|
// paths where every rung is a recognisable word (not just start and target).
|
||||||
|
export function initCommonWordSets(words3, words4) {
|
||||||
|
if (words3?.length) {
|
||||||
|
const arr = words3.map(w => String(w).toUpperCase()).filter(w => w.length === 3);
|
||||||
|
commonSets[3] = new Set(arr);
|
||||||
|
}
|
||||||
|
if (words4?.length) {
|
||||||
|
const arr = words4.map(w => String(w).toUpperCase()).filter(w => w.length === 4);
|
||||||
|
commonSets[4] = new Set(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setFor(length, words) {
|
function setFor(length, words) {
|
||||||
const arr = (words ?? []).map(w => String(w).toUpperCase()).filter(w => w.length === length);
|
const arr = (words ?? []).map(w => String(w).toUpperCase()).filter(w => w.length === length);
|
||||||
wordSets[length] = new Set(arr);
|
wordSets[length] = new Set(arr);
|
||||||
|
|
@ -70,8 +84,9 @@ export function neighbors(word) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BFS from `start`; returns Map<word, distance> over the connected component.
|
// BFS from `start`; returns Map<word, distance> over the connected component.
|
||||||
// `maxDepth` (optional) bounds the search for puzzle generation.
|
// `maxDepth` bounds the search for puzzle generation.
|
||||||
function bfsDistances(start, maxDepth = Infinity) {
|
// `nodeSet` restricts traversal to words in that set (for common-only BFS).
|
||||||
|
function bfsDistances(start, maxDepth = Infinity, nodeSet = null) {
|
||||||
const dist = new Map([[start, 0]]);
|
const dist = new Map([[start, 0]]);
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
@ -80,7 +95,7 @@ function bfsDistances(start, maxDepth = Infinity) {
|
||||||
const d = dist.get(w);
|
const d = dist.get(w);
|
||||||
if (d >= maxDepth) continue;
|
if (d >= maxDepth) continue;
|
||||||
for (const n of neighbors(w)) {
|
for (const n of neighbors(w)) {
|
||||||
if (!dist.has(n)) {
|
if (!dist.has(n) && (!nodeSet || nodeSet.has(n))) {
|
||||||
dist.set(n, d + 1);
|
dist.set(n, d + 1);
|
||||||
queue.push(n);
|
queue.push(n);
|
||||||
}
|
}
|
||||||
|
|
@ -119,14 +134,15 @@ export function shortestPath(from, to) {
|
||||||
|
|
||||||
// Build a puzzle whose optimal solution is exactly `par` steps. Retries with
|
// Build a puzzle whose optimal solution is exactly `par` steps. Retries with
|
||||||
// fresh random starts; relaxes to the deepest reachable target if `par` proves
|
// fresh random starts; relaxes to the deepest reachable target if `par` proves
|
||||||
// hard to hit for the chosen start.
|
// hard to hit for the chosen start. When `nodeSet` is provided, BFS and the
|
||||||
function generatePuzzleWithPar(length, par, attempts = 300) {
|
// candidate pool are both restricted to that set so every rung is a word in it.
|
||||||
const pool = wordArrays[length] ?? [];
|
function generatePuzzleWithPar(length, par, attempts = 300, nodeSet = null) {
|
||||||
|
const pool = nodeSet ? [...nodeSet].filter(w => w.length === length) : (wordArrays[length] ?? []);
|
||||||
if (pool.length === 0) return null;
|
if (pool.length === 0) return null;
|
||||||
|
|
||||||
for (let a = 0; a < attempts; a++) {
|
for (let a = 0; a < attempts; a++) {
|
||||||
const start = randomFrom(pool);
|
const start = randomFrom(pool);
|
||||||
const dist = bfsDistances(start, par);
|
const dist = bfsDistances(start, par, nodeSet);
|
||||||
const exact = [];
|
const exact = [];
|
||||||
let deepest = null, deepestD = 0;
|
let deepest = null, deepestD = 0;
|
||||||
for (const [word, d] of dist) {
|
for (const [word, d] of dist) {
|
||||||
|
|
@ -151,26 +167,37 @@ function pickPar(length) {
|
||||||
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to generate a common-word-only puzzle first; fall back to the full
|
||||||
|
// dictionary if the common subgraph is too sparse or too small.
|
||||||
|
function makePuzzle(length, par, nodeSet = null) {
|
||||||
|
if (nodeSet?.size >= 20) {
|
||||||
|
const p = generatePuzzleWithPar(length, par, 200, nodeSet);
|
||||||
|
if (p) return p;
|
||||||
|
}
|
||||||
|
return generatePuzzleWithPar(length, par) ?? fallbackPuzzle(length);
|
||||||
|
}
|
||||||
|
|
||||||
// Single puzzle (solo mode).
|
// Single puzzle (solo mode).
|
||||||
export function generatePuzzle(length) {
|
export function generatePuzzle(length) {
|
||||||
const L = LENGTHS.includes(length) ? length : 4;
|
const L = LENGTHS.includes(length) ? length : 4;
|
||||||
return generatePuzzleWithPar(L, pickPar(L)) ?? fallbackPuzzle(L);
|
return makePuzzle(L, pickPar(L), commonSets[L]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two distinct puzzles of equal par (versus mode — fair race).
|
// Two distinct puzzles of equal par (versus mode — fair race).
|
||||||
export function generateVersusPuzzles(length) {
|
export function generateVersusPuzzles(length) {
|
||||||
const L = LENGTHS.includes(length) ? length : 4;
|
const L = LENGTHS.includes(length) ? length : 4;
|
||||||
const par = pickPar(L);
|
const par = pickPar(L);
|
||||||
const player = generatePuzzleWithPar(L, par) ?? fallbackPuzzle(L);
|
const cs = commonSets[L];
|
||||||
|
const player = makePuzzle(L, par, cs);
|
||||||
let opponent = null;
|
let opponent = null;
|
||||||
for (let a = 0; a < 20; a++) {
|
for (let a = 0; a < 20; a++) {
|
||||||
const cand = generatePuzzleWithPar(L, player.par);
|
const cand = makePuzzle(L, player.par, cs);
|
||||||
if (cand && cand.start !== player.start && cand.target !== player.target) {
|
if (cand && cand.start !== player.start && cand.target !== player.target) {
|
||||||
opponent = cand;
|
opponent = cand;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { player, opponent: opponent ?? generatePuzzleWithPar(L, player.par) ?? fallbackPuzzle(L) };
|
return { player, opponent: opponent ?? makePuzzle(L, player.par, cs) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Degenerate safety net: a start word and one neighbour (par 1). Only reached if
|
// Degenerate safety net: a start word and one neighbour (par 1). Only reached if
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { initScrabbleDictionary, isValidWord, chooseMove } from './scrabbleEngin
|
||||||
import { initGhostDictionary, judge as ghostJudge, chooseLetter as ghostChooseLetter, suggestWords as ghostSuggestWords } from './ghostEngine.js';
|
import { initGhostDictionary, judge as ghostJudge, chooseLetter as ghostChooseLetter, suggestWords as ghostSuggestWords } from './ghostEngine.js';
|
||||||
import {
|
import {
|
||||||
initWordLadderDictionary,
|
initWordLadderDictionary,
|
||||||
|
initCommonWordSets as ladderInitCommon,
|
||||||
generatePuzzle as ladderGeneratePuzzle,
|
generatePuzzle as ladderGeneratePuzzle,
|
||||||
generateVersusPuzzles as ladderGenerateVersus,
|
generateVersusPuzzles as ladderGenerateVersus,
|
||||||
chooseAIMove as ladderChooseAIMove,
|
chooseAIMove as ladderChooseAIMove,
|
||||||
|
|
@ -22,12 +23,14 @@ import { initBoggleDictionary, rollBoard, solveBoard } from './boggleEngine.js';
|
||||||
import { initSpellingBeeDictionary, generatePuzzle as spellingBeeGenerate } from './spellingBeeEngine.js';
|
import { initSpellingBeeDictionary, generatePuzzle as spellingBeeGenerate } from './spellingBeeEngine.js';
|
||||||
import { initMiniCrosswordPuzzles, getPuzzle as miniCrosswordGet } from './miniCrosswordEngine.js';
|
import { initMiniCrosswordPuzzles, getPuzzle as miniCrosswordGet } from './miniCrosswordEngine.js';
|
||||||
import {
|
import {
|
||||||
initKiitosDictionary, isValidKiitosWord, hasCompletion as kiitosHasCompletion,
|
initKiitosDictionary, initKiitosCommonWords, isValidKiitosWord,
|
||||||
findWord as kiitosFindWord, chooseMove as kiitosChooseMove, MIN_WORD_LEN as KIITOS_MIN,
|
hasCompletion as kiitosHasCompletion, findWord as kiitosFindWord,
|
||||||
|
chooseMove as kiitosChooseMove, MIN_WORD_LEN as KIITOS_MIN,
|
||||||
} from './kiitosEngine.js';
|
} from './kiitosEngine.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const WORDLIST_PATH = path.join(__dirname, '../data/wordlists/enable1.txt');
|
const WORDLIST_PATH = path.join(__dirname, '../data/wordlists/enable1.txt');
|
||||||
|
const COMMON_PATH = path.join(__dirname, '../data/wordlists/common.txt');
|
||||||
|
|
||||||
// Common words used to build a player-friendly Wordle answer pool.
|
// Common words used to build a player-friendly Wordle answer pool.
|
||||||
// These are well-known 5-letter English words that make fair Wordle puzzles.
|
// These are well-known 5-letter English words that make fair Wordle puzzles.
|
||||||
|
|
@ -170,6 +173,21 @@ function loadWordLists() {
|
||||||
const kiitosStats = initKiitosDictionary(kiitosWords);
|
const kiitosStats = initKiitosDictionary(kiitosWords);
|
||||||
console.log(`[words] loaded ${kiitosStats.words} Kiitos words (4–12 letters)`);
|
console.log(`[words] loaded ${kiitosStats.words} Kiitos words (4–12 letters)`);
|
||||||
|
|
||||||
|
// Kiitos AI common word preference: load frequency-filtered word list so the AI
|
||||||
|
// targets recognizable words instead of obscure ENABLE entries.
|
||||||
|
try {
|
||||||
|
const commonRaw = fs.readFileSync(COMMON_PATH, 'utf8');
|
||||||
|
const commonWords = commonRaw.split('\n').map(w => w.trim().toUpperCase()).filter(Boolean);
|
||||||
|
const commonStats = initKiitosCommonWords(commonWords);
|
||||||
|
console.log(`[words] loaded ${commonStats.common} Kiitos common words for AI targeting`);
|
||||||
|
const common3 = commonWords.filter(w => /^[A-Z]{3}$/.test(w));
|
||||||
|
const common4 = commonWords.filter(w => /^[A-Z]{4}$/.test(w));
|
||||||
|
ladderInitCommon(common3, common4);
|
||||||
|
console.log(`[words] loaded Word Ladder common words (${common3.length} 3-letter, ${common4.length} 4-letter)`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[words] common.txt not found; Kiitos AI will use full ENABLE list');
|
||||||
|
}
|
||||||
|
|
||||||
// Answer pool: prefer curated common words that are also in ENABLE;
|
// Answer pool: prefer curated common words that are also in ENABLE;
|
||||||
// supplement with additional ENABLE words up to a healthy pool size.
|
// supplement with additional ENABLE words up to a healthy pool size.
|
||||||
const curated = [...COMMON_WORDS].filter(w => enableFive.has(w));
|
const curated = [...COMMON_WORDS].filter(w => enableFive.has(w));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue