Building a Generic Rules System for a Web Based 2d Game Engine
Explore the process of designing a versatile and scalable rules system for a web-based 2D game engine. This article delves into the principles of creating a generic architecture that can adapt to various game types, ensuring flexibility and efficiency in development. Perfect for creators looking to build robust game worlds, writers with vast stories to tell, or developers looking to simplify their gaming experience.

The Challenge: Building for Multiple Game Types
When I set out to build a web-based 2D game engine, I quickly realized that the real challenge wasn't rendering sprites or handling input—it was creating a system flexible enough to support radically different game types. A turn-based RPG has completely different rules from a real-time strategy game, which differs entirely from a puzzle platformer.
The solution? A generic rules system that could adapt to any game type while maintaining performance and simplicity.
This is Where Inheritance is Your Friend
The seemingly dreaded JavaScript class
keyword becomes your best ally when building extensible game systems. Despite the functional programming trend in modern JavaScript, object-oriented patterns shine in game development where entities naturally inherit behaviors and properties.
Let's start with a base Item
class that establishes the foundation for all game objects:
export default class Item {
name: string
weight: number
value: number
constructor(name: string, weight: number, value: number) {
this.name = name
this.weight = weight
this.value = value
}
describe(): string {
return `${this.name} weighs ${this.weight} lbs and is worth ${this.value} gold pieces.`
}
// Generic method that can be overridden
use(character: Character): GameEffect | null {
return null // Base items can't be used
}
// Hook for custom behaviors
onPickup(character: Character): void {
// Override in subclasses for special pickup effects
}
}
Extending the Base: Specialized Item Types
Now we can create specialized items that inherit from our base class:
class Weapon extends Item {
damage: number
durability: number
weaponType: WeaponType
constructor(name: string, weight: number, value: number, damage: number, weaponType: WeaponType) {
super(name, weight, value)
this.damage = damage
this.durability = 100
this.weaponType = weaponType
}
use(character: Character): GameEffect {
return new AttackEffect(this.damage, this.weaponType)
}
describe(): string {
return `${super.describe()} It deals ${this.damage} damage and is ${this.durability}% intact.`
}
}
class Potion extends Item {
healAmount: number
potionType: PotionType
constructor(name: string, weight: number, value: number, healAmount: number, type: PotionType) {
super(name, weight, value)
this.healAmount = healAmount
this.potionType = type
}
use(character: Character): GameEffect {
return new HealEffect(this.healAmount, this.potionType)
}
onPickup(character: Character): void {
if (this.potionType === PotionType.CURSED) {
character.applyEffect(new CurseEffect())
}
}
}
The Rules Engine: Making It All Work Together
The magic happens in the rules engine—a system that coordinates all these objects and their interactions:
class GameRulesEngine {
private rules: Map<string, Rule> = new Map()
private effects: GameEffect[] = []
addRule(name: string, rule: Rule): void {
this.rules.set(name, rule)
}
processAction(action: GameAction): GameResult {
const applicableRules = this.findApplicableRules(action)
let result = new GameResult(action)
for (const rule of applicableRules) {
result = rule.apply(result, this.gameState)
}
this.processEffects(result)
return result
}
private findApplicableRules(action: GameAction): Rule[] {
return Array.from(this.rules.values())
.filter(rule => rule.appliesTo(action))
.sort((a, b) => a.priority - b.priority)
}
}
Adapting to Different Game Types
Here's where the system's flexibility really shines. The same base classes can support vastly different game mechanics:
Turn-Based RPG Rules:
class TurnBasedRule implements Rule {
appliesTo(action: GameAction): boolean {
return action.type === ActionType.COMBAT
}
apply(result: GameResult, gameState: GameState): GameResult {
// Ensure actions happen in turn order
if (!gameState.isPlayerTurn && result.actor.type === ActorType.PLAYER) {
return result.withError("Not your turn!")
}
return result
}
}
Real-Time Strategy Rules:
class ResourceManagementRule implements Rule {
appliesTo(action: GameAction): boolean {
return action.requiresResources
}
apply(result: GameResult, gameState: GameState): GameResult {
const cost = action.getResourceCost()
if (!gameState.player.hasResources(cost)) {
return result.withError("Insufficient resources")
}
gameState.player.deductResources(cost)
return result
}
}
Performance Considerations
Web-based games face unique performance challenges. The rules engine must be efficient enough to handle real-time gameplay:
class OptimizedRulesEngine extends GameRulesEngine {
private ruleCache: Map<string, Rule[]> = new Map()
processAction(action: GameAction): GameResult {
const cacheKey = action.getCacheKey()
if (!this.ruleCache.has(cacheKey)) {
this.ruleCache.set(cacheKey, this.findApplicableRules(action))
}
const rules = this.ruleCache.get(cacheKey)!
return this.applyRules(rules, action)
}
// Use requestAnimationFrame for smooth gameplay
private processEffectsAsync(effects: GameEffect[]): void {
const processChunk = (startIndex: number) => {
const endIndex = Math.min(startIndex + 10, effects.length)
for (let i = startIndex; i < endIndex; i++) {
this.processEffect(effects[i])
}
if (endIndex < effects.length) {
requestAnimationFrame(() => processChunk(endIndex))
}
}
requestAnimationFrame(() => processChunk(0))
}
}
Handling Edge Cases and Extensibility
A robust rules system must handle unexpected interactions gracefully:
interface Rule {
name: string
priority: number
appliesTo(action: GameAction): boolean
apply(result: GameResult, gameState: GameState): GameResult
// Allow rules to prevent other rules from applying
blocksRules?: string[]
// Allow rules to depend on other rules
requiresRules?: string[]
}
class ConditionalRule implements Rule {
constructor(
public name: string,
public priority: number,
private condition: (action: GameAction, gameState: GameState) => boolean,
private effect: (result: GameResult, gameState: GameState) => GameResult
) {}
appliesTo(action: GameAction): boolean {
return this.condition(action, gameState)
}
apply(result: GameResult, gameState: GameState): GameResult {
return this.effect(result, gameState)
}
}
Testing the System
A generic rules system requires comprehensive testing to ensure reliability across different game types:
describe('GameRulesEngine', () => {
let engine: GameRulesEngine
let gameState: GameState
beforeEach(() => {
engine = new GameRulesEngine()
gameState = new GameState()
})
test('applies rules in priority order', () => {
engine.addRule('high-priority', new MockRule('high', 1))
engine.addRule('low-priority', new MockRule('low', 10))
const action = new GameAction(ActionType.TEST)
const result = engine.processAction(action)
expect(result.appliedRules).toEqual(['high', 'low'])
})
test('handles rule conflicts gracefully', () => {
const blockingRule = new MockRule('blocker', 1, ['blocked'])
const blockedRule = new MockRule('blocked', 2)
engine.addRule('blocker', blockingRule)
engine.addRule('blocked', blockedRule)
const result = engine.processAction(new GameAction(ActionType.TEST))
expect(result.appliedRules).not.toContain('blocked')
})
})
Real-World Applications
This architecture has proven effective across multiple game types I've built:
- Text-based RPGs where complex item interactions drive the narrative
- Strategy games requiring resource management and unit coordination
- Puzzle games where rule combinations create emergent gameplay
The key insight is that games are fundamentally rule-driven systems. By creating a flexible, extensible rules engine, you're not just building a game—you're building a platform for infinite game possibilities.
Conclusion
Building a generic rules system for a web-based 2D game engine taught me that the best abstractions come from understanding the commonalities between seemingly different systems. Whether you're managing inventory in an RPG or resources in an RTS, the underlying patterns are remarkably similar.
The investment in creating a robust, extensible foundation pays dividends throughout development. Instead of hardcoding game logic, you're composing rules that can be mixed, matched, and modified to create entirely new gameplay experiences.
For developers embarking on similar projects, my advice is simple: start with the rules, not the graphics. A solid rules engine will carry your game much further than flashy visuals ever could.