After the paper prototype, I started by building only the card combat. The cards are the core of the game, so I wanted the combat system solid before touching anything else.
Here is the final prototype UI. The player’s Hand sits at the bottom, the Field and Hero skill are in the middle, and the Hero stats are at the top. The opponent mirrors this setup on the right.

This is the final prototype and it is fully playable, but it was not how I started. Just like the paper prototype, my first goal was not having a human player. I wanted to simulate matches between two AI bots.
TL;DR;
I built Arden Card’s combat system to support AI bot simulation. That let me run hundreds of matches in seconds, pitting each Hero against another, then aggregate the data to drive balance.
Character.Guardian Win Ratio: 0.490000 | Character.Artisan Win Ratio: 0.510000
Average Turn Count: 34.669998
Min Turn Count: 15
Max Turn Count: 90
In this example, the Guardian wins against the Artisan roughly 49% of the time. I used this kind of data to spot degenerate Heroes and playstyles.
Card Combat

Above is a high-level diagram of the combat system. The BattleOrchestrator is the brain. It processes Actions, which describe moves a player or AI can make given the current Game Board state.
When an Action is processed, the Game Board mutates. Cards can be drawn, attacks can land, damage is applied, and buffs or debuffs resolve. This setup let me build AI that selects good actions using light heuristics. Here’s how the heuristic scores Actions:
const TMap<EArdenCardActionType, int32> ActionTypeScores = {
TPair<EArdenCardActionType, int32>(EArdenCardActionType::Draw, 4),
TPair<EArdenCardActionType, int32>(EArdenCardActionType::Play, 10),
TPair<EArdenCardActionType, int32>(EArdenCardActionType::ExecuteCard, 10),
TPair<EArdenCardActionType, int32>(EArdenCardActionType::ExecuteCharacterSkill, 10),
};
const TMap<FName, int32> TagScores = {
TPair<FName, int32>("Tag.Finisher", 3),
TPair<FName, int32>("Tag.Risky", -1),
TPair<FName, int32>("Tag.Strong", 2),
TPair<FName, int32>("Tag.Neutral", 0),
TPair<FName, int32>("Tag.Buff", 2),
TPair<FName, int32>("Tag.Debuff", 1),
TPair<FName, int32>("Tag.Weak", -1),
TPair<FName, int32>("Tag.Heal", 1),
TPair<FName, int32>("Tag.Shield", 1),
};
Each card is defined with a set of Tags. The AI uses them to score each Action. It also looks at the current Game Board state to adjust those scores, for example:
if (Tags.Contains("Tag.Heal")) {
if (Character->GetHP() >= 100) {
Score -= 4;
}
else if (Character->GetHP() >= 80) {
Score -= 2;
}
}
Each AI then sends its Action to the BattleOrchestrator.
Building the Cards
Every card follows the same rules. Each one has AP costs and a primary type.
Primary Card Types:
- Skill — Placed on the battlefield and can be immediately executed.
- Construct — Placed on the battlefield and must be constructed before execution is available.
- Enhancement — Executes immediately and buffs the Hero.
- Special — Executes immediately, changing the battlefield, Hero state, and more. Usable once per match.
The primary type determines how a card is played and whether it takes a field slot. Whether it goes to the Field or executes immediately, each card runs a series of Effects that can:
- Damage another card or hero
- Heal a card or hero
- Buff or Debuff the hero
- Move a card between the Field and Hand
Card Effects

The card defined above is from the Artisan’s deck. This is one of the “Finisher” cards. In the game, a potential strategy is to protect this card while it is constructed and use a strength buff to increase its damage output. The card’s configuration defines its costs, cooldowns, charge rules, targeting rules, and the effects.
From the screenshot, this card can only target the Enemy Character and deals 24 damage. The effect’s probability provides an extra roll. While this card is 100%, other cards use probability to create different types of effects. Even with a 100% effect, each card action still uses the Hero’s Accuracy stat to determine hit or miss.
The following screenshot shows the Effects for Artisan’s Magick Cube card.

This card has three possible effects, but the Effect Resolution Mode is Mutually Exclusive. That means only one effect runs per execution. Each effect has a 50% probability, so there is the initial Accuracy roll and then a second roll to see if the chosen effect fires.
Now that you’ve seen the configuration that governs each card, how did I go about balance?
Balancing the Cards
After the initial combat system was in place, I continuously ran Hero matchups to see how the game played, which cards were too strong or weak, and which Heroes had unfair advantages. This required telemetry so I could drive balance with data. The BattleOrchestrator outputs every action, and my MatchSimulator records it all and writes CSV files.
AI Simulation
| matchup | winner | character_id | wins | total_matches | win_rate |
|---|---|---|---|---|---|
| Artisan vs Smasher | PlayerA | Character.Artisan | 143 | 250 | 57.2% |
| Artisan vs Smasher | PlayerB | Character.Smasher | 107 | 250 | 42.8% |
And for these same rounds, here are the card usage rates:
| matchup | player | character_id | card_id | uses | usage_rate |
|---|---|---|---|---|---|
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.ToyBox | 1007 | 14.68% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.BoltTower | 863 | 12.58% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Common.MajorHealPotion | 825 | 12.02% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Common.MinorHealPotion | 803 | 11.70% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.Reinforce | 670 | 9.77% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.CloudGenerator | 651 | 9.49% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.MagickCannon | 455 | 6.63% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.BlightEngine | 401 | 5.84% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.Forge | 362 | 5.28% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.ShieldGenerator | 318 | 4.63% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.MagickCube | 273 | 3.98% |
| Artisan vs Smasher | PlayerA | Character.Artisan | Card.Artisan.Deconstruct | 233 | 3.40% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.OneTwoPunch | 782 | 11.07% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.SpinningKick | 747 | 10.57% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.BrutalPunch | 741 | 10.49% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.Grapple | 700 | 9.91% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.WindStorm | 663 | 9.38% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.ComboStrike | 639 | 9.04% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.CollateralShock | 580 | 8.21% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Common.MajorHealPotion | 568 | 8.04% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Common.MinorHealPotion | 545 | 7.71% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.Enrage | 458 | 6.48% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.WalkInTheShadows | 369 | 5.22% |
| Artisan vs Smasher | PlayerB | Character.Smasher | Card.Smasher.LightningStrike | 274 | 3.88% |
Across these 250 matches, Card.Artisan.ToyBox was the most used card for the Artisan. Card.Smasher.OneTwoPunch was most used by the Smasher. That lines up with their Costs and Cooldowns:
Card.Artisan.ToyBox
| Property | Value |
|---|---|
| Struct.APCostToPlay | 1 |
| Struct.APCostToExecute | 1 |
| Struct.CooldownTurns | 1 |
| Struct.SetupTurns | 1 |
Card.Smasher.OneTwoPunch
| Property | Value |
|---|---|
| Struct.APCostToPlay | 1 |
| Struct.APCostToExecute | 1 |
| Struct.CooldownTurns | 2 |
| Struct.SetupTurns | 0 |
Using Simulation Data
Once I had data across Hero matchups, I started versioning card data. When I found win ratios like 80% / 20%, I would update card configurations. But it was never just a single-card buff or nerf. I had to keep the whole matchup matrix balanced. For example:
Artisan vs Smasher: 28% / 72%
Guardian vs Smasher: 40% / 60%
Guardian vs Artisan: 48% / 52%
At first glance, it looks like Artisan needs a buff because of the 28% / 72% result against Smasher. But Guardian vs Artisan is near 50/50. If I buff Artisan, I risk breaking that matchup or needing to buff both Artisan and Guardian. In this scenario, I would actually nerf the Smasher. Turn count was another factor, and it also shifted with buffs and nerfs. This happened more times than I could count, but with each data set I recorded, I continued to dial in the combat until I reached a set of fair matchups:
Character.Guardian Win Ratio: 0.490000 | Character.Artisan Win Ratio: 0.510000 | Average Turn Count: 34.669998 | Min Turn Count: 15 | Max Turn Count: 90
Character.Guardian Win Ratio: 0.500000 | Character.Smasher Win Ratio: 0.500000 | Average Turn Count: 43.360001 | Min Turn Count: 15 | Max Turn Count: 82
Character.Artisan Win Ratio: 0.350000 | Character.Smasher Win Ratio: 0.650000 | Average Turn Count: 54.070000 | Min Turn Count: 11 | Max Turn Count: 160
Where to Next
That is a high-level overview of Arden Card’s combat system and how I balanced the cards across Heroes. Once I felt good about the numbers, I built the human-playable combat. With that handled, the next step was the rest of the game: how combat fits into the RPG, what the world looks like, and what other tactical systems are needed. The next article will walk through my decisions for the overworld and how the whole game is coming together.
Combat Prototype: https://mini-game-dev.itch.io/project-arden-card
Up Next: Overworld & the Rules Engine
