Polymorphic Cell Objects

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);
  }
}
ℹ️
The 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
}
🔎 Duck typing: the checks 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.