Bloodlust & Harmony

Why my auto-battler has a poker mode

Updated Jul 30, 2026

Poker mode betting: three hidden unit cards over a translucent ghost of the player's own army standing on the arena pit, with a raise-to slider and pot odds
Your turn: two cards revealed, a third to come, and a ghost of the army you would spawn standing on the pit. Only you can see it.

Bloodlust & Harmony is a Castle Fight-style auto-battler RTS: you build spawners, waves of units march out on their own, and the last castle standing wins. Poker mode throws all of that out. There are no castles, no economy, no build hotbar. Instead you sit at a table, you are dealt hidden cards, you bet chips, and at showdown your cards turn into armies that fight to the death in a pit at the center of the arena. Last army standing takes the pot.

If you spent time in StarCraft 2's arcade years ago you might remember a custom mode called BattlePoker: poker structure, but the showdown was a unit brawl. That idea lodged in my head and never left. This is my full-fat take on it, built into an actual auto-battler with 140-plus units, elemental damage, a boon system, and a full tournament ladder on top.

But the honest reason it exists first, before the campaign or half the features I actually plan to ship, is that it is the best debugging tool I have ever built. More on that in a second.

The pitch, in one sentence

It is Texas Hold'em where your hole cards are armies.

Each seat is dealt one or two hidden unit cards. A card is not "a Spearman," it is "a Spearman card, count rolled somewhere in that unit's own range," so the same card might be one body or twelve. You bet across staged betting rounds, and every player still in the hand at showdown has their army spawned in a ring around the pit, facing inward. Then the game's normal unit AI takes over and the armies fight. It supports 2 to 9 seats, free for all, and it is a real tournament: antes, blinds that escalate on a timer, raises and re-raises, all-ins, eliminations, and a last-stack-standing champion.

Poker mode lobby: five free-for-all seats picking colors, with The Croupier, dealer of the pit, posed on the right
The table fills with up to nine seats. The Croupier deals the hands and takes a quiet cut of every soul that falls.

Why I built it first

Here is the thing about an auto-battler with 140-plus units and roughly ninety different abilities: I cannot possibly playtest every matchup by hand. What happens when a stealth assassin meets an all-flying army? When a poison mage attacks something immune to poison? When a summon-on-kill proc lands the exact frame its owner dies? I would never think to set most of those fights up on purpose.

Poker mode sets them up for me. Every showdown is a random army against a random army, several times a hand, hand after hand. In one sitting I watch hundreds of compositions collide that I never designed. Half the bugs later in this post surfaced that way: poker kept throwing units at the wall faster than I could dream them up. It is a fuzz-tester with a betting round attached.

That is possible because of how the game is built underneath. Combat runs on Unreal's Gameplay Ability System, or GAS. In plain terms: every stat a unit has (health, armor, attack speed) is an "attribute," every buff, debuff, cooldown, and damage-over-time tick is a stacked, replicated "gameplay effect," and every attack, spell, or passive is an "ability." It is a mountain of machinery, but the payoff is that a unit's behavior is data, not one-off code. A Poisoned status, a Chilled slow, a lifesteal aura: under the hood they are the same kind of object wearing different numbers.

Layer the RTS on top of that and a game mode is mostly a new rulebook, not a new game. Same units, same abilities, same combat, different setup and win condition. That is why the game already ships five modes: Regular (the default castle fight), Ultimate (everything unlocked, chaos), Draft (ban and pick from a shared pool), Poker, and Sandbox (a spawn-anything playground that is the deliberate version of what poker does by accident). Each one reuses the same foundation. Poker was the one that also happened to be the most useful to me as a developer, so it jumped the queue.

Staged streets: the part I am proudest of

The cleverest bit of design here is small. The whole hand is dealt on the server up front, but it is revealed one card per street. Card one, betting round, card two revealed, betting round, showdown. A two-card hand plays as two betting rounds, and you are wagering on partial information exactly the way Hold'em hole cards arrive one at a time.

That single choice is what makes it feel like poker instead of a slot machine. You raise on a strong opener, see your second card, and either commit or fold. And because a card carries a rolled unit count, "strong" is a read, not a certainty: a wide-range card is a swingier draw, which is its own kind of tell.

One more poker-y touch. Each hand can roll a boon (or a curse), a modifier applied to every qualifying army at showdown: more attack, halved shields, sometimes a neutral guard army that is hostile to everyone. The roll is announced before betting, as glowing text on the pit floor and on the HUD. It is public information you price into your bets. If tonight's boon favors heavy infantry and you are holding archers, that matters, and so does the fact that your opponent can see you seeing it. You can read the whole deck and the full boon pool on the Poker mode page.

When everyone folds to one player, that player takes the pot uncontested. No reveal, no battle, any uncalled excess refunded. And the payouts are commitment-layered side pots, not a naive split: a player who went all-in early for a short stack can only win the pot they actually paid into. A latecomer's deep money is not theirs to claim.

Poker mode showdown: two armies clash on the pit under the Graveborn Bulwark boon, floating damage numbers everywhere, the roster tracking each seat's units alive
Showdown. The wagered armies spawn in a ring and fight it out, the boon named across the floor, the roster counting who is still alive.

Now the bugs

Almost every one of these had the same shape: two perfectly reasonable systems combining into a symptom that pointed at neither. That is the theme, so watch for it.

The armies that spawned under the floor

Symptom: armies "never spawned," every single hand resolved as a draw, and the result splash tied every seat forever. A poker mode where nobody ever wins is not a great look.

The cause was two layers deep. Showdown armies spawn on a ring at a fixed radius from the pit center. If the pit is not perfectly circular, or the center marker is off, or the ring is rotated, a full-radius ring point can land off the pit entirely, out over the void. The formation spawner only searches a small box around each unit for valid ground, so it failed every fallback and units spawned in mid-air and fell.

And here is the part with no fingerprints: units have no "fell out of the world" handler, so anything that drops below the map's kill plane is silently destroyed by the engine on its first movement tick. No death log. No corpse. No trace. Zero survivors then tied every seat at death-time zero, and the payout code dutifully split the pot evenly among them. A perfect draw, every hand, from three systems each behaving exactly as designed.

The fix walks the spawn ring inward (100 percent radius, then 75, 50, 25, then dead center) projecting onto the navmesh until it finds real ground, and logs a warning when it has to pull in, which is the game telling you the pit is smaller than the ring in that direction. Plus a hard authoring rule: on an arena map, the kill plane sits well below the pit floor across the entire ring.

I closed the lobby slots and nothing changed

Symptom: the host closes seats in the lobby, the table comes up with the same number of players anyway. The classic "I fixed it and nothing happened," except the fix was real.

Two independent bot sources were stacking and cancelling out to the exact seat. The shared match-start path spawned a real AI bot for every unclaimed slot. Separately, poker's own filler topped the table up to the create-server menu's max-players value, which is never decremented when you close a slot. So closing three seats removed three real bots and appended exactly three filler bots. The two systems never collided by name, so they simply added, and the table size never moved.

Poker now owns its bots outright and skips the shared spawn entirely, along with the castle and build-zone passes that are meaningless on a castleless arena anyway. The target is simply the count of lobby slots that are not closed.

The stalemate you cannot predict

Some showdowns physically cannot finish. An all-flying army against an army with no anti-air unit (and "can target air" defaults to off, so most ground units genuinely cannot hit a flyer at all). Or a lone untargetable summon, which is both unregistered from the targeting index and granted invulnerability, so the surviving-army count can never drop to one. The pit just sits there, frozen, for the entire battle timer.

The obvious fix is capability analysis: ask "can army A ever hit army B?" I rejected it outright, because it has to predict, and prediction is where subtle rules (immunities, flight, targeting quirks) go to betray you. Instead the detector measures the actual outcome. Nothing has swung, nothing has taken damage, and the world's unit total has not changed for N seconds? Then nothing is going to. It folds into a poll that was already running, at zero extra cost.

There are two windows: a long no-contact window (45 seconds) that covers both the march in from the spawn ring and the "these two can never touch" case, then a short idle window (10 seconds) once any combat has actually happened. The asymmetry in what counts as "progress" is deliberate. A false progress reading just falls back to the old time cap, harmless. A false idle reading would end a live fight early and turn someone's win into a split, so the bar for declaring idle is high. When it does trigger, the pot chops, which is nearly chip-neutral since each seat gets its own contribution back. And if three stalemates happen in a row, the log warns that the deck may be hiding a mutually-unhittable matchup.

The index that lies

A trap I walked into more than once. The game keeps a fast server-side index of combat targets, and it is the natural thing to count survivors with. But stealthed and untargetable units unregister themselves from it. So an index tally counts a stealthed Assassin as dead, and ends the round while it is still alive and, frequently, winning.

Every survivor count in poker iterates the real actors instead. The same lie bit the minimap independently: the index is server-only, so a client reading it drew zero dots. The standing rule now is that anything counting survivors or running on a client walks the actual units, never the index.

The summons that never left

Found this the week of ship: summoned units left standing on the pit after a round ended, piling up hand after hand like the cleaning crew never came.

Hand cleanup swept units by faction, meaning anything belonging to a seat. But a summon inherits its faction from whatever summoned it, through a weak reference, and that reference comes back null when the summoner died during the tiny window between an attack landing and its damage resolving. In a fight to the death that is routine: a summon-on-kill proc regularly lands the same instant its owner dies. The orphan ends up with an invalid faction tag, which matched neither the cleanup sweep nor any seat's alive-count bucket. So it never held the round open, which is exactly why the hand still resolved correctly and the only visible symptom was litter.

The sweep now also clears invalid-faction units (always garbage on an arena map), and a new diagnostic names anything left standing, because the pit is supposed to be empty between hands and any survivor is a spawn path I have not covered yet. Honest caveat: the root cause is only patched for poker. An ownerless summon still spawns inert in the other modes, where it is harmless because it dies with the match.

A one-line horror: the dangling string

The poker turn timer re-arms the very timer handle whose code is currently running, which relocates the captured seat name in memory. Any function down that call chain that took the seat name by reference ended up holding a dangling pointer, and the log filled with garbage names. The fix is a rule, not a patch: every seat-name parameter on the timer and bot chain is passed by value. Small bug, genuinely nasty to find.

The owner-only army preview

The newest feature is also the one I most enjoyed building. While you are holding cards and betting, a translucent ghost of your army stands on the pit at the exact spot it will spawn at showdown, in formation, playing idle animations, visible only to you (that is the ghost in the lead image up top). It grows as each street reveals another card. It swaps when you redraw. It re-spaces around the ring live as other players fold, because where you stand depends on how many seats are still in the hand. And it vanishes the instant you fold, get eliminated, or the real armies spawn.

The best part is that owner-only visibility needed no network code at all. The preview is created on your client and never replicates, so no other machine can possibly have one. It is the same trick the building-placement ghost uses in the main game: the cheapest netcode is the netcode you never write.

Getting it to not lie was the real work. The ghost has to agree with the server about where the army will land, so the ring-angle math lives in one shared function that both the server spawn and the client preview call, and they physically cannot drift. The formation layout is likewise a single shared pure-math routine, so the ghost stands in the identical shape the real army will. The one thing a client cannot mirror is the server's navmesh projection (client navmesh does not exist under dynamic generation), so the preview walks the same inward radius with ground traces instead, and a trace that hits nothing means "over the void," the same guard the server uses.

Visibility is a state predicate re-checked every frame, never an edge trigger, and that distinction earned its keep. Your hand is never re-sent when you fold, so the data goes stale rather than empty. And an uncontested pot skips the battle entirely, so "tear down when the real units appear" would have been wrong too. The only trustworthy teardown signal is the replicated seat state, so that is what it reads.

Ranked, quietly

Every player carries a poker rating starting at 1200, plus tournaments played, wins, and hands won, all saved locally and shown in the table roster so you can size up the seat next to you. Stats move for every completed match, but rating only moves when at least two humans are seated, so grinding bots does nothing for your number. The math is pairwise Elo against every other human in the match, averaged, with finish placement driving the score and same-hand busts sharing a place. A Steam leaderboard hook is wired and waiting on the board itself to exist; for now it logs. All the rating logic is in C++, and only the final Steam call would ever live in Blueprint.

Where this is heading

Poker mode is the first proof that this engine can wear a lot of hats, and I have no intention of stopping at five modes. The next two I want are an arena mode (a tighter, combat-only brawl with no poker wrapper) and a tower defense mode, both of which reuse the exact same units, abilities, and GAS combat that everything else does. Every new mode is cheaper than the last, because the hard part, the 140 units and their ninety-odd abilities, is already built and already data-driven.

And the more modes I have, the more a bigger shape comes into focus. Once poker night, an arena duel, a castle siege, and a tower defense stand are all real, playable pieces, they stop looking like a mode list and start looking like the chapters of a campaign. I want single-player and co-op adventures that string these modes together into one story: defend a pass in tower defense, settle a rivalry in the arena, gamble for reinforcements at a poker table between battles. That is the real ambition sitting behind all of this. Poker mode is just the first hat.

The lesson, again

Read back through those bugs and the pattern is almost funny. Ring geometry, navmesh projection, and the engine's kill plane, each reasonable, combining into "every hand is a draw." Two bot spawners, each correct, adding up to "closing slots does nothing." A weak reference and a fight-to-the-death timing window producing invisible litter. None of the symptoms pointed at their causes, and none of the causes were individually wrong.

That is most of game development, honestly. The systems are fine. It is the seams between them where the interesting failures live, and poker mode, being an entire second game bolted onto the first, is nothing but seams. It was worth it, and not only because it is fun. Your hole cards are armies. Come find out what that feels like in the alpha.