@AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
House of Entropy — a web app that lets users walk Borges' Library of Babel in first-person 3D. Faithful Borges architecture (hexagonal rooms, shelves of books), grounded first-person walk, and books whose text is generated on demand from a true reversible coordinate↔text bijection (no storage — every page is computed).
npm run dev # Next.js dev server (Turbopack) at localhost:3000
npm run build # production build
npm run lint # eslint
npx tsc --noEmit # typecheck (do this after edits; there is no test suite)There is no test runner. Correctness of the math (hex grid, page bijection) is verified with throwaway Node scripts in /tmp — write a .mjs that inlines the function and run node /tmp/x.mjs. This is the established pattern for validating geometry/bijection changes before wiring them into the React tree.
reactStrictMode: falseinnext.config.tsis load-bearing. StrictMode double-mounts effects, which makes R3F create/destroy the WebGL context twice → "THREE.WebGLRenderer: Context Lost" (black screen + sad-face). Do not re-enable it.tsconfigtarget is ES2020 — required for BigInt literals (0n) used by the page engine.- The page engine uses BigInt over a 4942-digit modulus (35^3200).
pageText()is ~4ms. NEVER call it per-frame — only on user action (clicking a book, flipping a page). - R3F components are client-only;
Sceneand DOM overlays are imported vianext/dynamicwithssr: false.
Two layers: a pure-math core in lib/, and the R3F scene graph in components/.
- Borges constants. ALPHABET is the single source of truth for the page engine (
BASE = ALPHABET.length); it was expanded past the faithful 25 to 35 (full a–z +space , . ? ! - ; : ') so search input is typeable. Page is 80×40, book 410 pages. - Flat-top hexagon vertices/walls;
hexWalls()gives per-wall midpoint/rotation used to place walls, shelves, and colliders. - Hex grid "flower" topology (subtle): a plain hex grid is not 2-colourable, so you cannot give every hex exactly 3 evenly-spaced doors that line up. Solution: 1/3 of hexes are sealed centres (
isCenter(q,r) = (q+2r)%3===0), never rendered or entered. Each ring hex is bordered by 3 centres on alternating walls, leaving exactly 3 matching doors.isDoor/neighborOf/hexToWorld/WALL_STEPencode this; axial steps brute-force verified (shared-edge gap 0). - BigInt-coord variants (
isCenterBig,isDoorBig,neighborOfBig) exist because of the floating origin (below) — true coords are astronomically large.
Reversible bijection between a coordinate and its 3200-char page text. coordToAddress packs the coordinate (mixed-radix + Cantor pairing of zig-zagged q,r); an affine cipher (A·addr + C) mod BASE^3200 scrambles it (A is a full-width multiplier so even tiny addresses produce full gibberish); base-decode → text. There are BigInt-coord variants (PageCoordBig, coordToAddressBig, pageTextBig) for the floating origin.
- Reverse search:
containsSearch(text)builds a page that contains the query embedded in deterministic noise, then inverts the affine to its address;containsSearchWords(text)does the same but fills the background with plausible English words (lib/words.ts).pageFromAddrHextravels to a raw0x…address.addrHexToCoordBigrecovers the true (enormous) BigInt coordinate. - The tutorial book lives at a real coordinate in spawn hex (1,0);
isTutorialBook/tutorialPagesgive it gold styling + authored intro text.
A searched page's hex is ~10^2000 away — impossible in float space. worldStore.ts holds a BigInt origin (Q0,R0); rendering uses small local coords, true content coord = origin + local. On a search arrival the origin rebases onto the found hex, so the player physically stands and walks in that region. See memory/origin-rebasing.md.
Scene.tsx—<Canvas>, fog, a single player-followingFollowLamp(per-hex lights were the main perf cost),<Physics>(rapier),<FlyThrough>.DEBUGflag swaps in anOrbitControlsfly-cam + rapier wireframes.HexGrid.tsx— spawns/despawns hexes withinRINGof the player; content uses the true BigInt coord viaworldStore, rendering uses localhexToWorld. Skips sealed centres.HexRoom.tsx— one hex from a BigInt true coord (tq,tr): floor/ceiling/walls, per-wall door/solid fromisDoorBig, bookshelves + colliders.<Edges>give the dark outlines.Player.tsx— rapier capsule, WASD accel-lerp, pointer-lock look, smoothed camera follow; freezes while reading/flying; re-locks pointer on close; teleports the capsule on search arrival.Bookshelf.tsx— instanced books (deterministic look + hover highlight). Each book is ONE merged geometry of solid thick boards (spine slab + left/right/tail/back cover boards, open top) plus a recessed cream page block, built bybookGeometry()viaboxBetween+mergeGeometrieswith three material groups (MAT_SPINE/PAGES/COVER). Solid boards (not thin box faces) render from every angle — earlier thin-shell versions were 1px / invisible from inside. The spine slab's room-facing+zis the atlas (per-instanceaCellpicks one of 36 cells via anonBeforeCompileUV remap);pagesMatis excluded from the per-bookinstanceColortint viastripInstanceColorso pages stay cream while leather takes the hue. Each shelf clones the shared geometry (ownaCell). Walls are floor-to-ceiling shelving (SHELVES_PER_WALL = 7,BOOKS_PER_SHELF = 32);SHELF_TOP = WALL_HEIGHT - 0.7leaves headroom so the top row clears the ceiling. Click resolvesinstanceId→ opens the book at its BigInt coord. ChangingBOOKS_PER_SHELF/SHELVES_PER_WALLchanges the page-engine RADIX/address space — it's derived, stays consistent, but is not free to tweak.OpenBook.tsx(3D portrait book, reflows page text). Pages are unlitmeshBasicMaterialwithtoneMapped:falseso the follow-lamp/tone curve can't tint them brown; the leather board+spine are also unlit (a lit board bled warm-brown over the pages — the old "open book is all brown" bug). Per-meshrenderOrder(board 990 → wad 999 → ivory surface 1000 → gutter shadow 1001 → text 1002) decides draw order since everything runsdepthTest:falseto sit over the world; agutterTex()gradient darkens the inner margin at the spine. Tilt is gentle (-0.22rad, V half-angle0.06) so the spread reads nearly flat/aligned.BookOverlay.tsx(DOM: coord/address readout, copy buttons, start hint, flying veil),SearchBar.tsx(/to search; noise vs english-words toggle; accepts0x…addresses — does NOT trim the query, so leading/trailing spaces survive; only trims for the0xtest),FlyThrough.tsx(travel animation — passes the current floating origin tobuildTravelPathso door/centre tests use TRUE coords; otherwise post-rebase searches start in a locally-"sealed" hex and the route falls back to a wall-piercing straight line),Menu.tsx(Esc menu: name field + controls + players w/ hex+distance, AND the Tab-held<Roster/>),bookStore.ts+worldStore.ts+playerState.ts(stores;bookStorealso ownsmenuOpen/searchOpenflags folded intoisInputLocked).
Mix of real photographed PBR (Poly Haven, CC0) and procedural canvas textures, all shared singletons (built/loaded once, reused by every hex — GPU uploads each map once).
- Photo sets live in
public/textures/<dir>/as 1k jpg (diff/nor/rough);photoTex(dir)loads + caches them withRepeatWrappingand correct colorspaces (diff = sRGB, nor/rough = NoColorSpace). Currently floor =marble_tiles, wall =wood_plank_wall, ceiling =wooden_panels, door reveal = same plank wood as the wall (copied into its owndoor/dir so it can set its own repeat). This deliberately breaks the old "file-free, no assets" rule — the procedural surfaces looked too abstract; a few MB of jpg is the price. To add a new photo surface: dropdiff/nor/rough.jpg(resize to 1k, e.g.magick in.jpg -resize 1024x1024 -quality 82 out.jpg) and callphotoTex("<dir>"). Noaomap — it needs a 2nd UV set (uv2) and the diffuse already bakes occlusion. Poly Haven REST API (api.polyhaven.com/files/<asset>) gives direct CC0 jpg URLs. - Procedural (canvas) still used only for shelf wood (
woodTex/buildWood) and the book-spine atlas. The spine atlas is a 6×6 grid of 36 antique leather spines drawn near-white soinstanceColortints each volume;drawSpinedoes grain/hubs/gilt label/foot tooling/worn edges. Procedural normal maps come from a Sobel heightfield of the colour canvas (normalFromCanvas). - Floor uses world-space UV (set in
HexRoom,FLOOR_TILE) so tiles run continuously across hex boundaries; its texture stays atrepeat = 1. Wall/ceiling/door set their ownrepeat(ceiling 3×3, door 1×2) since their meshes use planar/face UVs.
Per-frame runtime state lives in plain module singletons read/written in useFrame (playerState.ts); UI-reactive state uses useSyncExternalStore singletons (bookStore.ts, worldStore.ts). Prefer these over context/props for anything touched every frame.
Durable design decisions and hard-won findings are in ~/.claude/projects/-Users-vsht-Documents-Dev-house-of-entropy/memory/ (indexed by MEMORY.md). Check there for the build-order roadmap, the hex topology proof, the page-engine details, and deferred polish TODOs (textures, book-flip animation).