fix: skribbl pre-pick label + gartic min 3 players + tests
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// Unit tests for server-side game logic. Uses node:test (no extra deps).
|
||||
// Run with: node test/unit.test.js (NOT `node --test`) — see comment at bottom.
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
|
||||
const {
|
||||
normalize,
|
||||
maskWord,
|
||||
pickRevealIndex,
|
||||
isCloseGuess,
|
||||
} = require("../server/word-utils.js");
|
||||
const roomStore = require("../server/room-store.js");
|
||||
const { genCode, getRoom, setRoom, deleteRoom, allRooms, makeId } = roomStore;
|
||||
const { makeRoom, makePlayer } = require("../server/game-state.js");
|
||||
|
||||
// Unref the room-store cleanup interval so node exits cleanly after tests.
|
||||
function unrefAllHandles() {
|
||||
const handles = process._getActiveHandles ? process._getActiveHandles() : [];
|
||||
for (const h of handles) {
|
||||
if (h && typeof h.unref === "function") {
|
||||
try { h.unref(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
unrefAllHandles();
|
||||
|
||||
// --- word-utils tests ---
|
||||
|
||||
test("normalize: lowercases and trims", () => {
|
||||
assert.strictEqual(normalize(" HELLO "), "hello");
|
||||
assert.strictEqual(normalize("Foo Bar"), "foo bar");
|
||||
assert.strictEqual(normalize("MULTI SPACE"), "multi space");
|
||||
assert.strictEqual(normalize(""), "");
|
||||
assert.strictEqual(normalize(undefined), "");
|
||||
assert.strictEqual(normalize(null), "");
|
||||
});
|
||||
|
||||
test("maskWord: blanks letters but preserves spaces", () => {
|
||||
const out = maskWord("ice cream");
|
||||
// letters become "_", space stays
|
||||
assert.strictEqual(out, "___ _____");
|
||||
});
|
||||
|
||||
test("maskWord: with all indices revealed produces lowercased word", () => {
|
||||
const word = "Apple";
|
||||
const revealed = new Set([0, 1, 2, 3, 4]);
|
||||
const out = maskWord(word, revealed);
|
||||
assert.strictEqual(out, "apple");
|
||||
});
|
||||
|
||||
test("maskWord: partial reveal mixes letters and underscores", () => {
|
||||
const out = maskWord("apple", new Set([0, 2]));
|
||||
// a _ p _ _
|
||||
assert.strictEqual(out, "a_p__");
|
||||
});
|
||||
|
||||
test("pickRevealIndex: picks unrevealed non-space index", () => {
|
||||
const word = "ab cd";
|
||||
const revealed = new Set([0]);
|
||||
const idx = pickRevealIndex(word, revealed);
|
||||
// valid candidate indices: 1, 3, 4
|
||||
assert.ok([1, 3, 4].includes(idx), `unexpected index ${idx}`);
|
||||
assert.notStrictEqual(idx, 2); // never the space
|
||||
});
|
||||
|
||||
test("pickRevealIndex: returns null when all letters revealed", () => {
|
||||
const word = "ab";
|
||||
const revealed = new Set([0, 1]);
|
||||
assert.strictEqual(pickRevealIndex(word, revealed), null);
|
||||
});
|
||||
|
||||
test("revealHint flow: applying picked index reveals exactly N more letters", () => {
|
||||
// simulate revealing: count underscores decreases by exactly 1 each step
|
||||
let revealed = new Set();
|
||||
const word = "rocket";
|
||||
let prevMask = maskWord(word, revealed);
|
||||
let prevUnderscores = (prevMask.match(/_/g) || []).length;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const idx = pickRevealIndex(word, revealed);
|
||||
assert.notStrictEqual(idx, null);
|
||||
revealed.add(idx);
|
||||
const m = maskWord(word, revealed);
|
||||
const newUnderscores = (m.match(/_/g) || []).length;
|
||||
assert.strictEqual(newUnderscores, prevUnderscores - 1, "exactly 1 letter revealed");
|
||||
prevUnderscores = newUnderscores;
|
||||
prevMask = m;
|
||||
}
|
||||
});
|
||||
|
||||
test("revealHint idempotence: re-applying same revealed set yields same mask", () => {
|
||||
const word = "tree";
|
||||
const revealed = new Set([0, 2]);
|
||||
const m1 = maskWord(word, revealed);
|
||||
const m2 = maskWord(word, revealed);
|
||||
assert.strictEqual(m1, m2);
|
||||
});
|
||||
|
||||
test("isCloseGuess: edit distance 1-2 returns true", () => {
|
||||
assert.strictEqual(isCloseGuess("aple", "apple"), true); // 1 edit
|
||||
assert.strictEqual(isCloseGuess("aplee", "apple"), true); // 1 edit
|
||||
assert.strictEqual(isCloseGuess("apple", "apple"), false); // exact match -> not "close"
|
||||
assert.strictEqual(isCloseGuess("xyz", "apple"), false); // too far
|
||||
});
|
||||
|
||||
// --- room-store tests ---
|
||||
|
||||
test("genCode: returns a unique 6-char alphanumeric code", () => {
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const c = genCode();
|
||||
assert.strictEqual(typeof c, "string");
|
||||
assert.strictEqual(c.length, 6);
|
||||
assert.ok(/^[A-Z0-9]+$/.test(c), `code ${c} should be uppercase alnum`);
|
||||
seen.add(c);
|
||||
}
|
||||
assert.strictEqual(seen.size, 10, "codes should be unique");
|
||||
});
|
||||
|
||||
test("setRoom + getRoom: returns same instance, case-insensitive lookup", () => {
|
||||
const code = genCode();
|
||||
const player = makePlayer({ nickname: "U", avatar: "x" });
|
||||
const room = makeRoom({ code, hostId: player.id, mode: "skribbl", settings: {} });
|
||||
room.players.push(player);
|
||||
setRoom(code, room);
|
||||
const got = getRoom(code);
|
||||
assert.strictEqual(got, room, "same instance");
|
||||
// lower-case lookup also works
|
||||
const gotLower = getRoom(code.toLowerCase());
|
||||
assert.strictEqual(gotLower, room);
|
||||
deleteRoom(code);
|
||||
assert.strictEqual(getRoom(code), undefined);
|
||||
});
|
||||
|
||||
test("makeId: returns a non-empty string id", () => {
|
||||
const id1 = makeId();
|
||||
const id2 = makeId();
|
||||
assert.strictEqual(typeof id1, "string");
|
||||
assert.ok(id1.length > 0);
|
||||
assert.notStrictEqual(id1, id2);
|
||||
});
|
||||
|
||||
test("allRooms: exposes the underlying Map", () => {
|
||||
const map = allRooms();
|
||||
assert.ok(map instanceof Map);
|
||||
});
|
||||
|
||||
// Force exit after a short tick — room-store uses an unrefable setInterval
|
||||
// that we already unrefed above, but be defensive.
|
||||
process.on("beforeExit", () => process.exit(0));
|
||||
setTimeout(() => process.exit(0), 1500).unref();
|
||||
Reference in New Issue
Block a user