CODE & GAMES

Marky Likey

2026

Open-source party game

A Jackbox-style medieval party game on Pat Engine: everyone plays on their phone, a shared screen runs the show, and every round asks how much the Marky likes this.

Repository
marklikey
Default branch
main
README source
README.md
Portfolio snapshot

Portfolio notes

Why I made it

A party game I built for my friends on top of Pat Engine. Everyone plays on their own phone, a shared screen runs the show, and one player is crowned the Marky for the night.

The scoring is the whole design. Points come from reading one specific person correctly rather than from being funny in the abstract, which makes the reveal the moment everyone leans toward the screen for.

Synced source

Inside Marky Likey

The project README is presented here as part of the portfolio, with links and source images kept intact.

A Jackbox-style party game with a medieval court theme: everyone plays on their own phone while a shared screen runs the show. One player is crowned the Marky. Everyone else is a courtier, and the whole game is one question asked over and over: how much does the Marky like this?

Each round the court writes answers to a fill-in-the-blank decree, the Marky privately rates a handful of those answers from 1 to 100, and every courtier guesses the number the Marky gave. Points come from reading one specific person correctly, not from being funny in the abstract.

No app to install: everyone opens the site on their phone, the TV opens /present, and a single Node process owns the game. It runs on Pat_Engine, my own vanilla-JS 2D browser engine, so there is no bundler, no framework, no build step, and one runtime dependency (ws).

Every screenshot on this page is a real game off the real server.

Why it exists

I built it for my friends, on top of Pat_Engine, wrapped around an inside joke of ours. It is self-hosted so we can play it whenever we want. Nothing to buy, and the prompt deck's non-commercial licence means there never will be.

How a game plays

A game is 3 rounds. Here is what it looks like in your hand: build a hero, answer the decree, then guess what the Marky gave it.

Build a hero
103 vector parts across 8 slots, 12 colour schemes
Answer the decree
60 seconds
Guess the Marky
1 to 100, and the face reacts
  1. Lobby. One phone creates the room and gets a 4-letter code. Everyone else joins by code or by a ?room=CODE link, builds a character, and one of them is crowned the Marky for the whole game. It runs with 2 players and the lobby tells you it is best with 4 or more.
  2. The Decree. Everyone, the Marky included, gets a fill-in-the-blank prompt and writes an answer. 60 seconds.
  3. The Judgement. The Marky rates 5 to 8 anonymous answers on a 1 to 100 slider, scaled to table size. Every courtier rates the same answers at the same time, but they are not answering "do I like this", they are answering "what did the Marky put". Selection is fair by default: at most one answer per submitter until everyone is represented.
  4. The Reveal. One answer at a time, the guesses land, then the Marky's real mark lands on top of them.
  5. The Court Standings. Round scores, the race chart, and a growing dossier on the Marky.
  6. The Coronation. After round 3: a podium, confetti, and the Court Honours.

The Marky is fixed for the whole game and cannot score. That is deliberate. Three rounds of reading one person is a much better arc than three rounds of a rotating judge, and it turns the Marky into the show rather than a competitor.

The reveal is the moment the rest of the game is built around. Every guess lands on the number line under the courtier who made it, the crowd's spread fills in behind as a histogram, and then the Marky's real mark drops in gold on top of it.

Scoring

Points come from the distance d between your guess and the Marky's actual rating:

DistancePoints
within 25 (BANG ON)
within 54
within 103
within 202
within 351
beyond 350

On top of that: +2 to the single closest guesser (ties each get the full bonus, no splitting), +2 to the person who submitted an answer the Marky rated 85 or higher, and +3 for the Marky's favourite of the round, which is just whichever answer they rated highest. The final round is worth 1.5x so the table stays alive until the last reveal.

The big screen

/present is a separate client that joins the same room as a non-player spectator and never sends a game intent. It can be opened, closed, or refreshed at any point in a game without touching it. It carries the room code, the countdown, the reveals, the music, and the charts.

The Scriptorium · everyone writesThe Court Deliberates · everyone guesses

It is also the only client that accumulates history, and that is what makes the charts possible. The server snapshot is a single moment in time, but a long-lived spectator watches every snapshot go by and builds the whole-game arc out of them: per-player accuracy, signed bias (are you a sycophant or a cynic), boldness, how far you drift from the crowd, and a taste map of the Marky.

Then the Coronation, and a round of trophies handed out to whoever the game's own data can pin one on: The Diviner, The Sycophant, The Cynic, The Bold Fool, The Lone Wolf, The Marky's Muse, Bullseye.

The CoronationThe Court Honours · derived from how you played, not always flattering

The big screen also owns the music. Instead of a loop per scene, one adaptive layered score morphs its intensity with the phase and spikes on the big beats.

The phone gets its own wrap-up: the champion, the Marky's favourite answer of the night, how well you read them, and the final standings.

The prompt deck

The deck is 2,351 single-answer prompts, all rewritten into the fantasy-court voice. Getting there was a three-stage pipeline, each stage a committed script with a committed cache so the whole thing is reproducible:

  1. npm run harvest-prompts pulls the prompt cards from json-against-humanity, keeps only the single-answer ones, cleans markup, dedupes, and sorts deterministically.
  2. npm run cull-prompts:prefilter and :finalize cut the deck down to prompts that work for this game specifically. The rating screen shows only the answer, never the prompt, so a prompt earns its place by reliably producing a vivid, standalone, ratable thing. Dated references, name placeholders, action cards, and near-duplicates go.
  3. npm run theme-prompts:prepare and :apply reskin the survivors into the royal court voice, discarding any rewrite that would lose the joke.

The rating and rewrite verdicts are cached in tools/ keyed by a hash of the source text, so a re-run reuses them instead of redoing the work.

Content warning: the deck is adult. It is derived from Cards Against Humanity, and it is crude in exactly the way you would expect. There is no clean or family mode.

Under the hood

The server decides everything

One Node process owns the phase machine, validates every intent, and broadcasts a snapshot built per recipient, so a client is only ever told what its own player is allowed to see. Rating items carry the answer text alone until the reveal: no prompt, no attribution. Clients render state and never decide it, which means a tampered client cannot cheat, only lie to itself.

Game balance lives in one file, game/shared/rules.js, imported by both the Node server and the browser. Scoring bands, timers, the player floor, and the room lifecycle all have exactly one definition.

Holding up on real phones

  • A sleeping phone keeps its seat. An empty room lingers for 5 minutes before teardown, and every reconnect rebinds the same member by token instead of spawning a new one.
  • The keyboard never moves the game. Text-entry screens reserve a fixed band at the bottom and lay out above it, so the screen does not compress or jump when the keyboard opens.
  • Room codes are unambiguous. 4 characters, letters only, with I, O and L removed from the alphabet.
  • Drawn, not laid out. The client is a single Canvas2D surface sized to the safe area, so the notch and the home indicator never clip a button. The only DOM in the whole client is one text input bridged in for typing, because nothing beats the native keyboard.

Built on Pat_Engine

The engine underneath is Pat_Engine, a no-build, vanilla-JS 2D engine I extracted from an earlier project of mine. Marky Likey is scaffolded from it and keeps its layering rules intact, which is the interesting constraint: none of the game leaked upward.

The hard rule is that the engine must never know what game it runs. No game noun appears anywhere under engine/ or editors/. In practice that means:

  • Art is a declarative Canvas2D interpreter. Characters are JSON shape trees with states and animators, not images.
  • VFX and sequences are data. A reaction (a sound, an effect, and a state change over time) is authored once in data/fx-sequences.json and fired by id, instead of being scattered across scene code as play plus addEffect plus setTimeout.
  • Sound is a Web Audio synth and sampler, so the SFX set is authored as JSON layers (data/sfx.json) rather than as a folder of recordings. The one binary it leans on is a MIDI file that several cues and the music director read their melodies from.
  • Networking is a generic RoomServer: rooms, codes, identities, reconnect tokens, a host role, and lifecycle. Marky Likey plugs into it with a hooks object and keeps all its game state in an opaque per-member data bag.
  • Editors are a game-agnostic suite driven entirely by data/editor-manifest.json.

Extending the engine means adding a generic primitive any game could use, never a game-specific branch. Anything that would not make sense unchanged in a completely different game lives in game/ or data/. AGENTS.md is the full guide and ENGINE.md is the terse API cheat sheet.

The art is a file you can edit

/editor is a five-tab suite (art, VFX, sequences, soundboard, music) that writes straight back into data/ through the save API. A hat is a tree of paths and arcs in a JSON file, not an exported image, so there is nothing between drawing a part and seeing it on a phone.

Run it

npm install
npm start

To play with other people on your network, bind to the LAN and hand out your machine's address:

HOST=0.0.0.0 npm start

Configuration

  • PORT sets the HTTP port (default 6970). The WebSocket server shares it.
  • HOST sets the bind address (default 127.0.0.1, loopback only).
  • EDITOR_PASSWORD gates the editor suite and its save API behind a login. Unset by default. On a non-loopback bind with no password set, the editor and save routes return 404 instead of being exposed to the network, so a LAN game never ships an open write API.

The save API only accepts an allowlist of data files and writes timestamped backups under data/.backups/, which are never served over HTTP.

Tests

npm test

Node's built-in test runner with a loader remap for the browser-absolute import paths, and zero test dependencies. Coverage spans the pure layers that matter: scoring, the phase machine, snapshot shaping, room and reconnect behaviour, the prompt pipeline, the art interpreter, the audio synth, the charts, and mobile layout.

Deploy

deploy.ps1 builds the container and ships it to Google Cloud Run, then attaches it to an existing load balancer under a subdomain. Two constraints drive that config: rooms live in memory inside a single RoomServer, so the service is pinned to one instance (two instances would split a room across processes), and WebSocket connections need session affinity plus a long request timeout to survive a full game.

Layout

game/       the game. scenes, server room logic, shared rules, the phone and present clients.
game/shared/    imported by BOTH the Node server and the browser: rules, messages, pacing, copy.
engine/     game-agnostic runtime (render, audio, fx, net, core loop). Never mentions this game.
editors/    game-agnostic editor suite, driven by data/editor-manifest.json.
data/       content as JSON: character art, VFX, SFX, music, sequences, the prompt deck.
assets/     the few binaries: the MIDI the sound layers read their melodies from.
server/     static file serving plus the editor save API.
tools/      one-off dev scripts: the prompt pipeline and the MIDI theme builder.
tests/      Node test-runner suites.
docs/       the screenshots on this page.

Credits and licensing

Prompt content is derived from Cards Against Humanity (cardsagainsthumanity.com), sourced via crhallberg/json-against-humanity and released under CC BY-NC-SA 4.0. That carries three obligations, all of which apply here: attribution, non-commercial use, and share-alike on anything derived from it. This game is non-commercial and always will be. See data/NOTICE for the full attribution.

Everything else, the engine, the game code, the editors, the art, and the sound, is mine and is MIT licensed. See LICENSE, which spells out the split.