Turn-Based Strategy
Grids, probability, and agonizing decisions — 95% to hit. Missed.
"In preparing for battle I have always found that plans are useless, but planning is indispensable." — Dwight D. Eisenhower
Prerequisites
| Module | What You Used From It |
|---|---|
| Module 7 - Roguelike | Grid-based world representation, turn-based input loops, entity-on-grid movement |
| Module 6 - Tower Defense | Pathfinding (A* or BFS), tile cost considerations, spatial reasoning |
Week 1: History & Design Theory
The Origin
Turn-based tactics games descended directly from tabletop war games — hex grids, cardboard counters, and probability tables. The digital translation began with early titles like Nobunaga's Ambition (1983) and the original Fire Emblem (1990), which moved the war game to a console screen and replaced dice rolls with hidden random number generators. The genre's core promise is giving the player time to think. Unlike an RTS where the clock never stops, a turn-based strategy game says: "Here is a complex situation. Take as long as you need to find the best move." This deliberate pacing attracted players who valued planning over reflexes, and the genre became the home of some of the deepest tactical systems in all of gaming.
How the Genre Evolved
Fire Emblem (Intelligent Systems, 1990): The original Fire Emblem established the tactical RPG template: grid-based maps, character permadeath, weapon triangle (swords beat axes beat lances), and movement ranges defined by unit class. Its genius was making each unit a named character with personality and relationships — losing a unit was not just a tactical setback but an emotional loss.
XCOM: Enemy Unknown (Firaxis, 2012): Firaxis reinvented the tactical game for a modern audience. Its cover system — half cover and full cover modifying hit probability — made positioning the central tactical decision. Its visible percentage displays ("72% to hit") created agonizing risk-reward calculations that the player could see and reason about.
Into the Breach (Subset Games, 2018): Into the Breach stripped the genre to its absolute essence: small grids, few units, and complete information. Every enemy telegraphs its next attack, and the player's job is to figure out how to prevent the most damage with limited actions. It proved that turn-based tactics could be a puzzle game — deterministic, transparent, and brutally elegant.
What Makes Turn-Based Strategy "Great"
A great turn-based strategy game makes every decision feel like it matters. The best entries create situations where the player can see multiple viable options, each with clear tradeoffs: advance this sniper to high ground for a better angle but expose her to flanking fire, or keep her in cover with a worse shot? The probability system must be transparent enough that the player can make informed bets but uncertain enough that no plan is guaranteed. And the consequences must be real. When a plan comes together despite uncertainty, the satisfaction is profound. When it falls apart because of a 5% miss, the agony is equally memorable.
The Essential Mechanic
Positioning units on a grid where cover, range, and probability determine outcomes.
Week 2: Build the MVP
What You're Building
A tactical combat game on a grid where the player controls 3-4 units against a group of enemies. Each unit has action points, a movement range, and an attack with a hit probability modified by cover and range. The map has cover objects that units can hide behind. Turns alternate between the player and the enemy. Win by eliminating all enemies; lose if all player units fall.
Core Concepts (Must Implement)
1. Movement Range Calculation on a Grid
From a unit's current position, calculate which tiles it can reach given its movement budget. This is a breadth-first search (BFS) where each tile has a movement cost, and the search stops when the budget is exhausted.
function calculate_movement_range(unit):
start = unit.grid_position
budget = unit.move_range // e.g., 5 tiles
reachable = {} // tile -> remaining_budget
queue = [(start, budget)]
reachable[start] = budget
while queue is not empty:
current, remaining = queue.pop_front()
for neighbor in get_adjacent_tiles(current):
if not is_walkable(neighbor): continue
if is_occupied_by_enemy(neighbor): continue
cost = get_tile_move_cost(neighbor)
new_remaining = remaining - cost
if new_remaining >= 0 and new_remaining > reachable.get(neighbor, -1):
reachable[neighbor] = new_remaining
queue.append((neighbor, new_remaining))
return reachable
function get_tile_move_cost(tile):
if tile.terrain == "normal": return 1
if tile.terrain == "rough": return 2
if tile.terrain == "water": return 3
return 1 Why it matters: Movement range visualization is how the player plans. Seeing the blue-highlighted tiles tells them exactly what is possible this turn. The BFS with variable tile costs adds tactical depth — rough terrain becomes a natural choke point, and high-cost tiles create interesting tradeoffs between the short path and the safe path.
Click on the unit (blue circle) to select it and see reachable tiles highlighted. Different terrain costs more movement: grass=1, forest=2, mountain=3, water=impassable. Click a highlighted tile to move the unit. Press "Next Turn" to reset movement.
2. Action Point System
Each unit gets a fixed number of action points (AP) per turn. Moving costs AP. Attacking costs AP. The player must decide how to spend them — a unit that moves far cannot attack, and a unit that attacks twice cannot move.
ACTIONS = {
"move": { ap_cost: 1 },
"attack": { ap_cost: 1 },
"overwatch": { ap_cost: 2 },
"heal": { ap_cost: 1 }
}
function start_player_turn():
for unit in player_units:
unit.ap = unit.max_ap // Typically 2
unit.has_moved = false
unit.has_attacked = false
function try_action(unit, action_type):
cost = ACTIONS[action_type].ap_cost
if unit.ap < cost:
show_message("Not enough action points")
return false
unit.ap -= cost
return true Why it matters: Action points create the fundamental tension in turn-based tactics: economy of action. With only 2 AP, every point matters. Do you move into cover or stay exposed and attack twice? AP turns each unit's turn into a small optimization problem with real consequences.
3. Hit Probability / Accuracy System
When a unit attacks, the hit chance is calculated from base accuracy modified by distance, cover, flanking, and elevation. The percentage is displayed to the player before they commit, creating an informed gamble.
BASE_HIT_CHANCE = 75
COVER_MODIFIERS = {
"none": 0,
"half": -25,
"full": -40
}
RANGE_PENALTY = -5 // Per tile beyond optimal range
FLANK_BONUS = 25
function calculate_hit_chance(attacker, defender):
chance = BASE_HIT_CHANCE
cover = get_cover_between(attacker.position, defender.position)
chance += COVER_MODIFIERS[cover]
dist = grid_distance(attacker.position, defender.position)
if dist > attacker.optimal_range:
chance += RANGE_PENALTY * (dist - attacker.optimal_range)
if is_flanking(attacker.position, defender):
chance += FLANK_BONUS
chance = clamp(chance, 5, 95)
return chance
function attempt_attack(attacker, defender):
hit_chance = calculate_hit_chance(attacker, defender)
display_shot_preview(hit_chance, attacker.damage)
if player_confirms_attack():
roll = random(0, 100)
if roll < hit_chance:
apply_damage(defender, attacker.damage)
show_result("HIT!")
else:
show_result("MISSED") Why it matters: The hit probability system is the emotional core of the genre. Showing the player "72% to hit" before they shoot creates a moment of genuine tension — they chose to take this shot knowing it might miss. This transforms RNG from something that happens TO the player into a risk the player consciously accepts.
Adjust range, cover type, and flanking to see how hit probability changes. Click "Fire!" to roll the dice and see the result.
4. Cover System on a Grid
Certain tiles or tile edges provide cover. A unit standing behind cover receives a defensive bonus against attacks from the covered direction. Cover is directional — it protects from one side but not from flanking.
cover_objects = [
{ position: [3, 4], type: "half", blocks_movement: false },
{ position: [5, 2], type: "full", blocks_movement: true },
{ position: [7, 6], type: "half", blocks_movement: false }
]
function get_cover_for_unit(unit_pos, attacker_pos):
dx = sign(attacker_pos.x - unit_pos.x)
dy = sign(attacker_pos.y - unit_pos.y)
check_positions = []
if dx != 0: check_positions.append({ x: unit_pos.x + dx, y: unit_pos.y })
if dy != 0: check_positions.append({ x: unit_pos.x, y: unit_pos.y + dy })
best_cover = "none"
for pos in check_positions:
for cover in cover_objects:
if cover.position == [pos.x, pos.y]:
if cover.type == "full": best_cover = "full"
elif cover.type == "half" and best_cover == "none":
best_cover = "half"
return best_cover Why it matters: Cover makes positioning the most important decision in the game. The same unit in the open dies in one hit; behind full cover, it can hold a position for turns. Cover creates a tactical vocabulary: "advance to the next cover," "flank the enemy's cover," "destroy their cover."
5. Fog of War with Unit Vision
Each unit reveals a radius of tiles around it. Tiles outside all units' vision are hidden. Enemy units in hidden tiles are invisible. This makes scouting and vision control a tactical resource.
function update_tactical_fog(player_units):
for x in 0 to grid_width:
for y in 0 to grid_height:
visible_map[x][y] = false
for unit in player_units:
if unit.is_dead: continue
for x in 0 to grid_width:
for y in 0 to grid_height:
dist = grid_distance(unit.position, [x, y])
if dist <= unit.vision_range:
if has_line_of_sight_grid(unit.position, [x, y]):
visible_map[x][y] = true Why it matters: Fog of war in a tactics game transforms information into a resource. Moving a unit forward is not just about positioning — it is about gaining vision. An enemy you cannot see is an enemy you cannot plan for.
6. Initiative / Turn Order
The system that determines when each side acts. Common approaches include "I-go-you-go" (player moves all units, then enemy moves all), alternating activation, or speed-based initiative.
// I-Go-You-Go (XCOM style)
current_phase = "player"
function start_player_phase():
current_phase = "player"
for unit in player_units:
unit.ap = unit.max_ap
function start_enemy_phase():
current_phase = "enemy"
for enemy in enemy_units:
enemy.ap = enemy.max_ap
execute_enemy_ai(enemy)
start_player_phase() Why it matters: Turn order profoundly affects strategy. In I-go-you-go, momentum swings are dramatic. In alternating systems, the opponent cannot chain multiple kills without the player getting a response.
7. Undo / Preview System
Showing the player what will happen before they commit. Movement preview shows the path and destination. Attack preview shows hit chance and damage. Undo for movement reduces frustration and focuses the game on decision-making.
move_history = []
function execute_move(unit, destination):
move_history.append({ unit: unit, from: unit.position, ap_spent: 1 })
unit.position = destination
unit.ap -= 1
function undo_last_move():
if len(move_history) == 0: return
last = move_history.pop()
if last.unit.has_attacked:
show_message("Cannot undo -- unit has already attacked")
move_history.append(last)
return
last.unit.position = last.from
last.unit.ap += last.ap_spent Why it matters: The preview system is what separates a tactics game from a guessing game. By showing the player exactly what will happen (or the probability of what will happen), the designer ensures that mistakes feel like the player's fault, not the game's.
Stretch Goals
- Overwatch / reaction fire: A unit spends AP to enter "overwatch" — it will automatically shoot the first enemy that moves within its line of sight during the enemy turn.
- Destructible cover: Explosives or heavy attacks can destroy cover objects, opening new sight lines.
- Unit classes: Sniper (long range, low movement), assault (short range, high movement), support (healer, buffer).
- Enemy intent display: Into the Breach style — show exactly what each enemy will do next turn.
MVP Spec
| Element | Minimum Viable Version |
|---|---|
| Grid | 8x8 to 12x12 tile grid with terrain and cover objects |
| Player Units | 3-4 units with HP, movement range, attack range, and damage |
| Enemy Units | 3-5 enemies with simple AI (move toward player, attack if in range) |
| Movement | BFS-based range display, click-to-move, path preview |
| Combat | Hit probability based on cover and range, displayed before confirming |
| Cover | At least 2 cover types (half and full) affecting hit chance |
| Action Points | 2 AP per unit per turn: move + attack, or double move, or double attack |
| Turn Structure | I-go-you-go: player moves all units, then all enemies act |
| Preview | Movement path preview, attack hit % preview, cover indicators |
| Win/Lose | Eliminate all enemies to win; all player units killed = lose |
Deliverable
A playable turn-based tactics game where the player positions 3-4 units on a grid with cover objects, moves them within calculated ranges, attacks enemies with probability-based hits modified by cover and range, and attempts to eliminate all enemies before being eliminated. The player should always see hit percentages before attacking and movement ranges before moving.
Analogies by Background
For Backend Developers
| Concept | Analogy |
|---|---|
| Movement Range (BFS) | Like a network hop-count query — BFS from a source node with a TTL (movement budget), where each edge has a weight, returning all reachable nodes. |
| Action Point System | Like API rate limits per request window — each unit gets N actions per turn, and the caller must decide how to spend them. |
| Hit Probability | Like probabilistic data structures (Bloom filters, HyperLogLog) — the system gives you a confidence percentage, not a guarantee. |
| Cover System | Like defense-in-depth in security — half cover is a firewall, full cover is a firewall plus WAF. Flanking bypasses both. |
| Initiative / Turn Order | Like round-robin vs. priority scheduling — different scheduling determines response latency. |
| Undo / Preview | Like dry-run mode in a deployment pipeline — preview changes before committing, rollback for non-destructive operations. |
For Frontend Developers
| Concept | Analogy |
|---|---|
| Movement Range (BFS) | Like calculating reachable routes in a navigation menu — determine which pages are reachable within N clicks. |
| Hit Probability | Like conversion rate predictions in A/B testing — "72% chance this variant wins" is the same kind of informed uncertainty. |
| Cover System | Like CSS overflow: hidden with directional clipping — cover blocks from one direction but not others. |
| Fog of War | Like virtualized rendering — only elements within the viewport are rendered; everything outside exists but is not displayed. |
| Undo / Preview | Like optimistic UI updates with rollback — show the expected result, let the user confirm, and revert if they change their mind. |
For Data / ML Engineers
| Concept | Analogy |
|---|---|
| Movement Range (BFS) | Like breadth-first graph traversal with edge weights — friends-of-friends within N hops applied to a spatial grid. |
| Hit Probability | Like model confidence scores — the system outputs a probability, and the decision-maker must choose whether to act on it. |
| Cover System | Like regularization — cover penalizes attacker accuracy, preventing guaranteed hits and forcing creative solutions. |
| Fog of War | Like partial observability in reinforcement learning — the agent does not see the full state, only a local observation. |
| Initiative / Turn Order | Like batch processing vs. online learning — I-go-you-go processes all samples in one batch before updating. |
Discussion Questions
- XCOM is famous for the "95% miss" — a nearly guaranteed shot that fails. Is this a design flaw or a feature? How does the 5-95% clamp on hit chance affect the player's relationship with probability?
- Into the Breach shows you exactly what every enemy will do on their next turn. XCOM hides enemy intentions behind fog of war. How does the amount of information available change the type of thinking the game rewards?
- Fire Emblem's permadeath means a fallen unit is gone forever. XCOM allows wounded soldiers to recover. How do different failure consequences change player behavior?
- Turn-based strategy games give the player unlimited time to think per turn. Does this make them "easier" than real-time games, or does it make them harder by removing the excuse of "I didn't have time to think"?