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:
- Resource Management: Economy drives everything
- Unit Control: Command armies and workers
- Base Building: Construct and expand
- Tech Trees: Research and progression
- Fog of War: Information as a resource
- Combat: Tactical engagements
Interactive Strategy Game Demo
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:
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
- Economy Balance: Careful resource income vs costs
- Unit Diversity: Each unit type should have a role
- Tech Trees: Meaningful progression choices
- AI Challenge: Progressive difficulty scaling
- Map Design: Strategic terrain placement
- Fog of War: Information as a resource
- Hotkeys: Efficient control schemes
- Feedback: Clear visual and audio cues
Key Takeaways
- ๐ฐ Resource management creates strategic depth
- ๐๏ธ Base building provides progression
- โ๏ธ Unit diversity enables tactics
- ๐บ๏ธ Fog of war adds uncertainty
- ๐ฌ Tech trees offer customization
- ๐ค AI opponents provide challenge
- ๐ Economy balance is critical
- ๐ฏ Clear objectives guide players
๐๏ธโโ๏ธ 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:
- Add a
<canvas id='rts' width='800' height='420'></canvas>element to the page and grab a 2D rendering context. - Define a
UNIT_STATSdict literal at module scope mapping each unit type (worker / soldier / archer) to{hp, attack, color}โ this is the externalized stats table theUnitconstructor will consult once per spawn. - Define a
Buildingclass whoseupdate(dt)rampsthis.progressuntil โฅ 100 then flipsthis.complete = trueAND callsthis.onComplete()EXACTLY ONCE;onComplete()is the EXCLUSIVE site that writesstate.income += this.ratePerSec. - Define a
Unitclass whose constructor callsObject.assign(this, UNIT_STATS[type])to copy the type's stats onto the instance, then checksstate.researched.has('military')AT CONSTRUCTION TIME and appliesthis.attack *= 1.25if true โ capturing the research-snapshot on the instance's own field forever. - Initialize a
stateobject holdingincome: 0, threeBuildinginstances (Farm, Mine, Barracks), an emptysoldiersarray, and an emptyresearchedSet. - Wire keyboard handlers: key 2 pushes a new
Unit('soldier')intostate.soldiers; key H runsUNIT_STATS.soldier.hp += 20(mutates the dict, propagates ONLY to subsequent spawns); key R runsstate.researched.add('military')(flips snapshot result for next spawn only). - In the animation loop, advance every building via
state.buildings.forEach(b => b.update(dt)), accumulatestate.totalIncome += state.income * dt, then render the three regions plus a HUD that shows income/s, total, currentUNIT_STATS.soldier.hp, pre-R vs post-R soldier counts with average attack per cohort, and the researched flag. - 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.hpto 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!