Most card games are a sequence of decisions. 13张 is one decision, made once, and then you have to sit there while it gets taken apart in front of you.
What It Is
Single-player 13张, also called Chinese Poker or Pusoy, against three AI opponents. A React module inside the Wanessa Labs Games Hub. Standard 52 cards, four players, thirteen each, the whole deck dealt.
You split your thirteen into three rows: three in front, five in the middle, five at the back. The back has to beat the middle, and the middle has to beat the front. Then everyone reveals, and every player is compared against every other player, row by row. Six comparisons, three rows each. One point per row you win.
No accounts, no backend, no network calls, no images. The cards are text and CSS, so the game works with the network switched off.
The Constraint Is the Whole Game
If you hold a flush and a straight, the straight goes in the middle and the flush goes at the back. You cannot put your stronger hand in the weaker row. Getting that wrong is called 相公, a mis-set, and in a real game it costs you every row.
That single rule is what makes the game interesting, because your best five cards are usually not available to you. The back row wants them, the middle row needs to stay under the back row, and the front row is only three cards, so it can never be a straight or a flush no matter what you are holding.
The interface prevents mis-sets rather than punishing them. The compare button stays off until your rows are legal, and the indicator tells you which constraint is failing instead of just refusing.
It Shares a Deck With Big Two, and Disagrees With It About Everything
The hub already had Big Two. Same 52 cards, same pairs and straights and flushes, so the sensible thing was to share the engine rather than write a third one.
Then the two games turn out to disagree about nearly every ordering:
| Big Two | 13张 | |
|---|---|---|
| Rank order | 3 low, 2 high | 2 low, ace high |
A-2-3-4-5 | ranks highest | ranks lowest |
2-3-4-5-6 | not a straight | a legal 6-high straight |
| Suit tiebreak | every comparison | ties only |
So the shared core is parameterised: rank order, straight rules and tiebreaks are data, not code. Two configs, one implementation.
One disagreement turned out not to need a switch at all. The wheel, A-2-3-4-5, ranks highest in Big Two and lowest here. But under Big Two the 2 is the strongest card in the deck, so the wheel wins on its own; under standard poker order the 2 is the weakest, so the same code puts it last. Both behaviours fall out of the primitives. Worth checking for that before reaching for a flag.
The Refactor Nearly Broke the Game Next Door
Big Two was already live and working. The rule was that its behaviour could not change, and its tests had to keep passing untouched at every step.
Sharing a deck almost broke it in a way that no amount of reading would have caught. The order the deck is built in feeds the shuffle, and the two games happened to build theirs in different suit orders. Big Two has tests that pin specific random seeds. Under one of them, changing the build order moved the three of diamonds from one player to another, and since whoever holds the three of diamonds leads the first trick, the game would have started differently.
That was found by testing the hypothesis before writing the code, not by watching a test go red afterwards. The fix is not a comment warning the next person. The shared function takes the suit order as a required argument with no default, so the mistake cannot be made.
Big Two ended up with two changed files, both smaller than before, and all fifty-nine of its tests passing with none of them edited.
Solving Every Arrangement
There are 72,072 ways to split thirteen cards into rows of three, five and five. That is small enough to check all of them, so the game does, with no pruning and no heuristic shortcuts. The best arrangement really is the best one.
The first version took 11.6 milliseconds on a desktop, worst case 14.9. A frame is 16 milliseconds, and dealing a round needs four of these. That is a dropped frame before a phone is anywhere near it.
The fix was not to search less. Pruning would have meant “best” quietly becoming “pretty good”. Instead:
Evaluating a five-card row 144,430 times is wasteful when a hand only contains 1,287 different five-card rows. Work them out once and look them up. And a row’s entire ranking, its category, its five cards and its tiebreak, fits into a 30-bit number, so comparing two rows is one integer comparison instead of walking two lists.
That took it to 4.2 milliseconds, with the inner loop allocating no memory at all. Same exhaustive search, same answer.
Packing a comparison into bits is the sort of clever that breaks silently later, so it is checked against the real comparison function across more than ten thousand pairs. If the bit layout ever drifts, a test fails rather than the game.
Opponents That Differ by Choice, Not by Accident
Both difficulty tiers run the same exhaustive solver. The easy bot looks at eight arrangements picked at random and takes the best of those; the hard bot takes the best of all 72,072.
Doing it that way means a difficulty tier cannot accidentally be strong because of a bug in its search, since there is only one search. Across thirty seeded games, the hard bot beats three easy bots by 376 points, and that number is pinned by a test, so a change that quietly flattens the difficulty curve fails the build.
The bots think at the moment you commit rather than when the cards are dealt, so the work happens behind the reveal instead of in front of you.
The Numbers That Are Not Mine to Choose
Chinese Poker has bonus scoring. Extra points for three of a kind in the front row, for a full house in the middle, for four of a kind at the back. There are also natural hands that win outright, like thirteen unique ranks, or six pairs and a spare.
Every one of those is implemented and tested. Detecting six pairs is a fact about a hand. But what six pairs is worth is not a fact, it is a house rule, and it varies by table.
So the detection ships complete and the point values ship empty, sitting in a config object waiting for a number. The game is entirely playable without them, and filling them in later needs no rewrite. Something the tests turned up along the way: the example hand in the game’s own documentation is secretly a six-pairs hand. Harmless today, since that scoring is off, so it is pinned with a test rather than left as a surprise.
Playing It on a Phone
Thirteen cards and three rows is a lot of dragging, so there are three ways to move a card and none of them are required. Tap a card then tap a row. Drag it, and the row you are over lights up. Or focus a card and press 1, 2 or 3, with 0 to send it back.
The rows sit above the tray so the tray falls under your thumb. Sort by rank to line up pairs and runs, sort by suit to see whether you actually have a flush. Auto-arrange runs the same solver the hard bot uses, so the hint and the opponent can never disagree.
Where It Landed
118 tests. Big Two untouched and still passing all fifty-nine of its own. The whole hub at 663 tests, green.
Live at games.wanessalabs.com/games/thirteen.

