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 |
- Lobby. One phone creates the room and gets a 4-letter code. Everyone else joins by code
or by a
?room=CODElink, 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. - The Decree. Everyone, the Marky included, gets a fill-in-the-blank prompt and writes an answer. 60 seconds.
- 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.
- The Reveal. One answer at a time, the guesses land, then the Marky's real mark lands on top of them.
- The Court Standings. Round scores, the race chart, and a growing dossier on the Marky.
- 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:
| Distance | Points |
|---|---|
| within 2 | 5 (BANG ON) |
| within 5 | 4 |
| within 10 | 3 |
| within 20 | 2 |
| within 35 | 1 |
| beyond 35 | 0 |
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 writes | The 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 Coronation | The 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:
npm run harvest-promptspulls the prompt cards from json-against-humanity, keeps only the single-answer ones, cleans markup, dedupes, and sorts deterministically.npm run cull-prompts:prefilterand:finalizecut 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.npm run theme-prompts:prepareand:applyreskin 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.jsonand fired by id, instead of being scattered across scene code asplayplusaddEffectplussetTimeout. - 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
- Game: http://localhost:6970/
- Big screen: http://localhost:6970/present
- Editor suite: http://localhost:6970/editor
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
PORTsets the HTTP port (default6970). The WebSocket server shares it.HOSTsets the bind address (default127.0.0.1, loopback only).EDITOR_PASSWORDgates 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.