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 achieved with the extends
keyword and, when needed, with super
to access the parent constructor or methods. It abstracts apparently heterogeneous entities behind a common interface so they can be treated uniformly.
In the previous chapter, you implemented several classes separately and had to handle each one on its own—by design. Here we move beyond that: with inheritance and polymorphism we organize Elf
, Goblin
, and Tonic
under a shared Entity
superclass. This simplifies code shared across the hierarchy, makes interactions more consistent, and makes contracts easier to enforce.
This chapter teaches
- Inheritance & polymorphism (with JavaScript’s duck typing)
- Building and using a class hierarchy with Quadrille
- Carrying contracts and invariants (selection, display, movement) across types
From ad-hoc handling to a hierarchy
In Cell Objects, each class had to be handled separately—effective for one-off use case, but quite limiting for scalability and reuse. Now we define a shared Entity
base with common state/behavior (grid reference, position, move
, display
) and derive specialized subclasses: two creatures (Elf
, Goblin
) and a consumable artifact (Tonic
). Interaction (select, move, attack, pick-up) is written once against the shared interface.
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
Strengths and limitations of this design
- Polymorphic display. Subclasses override
display()
to customize visuals, reusing common behavior (e.g., selection glow) and adding details like a life bar. - One invariant, many types. The click invariant—“
selected
is eithernull
or an entity”—remains, while the type hierarchy supplies the details. Mouse logic stays in four cases: select, deselect, act (attack/pick-up/move), or switch. - Duck typing with optional chaining. Properties are accessed safely and concisely, avoiding
null
/undefined
errors and keeping the code adaptable to new subclasses. - Missed abstraction. Interaction logic (
attack
,pick-up
,move
) still lives inmousePressed()
. A more scalable design would move it into aninteract(target)
method onEntity
and its subclasses—this is proposed as an exercise below.
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 invoking parent logic with super.display()
. For example, Creature.display()
extends Entity.display()
adding a life bar, whereas Elf
and Goblin
reuse the inherited version unchanged.
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 a method.Try this
- Add auras & states. Reuse the base glow and extend it with icons or halos for states (e.g., 🛡️ shield, ☠️ poison); fade them in
display()
without touching mouse logic. - Damage/heal feedback. Briefly flash the emoji (scale or alpha) when
heal(±n)
is called; keep the effect insidedisplay()
so interactions remain unchanged. - Subclass specializations. Add a
Healer
(green bar, low power, grants+1
to adjacent allies) and aBrute
(wide life bar, knockback effect). Both inheritCreature
. - No-
super
experiment. Temporarily dropsuper.display()
in one subclass and re-implement the selection glow—observe the duplication pressure that inheritance fixes. - Decorator exercise. Write a tiny
withOutline(entity)
wrapper that returns an object with adisplay()
method calling the inner one, then drawing an outline; swap it in to confirm the contract holds.
Mouse Interaction as Invariants
Mouse interaction remains simple by keeping the same invariant from the previous chapter: selected
is always either null
or a cell object. With this in place, each click falls into four cases—select, deselect, act, or switch. Inheritance makes actions like move()
available across subclasses, while duck typing with optional chaining keeps the logic concise and flexible across different entity types.
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
}
Instead of checking class names, the logic uses
cell?.healing
and cell?.team
. If a property exists, it drives the action; if not, it’s skipped. This avoids anti-pattern type checks, keeps the code concise, and makes interactions flexible across different entities.Try this
- Drag-to-move. Replace two-click moves with drag: on
mousePressed
setselected
; onmouseDragged
preview the target cell; onmouseReleased
run the same “act” step (pick-up/attack/move/switch). - Hover previews. While dragging, show legal targets (empty, enemy, tonic) with subtle highlights; keep the invariant and reuse the existing checks (
cell?.team
,cell?.healing
). - Snap vs. cancel. If release occurs outside the board, cancel; if on your own piece, switch selection. Keep the four cases intact.
- Shift-drag path. With
SHIFT
held, allow a multi-cell slide (row/col straight line); still callselected.move(row,col)
once at release. - Timeline hooks. After each successful action on release, append a snapshot to your Timelines; add
z
/y
keys for undo/redo to test consistency. - Abstract interaction. Move the click logic into an
interact(target)
method onEntity
. Subclasses (Tonic
,Creature
, etc.) override it, somousePressed()
reduces toselected?.interact(cell)
.
References
- JavaScript
extends
(MDN) — subclassing andsuper
. - Optional chaining
?.
(MDN) — concise, safe property access. - Quadrille API —
drawQuadrille
(display contract),fill
,clear
,isEmpty
,factory
. - Related chapters: Cell Objects · Cell Effects · Layered Boards · Timelines.
Further Reading
- Cardelli, L., & Wegner, P. (1985). On Understanding Types, Data Abstraction, and Polymorphism. ACM Computing Surveys — classic on polymorphism and subtyping.
- Liskov, B., & Wing, J. M. (1994). A Behavioral Notion of Subtyping. ACM TOPLAS — the LSP foundation for substitutability.
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns. Addison-Wesley — especially Strategy and Visitor for interactions.
- Cook, W. (1989). Inheritance is not Subtyping — subtle distinctions when designing class hierarchies.
- Nystrom, R. (2021). Crafting Interpreters (free online) — practical take on OOP runtimes; helpful perspective on method dispatch.