In Into the Breach style tactics, enemies show exactly where they will strike, and your turn is about pushing them so the attack lands somewhere else. It looks like a lot of special cases, but most of it falls out of a few structural choices. Here’s how I built it in plain GDScript, in case it helps anyone making a grid tactics game.
1. The rules never touch the screen
The board is a plain RefCounted class: terrain in an array, units as small data objects, an occupancy lookup. Every rule (move, attack, push, bump, fall, explode) changes that data and returns a list of events as plain dictionaries:
func _push(target: Piece, dir: Vector2i, events: Array) -> void:
var from := target.cell
var to := from + dir
var blocker := piece_at(to)
if not in_bounds(to) or terrain_at(to) == Terrain.ROCK or blocker != null:
events.append({"t": "bump", "id": target.id, "cell": from, "into": to})
_damage(target, BUMP_DAMAGE, events, true)
if blocker != null:
_damage(blocker, BUMP_DAMAGE, events, true)
return
_place(target, to)
events.append({"t": "push", "id": target.id, "from": from, "to": to})
if terrain_at(to) == Terrain.CHASM and not target.flying:
events.append({"t": "fall", "id": target.id, "cell": to})
The rules finish instantly. The battle view then walks the event list and awaits a tween for each one. Chain reactions (a push into a barrel that explodes and kills something that explodes) need no special handling: they’re just more events in the list, in the order they happened.
2. Preview = run the real rules on a copy
Because the board is plain data, cloning it is cheap (duplicate the arrays, clone each unit). When the player points at a target, the game clones the board, runs the actual attack on the copy and draws the result: where everyone ends up, who takes damage, who dies.
func preview_attack(p: Piece, dir: Vector2i, dist: int = 1) -> Dictionary:
var copy := board.clone()
var events := copy.attack(copy.get_piece(p.id), dir, dist)
return {"board": copy, "events": events}
There’s no separate “prediction” code to drift out of sync with the real rules, so the preview is always right (the stress tests check this on hundreds of random attacks anyway). Undo is the same trick: push a clone before each move, pop it to undo.
3. Enemies store a direction, not a target cell
This is the one that makes pushing feel good. An enemy’s announced attack is saved as “fire east” (plus a distance for artillery), not “hit cell (4, 2)”. When its turn comes, it fires that direction from wherever it now stands. So if you push an enemy one cell, its attack moves with it, and can end up hitting another enemy. Attacks fire in a fixed, numbered order, which the player can see, so “push the first one so it hits the second” is a plan you can make.
4. Enemy AI just tries everything
On an 8x8 board, brute force is fine. Each enemy tries every cell it can walk to and every way it can aim from there, works out what that attack would hit on the board as it stands, and scores it with a handful of weights: damage to the squad, kills, pushing someone into a chasm, a penalty for hitting its own allies, and a big penalty for standing where an ally’s announced attack will land. A whole enemy turn plans in about 3 ms. Tuning the AI is changing a few constants, and a seeded random tie-break means the same seed always plays out the same way, which makes bugs reproducible.
5. A bot that plays your side
Since the rules don’t need the screen, a simple squad bot can play hundreds of battles and full runs headless. The tests use it to check that the board stays valid after every action (no two units on one cell, nothing standing in a chasm, HP never above max), and it’s a quick way to see whether a balance change made the game easier or harder.
I’ve packaged this up as Tactics Kit, a complete tactics roguelite template for Godot 4: a squad of three, six enemies including a boss, pushes, bumps, chasms and explosive barrels, cargo to protect, a branching run map with shops and upgrades, mouse, keyboard and gamepad, and units, abilities and encounters you edit as resources in the Inspector:
There’s a free browser demo if you want to try the pushing first: Tactics Kit - Playable Demo by Spare Parts Studio
Happy to answer questions about any of it.