Last time I posted about Tactics Kit it was about the rules side (events, previews on a cloned board). This one’s about how the enemies decide what to do, because it ended up a lot simpler than I expected.
Each enemy just tries everything. On an 8x8 board that’s cheap, so for every cell it can walk to and every way it can aim from there, it scores the attack and keeps the best one:
for c in cells:
var approach := -APPROACH * _distance_to_squad(c, squad)
if fire_lanes.has(c):
approach += IN_ALLY_FIRE
for t in b.targets(e, c):
var s := score(b, e, c, t["dir"], int(t["dist"])) + approach
s += battle.rng.randf() * 0.01
if s > best_score:
best_score = s
best_cell = c
best_aim = {"dir": t["dir"], "dist": int(t["dist"])}
Then it walks there and shows you the attack. Nothing fires until after your turn, so you always get a chance to dodge, block or shove it.
The weights are basically its whole personality:
const HIT_SQUAD := 10.0 # per point of damage to a squad member
const KILL_SQUAD := 20.0
const PUSH_TO_DEATH := 25.0 # pushing a squad member into a chasm
const BUMP_SQUAD := 4.0
const HIT_ALLY := -8.0 # per point of damage to another enemy
const HIT_OBJECT := 2.0
const OBJECT_NEAR_SQUAD := 8.0 # a barrel that would explode next to a squad member
const APPROACH := 0.6 # prefer standing closer to the squad when nothing can be hit
const IN_ALLY_FIRE := -40.0 # standing where an ally's announced attack will land or pass through
const WASTED := -3.0 # an attack that can only hit a rock or the edge of the map
const SIGHT := 1.5 # aiming down a line that has a squad member or cargo somewhere along it
A few of these are only there for feel. SIGHT makes a gunner line up on you even when something’s in the way right now, so it looks like it means it. WASTED stops enemies proudly announcing an attack into a wall, which looked really dumb. APPROACH just stops them standing around when there’s nothing to hit.
The only coordination between enemies is fire lanes. They plan one at a time in attack order, and every cell an announced attack passes through gets marked. Anyone planning after that loses 40 points for standing in one, and 8 per point of damage for aiming at an ally.
Ties get a tiny bit of seeded noise (0.01 at most) so they don’t all pick the top left cell. Same seed, same battle, which made any weird move easy to replay.
Then I wanted to know which weights actually do anything. The kit has a bot that plays your side, so I ran every encounter except the boss on 40 seeds (200 battles) with three versions of the AI:
- As shipped: 44 damage to the squad, 50 damage enemies did to each other, 13 squad members lost
- Only damage, kills and walking closer: 40 to the squad, 58 to each other, 10 lost
- Ally avoidance off: 50 to the squad, 77 to each other, 10 lost
Honestly, most weights barely changed how much damage landed. Telegraphed attacks are meant to be dodged, and the bot dodges most of them whatever the AI does. The one that clearly mattered was ally avoidance. With it off, enemies hit each other about 50% more (77 vs 50), which makes them look clumsy and hands you free kills.
So if you want easier enemies, turning ally avoidance down does more than lowering damage. Harder enemies probably need more of them or attacks that are harder to dodge, not smarter scoring. A bot isn’t a person though, so I’d treat the numbers as a direction, not gospel
It’s all in Tactics Kit if you want to poke at the code: Tactics Kit - Godot 4 Tactics Roguelite Template by Spare Parts Studio
Free browser demo: Tactics Kit - Playable Demo by Spare Parts Studio
Happy to answer questions about any of it