proof
Nothing here is hidden. The entire space is produced by the code below, running in the browser, and anyone can open the console and derive any room themselves given the path to it. There is no server call to intercept and no stored data to audit. What follows is what actually runs.
deriving a room identifier
const ORIGIN_SEED = "cell:origin:v1";
async function sha256(input) {
const bytes = new TextEncoder().encode(input);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return new Uint8Array(digest);
}
async function deriveRoom(parentId, exitIndex) {
const input = parentId === null
? ORIGIN_SEED
: `${parentId}:${exitIndex}`;
const bytes = await sha256(input);
const id = Array.from(bytes.slice(0, 8))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
return { id, bytes };
}The origin room takes a fixed seed. Every other room takes its parent's identifier joined with the index of the exit chosen. Because the input is fully determined by the path, the output is too, and the same walk always lands in the same room.
reading the hash
function reader(bytes) {
let i = 8; // first 8 bytes are the id
return {
byte() {
return bytes[i++ % bytes.length];
},
pick(list) {
return list[this.byte() % list.length];
},
range(min, max) {
return min + (this.byte() % (max - min + 1));
}
};
}Everything about a room is read sequentially from the bytes after the identifier. Because the reader is consumed in a fixed order, the same hash always yields the same name, the same description, the same drawing, and the same exits.
name and description
const SHAPES = ["narrow", "shallow", "blank", "long", "flat", "grey"];
const KINDS = ["hall", "chamber", "corridor", "recess", "landing", "bay"];
const OPENERS = [
"The space is roughly square and gives nothing away at the threshold.",
"Entering, the first thing to settle on is a row of empty shelves.",
"The room reads as short before anything else about it registers."
];
const DETAILS = [
"The floor slopes slightly toward one corner.",
"The room contains a plastic drum with no lid.",
"Someone has left a folded cloth on the floor.",
"The far wall has been patched and not repainted.",
"A low tone comes through the wall and does not change."
];
function describe(r) {
const name = `${r.pick(SHAPES)} ${r.pick(KINDS)}`;
const body = [r.pick(OPENERS)];
const count = r.range(3, 5);
const used = new Set();
while (used.size < count) {
const line = r.pick(DETAILS);
if (!used.has(line)) { used.add(line); body.push(line); }
}
return { name, body: body.join(" ") };
}The pools are finite, so rooms share sentences. What differs is which sentences appear, in what order, and how many. The description is not written, it is selected.
exits
const EXIT_FORMS = [
r => `past the ${r.pick(["drain cover", "metal ladder", "plastic drum"])}`,
r => `through the ${r.pick(["narrow gap", "low hatch", "propped door"])}`,
r => `around the ${r.pick(["stacked pallets", "hanging sacking"])}`,
r => `under the ${r.pick(["run of conduit", "low beam"])}`
];
function exits(r) {
return [0, 1, 2, 3].map(i => ({
index: i,
label: EXIT_FORMS[r.byte() % EXIT_FORMS.length](r)
}));
}Four exits, each labelled from the same hash, each carrying the index that will be hashed with this room's identifier to produce whatever lies beyond it. The label is cosmetic. The index is what does the work.
destruction
const destroyed = new Set();
function goBack(path) {
const leaving = path[path.length - 1];
destroyed.add(leaving.id);
return path.slice(0, -1);
}
async function visibleExits(room) {
const out = [];
for (const exit of exits(reader(room.bytes))) {
let child = await deriveRoom(room.id, exit.index);
let salt = 0;
while (destroyed.has(child.id)) {
salt += 1;
child = await deriveRoom(room.id, `${exit.index}r${salt}`);
}
out.push({ ...exit, target: child.id });
}
return out;
}An exit leading to a destroyed room is re derived with a salt until it points somewhere else. The check runs every time a room renders, so once a room is in the destroyed set there is no arrangement of choices that reaches it again.
The set lives in memory. It is not written to storage, not sent anywhere, and not recoverable. Closing the tab empties it, which is why a refresh restores every room that was destroyed.
what this means
Because all of it runs client side, there is nothing to trust. There is no claim being made about a server behaving correctly, no record that could be edited, and no state that could disagree with what is displayed. The function is on the page. Anyone can read it, run it, and confirm that a given path produces the room it says it does.
