← Original custom skills library Ryan's portfolio

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

  1. 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.js for markup/styles/controller, public/eat-my-face-motion.js for simulation, and public/eat-my-face-3d.js for 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 uses public/arcade.html, public/arcade.css, public/arcade.js, public/arcade-motion.js, and public/arcade-3d.js instead.
  2. 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 at x=4 for y=1..7 except 3, at x=8 for y=3..9 except 7, and at x=11 for y=1..6 except 4. 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.
  3. Provide a pure movement API such as cameraRelativeMovement(input,yaw), orbitCamera(camera,dx,dy), and createMovement(walls,columns,rows). The movement object exposes isWall, canOccupy, move, clearPath, and chase; the controller owns the game state, clock, and outcomes. Use circle-vs-wall checks with BODY_RADIUS = 0.22, not point checks. Sweep movement in increments no larger than 0.06 world 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.
  4. Keep movement continuous and camera-relative, not tile-step. The shipped PLAYER_SPEED is 3.2 world units/second; map forward/back and left/right through the current camera yaw. Cats use CAT_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 about 0.2 seconds, 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 update facing for moves longer than 0.001 so 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.
  5. Keep four distinct cats and preserve the 1,500 ms grace period before they become active. A cat captures at distance <= 0.42; the mouse wins when within < 0.6 of 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 in ready, enters running, and finishes once as won or lost; it also loses when its 45,000 ms timer expires. Restart from the original positions and reset camera, clock, cats, and held input. The cats should not move during the head start.
  6. 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/120 second, caps the elapsed simulation frame to 100 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.
  7. Build a local Three.js scene with a 15 x 11 floor and cheese-textured box walls. Ryan's renderer uses CELL = 3, walls 3 x 2.8 x 3, a 62 degree 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.
  8. Implement an orbiting third-person follow camera independent of movement. The shipped orbit distance is 5.2, yaw changes by 0.006 radians per horizontal drag pixel, pitch changes by 0.005 per vertical pixel and clamps to 0.18..1.05 radians. 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 about 5/second) so it never pops. Hide the player model below a 0.95 camera distance and show it again only above 1.3; that hysteresis stops the mouse flickering when the camera hovers near a wall. Render cats turning more slowly than the mouse (blend rate 11 vs 18) so 90-degree grid-corner turns read as turns, not snaps. Add a small negative shadow bias (-0.0005) with the 0.04 normal 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 most 1.5, and show a useful recovery message on WebGL context loss.
  9. 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.
  10. Reuse shared responsive fullscreen behavior when available: public/arcade-screen.js and public/arcade-screen.css operate on [data-game-screen], [data-game-screen-toggle], and [data-game-screen-status]. On a max-width 900px or 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, rightmost Exit at least 44 x 44 pixels. Honor Escape, safe areas, and an unavailable/denied Fullscreen API. Do not implement a competing fullscreen state machine.
  11. 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.
  12. 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.js locally (Three.js is pinned to 0.186.1). The portrait is loaded by TextureLoader.loadAsync("/images/ryan-mcpherson.jpg"), not embedded or fetched from a portrait CDN; the applicable Hosting directive includes img-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.
  13. 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. Call playSound(name) from controller events only: start on a new round, cats when the head start ends, step every 0.55 world units moved, warning once when a cat comes within 2.2 (re-armed only after every cat is beyond 3.0), tick for each of the last five seconds, and win or lose at the result. Each sound gets slight random pitch variation and a minimum repeat gap. Bind an accessible mute button (bindMuteButton, at least 44 x 44, placed just before the Full screen/Exit control) that persists to localStorage key arcade-sound-muted.
  14. 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 with tools/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 in public/audio/provenance.json. Play themes with playMusic(THEMES.<game>.url) from arcade-audio.js: fetch plus decodeAudioData (covered by connect-src 'self'), a looping AudioBufferSourceNode on a music bus at -10 dB under the effects, a 1.5 s fade-in, crossfade between themes, and stopMusic({ fade }). Each THEMES entry stores its exact loop length, and playback sets loopStart/loopEnd to 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 calls playMusic in startGame and stopMusic({ fade: .4 }) just before the win/lose sting.

Maintaining Ryan's implementation

  1. 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, and public/arcade-screen.css, plus tests/arcade.test.js, tests/arcade-audio.test.js, tests/arcade-screen.test.js, tests/arcade-mobile-browser.py, and README.md arcade 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.
  2. Preserve exported motion names and values: PLAYER_SPEED = 3.2, CAT_SPEED = 1.65, BODY_RADIUS = 0.22, and CATCH_DISTANCE = 0.42; preserve cameraRelativeMovement, orbitCamera, and createMovement. arcade.js owns COLUMNS = 15, ROWS = 11, GAME_LENGTH_MS = 45000, CAT_HEAD_START_MS = 1500, the four spawn positions, and cheese coordinate. It calls createMazeView(canvas,{walls,columns,rows,cheese}); the view provides startLoop(frame), draw({mouse,cats,cameraOrbit,gameState},time), and reset().
  3. 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.
  4. Keep public gameplay independent of the shared account control. The game page uses arcade.js and arcade-screen.js as 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.
  5. The face is a self-hosted photo at /images/ryan-mcpherson.jpg, used as one texture on four sides of the goal cheese. Hosting CSP img-src permits '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.
  6. 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

  1. From the web project root, run node --test tests/arcade.test.js tests/arcade-audio.test.js tests/arcade-screen.test.js. The simulation-only regression command is node --test tests/arcade.test.js; fullscreen/accessibility state tests run with node --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 through 1,499 ms and activating at 1,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 mocks AudioContext: sounds stay silent before a gesture, every recipe synthesizes with positive exponential-ramp targets, repeats are rate limited, and mute persists. In the browser, count OscillatorNode/AudioBufferSourceNode starts to prove sounds play and that muting silences them; headless audio output itself is not audible proof.
  2. Serve the site locally at the browser test's default http://127.0.0.1:4175, or set ARCADE_TEST_BASE to the preview URL. Run python tests/arcade-mobile-browser.py; the script tests all three arcade pages with a stub signed-out session and records browser errors. It covers portrait 320x568, portrait 390x844, and landscape 844x390, 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.
  3. Set ARCADE_TEST_CHROME to 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 run python 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.
  4. 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

  1. 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.
  2. 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.