Polymorphic Cell Objects
Inheritance allows you to define a new class based on an existing one, extending functionality and promoting code reuse. In JavaScript, inheritance is expressed with the extends
keyword. It abstracts heterogeneous entities into a single type, enabling them to be treated uniformly.
In the previous chapter, you implemented different base classes separately. Each had to be handled in its own way—an intentional limitation to highlight the problem. Here, we move beyond that: using inheritance and polymorphism, we unify diverse entities (Elf
, Goblin
, Tonic
) under a hierarchy rooted in a common Entity
superclass. This makes the code cleaner, interactions more consistent, and contracts easier to enforce.
This chapter teaches
- Inheritance and polymorphism (plus JavaScript’s duck typing)
- Using a class hierarchy with Quadrille
- How contracts and invariants carry over across types
From Base Classes to a Hierarchy
This demo extends the Cell Objects example into a small RPG skirmish: two creature types (Elf
, Goblin
) take turns moving, attacking, or picking up Tonic
items. Thanks to inheritance, they all derive from the same Entity
base and can be selected, drawn, and moved through the same invariant logic.
code
Quadrille.cellLength = 50;
const cellLength = Quadrille.cellLength;
const cols = 8, rows = 8;
let game;
let selected = null;
let currentTeam = 'Elf';
let randomMode;
// === BASE ENTITY ===
// Generic grid object with a symbol and position.
class Entity {
constructor(game, emoji, row, col) {
this.game = game;
this.emoji = emoji;
this.row = row;
this.col = col;
}
// Draw emoji, highlight if selected
display() {
textAlign(CENTER, CENTER);
textSize(cellLength * 0.5);
text(this.emoji, 0, 0);
if (selected === this) {
noStroke();
fill(255, 255, 0, 80);
rectMode(CENTER);
rect(0, 0, cellLength, cellLength);
}
}
// Move if target cell is empty
move(newRow, newCol) {
if (this.game.isEmpty(newRow, newCol)) {
this.game.fill(newRow, newCol, this);
this.game.clear(this.row, this.col);
this.row = newRow;
this.col = newCol;
return true;
}
return false;
}
}
// === TONIC ===
// Consumable that heals creatures.
class Tonic extends Entity {
constructor(game, row, col) {
super(game, '💊', row, col);
this.healing = 3; // >0 heals
}
}
// === CREATURE ===
// Living entity with life and power.
class Creature extends Entity {
constructor(game, emoji, row, col, maxLife, power = 3) {
super(game, emoji, row, col);
this.life = this.maxLife = maxLife;
this.power = power;
}
// Draw base + life bar
display() {
super.display();
const barWidth = cellLength * 0.4;
const barHeight = cellLength * 0.08;
const barY = cellLength * 0.35;
stroke(0);
fill(this.color);
rectMode(CENTER);
rect(0, barY, map(this.life, 0, this.maxLife, 0, barWidth), barHeight);
}
// Heal or damage. Return true if alive.
heal(amount) {
this.life = constrain(this.life + amount, 0, this.maxLife);
return this.life > 0;
}
}
// === CREATURE TYPES ===
class Elf extends Creature {
constructor(game, row, col) {
super(game, '🧝', row, col, 12, 4);
this.team = 'Elf';
this.color = 'dodgerblue';
}
}
class Goblin extends Creature {
constructor(game, row, col) {
super(game, '👺', row, col, 8, 3);
this.team = 'Goblin';
this.color = 'crimson';
}
}
// === P5 SKETCH ===
function setup() {
createCanvas(cols * cellLength, rows * cellLength);
reset();
}
function draw() {
background('LemonChiffon');
drawQuadrille(game, { options: { origin: CENTER } });
}
function mousePressed() {
const row = game.mouseRow, col = game.mouseCol;
const cell = game.read(row, col);
// First click: select only if it's your team's turn
if (!selected) {
selected = cell?.team === currentTeam ? cell : null;
return;
}
// Same-cell click: deselect current piece
if (selected === cell) {
selected = null;
return;
}
// Picking up a tonic (healing item)
if (cell?.healing) {
const before = selected.life;
selected.heal(cell.healing);
const delta = selected.life - before;
delta > 0 && console.log(`Picked medicine 💊, life +${delta}`);
game.clear(row, col);
selected = null;
currentTeam = currentTeam === 'Elf' ? 'Goblin' : 'Elf';
return;
}
// Attacking an enemy creature
if (cell?.team && cell.team !== currentTeam) {
if (!cell.heal(-selected.power)) { // returns false if dead
console.log(`${cell.team} defeated!`);
game.clear(row, col);
}
selected = null;
currentTeam = currentTeam === 'Elf' ? 'Goblin' : 'Elf';
return;
}
// Move to empty cell
if (selected.move(row, col)) {
selected = null;
currentTeam = currentTeam === 'Elf' ? 'Goblin' : 'Elf';
return;
}
// Re-select another of your own team
selected = cell?.team === currentTeam ? cell : null;
}
function keyPressed() {
(key === 'r' || key === 'R') && reset();
(key === 'm' || key === 'M') && (randomMode = !randomMode, reset());
}
function reset() {
game = createQuadrille(cols, rows);
const elf = Quadrille.factory(({ row, col }) => new Elf(game, row, col));
const goblin = Quadrille.factory(({ row, col }) => new Goblin(game, row, col));
const tonic = Quadrille.factory(({ row, col }) => new Tonic(game, row, col));
randomMode
? game.rand(cols, elf)
.rand(cols, goblin)
.rand(4, tonic)
: game.fill(({ row }) => row === 0, elf)
.fill(({ row }) => row === cols - 1, goblin)
.rand(4, tonic);
currentTeam = 'Elf';
selected = null;
}
The class diagram below shows how inheritance organizes shared behavior (Entity
), enriches it with life mechanics (Creature
), and then specializes into concrete types (Elf
, Goblin
, Tonic
):
classDiagram direction BT class Entity { +Quadrille game +string emoji +number row +number col +move(newRow number, newCol number) boolean +display() void } class Creature { +number life +number maxLife +number power +heal(amount number) boolean +display() void } class Elf { +string team +string color } class Goblin { +string team +string color } class Tonic { +number healing } Creature --|> Entity Elf --|> Creature Goblin --|> Creature Tonic --|> Entity
🧩 Try this
- Implement additional subclasses (
Orc
,Wizard
,Healer
) with distinct stats or visuals. - Add new artifacts (🍎, 💣) as
Entity
subclasses with their own interactions.
Polymorphic Displays
Inheritance allows subclasses to override methods while still calling their parent logic with the super
keyword. Here, Creature.display()
augments the base Entity.display()
with a life bar, while Elf
and Goblin
simply reuse the same implementation.
Polymorphic display()
class Entity {
display() {
textAlign(CENTER, CENTER);
textSize(cellLength * 0.5);
text(this.emoji, 0, 0);
if (selected === this) {
noStroke();
fill(255, 255, 0, 80);
rectMode(CENTER);
rect(0, 0, cellLength, cellLength);
}
}
}
class Creature extends Entity {
display() {
super.display(); // call Entity.display
const barWidth = cellLength * 0.4;
const barHeight = cellLength * 0.08;
const barY = cellLength * 0.35;
stroke(0);
fill(this.color);
rectMode(CENTER);
rect(0, barY, map(this.life, 0, this.maxLife, 0, barWidth), barHeight);
}
}
super
keyword works in JavaScript much like in Java or Python: it calls the parent’s constructor or method. (C++ uses a different syntax, but the principle is the same.)Mouse Interaction as Invariants
As in the previous chapter, the invariant is that selected
is always either null
or an entity. But now polymorphism and duck typing let us avoid anti-patterns like typeof
checks. Instead, the code relies on properties guaranteed by the hierarchy (team
, healing
, move
, heal
), accessed with optional chaining (?.
) to guard against null
.
mousePressed()
function mousePressed() {
const row = game.mouseRow, col = game.mouseCol;
const cell = game.read(row, col);
if (!selected) { // select only from current team
selected = cell?.team === currentTeam ? cell : null;
return;
}
if (selected === cell) { selected = null; return; } // deselect
if (cell?.healing) { /* pick tonic */ return; } // duck typing: healing
if (cell?.team && cell.team !== currentTeam) { /* attack */ return; }
if (selected.move(row, col)) { /* move */ return; } // inherited move()
selected = cell?.team === currentTeam ? cell : null; // switch
}
cell?.healing
and cell?.team
don’t care about the class name. They simply test whether the property exists—if so, it’s treated accordingly. This avoids brittle type checks and keeps interaction rules flexible.References
Further Reading
- Experiment with overriding
display
as a method vs. property and confirm how Quadrille enforces its display contract. - Explore advanced OOP patterns such as mixins, traits, or Entity–Component Systems (ECS) to structure more complex games.
- Compare JavaScript’s prototypal inheritance with classical inheritance in languages like Java or C++ to understand subtle differences in polymorphism.