73 lines
2.4 KiB
JavaScript
73 lines
2.4 KiB
JavaScript
// Pure Word Ladder logic — no Phaser dependency.
|
|
//
|
|
// A ladder transforms START into TARGET by changing exactly one letter per rung,
|
|
// where every rung is a real word of the same length. State tracks the rungs
|
|
// played so far (rungs[0] is always the start word).
|
|
|
|
export function createInitialState({ start, target, par, length }) {
|
|
const S = String(start).toUpperCase();
|
|
return {
|
|
start: S,
|
|
target: String(target).toUpperCase(),
|
|
par: par ?? null,
|
|
length: length ?? S.length,
|
|
rungs: [S],
|
|
status: 'playing', // 'playing' | 'won'
|
|
};
|
|
}
|
|
|
|
export function lastRung(state) {
|
|
return state.rungs[state.rungs.length - 1];
|
|
}
|
|
|
|
// True when a and b are the same length and differ in exactly one position.
|
|
export function isOneLetterDifferent(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
let diff = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) diff++;
|
|
if (diff > 1) return false;
|
|
}
|
|
return diff === 1;
|
|
}
|
|
|
|
// Index of the single differing position between two equal-length words, or -1.
|
|
export function changedIndex(prev, word) {
|
|
if (!prev || prev.length !== word.length) return -1;
|
|
for (let i = 0; i < word.length; i++) {
|
|
if (prev[i] !== word[i]) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Attempt to append `word` as the next rung. Returns { ok, state } on success or
|
|
// { ok: false, reason } with a short message suitable for display.
|
|
export function tryAddRung(state, word, validSet) {
|
|
if (state.status !== 'playing') return { ok: false, reason: 'Round over' };
|
|
const W = String(word).toUpperCase();
|
|
|
|
if (W.length !== state.length) return { ok: false, reason: 'Not enough letters' };
|
|
if (!isOneLetterDifferent(lastRung(state), W)) return { ok: false, reason: 'Change exactly one letter' };
|
|
if (!validSet.has(W)) return { ok: false, reason: 'Not in word list' };
|
|
if (state.rungs.includes(W)) return { ok: false, reason: 'Word already used' };
|
|
|
|
const rungs = [...state.rungs, W];
|
|
const status = W === state.target ? 'won' : 'playing';
|
|
return { ok: true, state: { ...state, rungs, status } };
|
|
}
|
|
|
|
// Remove the most recent rung (never the start word).
|
|
export function undoRung(state) {
|
|
if (state.rungs.length <= 1) return state;
|
|
return { ...state, rungs: state.rungs.slice(0, -1), status: 'playing' };
|
|
}
|
|
|
|
export function isSolved(state) {
|
|
return lastRung(state) === state.target;
|
|
}
|
|
|
|
// Steps taken so far (rungs beyond the start word).
|
|
export function stepsTaken(state) {
|
|
return state.rungs.length - 1;
|
|
}
|