A repeated two-player battle-of-the-sexes game. Two players repeatedly choose moves and only score when their choices line up — but they each prefer a different match to win. The challenge is reading your rival and steering them toward your outcome over many rounds.
| X | Y | Z | |
|---|---|---|---|
| X | 150,50 | 0,0 | 0,0 |
| Y | 0,0 | 50,150 | 0,0 |
| Z | 0,0 | 0,0 | 100,100 |
Each cell is the points scored by Player A and Player B respectively for a combination of choices. Columns are Player A's move, rows are Player B's.
Example: if both players choose X, Player A gains 150 points and Player B gets 50.
Players only score when they match (the diagonal), but their interests conflict over which match. Crucially, every match splits the same 200-point pie: Z divides it fairly (100/100), while X and Y split it 150/50 in one player's favour. A miss destroys the pie entirely (0/0).
Because fighting can't grow the pie — only shift who gets it, while risking the whole thing — the fair Z is the smart default. This is the classic battle of the sexes: both want to coordinate, neither wants to give in. The edge comes from reading your opponent and pushing for the 150 only when they'll actually concede.
In the web version you face one of seven secret AI strategies, hidden until the end. A "suspects" panel narrows down who it could be as you watch them play, and naming your rival correctly at the end earns a points bonus — so reading the opponent literally pays off.
The web app has an optional ZK Mode toggle backed by a Noir
zero-knowledge circuit (circuits/standoff). It is a learning exercise, not a feature the
game needs. In a single-player browser game the bot runs on your own machine and you could
just read its code, so hiding it cryptographically solves no real problem here. ZK Mode
exists to explore how commit-and-prove works in a familiar setting; the transferable part is
the working Noir + Barretenberg proving pipeline, which would matter in a setting where the
bot is run by an untrusted third party (a server, an opponent, an on-chain contract) that you
can't inspect.
- Commit. At game start the bot picks a hidden strategy id and a random salt, and
publishes a Pedersen commitment
hash(strategy_id, salt). This locks in its identity without revealing it — it can't change its story later. - Prove each move. After every round the circuit takes the commitment plus the public
game state (the bot's role, the round number, the human's move history, and the move the
bot just played) as public inputs, and the
strategy_id+saltas private inputs. It proves two things: the private id/salt hash to the published commitment, and replaying that committed strategy over the public history produces exactly the move that was played. - Verify. The proof is checked in the browser; a per-round badge shows verified / failed. Crucially the strategy id stays secret throughout — you learn that the move was honest, never which strategy it was, so the end-game guessing challenge survives.
The circuit covers six of the seven bots. Random/Dice is excluded because its move is a coin flip, not a function of the public history — there is nothing deterministic to prove. To keep ZK Mode from leaking information, its opponent pool is restricted to those six, so a commitment always exists and its presence reveals nothing.
See circuits/standoff/README.md (the circuit) and web/src/zk/README.md (the web wiring)
for specifics, encodings, and how to run the tests.