Final Portfolio

This is my final portfolio ik its long

CS111 Portfolio Checklist

Done Category Objective Evidence
OOPWriting ClassesGameLevelForestDeath, NpcAiChat, and 5 other level classes
OOPMethods & ParameterscleanAndTransition(targetLevelClass, primaryGame), _bubble(text, who), launchSublevel(levelClass)
OOPInstantiation & Objectsthis.classes arrays in every level; 12 objects in GameLevelMazeSub
OOPInheritance (Basic)GameObject → Character → Player / Npc, plus Barrier, Coin, GameEnvBackground
OOPMethod Overridinginteract and reaction overridden on every NPC sprite config
OOPConstructor Chainingnew NpcAiChat(...) x3 in GameLevelForestWin; new GameControl(..., { parentControl })
Control StructuresIterationFisher-Yates for loop, forEach on container children, .map() for door sprites, for loop for typing dots
Control StructuresConditionalsNPC interaction guard (if dialogueSystem && isDialogueOpen), Exit Warden transition, NpcAiChat.close() guard
Control StructuresNested Conditions3-level nesting in GameLevelDoors.js; empty-input → try/catch → reply/error in NpcAiChat.send()
Data TypesNumbersSCALE_FACTOR, STEP_FACTOR, ANIMATION_RATE, pixel dimensions, Math.round(rx * width)
Data TypesStringsid: 'The Wraith', sprite src paths, template literals for CSS and error messages
Data TypesBooleansGRAVITY: false/true, isCorrect, isOpen() returning !!this.container && ...
Data TypesArraysdialogues[], doorConfigs[], xPositions[], this.history[], this.classes[]
Data TypesObjects (JSON)Full sprite config objects (sprite_data_r2d2), API request body via JSON.stringify(...)
OperatorsMathematicalMath.round(rx * width), Math.floor(Math.random() * n), % taunts.length, _tauntIndex++
OperatorsString OperationsTemplate literals for CSS (justify-content:${...}), error strings (API ${res.status}), key lookup with || fallback
OperatorsBoolean Expressions&& interaction guard, || fallback for parentControl, ! negation, ?. and ?? in API reply
Input / OutputKeyboard InputPlayer controlled via engine key event listeners (arrow keys / WASD)
Input / OutputCanvas Renderingdraw() via GameEngine; sprites, backgrounds, platforms rendered each level
Input / OutputGameEnv ConfigurationgameEnv passed into every level constructor; GameControl and level arrays configured in each file
Input / OutputAPI Integrationfetch POST to AI NPC API in NpcAiChat._ask() with model and messages
Input / OutputAsynchronous I/Oasync _ask() method; await fetch(...) in NpcAiChat
Input / OutputJSON ParsingJSON.stringify(...) for API body; data.content.find(b => b.type === 'text')?.text ?? '...'
DocumentationCode CommentsInline comments throughout all level files explaining logic
DocumentationMini-Lesson DocumentationPortfolio page with embedded live game runners
DocumentationCode HighlightsAnnotated code snippets with explanations for every objective
DebuggingConsole Debuggingconsole.log("Initializing GameLevelForestDeath..."), console.error('NPC AI error:', e)
DebuggingHit Box VisualizationTuned widthPercentage and heightPercentage on all sprite hitboxes in GameLevelMazeSub.js by toggling the GameEngine's hitbox overlay
DebuggingSource-Level DebuggingSet a breakpoint on line 54 of GameLevelForestDeath.js to step through if (dist < 50 && !chaseState.caught) and find the correct catch threshold
DebuggingNetwork DebuggingUsed the Network tab to find GameLevelForestSub.js returning a 404 while all other scripts loaded 200 — traced to a mismatched import path
DebuggingApplication DebuggingInspected the DOM to find stale canvas elements left behind after sublevel transitions; fixed with Array.from(gameContainer.children).forEach(...) cleanup
DebuggingElement InspectionConsole showed TypeError: Failed to fetch dynamically imported module from GameExecutor.js:338; traced to a renamed file with an outdated import string
TestingGameplay TestingLive game runner embeds for every level in the portfolio
TestingIntegration TestingLive NPC AI chat in GameLevelForestWin
TestingAPI Error Handlingtry/catch in NpcAiChat.send(); console.error('NPC AI error:', e)

Writing Classes

My game, Escape the Tower, is built around six custom level classes that sit on top of the GameEngine’s base system. Each one is its own file and its own class — GameLevelForestDeath is a good example to walk through since it shows the core pattern clearly.

The constructor takes a gameEnv reference, then builds a this.classes array that the engine uses to figure out what objects to create when the level loads:

// GameLevelForestDeath.js
class GameLevelForestDeath {
  constructor(gameEnv) {
    console.log("Initializing GameLevelForestDeath...");
    this.gameEnv = gameEnv;
    this.classes = [
      { class: GameEnvBackground, data: image_data_bg       },
      { class: Player,            data: sprite_data_player   },
      { class: Npc,               data: sprite_data_beckoner },
      { class: Npc,               data: sprite_data_victim   },
    ];
  }
}
export default GameLevelForestDeath;

Every one of my seven levels — GameLevelMaze, GameLevelMazeSub, GameLevelDoors, GameLevelForest, GameLevelForestSub, GameLevelForestDeath, and GameLevelForestWin — is built this same way. They each have their own NPC setups, sprite configs, and transition logic, but they all share this constructor pattern.

You can play the Forest Death level below to see GameLevelForestDeath running live:

Challenge

Play the Forest Death level. Use WASD or arrow keys to move. Approach the NPCs to trigger interactions!

Lines: 1 Characters: 0
Game Status: Not Started

Methods & Parameters

Two helpers I wrote show up repeatedly across the level files. cleanAndTransition in GameLevelDoors.js takes two parameters — the target level class and the game instance — and handles the fade + swap sequence:

// GameLevelDoors.js
function cleanAndTransition(targetLevelClass, primaryGame) {
  // creates a fade overlay, clears the game container,
  // then calls primaryGame.transitionToLevel()
}

launchSublevel in GameLevelForestSub.js wraps everything needed to spin up a game-inside-a-game into a single one-parameter call:

// GameLevelForestSub.js
function launchSublevel(levelClass) {
  const primaryGame = gameEnv.gameControl;
  // ... fade, pause, new GameControl, start
}

Run this to see the actual return values from both functions:

Code Runner Challenge

Run the code to see the method signatures from the Escape Game printed with real output.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Instantiation & Objects

The GameEngine reads this.classes and calls new class(data, gameEnv) for each entry automatically — so my job in each level is just to build that array correctly. GameLevelMazeSub.js is my most object-heavy level with 12 entries total, including six barrier platforms, a coin, three NPCs, and the player:

// GameLevelMazeSub.js
this.classes = [
  { class: GameEnvBackground, data: image_data_cave },
  { class: Barrier, data: floor },
  { class: Barrier, data: step1 },
  { class: Barrier, data: step2 },
  { class: Barrier, data: step3 },
  { class: Barrier, data: step4 },
  { class: Barrier, data: step5 },
  { class: Coin,    data: sprite_data_coin    },
  { class: Npc,     data: sprite_data_shadow  },
  { class: Npc,     data: sprite_data_lantern },
  { class: Npc,     data: sprite_data_warden  },
  { class: Player,  data: sprite_data_octopus },
];

GameLevelDoors.js takes it further by generating door instances dynamically through .map() and spreading them into the classes array at the end:

// GameLevelDoors.js — dynamic instantiation via .map()
const doorSprites = doorConfigs.map((cfg, i) => {
  const isCorrect = (i === correctIndex);
  return {
    ...doorDefaults,
    id: cfg.id,
    src: cfg.src,
    INIT_POSITION: { x: xPositions[i], y: 0.5 },
    interact: isCorrect ? function() { ... } : function() { ... }
  };
});

this.classes = [
  { class: GameEnvBackground, data: image_data_water },
  { class: Player,            data: sprite_data_octopus },
  ...doorSprites.map(data => ({ class: Npc, data }))
];

This runner simulates what the engine does when it processes this.classes:

Code Runner Challenge

Run the code to simulate how the GameEngine reads this.classes and instantiates all 12 objects from GameLevelMazeSub.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Play the Maze level to see all 12 objects running at once:

Challenge

Play the Maze Sub Level. Climb the staircase platforms to reach the Exit Warden and trigger a level transition!

Lines: 1 Characters: 0
Game Status: Not Started

Inheritance (Basic)

My game plugs into the GameEngine’s existing inheritance chain rather than rebuilding it from scratch. The hierarchy looks like this:

GameObject  (GameEngine base)
  └─ Character
       ├─ Player      (used in every level as the controllable octopus)
       └─ Npc         (used for all NPCs: Wraith, Warden, R2D2, doors, etc.)
  └─ GameEnvBackground
  └─ Barrier          (used in GameLevelMazeSub for staircase platforms)
  └─ Coin             (used in GameLevelDoors, GameLevelMazeSub, GameLevelForestWin)

All my level files import and use these engine classes via extends. NpcAiChat in GameLevelForestWin.js is a standalone class that doesn’t extend anything from the engine — so that file actually shows both patterns next to each other.

Play the full game here to see every class from the hierarchy active at once:

Challenge

Play the full Escape Game. Every class in the inheritance hierarchy is running live — Player, Npc, Barrier, Coin, and GameEnvBackground all instantiated from the same engine base.

Lines: 1 Characters: 0
Game Status: Not Started

Method Overriding

Every NPC in the game has its own interact function defined directly on its sprite data object, which overrides the default behavior from the base Npc class. The Strange Beckoner in GameLevelForestDeath.js is the most involved one — it tracks its own _tauntIndex, cycles through a list of taunts, and includes a button that sends the player back to a previous level:

// GameLevelForestDeath.js — sprite_data_beckoner
interact: function() {
  if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
    this.dialogueSystem.closeDialogue();
    return;
  }
  if (!this.dialogueSystem) this.dialogueSystem = new DialogueSystem();

  this._tauntIndex = (this._tauntIndex || 0);
  const taunts = [
    "Oh, you came back to talk to me? Interesting.",
    "Still here? I'd have thought the shame would have driven you off.",
    "You know the right path is still there. Not that it'll help you.",
    "BAWK BAWK. Classic. An absolute classic.",
    "Fine. The fork is back that way."
  ];
  const msg = taunts[this._tauntIndex % taunts.length];
  this._tauntIndex++;

  this.dialogueSystem.showDialogue(msg, "Strange Beckoner", this.spriteData.src);
  this.dialogueSystem.addButtons([
    {
      text: "Go back to the fork",
      action: () => {
        primaryGame.levelClasses      = [GameLevelForestSub];
        primaryGame.currentLevelIndex = 0;
        primaryGame.transitionToLevel();
      }
    },
    { text: "...", action: () => this.dialogueSystem.closeDialogue() }
  ]);
}

Run this to watch the taunt cycle in isolation — it wraps back around after 5 presses:

Code Runner Challenge

Run the code to see the Strange Beckoner's taunt cycle — the same logic from GameLevelForestDeath.js, cycling through taunts and wrapping back around.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Constructor Chaining

GameLevelForestWin.js creates three separate NpcAiChat instances inside its constructor, each with its own name, system prompt, and avatar image:

// GameLevelForestWin.js — three NpcAiChat instances chained from parent constructor
const r2d2Chat     = new NpcAiChat('R2D2',          PERSONA_R2D2,    "/images/.../r2_idle.png");
const elderChat    = new NpcAiChat('Village Elder',  PERSONA_ELDER,   "/images/.../tux.png");
const villagerChat = new NpcAiChat('Villager',       PERSONA_VILLAGER,"/images/.../octocat.png");

GameControl uses a parentControl option so a sublevel can hold a reference back to the game that launched it, which is how the nested game-in-game structure communicates:

// GameLevelForest.js — constructor chaining via GameControl
const gameInGame = new GameControl(gameEnv.game, levelArray, {
  parentControl: primaryGame
});
gameInGame.start();
gameInGame.gameOver = function() {
  primaryGame.resume();
};

Play the Forest Win level and talk to any of the three NPCs to see the separate chat instances in action:

Challenge

Play the Forest Win level. Walk up to R2D2, the Village Elder, or the Villager and interact with them. Each NPC has its own NpcAiChat instance created by constructor chaining!

Lines: 1 Characters: 0
Game Status: Not Started

Control Structures

Iteration

I used four different kinds of loops across the project, each in a spot where it made the most sense.

for loop — Fisher-Yates shuffle in GameLevelDoors.js randomizes which x-position each door gets assigned:

// GameLevelDoors.js
const xPositions = [0.2, 0.35, 0.5, 0.65, 0.8];
for (let i = xPositions.length - 1; i > 0; i--) {
  const j = Math.floor(Math.random() * (i + 1));
  [xPositions[i], xPositions[j]] = [xPositions[j], xPositions[i]];
}

forEach loop — used on every level transition to strip old canvas elements out of the container before the new level loads:

// GameLevelForestDeath.js
Array.from(gameContainer.children).forEach(child => {
  if (child.id !== 'promptDropDown') gameContainer.removeChild(child);
});

.map() loopGameLevelDoors.js transforms an array of door configs into a full array of sprite data objects:

// GameLevelDoors.js
const doorSprites = doorConfigs.map((cfg, i) => {
  const isCorrect = (i === correctIndex);
  return { ...doorDefaults, id: cfg.id, src: cfg.src, INIT_POSITION: { x: xPositions[i], y: 0.5 } };
});

for loopNpcAiChat._typingBubble() builds 3 animated dots with staggered delays:

// GameLevelForestWin.js
for (let i = 0; i < 3; i++) {
  const d = document.createElement('span');
  d.style.animation = `npcDot 1s ease-in-out ${i * 0.18}s infinite`;
  b.appendChild(d);
}

Run this to see the shuffle and .map() produce a fresh door layout each time:

Code Runner Challenge

Run the code to see the Fisher-Yates shuffle and .map() from GameLevelDoors.js. Run it multiple times — the door layout changes every time!

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Play the Doors level — the door order is different every single time the page loads:

Challenge

Play the Doors Level! The doors are shuffled into a random order every time. Find the correct door to advance to the Forest.

Lines: 1 Characters: 0
Game Status: Not Started

Conditionals

Interaction guard — every NPC across every level file uses this same pattern to check whether a dialogue box is already open before doing anything:

// GameLevelForest.js — sprite_data_wraith
interact: function() {
  if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
    this.dialogueSystem.closeDialogue();
    return;
  }
  if (!this.dialogueSystem) {
    this.dialogueSystem = new DialogueSystem();
  }
  this.showRandomDialogue();
}

State transition conditional — the Exit Warden in GameLevelMazeSub.js checks whether it should use a parent controller or the current one before triggering the transition:

// GameLevelMazeSub.js — Exit Warden
const topGame = primaryGame?.parentControl || primaryGame;
if (topGame) {
  topGame.levelClasses      = [GameLevelDoors];
  topGame.currentLevelIndex = 0;
  topGame.isPaused          = false;
  topGame.transitionToLevel();
}

Guard conditionalNpcAiChat.close() does nothing if the panel has already been removed from the page:

// GameLevelForestWin.js
close() {
  if (!this.isOpen()) return;
  const panel   = this.container.querySelector('.npc-chat-panel');
  const overlay = this.container.querySelector('.npc-chat-overlay');
  if (panel)   { panel.style.opacity = '0'; }
  if (overlay) { overlay.style.opacity = '0'; }
}

Run this to see the dialogue guard toggle back and forth across three calls:

Code Runner Challenge

Run the code to see the NPC dialogue guard and state transition conditional from the Escape Game. Watch how the dialogue state changes with each interact call.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Nested Conditions

GameLevelDoors.js has three levels of nesting stacked inside a single .map() call. The outer layer picks the correct vs. wrong door, the middle layer checks dialogue state, and the deepest layer fires the actual level transition:

// GameLevelDoors.js
const doorSprites = doorConfigs.map((cfg, i) => {
  const isCorrect = (i === correctIndex);           // level 1: correct door?

  return {
    interact: isCorrect
      ? function() {
          if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {  // level 2: dialogue open?
            this.dialogueSystem.closeDialogue();
            return;
          }
          if (!this.dialogueSystem) this.dialogueSystem = new DialogueSystem();
          this.dialogueSystem.addButtons([
            {
              text: "Enter",
              action: () => {
                this.dialogueSystem.closeDialogue();
                cleanAndTransition(GameLevelForest, gameEnv.gameControl); // level 3: fire transition
              }
            },
            { text: "Not yet", action: () => this.dialogueSystem.closeDialogue() }
          ]);
        }
      : function() {                                // else branch: dead end
          if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
            this.dialogueSystem.closeDialogue();
            return;
          }
          if (!this.dialogueSystem) this.dialogueSystem = new DialogueSystem();
          this.dialogueSystem.showDialogue(cfg.deadEnd, "Dead End!", this.spriteData.src);
        }
  };
});

Run this to see which door gets picked and what each branch outputs:

Code Runner Challenge

Run the code to see the nested door conditional logic from GameLevelDoors.js. The correct door changes every run — see which one it picks!

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Data Types

Numbers

Every level uses numbers for position, scale, animation speed, and pixel dimensions. The octopus player config in GameLevelForest.js is a clear example:

// GameLevelForest.js — numeric properties on sprite_data_octopus
const OCTOPUS_SCALE_FACTOR = 5;
const sprite_data_octopus = {
  SCALE_FACTOR:    OCTOPUS_SCALE_FACTOR,  // 5
  STEP_FACTOR:     1000,
  ANIMATION_RATE:  50,
  INIT_POSITION:   { x: 0.05, y: 0.85 },
  pixels:          { height: 250, width: 167 },
  hitbox:          { widthPercentage: 0.45, heightPercentage: 0.2 },
};

GameLevelMazeSub.js calculates all platform positions by multiplying ratios against the canvas dimensions at runtime:

// GameLevelMazeSub.js
function b(id, rx, ry, rw, rh) {
  return {
    x:      Math.round(rx * width),
    y:      Math.round(ry * height),
    width:  Math.round(rw * width),
    height: Math.round(rh * height),
  };
}
const step3 = b('step3', 0.41, 0.40, 0.22, 0.03);

Strings

Character IDs, image paths, dialogue text, and CSS values are all strings:

// GameLevelForest.js
const sprite_data_wraith = {
  id:       'The Wraith',
  greeting: "...it took my family. Both paths lead somewhere.",
  src:      "/images/projects/escape-game/tux.png",
};

NpcAiChat in GameLevelForestWin.js uses template literals to build error messages and dynamic CSS on the fly:

// GameLevelForestWin.js
throw new Error(`API ${res.status}`);

row.style.cssText = `display:flex;justify-content:${who === 'user' ? 'flex-end' : 'flex-start'}`;

d.style.animation = `npcDot 1s ease-in-out ${i * 0.18}s infinite`;

Booleans

Boolean values show up in gravity flags, selection states, and open/closed checks:

// GameLevelForest.js — GRAVITY flag
GRAVITY: false,    // octopus floats in the forest level

// GameLevelMaze.js — GRAVITY flag flipped for dungeon
GRAVITY: true,

// GameLevelDoors.js — boolean for correct door selection
const isCorrect = (i === correctIndex);   // true for exactly one door

// GameLevelForestWin.js — NpcAiChat.isOpen() returns boolean
isOpen() {
  return !!this.container && document.body.contains(this.container);
}

Arrays

Arrays hold dialogue lines, door configurations, position sets, chat history, and level class lists:

// GameLevelForest.js — dialogues array
dialogues: [
  "...it took my family. Both paths lead somewhere. Not all somewheres are safe.",
  "The trees shift when the fog comes in. I stopped trusting my eyes.",
  "I wandered left. I ended up here. I cannot leave.",
  "Follow the light... if you can find any."
],

// GameLevelDoors.js — array of door config objects
const doorConfigs = [
  { id: 'Blue Door',   src: "...", greeting: "...", deadEnd: "..." },
  { id: 'Brown Door',  src: "...", greeting: "...", deadEnd: "..." },
  { id: 'Green Door',  src: "...", greeting: "...", deadEnd: "..." },
  { id: 'Orange Door', src: "...", greeting: "...", deadEnd: "..." },
  { id: 'Red Door',    src: "...", greeting: "...", deadEnd: "..." },
];

// GameLevelForestWin.js — chat history as growing array
this.history = [];
this.history.push({ role: 'user',      content: text  });
this.history.push({ role: 'assistant', content: reply });

Objects (JSON)

Every sprite is configured as a nested object literal. The R2D2 sprite in GameLevelForestSub.js is one of the more detailed ones:

// GameLevelForestSub.js
const sprite_data_right = {
  id:             'R2D2',
  greeting:       "I have been waiting for someone to choose correctly.",
  src:            "/images/projects/escape-game/r2_idle.png",
  SCALE_FACTOR:   8,
  ANIMATION_RATE: 100,
  pixels:         { height: 223, width: 505 },
  INIT_POSITION:  { x: 0.82,    y: 0.35   },
  orientation:    { rows: 1, columns: 3   },
  down:           { row: 0, start: 0, columns: 3 },
  hitbox:         { widthPercentage: 0.1, heightPercentage: 0.2 },
};

The AI API request body is also assembled as a JSON object before being sent:

// GameLevelForestWin.js
body: JSON.stringify({
  model:      'claude-sonnet-4-20250514',
  max_tokens: 1000,
  system:     this.systemPrompt,
  messages:   this.history,
}),

Run this to see all five data types printed from real sprite config values:

Code Runner Challenge

Run the code to see all five data types — Numbers, Strings, Booleans, Arrays, and Objects — printed from real Escape Game sprite configs.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Operators

Mathematical

Barrier sizes come from multiplying ratios by canvas dimensions and rounding. Door shuffling uses Math.floor and Math.random. Typing dot delays use multiplication for staggering. Taunt cycling uses modulo so it wraps around:

// GameLevelMazeSub.js
x:      Math.round(rx * width),
y:      Math.round(ry * height),
width:  Math.round(rw * width),
height: Math.round(rh * height),

// GameLevelDoors.js
const correctIndex = Math.floor(Math.random() * doorConfigs.length);

// GameLevelForestWin.js
d.style.animation = `npcDot 1s ease-in-out ${i * 0.18}s infinite`;

// GameLevelForestDeath.js
const msg = taunts[this._tauntIndex % taunts.length];
this._tauntIndex++;

String Operations

Template literals handle error messages, CSS properties, and animation values throughout GameLevelForestWin.js. Key lookup uses an object literal as a map with a || fallback for unknown names:

// GameLevelForestWin.js
throw new Error(`API ${res.status}`);

row.style.cssText = `display:flex;justify-content:${who === 'user' ? 'flex-end' : 'flex-start'}`;

d.style.animation = `npcDot 1s ease-in-out ${i * 0.18}s infinite`;

// String key lookup in _greeting()
return {
  'R2D2':          'Bweeeep! You made it! Ask me anything!',
  'Village Elder': "We don't get many travellers here.",
  'Villager':      "Oh! A new face! It's been so long!",
}[this.npcName] || 'Hello, traveller.';

Boolean Expressions

&& and || chains guard NPC interactions and handle fallback state. ?. and ?? keep the AI reply safe when the response structure is unexpected:

// Every NPC interact — && compound expression
if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
  this.dialogueSystem.closeDialogue();
  return;
}

// GameLevelForestWin.js — && in isOpen()
return !!this.container && document.body.contains(this.container);

// GameLevelMazeSub.js — || fallback for nested game control
const topGame = primaryGame?.parentControl || primaryGame;

// GameLevelForestDeath.js — ! negation guard
if (!this.isOpen()) return;

// GameLevelForestWin.js — optional chaining + nullish coalescing
return data.content.find(b => b.type === 'text')?.text ?? '...';

Run this to see all three operator categories produce output from real game logic:

Code Runner Challenge

Run the code to see mathematical, string, and boolean operators all producing output from real Escape Game logic.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Play the full game one last time — every operator, type, and control structure covered above is running inside it:

Challenge

Play the full Escape Game as a final demo! Every operator, data type, and control structure covered in this portfolio is running live inside.

Lines: 1 Characters: 0
Game Status: Not Started

Input / Output

Keyboard Input

Player movement is entirely keyboard-driven. The keypress map in each level’s Player sprite config binds key codes to directions — 87 = W (up), 65 = A (left), 83 = S (down), 68 = D (right). The GameEngine reads this map every frame and moves the player accordingly:

// GameLevelForest.js — keypress map on sprite_data_octopus
keypress: { up: 87, left: 65, down: 83, right: 68 }

Every level defines this same map on its Player config. NPC interactions are also keyboard-triggered — the E key fires the interact function on whatever NPC the player is currently overlapping.

Canvas Rendering

Every sprite, background, platform, and NPC is rendered onto a <canvas> element each frame through the GameEngine’s draw() loop. The engine instantiates everything in this.classes, then calls draw() on every object every tick.

Each sprite config’s ANIMATION_RATE and orientation properties tell the engine which frame of the spritesheet to draw:

// GameLevelForestDeath.js — sprite_data_beckoner animation config
ANIMATION_RATE: 80,
pixels:      { height: 255, width: 150 },
orientation: { rows: 1, columns: 1 },
down:        { row: 0, start: 0, columns: 1 },

GameEnv Configuration

Every level constructor receives gameEnv, which gives access to the canvas dimensions, the asset path, and the active GameControl instance. All positions and sizes are computed from gameEnv.innerWidth and gameEnv.innerHeight so the game scales correctly at any resolution:

// GameLevelMazeSub.js — gameEnv consumed in constructor
constructor(gameEnv) {
  let width  = gameEnv.innerWidth;
  let height = gameEnv.innerHeight;
  let path   = gameEnv.path;

  // All positions derived from width/height — never hardcoded pixels
  const ledge3 = {
    x:      Math.round(width  * 0.40),
    y:      Math.round(height * 0.43),
    width:  Math.round(width  * 0.55),
    height: 18,
  };
}

DOM Output

Level transitions inject a full-screen black <div> overlay into the page and fade it in and out using CSS transitions:

// GameLevelMazeSub.js — fade overlay built and injected at transition time
const fade = document.createElement('div');
Object.assign(fade.style, {
  position: 'fixed',
  top: '0', left: '0',
  width: '100%', height: '100%',
  backgroundColor: '#000',
  opacity: '0',
  transition: 'opacity 0.8s ease-in-out',
  zIndex: '9999',
  pointerEvents: 'none'
});
document.body.appendChild(fade);

requestAnimationFrame(() => {
  fade.style.opacity = '1';
  setTimeout(() => {
    fade.style.opacity = '0';
    setTimeout(() => {
      if (fade.parentNode) fade.parentNode.removeChild(fade);
    }, 800);
  }, 800);
});

Run this to see the fade sequence logged step by step:

Code Runner Challenge

Run the code to see the fade overlay transition sequence from GameLevelMazeSub.js simulated with console output.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Asynchronous I/O

Level transitions are sequenced with requestAnimationFrame and nested setTimeout calls so nothing blocks the browser. BeckonerChaseController in GameLevelForestDeath.js uses real elapsed time to gradually increase the chase speed:

// GameLevelForestDeath.js — BeckonerChaseController.update()
update() {
  const elapsed = (Date.now() - this.startTime) / 1000;
  if (elapsed < 3) return;   // does nothing for the first 3 seconds

  // Speed increases the longer the player stays — capped at 2.5
  const speed = Math.min(1.2 + (elapsed - 3) * 0.025, 2.5);
}

Run this to see the speed ramp in isolation:

Code Runner Challenge

Run the code to see the BeckonerChaseController speed ramp from GameLevelForestDeath.js. Watch how the speed increases over time and caps at 2.5!

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

JSON Parsing

Sprite configs are deeply nested JSON-style object literals throughout the codebase. GameLevelDoors.js keeps things clean by defining a shared doorDefaults object and spreading it into each individual door config:

// GameLevelDoors.js — spreading doorDefaults into each door sprite config
const doorDefaults = {
  SCALE_FACTOR: 8, ANIMATION_RATE: 50,
  pixels:      { height: 414, width: 252 },
  orientation: { rows: 1, columns: 1 },
  hitbox:      { widthPercentage: 0.1, heightPercentage: 0.2 },
};

const doorSprites = doorConfigs.map((cfg, i) => ({
  ...doorDefaults,           // spread shared defaults in
  id:            cfg.id,
  src:           cfg.src,
  INIT_POSITION: { x: xPositions[i], y: 0.5 },
}));

Run this to see the spread and JSON.stringify output for all five doors:

Code Runner Challenge

Run the code to see the object spread and JSON.stringify from GameLevelDoors.js. Each door gets all shared defaults merged with its own config!

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Documentation

Code Comments

Every level file opens with a description comment and has inline comments throughout. GameLevelMazeSub.js goes the furthest — it includes an ASCII art diagram of the staircase platform layout directly in the source so anyone reading the file can visualize the geometry before looking at the numbers:

// GameLevelMazeSub.js — ASCII layout comment
//
//   ledge5  ░░░░░░░░░░░░░░░░░░░░░░░░░░░  (x 0.55–0.95, y ~0.12)
//   ledge4  ░░░░░░░░░░░░░░░░░░░░         (x 0.10–0.60, y ~0.27)
//   ledge3  ░░░░░░░░░░░░░░░░░░░░░░░░░░░  (x 0.40–0.95, y ~0.43)
//   ledge2  ░░░░░░░░░░░░░░░░░░░░         (x 0.05–0.55, y ~0.58)
//   ledge1  ░░░░░░░░░░░░░░░░░░░░░░░░░░░  (x 0.00–0.50, y ~0.78)
//   floor   ═══════════════════════════  (y 0.90)
//
// The player starts above ledge1 and must climb up-right, up-left, …

Mini-Lesson Documentation

This page is my mini-lesson. Each objective section has a prose explanation of the concept, a direct excerpt from the relevant source file, a code runner where the logic runs live in the browser, and in most cases a full game runner where you can play the level that contains it. The goal was to make every single objective observable, not just readable.


Code Highlights

Every section above pulls annotated excerpts straight from the actual source files, with comments pointing at the specific thing being demonstrated. The code runners are designed to strip out the game engine and run only the piece of logic being discussed — so you can see the exact output without having to play all the way through the game to hit that code path.


Debugging

Console Debugging

Every level constructor starts with a console.log so I could confirm loading order during development. Transition code uses console.warn for failures that shouldn’t crash the game:

// Every level file — initialization confirmation
console.log("Initializing GameLevelForestDeath...");
console.log("Initializing GameLevelMazeSub...");
console.log("Initializing GameLevelDoors...");

// GameLevelDoors.js — logs the correct door at load time
console.log('Correct door this run:', doorConfigs[correctIndex].id);

// Every transition function — warn instead of crashing on optional calls
console.warn('Could not hide parent canvas state', e);

Hit Box Visualization

During GameLevelMazeSub.js development, the GameEngine’s hitbox overlay was used to verify that all six barrier platforms were positioned and sized correctly. Without it, collisions on the zigzag staircase were inconsistent. Toggling it on let me visually confirm the hitbox boundaries and tune the widthPercentage and heightPercentage values until they matched the visible sprites:

// GameLevelMazeSub.js — narrow hitbox tuned via hitbox visualization
hitbox: { widthPercentage: 0.45, heightPercentage: 0.2 }

Source-Level Debugging

The BeckonerChaseController catch detection in GameLevelForestDeath.js wasn’t working correctly — the player was getting caught too early or not at all. I set a breakpoint inside update() and stepped through frame by frame, watching the dist variable change in the DevTools scope panel. That’s how I found the right threshold value to use:

// GameLevelForestDeath.js — catch threshold found via step-through debugging
const dist = Math.hypot(dx, dy) || 1;
if (dist < 50 && !chaseState.caught) {   // value found by watching dist in DevTools
  chaseState.caught = true;
  this.onCaught(beckoner, player);
}

Breakpoint set on line 54 of GameLevelForestDeath.js in Chrome DevTools

Network Debugging

Early in development, some level assets were silently 404-ing because of a wrong path prefix. I opened the Network tab, filtered by status, and found GameLevelForestSub.js returning a 404 while every other script loaded with 200. That pointed me directly to the mismatched import path:

// GameLevelForest.js — path prefix verified via Network tab
let path = gameEnv.path;
const sprite_data_wraith = {
  src: path + "/images/gamify/tux.png",   // Network tab confirmed this resolved correctly
};

Network tab showing GameLevelForestSub.js returning a 404

Application Debugging

Transitioning between nested sublevels was leaving old canvas elements in the DOM, which caused visual glitches when new levels loaded on top of them. I found this by opening the Elements panel and watching the DOM during a transition. The fix was the Array.from(gameContainer.children).forEach(...) cleanup that now runs in every transition function:

// GameLevelMazeSub.js — cleanup added after DOM inspection revealed stale canvases
const gameContainer = document.getElementById('gameContainer');
if (gameContainer) {
  Array.from(gameContainer.children).forEach(child => {
    if (child.id !== 'promptDropDown') gameContainer.removeChild(child);
  });
}

Element Inspection

A TypeError: Failed to fetch dynamically imported module kept appearing in the console from GameExecutor.js:338. The error message included the blob URL that failed, which I cross-referenced against the <script> tags the engine had injected. One of them had an outdated import path pointing to a file I had renamed. Fixing the import string resolved it immediately.

Chrome DevTools console showing a failed dynamic import error


Testing & Verification

Gameplay Testing

Every level has a live game runner embed on this page. I used these during development to test each level in isolation without having to play through from the beginning every time. They’re still live — clicking into any of them confirms the level is working right now:

Challenge

Play the full Escape Game for final verification! All levels, NPCs, transitions, and AI chat are live and testable right here.

Lines: 1 Characters: 0
Game Status: Not Started

Integration Testing

The trickiest integration to verify was the nested GameControl chain: GameLevelMazeGameLevelMazeSub (sublevel) → GameLevelDoors (via parentControl). I tested this by playing all the way through and checking that the parent game correctly paused when the sublevel launched, and that topGame.transitionToLevel() handed control back to the right level:

// GameLevelMazeSub.js — parentControl chain tested end-to-end
const topGame = primaryGame?.parentControl || primaryGame;
if (topGame) {
  topGame.levelClasses      = [GameLevelDoors];
  topGame.currentLevelIndex = 0;
  topGame.isPaused          = false;
  topGame.transitionToLevel();
}

Error Handling

Every transition wraps the optional hideCanvasState() call in a try/catch with a console.warn so a missing method doesn’t crash the level. BeckonerChaseController uses optional chaining on every game object lookup to stay safe during the frames before objects finish loading:

// GameLevelForest.js — try/catch around optional engine method
try {
  if (typeof primaryGame.hideCanvasState === 'function') {
    primaryGame.hideCanvasState();
  }
} catch(e) {
  console.warn('Could not hide parent canvas state', e);
}

// GameLevelForestDeath.js — optional chaining guards in BeckonerChaseController
const player   = this.gameEnv.gameObjects?.find(o => o?.spriteData?.id === 'Octopus');
const beckoner = this.gameEnv.gameObjects?.find(o => o?.spriteData?.id === 'Strange Beckoner');
if (!player || !beckoner) return;   // bail out safely if objects not ready yet

Run this to see both patterns tested against three different scenarios:

Code Runner Challenge

Run the code to see the try/catch and optional chaining error-handling patterns from the Escape Game tested against three different scenarios.

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...