Skip to main content

Strategy Game Mechanics

Building Complete Strategy Game Systems

Master strategy game mechanics! Build resource management, unit control, base building, fog of war, tech trees, and AI opponents! โš”๏ธ๐Ÿฐ๐Ÿ—บ๏ธ

Understanding Strategy Game Systems

๐ŸŽฎ Core Strategy Mechanics

Strategy games combine multiple systems to create deep tactical and strategic gameplay:

graph TD A["Strategy Game Core"] --> B["Economy System"] A --> C["Military System"] A --> D["Building System"] A --> E["Research System"] B --> F["Resource Gathering"] B --> G["Resource Storage"] B --> H["Trade/Exchange"] C --> I["Unit Production"] C --> J["Unit Control"] C --> K["Combat Resolution"] D --> L["Construction"] D --> M["Placement Rules"] D --> N["Building Upgrades"] E --> O["Tech Tree"] E --> P["Research Queue"] E --> Q["Unlocks & Bonuses"]
A strategy game's resource-to-unit pipeline shown in three columns. A header band labels each column RESOURCES, PRODUCTION, and UNITS. The left column has three stacked resource cells: Mine (a mountain icon with a gold nugget, stat 'Gold +10/s'); Farm (three wheat stalks with grain heads, stat 'Food +8/s'); Forest (a small evergreen tree with a green canopy and brown trunk, stat 'Wood (gathered)'). The center column is one tall Barracks cell: a shield with crossed swords, a build-cost pill reading 'Build: 300g + 100w', and a monospace list of trainable units with their costs (Soldier 100g+20f, Archer 120g+20f, Cavalry 200g+50f, Siege 300g+100w). The right column has two stacked unit cells: Soldier (a vertical sword icon, stats 'HP 100 | ATK 15', cost pill '100g + 20f') and Archer (a curved bow with a horizontal arrow, stats 'HP 60 | ATK 12 | RNG 5', cost pill '120g + 20f'). Three dashed colored curves funnel from each resource cell's right edge into the Barracks left edge: amber from Mine, green from Farm, brown from Forest. Two dashed blue arrows fan from the Barracks right edge to the Soldier and Archer cells.
A three-column resource pipeline โ€” RESOURCES โ†’ PRODUCTION โ†’ UNITS โ€” shown as a frozen snapshot of the lesson's RTS economy. Three resource sources (Mine, Farm, Forest) feed dashed colored flow lines into a central Barracks; the Barracks fans two dashed arrows out to the Soldier and Archer unit cells on the right, with example costs spelled out in monospace. The interactive demo lets you place these buildings, gather resources, and train any of the listed units; this diagram shows the economic pipeline that drives every decision in the live demo without depending on JavaScript.

Interactive Strategy Game Demo

A strategy game's resource โ†’ production โ†’ unit pipeline showing gold, wood, and food feeding production buildings that train units.
A strategy game's resource flow: gold/wood/food sources feed production buildings that train units. The interactive demo lets you build a base and command armies; this diagram shows the resource โ†’ production โ†’ military pipeline with example flow rates.

Build your base, gather resources, train units, and defeat the enemy!

Controls: Left-click to select, Right-click to command | Drag to box-select | Arrow keys or WASD to pan camera | Mouse wheel to zoom

Build Menu:

Unit Production:

Research:

๐Ÿ’ฐ Gold: 500 (+0/s)
๐ŸŒพ Food: 200 (+0/s)
๐Ÿชต Wood: 100 (+0/s)
๐Ÿ‘ฅ Population: 0/10

Strategy Game Implementation in Python

See the complete Python implementation for the full code including pathfinding, AI opponents, tech trees, and advanced combat mechanics.

Best Practices

โšก Strategy Game Best Practices

Key Takeaways

๐Ÿ‹๏ธโ€โ™‚๏ธ Practice Exercise

๐Ÿ‹๏ธโ€โ™‚๏ธ Exercise 1: Three Pillars of RTS Architecture โ€” Construction-Gate-at-onComplete + Stats-Table-Externalization + Snapshot-Research-Bonus in One Canvas Window

Objective: Build a runnable HTML5 canvas + JS program (roughly 60โ€“65 lines) that distills the lesson's GameMap + Unit + Building + AIController + gameState architecture into one cohesive demo where three orthogonal RTS-architecture disciplines are visible per frame on a single 800ร—420 canvas split into three labeled regions plus a HUD strip. (a) Building-lifecycle gate at onComplete() at BUILDING-LIFECYCLE scope โ€” Building.update(dt) ramps this.progress += dt * 25 until โ‰ฅ 100 then flips this.complete = true AND calls this.onComplete(), the EXCLUSIVE site that writes state.income += this.ratePerSec; during construction (progress < 100) the building exists on the canvas and occupies its slot but contributes ZERO income to the global tally, so the LEFT region renders three buildings (Farm, Mine, Barracks) under construction with per-building progress bars + a 'income/s' readout that stays at 0 until each building's progress hits 100, then ticks up at that building's per-second rate as a side-effect of the gate firing exactly once per building. The two-layer separation (data: ratePerSec in the constructor; gate: this.complete boolean check) is the same shape as chat-46 platformer_tilemap's is_solid-True-OOB invariant (default-to-deny at the boundary), chat-54 architecture_state_machines' on_exitโ†’reassignโ†’on_enter strict ordering, chat-56 architecture_component_systems' components-hold-state-systems-hold-per-tick-logic separation, and chat-57 architecture_event_systems' queued-emit-vs-immediate-emit timing modes, applied here at building-construction-lifecycle scope. (b) Stats-table externalization at UNIT/BUILDING-TYPE-DEFINITION scope โ€” DATA-DRIVEN EXTERNALIZATION; the Unit constructor holds a per-type dict literal UNIT_STATS = { worker: {...}, soldier: {...}, archer: {...} } and runs Object.assign(this, UNIT_STATS[type]) at every spawn; the MIDDLE region renders unit-spawn area where key 2 spawns soldiers using the dict lookup AND key H mutates UNIT_STATS.soldier.hp += 20 at runtime so the next-spawned soldier shows the new HP value while extant soldiers retain their old HP โ€” mutation propagates ONLY to subsequent spawns because Object.assign COPIES the dict's current values into the new instance at the moment of construction, never re-reading the dict thereafter. Same data-driven-externalization shape as the long second-half-Phase-8 streak โ€” chat-46 is_solid + chat-47 tile-palette JSON + chat-54 explicit transition table + chat-55 scene-stack lifecycle hooks + chat-56 required-components list + chat-57 listeners-by-event dict + chat-58 dataclass schema + chat-59 per-state perception + chat-60 TERRAIN_COST dict + chat-61 RUNNING-state stickiness + Selector child-list order + chat-62 personality-as-weighted-modifier dict + chat-63 local-rule additive forces + chat-79 skill-prerequisites-as-declarative-data-records โ€” applied here at UNIT-AND-BUILDING-TYPE-DEFINITION scope (the stats dict IS the canonical per-type definition the constructor consults exactly once per spawn). (c) Research bonus applied at creation time, not retroactively, at RESEARCH-EFFECT-TIMING scope โ€” CONTEXT-DETERMINES-CORRECT-CHOICE pattern; the Unit constructor checks if (state.researched.has('military')) this.attack *= 1.25 AT CONSTRUCTION TIME and the result is captured on the instance's own attack field forever, so a soldier spawned BEFORE the R key is pressed keeps its baseline attack (15) for the rest of its life while only soldiers spawned AFTER the R key receive the bonus (15 ร— 1.25 = 18.75). The RIGHT region renders a research panel with key R that flips state.researched.add('military') AND the HUD shows the soldier-count split into pre-R vs post-R cohorts with their average attack values, so the snapshot-vs-retroactive design choice is visible as concrete numbers per cohort. The alternative architecture โ€” live retroactive multiplier walking every extant unit after research completes โ€” is a legitimately different design choice with different gameplay rhythm: snapshot timing produces 'tech-up-THEN-rebuild' pacing where players must replace existing units to fully benefit (think AoE2's late-game army-replacement cycle); live timing produces 'research instantly empowers everything' pacing (think Civ's instant-tech-effect model). Both are valid; the lesson picks snapshot as a deliberate architectural choice. Same context-determines-correct-choice rhetoric as chat-49 polish_tweening / chat-50 polish_difficulty / chat-50 polish_playtesting / chat-52 client_server / chat-53 lag_compensation / chat-54 lobby / chat-60 pathfinding heuristic / chat-61 Selector vs Sequence / chat-62 utility-AI vs BT / chat-63 flocking emergence / chat-79 enum-roundtrip-vs-live-comparison-by-name โ€” chat-80/81 case is FIRST to make the snapshot-vs-retroactive distinction explicit at RESEARCH-EFFECT-TIMING scope. The HUD shows current income/s, total income accumulated, current UNIT_STATS.soldier.hp value, pre-R soldier count + average attack, post-R soldier count + average attack, and a researched: YES/NO indicator โ€” three orthogonal RTS-architecture disciplines visible per frame as concrete numbers and visibly-divergent cohort statistics. ADVANCES the genres module 5/9 โ†’ 6/9 partial at chat-81 M1; module-completeness stays 12/13 since genres doesn't close in one chat (3 genres lessons remain after chat-81: strategy_python / tower_defense / tower_defense_python).

Instructions:

  1. Add a <canvas id='rts' width='800' height='420'></canvas> element to the page and grab a 2D rendering context.
  2. Define a UNIT_STATS dict literal at module scope mapping each unit type (worker / soldier / archer) to {hp, attack, color} โ€” this is the externalized stats table the Unit constructor will consult once per spawn.
  3. Define a Building class whose update(dt) ramps this.progress until โ‰ฅ 100 then flips this.complete = true AND calls this.onComplete() EXACTLY ONCE; onComplete() is the EXCLUSIVE site that writes state.income += this.ratePerSec.
  4. Define a Unit class whose constructor calls Object.assign(this, UNIT_STATS[type]) to copy the type's stats onto the instance, then checks state.researched.has('military') AT CONSTRUCTION TIME and applies this.attack *= 1.25 if true โ€” capturing the research-snapshot on the instance's own field forever.
  5. Initialize a state object holding income: 0, three Building instances (Farm, Mine, Barracks), an empty soldiers array, and an empty researched Set.
  6. Wire keyboard handlers: key 2 pushes a new Unit('soldier') into state.soldiers; key H runs UNIT_STATS.soldier.hp += 20 (mutates the dict, propagates ONLY to subsequent spawns); key R runs state.researched.add('military') (flips snapshot result for next spawn only).
  7. In the animation loop, advance every building via state.buildings.forEach(b => b.update(dt)), accumulate state.totalIncome += state.income * dt, then render the three regions plus a HUD that shows income/s, total, current UNIT_STATS.soldier.hp, pre-R vs post-R soldier counts with average attack per cohort, and the researched flag.
  8. Verify all three axes by interacting: watch a building's progress bar fill, see income tick up the moment it crosses 100; press 2 to spawn a soldier with hp 100, press H to bump UNIT_STATS.soldier.hp to 120, press 2 again to see the new soldier spawn at hp 120 while the prior soldier stays at hp 100; press 2 a few times pre-R then press R then press 2 a few more times to see the post-R cohort show average attack 18.75 while the pre-R cohort stays at 15.
๐Ÿ’ก Hint

The three axes are three independent SHAPES of the same data โ€” the canvas is a window onto the same in-memory state from three orthogonal viewing angles per frame. Each renderer-read is O(1) per frame because the state is denormalized into the form the renderer wants: state.income is the running sum the gate maintains, the Unit instance's own attack field is the snapshot-frozen value, and UNIT_STATS.soldier.hp is the dict the next constructor will read. The maintenance cost lives in the WRITE paths: forgetting to call onComplete() exactly once at the gate boundary means income never starts (BUILDING-LIFECYCLE failure mode); reading UNIT_STATS in render() instead of capturing in the constructor means runtime mutations would propagate retroactively to extant units (UNIT-TYPE-DEFINITION failure mode); recomputing the research bonus in render() instead of capturing on the instance means pre-R cohorts would magically become post-R the frame R is pressed (RESEARCH-EFFECT-TIMING failure mode). Each axis has a specific failure mode if its discipline is broken, and each renderer-read stays cheap precisely because the discipline keeps the data in the right shape at the right time.

โœ… Example Solution
const ctx = document.getElementById('rts').getContext('2d');
const W = 800, H = 420;

// Axis B: Stats-table externalization โ€” adding a unit type = one dict entry
const UNIT_STATS = {
    worker:  { hp: 50,  attack: 5,  color: '#7fb069' },
    soldier: { hp: 100, attack: 15, color: '#d62828' },
    archer:  { hp: 60,  attack: 12, color: '#fcbf49' }
};

// Axis A: Building-lifecycle gate at onComplete()
class Building {
    constructor(name, x, ratePerSec) {
        this.name = name; this.x = x; this.ratePerSec = ratePerSec;
        this.progress = 0; this.complete = false;
    }
    update(dt) {
        if (this.complete) return;
        this.progress = Math.min(100, this.progress + dt * 25);
        if (this.progress >= 100) { this.complete = true; this.onComplete(); }
    }
    onComplete() { state.income += this.ratePerSec; }
}

// Axis C: Snapshot at construction โ€” bonus captured on instance forever
class Unit {
    constructor(type) {
        Object.assign(this, UNIT_STATS[type]);   // Axis B: copy stats once
        this.type = type;
        this.researchedAtSpawn = state.researched.has('military');
        if (this.researchedAtSpawn) this.attack *= 1.25;
    }
}

const state = {
    income: 0, totalIncome: 0,
    buildings: [
        new Building('Farm',     60,  2),
        new Building('Mine',    180,  5),
        new Building('Barracks', 300, 0)
    ],
    soldiers: [],
    researched: new Set()
};

window.addEventListener('keydown', e => {
    if (e.key === '2') state.soldiers.push(new Unit('soldier'));
    if (e.key === 'h') UNIT_STATS.soldier.hp += 20;       // mutates dict; propagates to next spawn only
    if (e.key === 'r') state.researched.add('military');  // flips snapshot for future spawns only
});

let last = performance.now();
function frame(now) {
    const dt = (now - last) / 1000; last = now;
    state.buildings.forEach(b => b.update(dt));
    state.totalIncome += state.income * dt;

    ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, W, H);
    state.buildings.forEach(b => {
        ctx.fillStyle = b.complete ? '#06d6a0' : '#555';
        ctx.fillRect(b.x, 60, 80, 80 * (b.progress / 100));
        ctx.strokeStyle = '#fff'; ctx.strokeRect(b.x, 60, 80, 80);
        ctx.fillStyle = '#fff'; ctx.font = '11px monospace';
        ctx.fillText(b.name + ' ' + Math.floor(b.progress) + '%', b.x, 50);
    });
    const pre  = state.soldiers.filter(s => !s.researchedAtSpawn);
    const post = state.soldiers.filter(s =>  s.researchedAtSpawn);
    const avg  = arr => arr.length ? (arr.reduce((s, u) => s + u.attack, 0) / arr.length).toFixed(2) : 'โ€”';
    ctx.fillStyle = '#fff'; ctx.font = '13px monospace';
    ctx.fillText('income/s: ' + state.income.toFixed(1) + '   total: ' + state.totalIncome.toFixed(0), 20, 200);
    ctx.fillText('UNIT_STATS.soldier.hp: ' + UNIT_STATS.soldier.hp, 20, 240);
    ctx.fillText('pre-R  soldiers: ' + pre.length  + '   avg attack ' + avg(pre),  20, 280);
    ctx.fillText('post-R soldiers: ' + post.length + '   avg attack ' + avg(post), 20, 300);
    ctx.fillText('researched military: ' + (state.researched.has('military') ? 'YES' : 'NO'), 20, 340);
    ctx.fillText('Keys: 2 = spawn soldier   H = +20 HP   R = research military', 20, 400);
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

๐ŸŽฏ Quick Quiz

Question 1: In the demo, Building.update(dt) ramps this.progress each frame but ONLY this.onComplete() writes state.income += this.ratePerSec, and onComplete() is called exactly once when this.progress first crosses 100. What does this construction-gate-at-onComplete design accomplish at BUILDING-LIFECYCLE scope?

Question 2: The demo's UNIT_STATS dict literal lives at module scope, and Unit's constructor runs Object.assign(this, UNIT_STATS[type]) at every spawn rather than holding a reference to UNIT_STATS[type]. Pressing H runs UNIT_STATS.soldier.hp += 20 โ€” the next-spawned soldier shows the new HP while extant soldiers retain their old HP. Why does the demo use this stats-table externalization shape at UNIT/BUILDING-TYPE-DEFINITION scope?

Question 3: The demo's Unit constructor checks if (state.researched.has('military')) this.attack *= 1.25 AT CONSTRUCTION TIME and the result is captured on the instance's own attack field forever. After the player spawns three soldiers, presses R, then spawns three more soldiers, the HUD shows pre-R soldiers at average attack 15 and post-R soldiers at average attack 18.75. Why does the demo apply the research bonus at creation time rather than retroactively walking every extant unit when R is pressed?

What's Next?

Now that you've mastered strategy game mechanics, next we'll explore puzzle game logic!