I built a bluffing card game and immediately wanted the version you can teach someone in ninety seconds. This is that one. Same tension, one number and one face to reason about instead of a ladder of poker hands.
The Problem
Liar’s Dice is a good game with a bad computer opponent problem. The bot either cheats by looking under your cup, or it plays its own arithmetic so plainly that you learn to read it in a few rounds and never lose again. Neither is a game.
There is a second problem that only shows up when you try to build it: bluffing needs the possibility of being wrong, and a bot that only ever bids things it can prove is not bluffing, it is reporting.
What It Is
Everyone rolls five dice under a cup. A bid claims that at least so many dice showing a face exist across the whole table, including the dice you cannot see. Raise the number, or keep it and raise the face. Or call the last bid a lie.
Then every cup comes up and you count. If the bid held, the caller loses a die. If it fell short, the bidder does. Lose all five and you are out.
Ones Are Wild, Until They Are Not
Ones count as any face. That is the standard rule and it is also the whole design, because of what happens next: the moment anybody bids on ones, they stop being wild for the rest of the round.
Bidding aces is therefore not just a claim, it is a decision to make everyone’s counting harder, including your own. It costs you a higher number to get there and it takes the wild card off the table for the whole room. That is why most rounds open on “one two” rather than “one one”, and nobody had to be told: the bots work it out from the scoring.
I had proposed a different fix for the same problem, borrowed from tournament rules. The house rule is better and the game is built on it.
The Opponents
Three difficulty tiers that differ in numbers, not in code. Same decision procedure, different constants: how far below even money they will still put their name to a claim, how quickly they call, how much they discount somebody who has been caught short before.
The tuning is the part I got wrong first. I set the calling threshold from theory: challenge anything that is under even money to be true. Measured over eighty matches, the best-tuned bot lost to the worst.
The reason is that a bid is evidence. People bid faces they are actually holding, so the naive probability runs low, and calling at “under even money” on a number that is biased low means calling hands that are genuinely better than even. The real threshold is about half what the theory says. The hardest tier now wins 81% against the easiest, and there is a comment in the file explaining why the number looks wrong, because the next person to read it will want to fix it.
The Part I Care About
The engine hands each seat a projection of the game containing its own dice, everyone else’s dice count, and nothing else. The bots receive exactly the same projection the screen does. There is no privileged view.
The usual way to test that is to serialise it and search for values that should not be there. That is close to useless here, because a die face is a number from one to six and so is every dice count, every quantity, every seat number. Instead: run two identical matches that differ only in what the opponent is holding, and require that the viewer’s screen renders byte for byte the same. If any part of the other hand reached the display, the two differ.
What I Learned
Two hundred and twenty-one passing tests, a clean typecheck, and a boundary proof did not stop the game shipping “You was wrong” at the end of every round. Or “You wins”. Or four em dashes in copy, including one in the share text, which is the only line that leaves the device.
None of those are logic errors, which is exactly why nothing caught them. A test suite asserts structure. It does not read. I found all of them by building the site, opening it on a phone-sized window, and looking at a screenshot.
Two more things I “found” turned out to be wrong: the card art that was not loading was below the fold and lazy, and the handoff screen that was not appearing was in uppercase against a case-sensitive check. Checking a negative result before acting on it saved two pointless fixes.
Not Included
No accounts, no server, no network. The whole game is a function from state and an action to the next state, plus something that draws it. It works on a plane.
No online multiplayer, and no money. Pass-and-play covers two people on one sofa, which is the version of this game I actually wanted.