first commit

This commit is contained in:
Lucas Roschnowski
2026-08-16 20:50:31 +02:00
parent 69a0e7a9f7
commit c5de95fade
8 changed files with 1748 additions and 1 deletions
+126
View File
@@ -0,0 +1,126 @@
/* Smoke test: create room w/ bot, 2 human clients via SSE, play until end. */
const BASE = "http://localhost:3000";
function sse(uid) {
return new Promise((resolve) => {
const { request } = require("http");
const res = request({ host: "localhost", port: 3000, path: `/api/events?uid=${uid}`, method: "GET" }, (r) => {
let buf = "";
const states = [];
const chats = [];
r.on("data", (c) => {
buf += c;
let idx;
while ((idx = buf.indexOf("\n\n")) >= 0) {
const chunk = buf.slice(0, idx); buf = buf.slice(idx + 2);
const lines = chunk.split("\n").filter(l => l.startsWith("data: "));
for (const l of lines) {
const m = JSON.parse(l.slice(6));
if (m.type === "state") states.push(m.snap);
else if (m.type === "chat") chats.push(m);
}
}
});
resolve({ states, chats, res });
});
res.on("error", () => {});
res.end();
});
}
const api = async (action, uid, name, extra = {}) => {
const r = await fetch(BASE + "/api", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, uid, name, ...extra }),
});
const d = await r.json();
if (!r.ok) throw new Error(`${action}: ${d.error}`);
return d;
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
// create room (host + auto bot)
const c1 = await api("create", null, "Alice", { roomName: "Testbord", addBot: true });
const uid1 = c1.uid;
// second player
const c2 = await api("join", null, "Bob", { roomId: c1.room });
const uid2 = c2.uid;
const [s1, s2] = await Promise.all([sse(uid1), sse(uid2)]);
// host starts
await api("start", uid1, "Alice");
console.log("started. waiting for bot rounds...");
const startT = Date.now();
let turns = 0;
while (Date.now() - startT < 240000) {
await sleep(1500);
const snap = s1.states[s1.states.length - 1];
if (!snap) continue;
if (snap.phase === "waiting") { console.log("GAME ENDED:", snap.log.slice(-3).map(l => l.t)); break; }
// alice acts if it's her turn/actor
const actor = snap.phase === "challenging" && snap.pending ? snap.pending.challenger : snap.turnUid;
if (actor === uid1) {
if (snap.phase === "acting") {
await api("roll", uid1, "Alice");
} else if (snap.phase === "peeking") {
await api("peek", uid1, "Alice", { revealed: true });
await sleep(200); const fresh = s1.states[s1.states.length - 1];
const myDice = fresh.myDice;
if (myDice) {
const truth = rankOf(myDice[0], myDice[1]);
const minR = fresh.lastClaim ? fresh.lastClaim.rank + 1 : 0;
let r = Math.max(minR, truth);
if (Math.random() < 0.3) r = Math.min(r + 1, 17); // occasional bluff
if (r < minR || r > 17) { await api("sendForward", uid1, "Alice"); }
else await api("declare", uid1, "Alice", { rank: r });
}
} else if (snap.phase === "challenging" && snap.pending && snap.pending.challenger === uid1) {
// bob's claim is on the table... but alice is the actor? (challenger) - decide randomly
await (Math.random() < 0.4 ? api("challenge", uid1, "Alice") : api("pass", uid1, "Alice"));
}
turns++;
}
const actor2 = snap.phase === "challenging" && snap.pending ? snap.pending.challenger : snap.turnUid;
if (actor2 === uid2) {
if (snap.phase === "acting") await api("roll", uid2, "Bob");
else if (snap.phase === "peeking") {
await api("peek", uid2, "Bob", { revealed: true });
await sleep(200); const fresh = s2.states[s2.states.length - 1];
const myDice = fresh.myDice;
if (myDice) {
const truth = rankOf(myDice[0], myDice[1]);
const minR = fresh.lastClaim ? fresh.lastClaim.rank + 1 : 0;
let r = Math.max(minR, truth);
if (Math.random() < 0.3) r = Math.min(r + 1, 17);
if (r < minR || r > 17) await api("sendForward", uid2, "Bob");
else await api("declare", uid2, "Bob", { rank: r });
}
} else if (snap.phase === "challenging" && snap.pending && snap.pending.challenger === uid2) {
await (Math.random() < 0.4 ? api("challenge", uid2, "Bob") : api("pass", uid2, "Bob"));
}
turns++;
}
// progress report
if (turns % 10 === 0) console.log(`...turns=${turns} phase=${snap.phase} scores=${JSON.stringify(snap.players.map(p => [p.name, p.score, p.eliminated]))}`);
}
// also test chat + leave
await api("chat", uid1, "Alice", { text: "hej med jer alle sammen! 🍺" });
await sleep(300);
console.log("chats received by bob:", s2.chats.length);
console.log("total turns driven:", turns);
const rooms = await (await fetch(BASE + "/api/rooms")).json();
console.log("rooms after:", JSON.stringify(rooms.rooms.map(r => ({ id: r.id, phase: r.phase, n: r.players.length }))));
process.exit(0);
})().catch(e => { console.error("SMOKE FAIL:", e); process.exit(1); });
/* local mirror of ranking for the test driver */
const COMBOS = [[1,2],[1,3],[6,6],[5,5],[4,4],[3,3],[2,2],[1,1],[6,5],[6,4]];
const DYN = [[6,3],[6,2],[5,4],[5,3],[5,2],[4,3],[4,2],[3,2]];
function rankOf(a, b) {
const x = Math.min(a, b), y = Math.max(a, b);
for (let i = 0; i < COMBOS.length; i++) if (COMBOS[i][0] === y && COMBOS[i][1] === x) return i;
const i = DYN.findIndex(c => c[0] === y && c[1] === x);
return i >= 0 ? 10 + i : -1;
}