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 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 either null 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 in mousePressed(). A more scalable design would move it into an interact(target) method on Entity 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);
  }
}
The 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 inside display() so interactions remain unchanged.
  • Subclass specializations. Add a Healer (green bar, low power, grants +1 to adjacent allies) and a Brute (wide life bar, knockback effect). Both inherit Creature.
  • No-super experiment. Temporarily drop super.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 a display() 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
}
Duck typing with optional chaining.
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 set selected; on mouseDragged preview the target cell; on mouseReleased 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 call selected.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 on Entity. Subclasses (Tonic, Creature, etc.) override it, so mousePressed() reduces to selected?.interact(cell).

References

Further Reading