I built a near-perfect engine for Flip Jungle, a 4x4 animal-chess game where every piece starts face-down. The point was not just to make a strong player. The engine is the measuring instrument: a copy with 512 times more search than another still loses 23% of their games.

That makes this a different kind of engine post. Banqi asks what to measure when the game has chance nodes. Dou Shou Qi asks how far classical search goes in a perfect-information animal game. Flip Jungle asks how much skill survives when the pieces start hidden. Less than it feels like.

Measure Result
search ladder, 1k to 512k nodes 242 Elo across a 512x compute range
512x more search loses 23% of games
8x more search loses 37 to 40%
any search vs random ~99% (random won 0 of 800 games)
endgame (≤5 pieces) vs exact tablebase 99 to 100% optimal
midgame (5 to 6 pieces) vs exact solver 200 of 200 moves optimal

The game

Flip Jungle is played on a 4x4 board with sixteen animals, eight a side. Every animal starts face-down. On your turn you flip a tile, which reveals a random animal, or move a face-up animal one square. You win by capturing all of the opponent’s pieces.

Captures go by rank: the stronger animal takes the weaker. Two exceptions matter. The rat takes the elephant, the weakest beating the strongest. And two animals of the same rank trade, both coming off the board.

Capture order, strongest to weakest

Elephant
Lion
Tiger
Leopard
Wolf
Dog
Cat
Rat

Each animal captures to the right. The rat also captures the elephant, and two animals of the same rank trade, both leaving the board.

Full rules with a playable board: mistboard.com/rules/jungle-flip.

Standard Dou Shou Qi, the perfect-information version from the last post, is pure calculation: you see the whole board. Flip Jungle hides the pieces, and a flipped tile’s identity is random. The flips are where the variance comes from.

The engine

Rust, alpha-beta search, a handcrafted evaluation, no neural network. Negamax with a transposition table, iterative deepening, quiescence on captures, and killer and history move ordering. The evaluation is material plus mobility, with a draw-contempt term. Strength is a node budget, not a clock, so the same position returns the same move on any machine and a measured gap belongs to the engine rather than the hardware.

The flips are chance nodes. At a flip, the value is the average over the pieces the bag could reveal, pruned with star-minimax, the chance-node form of alpha-beta. The move ordering earns its place: killer and history ordering halved the nodes needed to reach a given depth, checked against an unpruned search to confirm it returns identical values, so it buys about a ply of depth at the same budget without changing what the search computes.

Endgames are handled separately, by exact tablebases. Flip Jungle endgames are solvable by retrograde analysis: work backward from every finished position and record the result under best play. I stored the tables as a flat array with a perfect index (no keys), two bits for the result plus a byte for the distance to it, and built them in parallel across cores. The trap in parallelizing it: the table is halved by mapping each position to a color-canonical form, and a quiet move flips whose turn it is, so a position’s children can land in a different bucket than a naive per-bucket split assumes. The solve is monotone, a position only ever resolves from unknown to a fixed result, which is what makes the parallel build safe.

Measuring it

Self-play measures which of two versions is stronger, not whether either plays well: two copies of one engine share the same blind spots, so if both misjudge a position the score between them says nothing about it. Move quality gets graded against the tablebases instead.

On solvable endgames (up to five pieces) the engine played the optimal move 99 to 100% of the time, the misses being search depth that a deeper search cleared. A forward solver built on the tablebases reaches a little further, into five- and six-piece midgames with tiles still face-down; on 200 of those it matched the optimal move every time.

That coverage stops where exact computation stops, near the end of the game. The opening, where the flips happen, is too large to solve, so nothing here verifies the engine’s opening play. “Near-perfect” means near-perfect in the positions I can check.

The skill ceiling

To measure the opening without an exact answer, I compared engines of different strength. Four copies searching 1k, 8k, 64k, and 512k nodes played a round-robin of paired, color-swapped games, 120 per pairing, with ratings fit from the results:

1k nodes     +0
8k nodes    +89
64k nodes  +153
512k nodes +242

A 512-fold search increase buys about 242 Elo. In chess that much compute is worth several hundred. The steps stay near 64 to 89 Elo per 8x and flatten at the top: 320k to 640k nodes was worth 6 Elo.

The upset rates are larger than the ratings suggest. An 8x-stronger engine loses 37 to 40% of its games; the 512x-stronger one loses 23%. The flips decide enough games on their own that more search can’t close the gap.

One jump is large, at the bottom. A 1k-node search beats a random player about 99% of the time (random won 0 of 800 games across the tiers), a gap near 850 Elo. About 80% of the distance from random to perfect is that single step, from moving at random to searching at all. The ladder above it is shallow.

Tuning the evaluation did nothing. Better piece values, a rat-and-elephant term, and draw aversion all came back flat in paired self-play. In a game this chance-heavy, evaluation differences wash out.

Backgammon is the usual luck-compressed game; Flip Jungle is more compressed, with a smaller board, solved endgames, and a fully random opening.

Perfect against perfect

The ladder estimates the opening; it doesn’t solve it. Small enough games can be solved outright. I solved complete miniature versions end to end, opening flips included: two and four pieces, every tile face-down at the start, played to a terminal under the full rules.

Under perfect play by both sides they are drawn. The exact value is zero and the optimal line draws every time. Two perfect players can’t beat each other.

Solving a six-piece game from the opening, rather than a single endgame position, already crosses 40 million distinct positions: the same wall that puts the full game out of reach. So this is exact for the small versions and unproven for the whole sixteen-piece game.

It still fixes what perfect play is worth. Perfect against perfect is a draw. Perfect against a much weaker player is a near-certain win: the random mover lost all 800 of its games. The 23% a 512x-stronger engine drops sit between: two close players, the gap inside the variance, the flips deciding. The losses come from the gap between players, not from perfect play itself.

The state of the solve

Where the game stands against exact computation, layer by layer. The tables are clockless (they ignore the 40-ply no-progress rule) and cover every fully revealed position at each piece count; each entry stores the result under best play and the distance to it.

pieces positions longest forced win build
2 28,800 9 plies instant
3 1.9M 21 plies 2 s
4 79M 41 plies 200 s
5 2.3B 63 plies 4 hr on a laptop, 2.8 GB
6 46B unbuilt days, ~140 GB of RAM

Six pieces is buildable on a rented server and worth nothing to the engine: five-piece coverage already grades every endgame that reaches the tables, and search plays the rest near-optimally. Past that, solving the full game means the opening, and the opening is the flip tree: sixteen distinct pieces revealed in random order, on the order of 10^11 or more reachable states. The miniature versions from the previous section are solved through that tree end to end; the sixteen-piece game is not, and I see no path to it.

The distance column is the part I got wrong the first time. The original tables stored win, draw, or loss and nothing else, and that is enough to grade moves but not to finish games. Game 5 in the viewer below is what that looks like: from move 24 black has a tiger and a rat against a lone red elephant, a forced win in the tables (the rat traps the elephant in three plies, since an elephant can’t capture a rat). Black never gives the win away. It also never converts it. Every winning move scored the same, a mobility tiebreak picked among them, the losing side was happy to shuffle, and the repetition rule called it a draw. Eight turns in a row the engine held a forced win, played a move the tables grade as optimal, and made no progress at all.

Storing the distance fixes it: a win in 3 now outranks a win in 9, so the engine takes the shortest forced win and drags out forced losses. From won tablebase positions it now finishes in exactly the tabled distance, and the game-5 endgame ends rat takes elephant in three. Chess ran into the same thing decades ago, which is why Syzygy tables ship a distance metric next to win/draw/loss. Some lessons you get to rediscover on a 4x4 board.

Watch it play

The engine against itself at full strength. Five games, each on its own board. Step through any of them with the arrows, from all-face-down to a finished position.

Where it stands

Skill is real here. The 242-Elo ladder is a real gap, and over a long match the stronger engine wins. The ceiling is on single games: the deal and the flips decide a large share of any one game, so even a far stronger player loses often, and one result says little about who is better. Skill shows up in the average, not the game.

The engine is near-perfect where I can check it and unverified where I can’t, which is the opening. The one measurement I don’t have is a rating against a strong human, the only test left that doesn’t lean on the engine or its solver and inherit whatever they miss.

Resources: