Cache headers end the stale-bundle era; finished games rest their buttons
The static server sent no Cache-Control at all, so browsers heuristically cached index.html — and a stale index.html pins its user to last deploy's hashed assets no matter how often they reload. That is how "watch the whole game" could show someone an app without the feature minutes after it shipped. Hashed assets now cache forever (immutable); everything else revalidates. And per playtesting: a finished game offers no actions — isYourTurn now requires the playing phase, which retires the pick-up/drop/punch/ discard/end-turn row the moment the trophy drops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cdcd1de71a
commit
bed29c4992
@@ -0,0 +1,131 @@
|
||||
// Script a complete 2p game on the local server: walk together, punch to the death.
|
||||
import { WebSocket } from "ws";
|
||||
import { stepTarget, cellKey } from "./packages/engine/src/board.ts";
|
||||
|
||||
const url = "ws://localhost:8899/";
|
||||
function client() {
|
||||
const ws = new WebSocket(url);
|
||||
const queue = [];
|
||||
const waiters = [];
|
||||
ws.on("message", (d) => {
|
||||
const m = JSON.parse(d.toString());
|
||||
if (waiters.length) waiters.shift()(m);
|
||||
else queue.push(m);
|
||||
});
|
||||
const next = () => new Promise((res) => (queue.length ? res(queue.shift()) : waiters.push(res)));
|
||||
const send = (m) => ws.send(JSON.stringify(m));
|
||||
return { ws, next, send, queue };
|
||||
}
|
||||
const until = async (c, pred) => {
|
||||
for (;;) {
|
||||
const m = await c.next();
|
||||
if (pred(m)) return m;
|
||||
}
|
||||
};
|
||||
|
||||
const a = client();
|
||||
await until(a, (m) => m.type === "welcome");
|
||||
a.send({ type: "create", name: "alice" });
|
||||
const seatA = await until(a, (m) => m.type === "seat");
|
||||
const room = await until(a, (m) => m.type === "room");
|
||||
const code = room.roomId;
|
||||
|
||||
const b = client();
|
||||
await until(b, (m) => m.type === "welcome");
|
||||
b.send({ type: "join", roomId: code, name: "bob" });
|
||||
await until(b, (m) => m.type === "seat");
|
||||
|
||||
a.send({ type: "start", expansion: false });
|
||||
let viewA = (await until(a, (m) => m.type === "state")).view;
|
||||
let viewB;
|
||||
|
||||
const me = (v, who) => v.players.find((p) => p.id === who);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const settle = async () => {
|
||||
await sleep(120);
|
||||
let err = null;
|
||||
for (const c of [a, b]) {
|
||||
while (c.queue.length) {
|
||||
const m = c.queue.shift();
|
||||
if (m.type === "state" && c === a) viewA = m.view;
|
||||
if (m.type === "error") err = m.message;
|
||||
}
|
||||
}
|
||||
return err;
|
||||
};
|
||||
const cmd = async (c, command) => {
|
||||
await settle();
|
||||
c.send({ type: "command", command });
|
||||
const err = await settle();
|
||||
return err ? { error: err } : { view: viewA };
|
||||
};
|
||||
const step = async (c, dir) => cmd(c, { type: "move", direction: dir });
|
||||
|
||||
let safety = 600;
|
||||
for (;;) {
|
||||
if (--safety <= 0) throw new Error("script ran away");
|
||||
await settle();
|
||||
const who = viewA.activePlayerId;
|
||||
const active = who === "alice" ? a : b;
|
||||
const other = who === "alice" ? b : a;
|
||||
const av = viewA;
|
||||
if (av.phase === "finished") break;
|
||||
const meP = me(av, who);
|
||||
const foe = av.players.find((p) => p.id !== who);
|
||||
const sameCell = meP.position.x === foe.position.x && meP.position.y === foe.position.y;
|
||||
let acted = false;
|
||||
if (sameCell && av.turn.round > 1 && !av.turn.attackUsed) {
|
||||
const r = await cmd(active, { type: "punch", targetId: foe.id });
|
||||
if (!r.error) {
|
||||
await cmd(other, { type: "pass" });
|
||||
acted = true;
|
||||
}
|
||||
}
|
||||
if (!acted && who === "alice" && av.turn.movementAllowance > av.turn.movementUsed) {
|
||||
// BFS the maze toward bob; take the first step of the shortest path.
|
||||
const board = av.board;
|
||||
const start = meP.position, goal = foe.position;
|
||||
const prev = new Map([[cellKey(start), null]]);
|
||||
let frontier = [start];
|
||||
let found = null;
|
||||
while (frontier.length && !found) {
|
||||
const nf = [];
|
||||
for (const c of frontier) {
|
||||
for (const d of ["N", "S", "E", "W"]) {
|
||||
const t = stepTarget(board, c, d);
|
||||
if (t.kind === "blocked") continue;
|
||||
const k = cellKey(t.to);
|
||||
if (prev.has(k)) continue;
|
||||
prev.set(k, { from: c, dir: d });
|
||||
if (k === cellKey(goal)) { found = t.to; break; }
|
||||
nf.push(t.to);
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
frontier = nf;
|
||||
}
|
||||
if (found) {
|
||||
let node = cellKey(found), hop = prev.get(node);
|
||||
while (hop && cellKey(hop.from) !== cellKey(start)) { node = cellKey(hop.from); hop = prev.get(node); }
|
||||
if (hop) {
|
||||
const r = await step(active, hop.dir);
|
||||
if (!r.error) continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const r = await cmd(active, { type: "endTurn", draw: 0 });
|
||||
if (r.error && /game is over/.test(r.error)) break;
|
||||
if (r.error) throw new Error("endTurn failed: " + r.error);
|
||||
}
|
||||
console.log("FINISHED. winner:", viewA.winner, "room:", code);
|
||||
|
||||
// Full replay request.
|
||||
a.send({ type: "catchUp", sinceSeq: 0, full: true });
|
||||
const cu = await until(a, (m) => m.type === "catchUp" || m.type === "error");
|
||||
if (cu.type === "error") { console.log("replay error:", cu.message); process.exit(1); }
|
||||
console.log("steps:", cu.steps.length);
|
||||
const s0 = cu.steps[0];
|
||||
console.log("step0 keys:", Object.keys(s0).join(","));
|
||||
console.log("step0.view.board present:", !!s0.view?.board, "cells:", s0.view ? Object.keys(s0.view.board.cells).length : 0);
|
||||
console.log("SEAT for browser:", JSON.stringify([{ roomId: code, name: "alice", token: seatA.token }]));
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user