Eat my face
Original authored for Ryan's arcade, with AI assistance, from the project's own game implementation and tests. No third-party skill text is imported, and publication is not a blanket license to the site's code, art, brands, or other content. The portrait on the cheese is Ryan's likeness: use your own photo or an image you have permission to use; never use another person's likeness without their consent.
Build from scratch
- Start by deciding whether you are making a fresh static game or extending Ryan's website. For a standalone game, a useful separation is
public/eat-my-face.html,public/eat-my-face.css,public/eat-my-face.jsfor markup/styles/controller,public/eat-my-face-motion.jsfor simulation, andpublic/eat-my-face-3d.jsfor rendering. Keep auth and shared mobile-screen code separate. Vendor and pin Three.js locally; do not fetch runtime modules from a CDN. Ryan's site usespublic/arcade.html,public/arcade.css,public/arcade.js,public/arcade-motion.js, andpublic/arcade-3d.jsinstead. - Define the maze and simulation without browser globals. The shipped board is 15 columns by 11 rows. Tiles are integer
(x,y)cells, stored as wall keys like"x,y"; out-of-bounds tiles are walls. The perimeter is closed. Interior walls are atx=4fory=1..7except3, atx=8fory=3..9except7, and atx=11fory=1..6except4. The mouse begins at(1,1), four cats at(13,1),(6,5),(2,9),(13,7), and the cheese at(13,9). Keep the coordinates and model positions in the same orientation. - Provide a pure movement API such as
cameraRelativeMovement(input,yaw),orbitCamera(camera,dx,dy), andcreateMovement(walls,columns,rows). The movement object exposesisWall,canOccupy,move,clearPath, andchase; the controller owns the game state, clock, and outcomes. Use circle-vs-wall checks withBODY_RADIUS = 0.22, not point checks. Sweep movement in increments no larger than0.06world units and test X and Y separately so the mouse/cats cannot tunnel through corners and can slide along walls. Normalize keyboard/D-pad diagonals while retaining analog stick strength and direction. - Keep movement continuous and camera-relative, not tile-step. The shipped
PLAYER_SPEEDis3.2world units/second; map forward/back and left/right through the current camera yaw. Cats useCAT_SPEED = 1.65, chase the mouse directly when the sampled path is clear, and otherwise follow a path found by breadth-first search over eight neighboring cells. Check diagonal path segments for clearance, repath at about0.2seconds, and when a cat loses sight of the mouse, send it straight to the next path waypoint if that segment is clear; recenter on the nearest cell only when it is not, because recentering first can send the cat backwards and makes it visibly twitch. Only updatefacingfor moves longer than0.001so wall-scrape micro-moves do not flicker the model's heading. Do not let a path or a rendering update move a body through a wall. The simulation should remain deterministic: no random chase decisions or DOM/rendering dependencies. - Keep four distinct cats and preserve the
1,500 msgrace period before they become active. A cat captures at distance<= 0.42; the mouse wins when within< 0.6of the cheese. Check outcomes during small movement substeps after the mouse move and after the cats move, so a fast frame does not skip a catch or goal. The round starts inready, entersrunning, and finishes once aswonorlost; it also loses when its45,000 mstimer expires. Restart from the original positions and reset camera, clock, cats, and held input. The cats should not move during the head start. - Use a fixed small update step for simulation and cap unusually long frame updates. Ryan's controller divides each frame into steps no larger than
1/120second, caps the elapsed simulation frame to100 ms, and separately tracks elapsed round time. The pure motion tests exercise 30, 60, and 144 FPS; keep displacement per simulated second independent of render frequency. Pause the timer when the game is hidden or the page is inactive. - Build a local Three.js scene with a
15 x 11floor and cheese-textured box walls. Ryan's renderer usesCELL = 3, walls3 x 2.8 x 3, a62degree perspective camera, hemisphere plus directional light, shadows, ACES filmic tone mapping, and distance fog. Build simple mouse/cat models and a large cheese wheel with a bright goal beacon; the original animals are procedural meshes, not external assets. Keep the 2D radar separate from the 3D scene, with walls, mouse, cats, and cheese visible in distinct colors. - Implement an orbiting third-person follow camera independent of movement. The shipped orbit distance is
5.2, yaw changes by0.006radians per horizontal drag pixel, pitch changes by0.005per vertical pixel and clamps to0.18..1.05radians. Raycast from the camera target toward the desired camera position against wall meshes; sample the center and five nearby offsets to prevent the camera near plane clipping at corners. Pull the camera in instantly when a wall intrudes but ease it back out (exponential, rate about5/second) so it never pops. Hide the player model below a0.95camera distance and show it again only above1.3; that hysteresis stops the mouse flickering when the camera hovers near a wall. Render cats turning more slowly than the mouse (blend rate11vs18) so 90-degree grid-corner turns read as turns, not snaps. Add a small negative shadowbias(-0.0005) with the0.04normal bias, and enable anisotropic filtering on the wall texture to stop shimmering at grazing angles. Provide a resize observer, clamp renderer pixel ratio to at most1.5, and show a useful recovery message on WebGL context loss. - Put inputs and presentation in the controller. Support WASD and arrows, normalized diagonal movement, an analog touch stick, independently held touch D-pad directions, pointer drag on the maze and the labeled camera strip, and keyboard orbit (
Q/E) and tilt (R/F). Keep camera drag independent of movement, allow camera adjustment before play and after a round, and support simultaneous stick and camera pointers. Provide the remaining-time HUD, accessible status, radar, start/replay overlay, render-error area, and a clear win/loss result. Clear every held key, stick, D-pad pointer, and drag on blur, hidden tab, pointer cancellation/lost capture, screen-mode change, and leaving play. - Reuse shared responsive fullscreen behavior when available:
public/arcade-screen.jsandpublic/arcade-screen.cssoperate on[data-game-screen],[data-game-screen-toggle], and[data-game-screen-status]. On a max-width900pxor coarse-pointer device, enter viewport focus when the game loads; don't request native fullscreen without a user gesture. The first eligible game-button tap may request native fullscreen. Keep the explicit desktop button and an obvious, rightmostExitat least44 x 44pixels. Honor Escape, safe areas, and an unavailable/denied Fullscreen API. Do not implement a competing fullscreen state machine. - Ryan's game play is public and does not depend on sign-in. Keep the simulation and maze controls independent of account state. A generic standalone version should use the host's established account policy for any optional shared features rather than inventing credentials or weakening its policy.
- Follow strict Hosting CSP: load JavaScript and CSS from external same-origin files; no inline handlers, inline executable scripts,
eval, or new CDN dependencies. Ryan's game imports./vendor/three/three.module.jslocally (Three.js is pinned to0.186.1). The portrait is loaded byTextureLoader.loadAsync("/images/ryan-mcpherson.jpg"), not embedded or fetched from a portrait CDN; the applicable Hosting directive includesimg-src 'self' data: blob: ..., so the same-origin file is allowed. The service worker precaches this public image. A sign-in gate does not make a public image private: get consent, disclose the image's public/offline availability, and do not cache a portrait unless that is intended. If the portrait fails to load, Ryan's async view initialization fails and leaves the start button disabled; handle optional portraits explicitly if your game should work without one. - Add sound with the shared procedural module
public/arcade-audio.js: Web Audio synthesis, so there are no audio files to license, precache, or allow through the CSP. The audio context is created and resumed on the first pointer/key gesture, suspended while the page is hidden, and routed through SFX/loop buses set in decibels with headroom. CallplaySound(name)from controller events only:starton a new round,catswhen the head start ends,stepevery0.55world units moved,warningonce when a cat comes within2.2(re-armed only after every cat is beyond3.0),tickfor each of the last five seconds, andwinorloseat the result. Each sound gets slight random pitch variation and a minimum repeat gap. Bind an accessible mute button (bindMuteButton, at least44 x 44, placed just before the Full screen/Exit control) that persists tolocalStoragekeyarcade-sound-muted. - Add an original looping theme. Generate with a local text-to-music model (we used ACE-Step 1.5 XL-SFT, MIT, via
Generate-Audio.cmd -Style <preset> -Prompt "..." -Duration 48 -Seed 42) using an original instrumental prompt that names no artists or songs and asks for steady energy with no intro, breakdown, or ending. Do not trust the preset BPM: measure the real repeat period withtools/make-music-loop.py(onset autocorrelation plus a spectral self-similarity lag curve; the maze take came out at 110 BPM, not the preset 112), cut a bar-aligned 16-bar loop at the most self-similar start, crossfade the seam for 120 ms (equal power), apply linear gain to about -16 LUFS with at least 1 dB true-peak headroom, and encode AAC 112 kbps M4A (roughly 0.4 to 0.55 MB). Record model, preset, seed, steps, prompt, loop start, and loop length inpublic/audio/provenance.json. Play themes withplayMusic(THEMES.<game>.url)fromarcade-audio.js: fetch plusdecodeAudioData(covered byconnect-src 'self'), a loopingAudioBufferSourceNodeon a music bus at -10 dB under the effects, a 1.5 s fade-in, crossfade between themes, andstopMusic({ fade }). EachTHEMESentry stores its exact loop length, and playback setsloopStart/loopEndto that window (skipping any AAC priming silence the browser keeps, with a 2 ms tolerance), so the loop never clicks or gaps. Music waits for the first gesture, shares the mute button, pauses with the hidden page, and fails silently if a file is missing. The service worker caches theme files at runtime (cache-first) rather than precaching about 1.5 MB, so filenames are immutable: ship a new take under a new name. The maze callsplayMusicinstartGameandstopMusic({ fade: .4 })just before the win/lose sting.
Maintaining Ryan's implementation
- Before editing, read
public/arcade.html,public/arcade.css,public/arcade.js,public/arcade-motion.js,public/arcade-3d.js,public/arcade-audio.js,public/arcade-screen.js, andpublic/arcade-screen.css, plustests/arcade.test.js,tests/arcade-audio.test.js,tests/arcade-screen.test.js,tests/arcade-mobile-browser.py, andREADME.mdarcade notes. The controller builds the wall set and owns keyboard/touch input, timer, round state, HUD, and radar. The motion module owns geometric movement and pursuit. The renderer owns Three.js assets, camera, and canvas. Keep these contracts separate. - Preserve exported motion names and values:
PLAYER_SPEED = 3.2,CAT_SPEED = 1.65,BODY_RADIUS = 0.22, andCATCH_DISTANCE = 0.42; preservecameraRelativeMovement,orbitCamera, andcreateMovement.arcade.jsownsCOLUMNS = 15,ROWS = 11,GAME_LENGTH_MS = 45000,CAT_HEAD_START_MS = 1500, the four spawn positions, and cheese coordinate. It callscreateMazeView(canvas,{walls,columns,rows,cheese}); the view providesstartLoop(frame),draw({mouse,cats,cameraOrbit,gameState},time), andreset(). - Do not merge renderer, DOM, or Firebase into
arcade-motion.js; its world functions are tested under Node. Preserve collision clearance, direct pursuit plus wall-aware waypoint chasing, independent camera orbit, outcome checks, speed consistency, restart semantics, and the 1.5-second cat delay. If tuning changes the maze or a constant, update focused tests and gameplay copy together. - Keep public gameplay independent of the shared account control. The game page uses
arcade.jsandarcade-screen.jsas external modules/stylesheets; account-backed ride sharing remains optional and separately verified. Input is cleared by blur, visibility, pointer release/cancel,game-screen-change, and hidden game content. - The face is a self-hosted photo at
/images/ryan-mcpherson.jpg, used as one texture on four sides of the goal cheese. Hosting CSPimg-srcpermits'self'; service worker caches the portrait. Respect the subject's likeness and consent when changing the photo, cache behavior, or game visibility. No extra account/ID data is needed in this skill. The published skill must not include user-specific absolute paths or secrets. - When adding a game file or asset to the installed site, update the explicit skill publication allowlist and generated pages if the published skills change, and add new files to the service-worker precache with a cache-version bump. Do not claim a release, deployment, or offline support until the corresponding publication and cache changes are tested.
Testing and verification
- From the
webproject root, runnode --test tests/arcade.test.js tests/arcade-audio.test.js tests/arcade-screen.test.js. The simulation-only regression command isnode --test tests/arcade.test.js; fullscreen/accessibility state tests run withnode --test tests/arcade-screen.test.js. Add or retain deterministic assertions for cats capturing at the radius, eating the cheese to win, movement stopping at walls without tunneling, cats remaining inactive through1,499 msand activating at1,500 ms, and equal movement distance at 30/60/144 FPS. Include blocked-corner and cat-path tests when modifying chase logic, plus the regression that pursuing cats never reverse direction at range. The audio test mocksAudioContext: sounds stay silent before a gesture, every recipe synthesizes with positive exponential-ramp targets, repeats are rate limited, and mute persists. In the browser, countOscillatorNode/AudioBufferSourceNodestarts to prove sounds play and that muting silences them; headless audio output itself is not audible proof. - Serve the site locally at the browser test's default
http://127.0.0.1:4175, or setARCADE_TEST_BASEto the preview URL. Runpython tests/arcade-mobile-browser.py; the script tests all three arcade pages with a stub signed-out session and records browser errors. It covers portrait320x568, portrait390x844, and landscape844x390, checks top-right Exit dimensions/placement and overflow, verifies explicit exit survives resize, and confirms sign-out does not lock gameplay. The separate native-fullscreen pass runs only when browser support permits. - Set
ARCADE_TEST_CHROMEto the installed Chromium headless-shell executable when it is not on the script's default path. For example, in PowerShell set$env:ARCADE_TEST_BASE='http://127.0.0.1:4175'; $env:ARCADE_TEST_CHROME='<path-to-headless-chromium>'and runpython tests/arcade-mobile-browser.py. Keep environment values local; never put credentials in the test. The browser test stubs auth and uses SwiftShader, so it is not proof of live Google/GitHub authorization or real-device GPU performance. - Also verify the actual game at desktop size such as
1280x800: start, move diagonally, orbit/tilt, collide with a wall, watch the timer and radar, win, lose, replay, and test keyboard/touch/pointer-release behavior as applicable. Check keyboard focus, screen-reader labels, resize, console errors, and WebGL initialization/context-loss messaging. When changing Hosting headers, confirm portrait requests work under the deployed CSP; a header-free preview alone is insufficient.
Acceptance
- Report the exact changed files and behavior, exact commands and pass/fail results, tested viewports, and any unverified manual, account, CSP, or deployment cases. Separate deterministic Node evidence from browser evidence. Never claim real-device performance based on SwiftShader.
- Be honest about limitations: the browser test's auth is a stub, client-side gating is not a server authorization boundary, fullscreen APIs can be denied, and a public self-hosted portrait remains public even when the game UI is gated. Don't claim a portrait consent flow or a deployment unless one was actually implemented and verified.
Install this custom skill
Review and read the raw SKILL.md or download SKILL.md. Create a folder named eat-my-face in your agent's supported skills directory and save the file there as SKILL.md.
Project-local Copilot example: .github\skills\eat-my-face\SKILL.md. Personal installation, only if you choose it: .agents\skills\eat-my-face\SKILL.md under your profile. Copy only this authored file, not any downloaded skill collection. Inspect another project's code before adapting these Ryan-specific workflows.
The instruction file is usable independently of MCP. Local MCP setup is a separate option; no public MCP protocol endpoint is deployed.
Catalog name: eat-my-face. Source: original-custom. Size: 17263 UTF-8 bytes. JSON catalog and provenance.