A practical exercise in learning closures by building a tiny idle game - no frameworks, just vanilla JavaScript.
Early in my development journey, I struggled with JavaScript closures. The concept felt abstract and slippery - I could read the definitions, but they didn't quite click. So I did what I often do when learning something new: I built a small project that forced me to use them extensively.
The result was Closures & Callstacks, a simple browser-based idle game where a party of adventurers battles a dragon. Built with nothing but vanilla HTML, CSS, and JavaScript - no frameworks, no libraries - it served its purpose: by structuring the entire application around factory functions and closures, I finally internalised how they work.
The Game
The premise is straightforward: you generate a party of three adventurers (fighters, wizards, and clerics, each with their own ASCII art representation), then watch them battle an ancient dragon in turn-based combat. Characters attack enemies or heal allies based on simple AI, with health bars updating in real-time and a combat log narrating the action. Victory or defeat depends on whether your party can whittle down the dragon's health before it stomps and firebreathes your adventurers into oblivion.
The Core Pattern: Factory Functions
The game's architecture revolves around factory functions - functions that return objects with methods. These methods "close over" private variables, creating encapsulated state without needing classes or the new keyword.
Here's a simplified version of the health system:
function healthFunctions() {
let maxHealth = 0;
let currentHealth = 0;
let isKO;
return {
setMaxHealth: function(maxHP) {
maxHealth = maxHP;
currentHealth = maxHealth;
isKO = false;
return maxHealth;
},
getCurrentHealth: function() {
return currentHealth;
},
takeDamage: function(damage) {
currentHealth = Math.max((currentHealth -= damage), 0);
isKO = currentHealth === 0 ? true : false;
return currentHealth;
},
healDamage: function(heal) {
if (isKO) {
isKO = false;
}
currentHealth = Math.min((currentHealth += heal), maxHealth);
return currentHealth;
},
isKO: function() {
return isKO;
}
};
}
The variables maxHealth, currentHealth, and isKO are truly private. There's no way to access them directly from outside the function - you can only interact with them through the returned methods. Each method maintains a reference to these variables through closure, even after healthFunctions() has finished executing.
Building Characters with Closures
The player factory follows the same pattern but at a larger scale:
function playerFunctions() {
let _id;
let _name;
let _playerClass;
let _allies = {};
let _enemies = {};
let _profBonus = 1;
let playerAttack = {};
let playerBuffs = {};
const playerHealth = healthFunctions();
return {
init: function(name, playerClass, allies, enemies, id) {
_id = id;
_name = name;
_allies = allies;
_enemies = enemies;
_playerClass = playerClass;
playerHealth.setMaxHealth(calculateHP(playerClass));
playerAttack = attackFunctions(_profBonus, _playerClass);
playerBuffs = buffFunctions(_profBonus, _playerClass);
},
getName: function() {
return _name;
},
get playerClass() {
return _playerClass;
},
get health() {
return playerHealth;
},
takeTurn: function() {
if (this.health.isKO(true)) {
return;
}
// ... game logic for taking actions
}
};
}
Each character created by playerFunctions() maintains its own private state. The playerHealth variable holds a reference to a health system (itself created by a factory function), and all the character's methods can access it through closure.
What I Learned
Building this game made closures tangible for me. Instead of being an abstract concept, they became a practical tool for:
- Data Privacy: No risk of external code accidentally modifying internal state
- Encapsulation: Each character or system manages its own data
-
Composition: Factory functions can call other factory functions (like
playerHealth = healthFunctions()) to build complex behaviours
Is this the most efficient way to structure a game? Probably not. The code has plenty of rough edges I'd refactor now (I’ve chosen to leave the code as-is for posterity!). But as a learning exercise, it was invaluable. By forcing myself to use closures everywhere - for health tracking, attack systems, buff management, and game state - I developed an intuitive understanding that stuck.
Sometimes the best way to learn a concept is to build something with it, even if that something is a simple dragon-fighting game with ASCII art characters.

Top comments (0)