Play · the engine, not a simulator of it

The engine plays real games — and checks itself while it does

This project has a complete rules engine, and a board that is a client for it. Every control the board offers comes from legalActions(); the page never decides for itself whether something is playable. And with --check strict, an independent checker audits every action against the Comprehensive Rules — including the CPU's — and names the clause that broke, on the card, in the header, in the log.

It plays out of the same SQLite database the rest of this site is built from, so the cards it deals are the cards on the Cards page, and the rules it enforces are the ones on the Rules page, cited by the same clause numbers.

There is no game to start on this website. The engine runs on your own machine.

The board is a program, not a page: it asks a local process for the deck catalogue, the game state and the result of every action, and that process reads a 450 MB card database and a native SQLite addon that a static site cannot host. So this site does not serve a board, and it will not pretend to — a button here that opened a dead table would be worse than this paragraph.

Public play is a goal, not a fact. The project's platform notes settle it as a real objective, behind real accounts rather than the loopback binding it has today. Until that exists, everything below is how to run it yourself, and this page is the honest version of the Play door rather than a link into nothing.

Running it yourself

Four steps, from a checkout of this repository. Node 20 or newer; no build step. The repository is not public today, so there is deliberately no clone link here — a link that 404s is the exact thing this page was written to stop doing.

  1. Install

    npm install
  2. Build the datastore

    npm run setup

    Creates data/optcg.db and pulls the card spine and the banlist. It is a real download and it is not committed to the repository, because a datastore that can be rebuilt from its sources is not source. Nothing that touches cards works until this has run — including the board.

  3. Start the play server

    npm run cli -- play serve --port 8787 --check strict

    It prints the address it bound to, the rules profile it is checking at and how many invariants that is, and every deck folder it scanned with the decks it found — including the ones it refuses to play, and why. There is no bundled default deck: a game played on invented cards would tell you nothing, so if no .deck file is found it says so instead of making one up.

  4. Open the address it printed

    http://127.0.0.1:8787

    That address is your own computer, and this website has nothing to do with it. It is written here as text rather than as a link on purpose: until you have run the command above on the machine you are reading this on, there is nothing listening there, and a browser that says it cannot connect is telling you the truth. If you chose a different port, change the number.

    It lands on the launcher, not the board itself — a hand of four cards: play a game against a friend or the CPU, build a deck, see what is winning, or ask the machine to run a benchmark. Play a game is the one that opens the board this page describes.

What --check strict buys

A rules checker that lives inside the engine it is checking is not a checker. This one lives in src/rules, holds its own registry of invariants, and asks the engine nothing — it reads the resulting position and decides for itself whether that position is legal.

Three settings, and they are not opinions

off — nothing runs. No cost, and no evidence that anything followed the rules.

fast — the default: 29 of the 40 invariants, the ones that cost O(1) or a pass over the field, plus both conservation laws.

strict — all 40. The extra eleven are full sweeps over every card instance in the game: that areas hold only their own card type, that a refresh really did set every card active and return every given DON!!, that turn- and battle-duration effects were cleared, that once-per-turn marks persisted, that a card's identity never changed under it, and that the deck each player registered is still a legal deck.

Counts from the registry in src/rules, via registrySize() in src/engine/audit.ts. The server prints the live number on startup — trust that over this page.

Every invariant cites a clause

All 40 name a distinct clause of the Comprehensive Rules v1.2.0 — CR 3-7-6 for "you cannot have a sixth Character", and so on. A finding is therefore never "the engine did something odd"; it is a rule, by number, that you can go and read.

Read the Comprehensive Rules and the official Q&A →

Strict costs roughly five times what fast costs per action (single-digit microseconds either way, measured in src/rules/bench.test.ts). For one human game that is free; it is bulk simulation, not you, that has to care.

Where a finding shows up

Four places at once, and none of them need opening. A broken rule is not a notification that fades — it stays until the game is restarted or the action is undone, because a decision made on an illegal board is worth nothing.

  • headerA red pill, next to the turn counter, that is impossible to miss.
  • rules barA bar pinned under the header naming the clause and what it found.
  • on the cardA marker on the specific card the clause is about.
  • logA red line in the game log, in sequence with the action that caused it.

What the board actually is

A mat you play on, not a form you fill in

Cards are dragged with pointer events onto a playmat pinned to the real 24×14-inch frame, with the zones positioned at fractions measured off the official playsheet rather than guessed. Drop a card on nothing and nothing happens — no request is sent. Clicking works too, and so does Tab then Enter: the drag is a convenience over the same action list, never a second way of deciding what is legal.

It will teach you the game while you play

A coach panel sits in the rail and reads the same view the board is drawn from: it names the phase you are in, explains what every option on offer would actually do, and cites the Comprehensive Rules clause it comes from. It never presses anything — it has no route to the action endpoint. The setup screen also offers a guided game: the same engine and the same decks, with a track of eight goals that tick off when the game can be seen to have reached them. No step ever blocks, hides or reorders a legal action.

Never played before? How the game is played is the written version — the two ways to lose, the five phases, and how a battle resolves, each citing its clause. That one is a page on this site, so you can read it right now.

The opponent is a policy you choose

heuristic is a scripted decent player and the sensible default; ismcts searches and is slow; random is a baseline rather than an opponent. Benchmark mode plays a fixed, seeded sequence of mirror games with the seat alternating, and writes each result to the database as it ends — so the record is the server's, not the browser's, and closing the tab cannot lose it.

Gaps are loud, on purpose

A card whose ability the engine cannot execute wears a badge before it is played. When one is skipped, the log says so and the counter in the header goes up. That is the whole reason to play this by hand: a person notices "my [On Play] did nothing" in one second, where a 400-game batch reports it as a number nobody reads.

Finish a game and it goes on the record, and the launcher turns that record into achievements on its own — nothing is claimed and nothing is submitted. Eleven exist, on shelves that get harder to fake: some just want you to sit down and finish a game, others want breadth across many Leaders, many days or many opponents, and one only unlocks once a benchmark record is wide enough that a confidence interval says you are actually beating the policy rather than having had a good run. An earned achievement can release a cosmetic — a playmat, a sleeve, a DON!! back, a title. There is no checkout anywhere in it. Nothing costs money, quotes a price, or links out.

Two people, one server

The same board takes two players. One of you hosts a table, the other joins the second seat, and no CPU policy is involved at all. The server holds the game and hands each browser only that seat's view, so neither of you can see the other's hand however hard you look at the page. Both players pick from the host's deck folders, and a request names a deck by name, never by path — there is no upload.

Which is also why it is on loopback, and why this site has no table on it.

The listener is bound to 127.0.0.1 in the source, so the second player has to be someone who can already reach that machine. Sign in with Google exists in src/play/auth.ts — server-side code exchange, a required state parameter, an HttpOnly session cookie — but today it is optional: with no client id configured every auth route answers 503, no route refuses an unsigned caller, and sessions live in memory and are lost on restart.

The per-seat tokens are not a login either. They are a boundary between the two players — they decide which hand you are shown. Making sign-in required, and sessions durable, is the work between here and a public table; it is written down as such rather than glossed over.

Taking a move back is your opponent's call. If you back out of something that never resolved, or the board did not change in any way either of you can see, it just happens. Anything else is a request they can refuse, play stops for both of you until they answer, and the ask and the answer both go in the log — an undo cannot quietly launder itself out of the record.

Before you sit down: what it cannot play

Card abilities are compiled from the database into something the engine can execute. Not all of them compile. An ability flagged unparsed is skipped, not approximated — the engine does not guess at what the card meant, and the board tells you every time it happens.

abilities
flagged unparsed
executable
cards in the pool

Reading ../data/meta.json…

Each game's pool is smaller than the whole database — it is built from the two decks in play — so the board reports the coverage of that game on its own setup screen. The figures here are the corpus-wide ones, which is the worst case, not the case you will meet.

Things this will not do

  • It will not deal you a hand from this website. There is no hosted board and no offline one.
  • It will not play a deck it cannot resolve, and it will not quietly substitute a card it does not have.
  • It will not play an illegal decklist unless you deliberately tell it to, and it names the violated rule when it refuses.
  • It will not report a winrate from a game that broke a rule, or one the engine abandoned. Those are recorded and excluded, with the count of what was excluded shown beside the record.
  • It will not report a winrate at all from a sample too small to carry one — an unknown number is printed as unknown, never as 50%.
  • It is bound to 127.0.0.1 and is a developer tool today, not a service. Do not put it on the open internet in that state.