Last updated
How one family website went from a blank page to a full-blown multi-page world — with a driving game, a redesigned poker table, AI-powered hints, a family message board, and a full account system — all built through conversation.
It started with an idea: build a fun, personal website for a family — with a page for each member. The name? Brainrotters. The vibe? Dark backgrounds, neon glows, and a sense that someone genuinely designed this for their actual family, not a template.
The site was structured around six subpages — K, E, C, Gramma, M/P, and a secret page for John — each with its own color theme and personality. And it would live at brainrotters.com, uploaded via WinSCP and served by Hostway hosting.
Every single page is a standalone .html file. No React, no bundler, no dependencies
beyond Google Fonts. The whole site ships as six HTML files, a couple PHP files for the backend,
and whatever images the family wants to upload. Drag-and-drop via WinSCP to brainrotters.com.
The server ran PHP 4.4.9 (from 2008!) until a support call to Hostway got it upgraded to PHP 8. That upgrade unlocked the leaderboard and message board features.
The M/P page wasn't just a profile — it became a full browser game. The site owner drives a white Tesla around a little neighborhood map, picking up K, C, and E from their homes and delivering them to destinations (schools, shops, Spectrum, the lab). 120-second timer. 100 points per delivery.
Started as tap-and-hold steering, evolved through D-pad buttons, ended up as a drag joystick zone below the canvas.
When the Tesla is on a road, max speed doubles. Driving on the grass? Slow going.
Top 5 high scores with 🥇🥈🥉 medals. Enter a 3-letter name. Saved server-side via PHP to leaderboard.json.
The server was running PHP 4.4.9 (from 2008!). A call to Hostway got it upgraded to PHP 8 to enable the backend.
Gramma's page became something genuinely touching: a message board where each family member (K, C, E, M/P, and Gramma herself) has a color-coded card to leave a message. Each card is editable with a password. Messages reset every Monday morning automatically.
messages.php handles GET (return all messages as JSON) and POST (save a message with password auth). Passwords live in gramma_passwords.php — a simple array editable directly on the server. All five family members share the initial password "terra mia".
Auto-reset on Monday mornings works by checking the ISO year+week number against a stored value
in messages_last_reset.txt. First visit each Monday wipes messages.json clean.
K's page got the most ambitious feature of all: a fully working Texas Hold'em poker game — complete with hand evaluation, sequential AI turns, betting rounds, pot management, and a showdown. The opponents? E 🌙 (cautious), C ⚡ (aggressive), and Gramma 🔥 (balanced).
"The hardest bug to fix was that all the AI opponents were taking their turns at the same time — the whole flop would drop at once instead of proper sequential betting."
— The sequential turn queue problem, solved with a nextTurn() function and _actedThisStreet flagsEvery card (2–10) draws its suit symbols in the exact real-world positions using PIP_LAYOUTS coordinates.
J/Q/K got unique emojis per suit — 🥷🤴🎩🧙 for Jacks, 🧝👸🧜🧟 for Queens, 🧛👑🔮👺 for Kings. No external images needed.
Full Royal Flush → High Card detection. Tests all C(7,5)=21 five-card combinations from each player's 7 available cards.
E is cautious, C is aggressive, Gramma is balanced. Each reads hand strength vs pot odds to decide fold/call/raise.
The original poker game worked, but it looked like a coding exercise. In a new conversation, the whole visual language was rethought from scratch: a dark green felt table with a gold border glow, a warm cream color for cards, Cinzel serif fonts for the table title, and a proper "K's Table — Hold'em" header with the game tucked inside a rounded casino-style container.
Cards got a complete redesign. Number cards now use PIP_LAYOUTS — a coordinate grid that places suit symbols in the exact same positions as real playing cards (2s at top and bottom, 3s in a column, 7s with the classic offset arrangement). Face cards (J, Q, K) each got unique emoji per suit — 🥷🤴🎩🧙 for Jacks, 🧝👸🧜🧟 for Queens, 🧛👑🔮👺 for Kings.
PIP_LAYOUTS places suit symbols at precise coordinates matching real playing cards — the 7 of Hearts looks exactly like a real 7 of Hearts.
Each face card has a unique emoji per suit. The King of Spades is 👑, the Jack of Clubs is 🎩. No external images needed.
Tapping any face-up card shows a large zoomed overlay. On a phone screen, the pip positions are tiny — this solved the readability problem.
At showdown, opponent cards are revealed face-up. The winning hand name (e.g. "Flush", "Two Pair") appears in gold. A proper casino moment.
A gold "D" chip badge sits on the bottom border of whoever holds the button — rotating each hand, just like a real poker table.
Top pair, middle pair, bottom pair, underpair — each gets a distinct strength label so the AI hint knows exactly how vulnerable the hand is.
The idea: a 💡 Hint button that sends the current hand — hole cards, community cards, position, pot size, betting history — to a language model and gets back a one-sentence coaching tip like "RAISE, strong flush draw in middle position — semi-bluff with 9 outs."
This used the Groq API (free tier) with Meta's llama-3.3-70b-versatile model. The challenge wasn't connecting to the API — it was getting the hints to be consistently correct. A dedicated trainer.html tool was built to run batches of 15 test scenarios and evaluate hint quality. Getting from first attempt to 15/15 accuracy took many rounds of prompt iteration.
"The 8-billion parameter model got 7 out of 15 wrong. It hallucinated cards that weren't in the hand, raised when it should fold, and gave hints in ALL CAPS with line breaks. Switching back to the 70-billion model fixed everything — at the cost of using 5× more of the daily token limit."
— The model size tradeoff, discovered by running a batch comparisonThe original prompt was 306 words and used ~408 tokens per request. On the free tier (100,000 tokens/day), that meant only ~166 hints per day before hitting the limit. By stripping the prompt to its essential rules — 87 words, ~116 tokens — the daily capacity jumped to ~860 hints with room to spare.
The key insight was that the model doesn't need natural language explanations — it needs precise, unambiguous rules with no room for creative interpretation. Shorter and stricter outperformed longer and polite.
Hints use real poker terminology — semi-bluff, flush draw, pot odds, c-bet, gutshot, broadway. For someone learning the game, these terms are opaque. A 33-term dictionary was added: any poker term in a hint gets a gold dashed underline, and tapping it shows a popup definition.
The tooltip positions itself relative to the tapped element — appearing just below the word in the hint text, or just below the dealer button chip if you tap that. The popup uses the same gold aesthetic as the rest of the table.
After just two hints, the daily token limit was exhausted. The error message from Groq: "Used 99,645, Requested 484. Please try again in 1m 51s." The hint button would go grey and never come back. This needed a real fix.
The solution was a live countdown timer: parse the "try again in Xm Ys" from the error message, show "Daily token limit reached — try again in 1m 51s" with a ticking clock, and automatically re-enable the button when it hits zero. The trainer got similar treatment — distinguishing between per-minute rate limits (retry with backoff) and daily limits (stop immediately, show countdown, abort remaining scenarios).
Parses "try again in Xm Ys" from the API error. Ticks down every second. Button re-enables automatically at zero.
Requests-per-minute errors retry with exponential backoff. Tokens-per-day errors stop immediately and show the countdown — no point retrying a daily limit.
An AbortController kills any request hanging longer than 20 seconds, with a clear message pointing to the API console.
After compression, each hint costs ~116 tokens. The daily limit now supports a full family game session with room to spare.
Adding user accounts to a PHP/FTP site means building everything from scratch. Three files handle the whole system: auth.php (server-side register/login/logout/session check, passwords hashed with bcrypt, accounts stored in a SQLite database), auth.js (a modal login/register UI injected into every page via a single script tag), and admin.php (a password-protected dashboard for the site owner).
Sessions are managed server-side via PHP's native session handling — log in once and the site remembers you as you navigate between pages. The login modal appears as a gold-bordered overlay in the same aesthetic as the poker table.
Passwords are hashed with PHP's PASSWORD_BCRYPT — never stored in plain text. SQLite keeps the database as a single file on the server.
Shows total accounts, logins this week, total logins ever. Per-user table with join date, last login, weekly activity bar chart, and Active/Inactive badge.
Each family member can link their account to their message slot. When linked and logged in, the New button skips the password prompt entirely.
Adding a show/hide toggle to the password field required three iterations — the SVG eye icon kept being invisible against the dark background until moved outside the input field.
Adding login to any page takes two lines: a script tag in the head, and a span where the link should appear. Auth state propagates automatically on every page load.
If the admin account is already logged in via the regular site login, visiting admin.php skips the second login screen — no double authentication needed.
The M/P driving game got a batch of desktop-focused improvements. First: a new click-and-hold control scheme — hold the mouse anywhere on the map and the car smoothly steers toward the cursor, continuously updating as it moves. Arrow keys still work and take priority the instant any arrow key is pressed, so both control styles can be mixed naturally in the same session.
A subtle bug had to be fixed first: the mobile/desktop detection used
navigator.maxTouchPoints > 0, which returns true on desktop Chrome (it bakes in
touchscreen laptop support). Switching to a user-agent string check fixed it — real mobile
devices (Android/iPhone/iPad) get the joystick; everything else gets click-to-drive.
After each successful delivery, a glowing 🧋 Boba Stop spawns on the map. Drive over it to add 3 seconds to the timer (capped at 30). Missed ones accumulate — every delivery spawns a new one, and uncollected stops stay on the map until picked up.
Placement is constrained: each new boba stop must be at least two road-widths from every location box and every other existing boba stop. The spawn function builds all valid road-center candidates, filters them, and picks randomly from what remains. If the map is too crowded for a valid placement, the spawn skips silently.
Hold the mouse anywhere on the map. The car steers toward the cursor smoothly, updating continuously as it moves.
The instant any arrow key is pressed, keyboard mode takes over. Release all keys and click-to-drive resumes seamlessly.
Each delivery spawns a new boba stop on the map. Uncollected ones stay and accumulate. Each pickup adds 3 seconds, capped at 30.
New boba stops spawn at least 2 road-widths from every location and every other boba stop. No clustering.
If logged into the site when hitting a high score, the name entry field pre-fills with the first 3 letters of the account username.
Chrome's maxTouchPoints > 0 returns true even on desktops. Switched to user-agent detection so the joystick only shows on real mobile devices.
This wasn't "AI writes code while the human watches." It was a genuine back-and-forth — the site owner would describe what was wanted, Claude would build it, the site owner would use it and notice something off ("the cards animate on hover and they shouldn't"), and the two would fix it together. Over and over, in real-time.
Some things took unexpected turns: the PHP server turned out to be from 2008 and needed a support call to upgrade. Claude's file system had persistent I/O errors during one session. The Wikimedia card images blocked hotlinking. Every real project has this kind of friction — AI collaborator or not.
Six HTML pages, neon dark aesthetic, daily rotating quotes with mismatched Unsplash images (zombie quotes get K-pop images, etc.)
Canvas-based, iterated through three different control systems before landing on the virtual joystick. Added road speed boost, leaderboard, 30-second timer.
Discovered it was running PHP 4.4.9 from 2008. Rewrote the leaderboard backend twice — once for old PHP, once after the upgrade to PHP 8.
Five color-coded cards, password protection, 500-char limit, Monday auto-reset. A genuinely useful family feature.
The most complex feature. Debugged the sequential turn queue, redesigned cards three times (simple → pip layout → emoji face cards), fixed hover animations, removed corner suit symbols.
Full casino visual overhaul: dark felt table, gold border glow, Cinzel fonts, proper pip card layouts, emoji face cards, card zoom on tap, dealer button badge, win screen with opponent reveal.
Built a hint system using a free AI API. Iterated the prompt engineering until hitting 15/15 accuracy. Compressed the prompt 72% to stretch the daily token limit. Added a 33-term poker dictionary with tooltips.
Live countdown timers, RPM vs TPD error separation, AbortController timeouts. The trainer tool revealed the 8B model had a 47% error rate — staying with 70B was the right call.
PHP + SQLite + bcrypt from scratch. Login modal on every page. Admin dashboard with telemetry. Gramma's page linked to accounts — no password prompt when you're already logged in.
Click-to-hold controls for desktop, mobile detection bug fixed, arrow key priority, leaderboard username autofill, and a boba stop powerup system with smart placement logic.
Every "one small thing" — a tooltip position, an eyeball icon, a boba stop that respawns in the right place — got its own conversation turn. That's just how software gets good.
"The best thing about building with AI isn't that it's faster. It's that the human collaborator can try things they wouldn't have bothered trying alone — because the cost of 'let's just see what it looks like' is nearly zero."
Daily rotating quotes (15 total) with deliberately mismatched images. Dark neon landing page with links to all family members.
Full Texas Hold'em poker game. Casino aesthetic, pip cards, emoji face cards, AI hints via Groq, 33-term tooltip dictionary, dealer button, win screen with opponent reveal.
Purple theme. Five-card message board. Password-protected editing. Auto-resets Monday mornings. Account linking skips the password prompt if you're logged in.
Canvas driving game. Click-to-drive on desktop, virtual joystick on mobile. Pick up K, C, E, deliver them to destinations. Boba stop powerups. PHP leaderboard with username autofill. 30-second timer.
leaderboard.php + messages.php + gramma_passwords.php + auth.php + admin.php. Server-side JSON and SQLite storage. Upgraded from PHP 4 to PHP 8.
Full account system. bcrypt passwords, PHP sessions, SQLite database. Modal login on every page. Admin dashboard with login telemetry and account-linking for Gramma's page.
Turn-based tactical game — humans vs. zombies. Canvas 2D renderer, PHP/SQLite backend. Proposal system, fog of war, LOS, zombie AI, traits, infection, observer animation.
Tile painter for Lurkers maps. Separate layers: terrain, walls (N/E/S/W bitmask), objects (barrels, pillars, sandbags), and zones. Unified zoom with the game client.
Scenario editor for Lurkers — set starting positions, assign units with portrait previews, define win conditions. Portrait images clipped to circle with faction rings.
Admin UI for editing unit and trait definitions. Animated gold-tinted trait icons. Live stat display with raw vs. buffed values. Drag-to-assign traits, detection stats per unit.
After poker and driving games, the next challenge was the most ambitious: a full turn-based tactical game — humans vs. zombies, Fire Emblem style. Four human unit classes (soldier, scout, medic, sniper) face off against shamblers, runners, and bloated undead across hand-painted grid maps. Multiple players share control of the human side. The zombies run themselves.
The constraint: no game engine, no WebSockets, no frameworks. Just HTML Canvas, PHP, and a single SQLite file on shared hosting. The goal was a fully playable multiplayer game built entirely through conversation — designed, implemented, debugged, and balanced in about a week.
Players don't move immediately — they propose destinations. A ghost with a ? badge appears; once committed it shows ✓ and the game already treats the unit as there for fog-of-war. When everyone hits READY, all moves and attacks resolve simultaneously server-side.
The zombie phase runs entirely server-side. Each zombie independently checks sight (LOS-required), hearing (through walls), and bloodhound (global detection). Detected zombies charge and attack. Runners get +2 move on detection. Frenzy, infection, prone — all data-driven from the database.
Line of sight uses Supercover DDA — every tile the ray passes through is checked against wall edges (N/E/S/W bitmask) and opaque objects. Fog updates the moment you commit a proposal, not just at phase end. Zombie selection shows a red dashed sight box and amber dotted hearing box.
Body armor (40% damage reduction), scalpel (50% double-damage, lethal tag), undead (adds infection stacks — 2 HP/stack/turn), triage (heals + clears infection), volatile. All defined in the database; no hardcoded behavior in the engine.
The entire game state lives in lurkers_game.db: tables for games, units,
proposals, attack orders, players, and definition tables for unit types and traits.
Every action — move proposal, attack declaration, ready signal — is a JSON POST to
lurkers_game.php, which reads state, runs resolution logic, and returns
the new state in one round-trip. No WebSockets. No push. Just polling.
Observer players poll every 2 seconds, comparing updated_at timestamps.
When a phase resolves, a unified renderTransition() function plays combat
animations for everyone — not just the player who clicked READY. The game
genuinely feels multiplayer despite running on a $10/month shared host.
Once the core game was playable, the bugs came out of hiding. Zombies were moving through walls. Auto-attacks were landing on targets behind solid barriers. A runner appeared to phase through a wall entirely — it was actually going around the wall, but the start-to-end animation made it look like cheating. Each bug pointed to the same root cause: the game had no way to show its work.
The wall fixes were surgical. The LOS algorithm used && where it needed
|| — a diagonal step only blocked if both cardinal edges had walls,
which let a ray slide through a horizontal wall at an angle. One character changed; the bug
disappeared in both the JavaScript client and the PHP backend independently. The wander
function, the auto-attack resolver, and the BFS pathfinder each got their own version of the
same fix.
When a ranged human fires, a positional sound marker is saved server-side immediately — not at phase resolution. During the zombie turn, each zombie checks if the sound is within hearing range. If it is, the zombie charges toward the noise even if the shooter is hidden in fog. The marker clears at the start of the next human turn, so sounds are genuinely ephemeral: fire once, get chased once.
The "runner through a wall" confusion was the final push. The PHP backend now reconstructs the full tile-by-tile BFS path (tracking parent pointers) and sends it with every zombie move event. The client animates each step at 280 ms per tile — ease-in on the first tile, linear through the middle, ease-out on the last. A 5-tile frenzy sprint is now legible. No more teleportation.
A hidden debug bar (toggle with ~) grew into a real investigation toolkit: the SOUNDS checkbox renders 🔫 markers at every human gunshot position; the ⬇ HISTORY button downloads the full event log for the current game as JSON — every move, attack, infection tick, and death since turn 1. Bugs that were invisible became obvious within seconds.
When multiple zombies move simultaneously, their animations stagger by 140 ms. But with variable-length paths — a shambler taking one step vs. a frenzy runner taking five — the wait time can't be fixed. The client now computes max(i × stagger + path.length × stepDur) across all moving units so it always waits for the last step of the slowest zombie before playing combat.
A data-driven game needs a data editor. codex.html became the admin panel for the entire Lurkers world: edit unit stats, assign traits, tune detection ranges, preview portraits. All changes write directly to the SQLite database.
But a game also needs art. Each trait — scalpel, chainsaw, body armor, bloodhound — got its own icon. The pipeline: generate 2×2 sprite sheets in Midjourney (zombie woodblock style), run slice_icons.py to cut them into individual PNGs, apply a gold tint in the codex renderer. Every new trait category meant a new set of prompts and another round through the pipeline.
Midjourney outputs sprite sheets — 2×2 by default, larger sheets named with a suffix (e.g. Sheet4x4). The script detects the grid size from the filename, splits the sheet into individual PNGs, and applies a CELL_INSET to crop out MidJourney's border frames.
All trait icons are stored as regular PNGs. The codex renderer applies a gold tint at draw time using canvas globalCompositeOperation — no need to regenerate images when the color scheme changes. Zombie-side traits use amber; human traits use teal.
Raw stat (small, dim) next to buffed stat (large, bright gold) — inline, baseline-aligned. Assign a trait and the buffed column updates immediately. The trait assignment chips show × to remove; "+ ADD TRAIT" opens a dropdown of everything not yet assigned.
Unit portraits are circular-clipped in both the scenarios editor and the game client. The scenarios editor shows the first portrait from the pool in the unit picker; dragging a unit to the map uses the portrait clipped to a circle with a teal (human) or amber (zombie) ring.
The early chapters of this story were written in a chat interface. Paste the code. Get new code back. Paste it again. It worked — but the context window was a hard ceiling, and every session started cold.
Then came Claude Code: a VS Code extension where the AI lives directly inside the editor. It can read every file in the project, run terminal commands, make edits in place. No more copy-pasting. No more "here's the relevant section" — it just reads. The relationship between human and AI collaborator changed fundamentally.
No more copy-pasting code into a chat window. Claude Code reads any file in the project directly — all 2500 lines of lurkers_play.html, the PHP backend, the database schema. The full picture, always in context.
A persistent memory system captures what matters between conversations — game architecture, design decisions, gotchas (like PDO returning strings instead of ints). The next session starts informed, not cold.
Changes go directly into the file. The diff is visible in the editor. No intermediate step of "now paste this back." The feedback loop between human and AI collapsed from minutes to seconds.
All of this — the tactical game, the art pipeline, the codex admin UI, the multiplayer observer system — runs on a $10/month shared host with no frameworks, no CI/CD, no deployment pipeline. WinSCP. FTP. Done.
"Every new capability the tool gets is immediately visible in what the site becomes. The tool got better, so the site got bigger. That's a new kind of creative relationship."