Aryan Malik
My CS111 portfolio sprite debugging, game engine architecture, DevTools, and full course rubric alignment for the 3-level Slime Escape group game.
Why do programmers prefer dark mode? Because light attracts bugs. 😄
Aryan Malik CS111 Portfolio
🎮 Game Links
🛠️ Development Environment
📚 My Lessons
🏆 Class Progress
This blog is my full CS111 portfolio. It covers every learning objective from the course rubric, with live playable game runners and real code evidence for each requirement. Click any dropdown to expand it.
Live Game Runners
Three stages of development from a broken sprite direction to a full level with NPC and invisible barrier. Each runner is playable and editable live.
Stage 1 — Player moves but faces the wrong direction (rows 0 and 2 are swapped in the sprite config).
Challenge
Move with WASD — slime faces the WRONG direction. Rows 0 and 2 are swapped!
Stage 2 Rows 0 and 2 are swapped back to the correct positions. The slime now faces the right way in all 8 directions.
Challenge
Move with WASD — rows fixed. The slime now faces the correct direction!
Stage 3 Full level: correct player movement + an NPC you can interact with + an invisible collision barrier (fromOverlay: true is required or the player walks straight through it).
Challenge
Move with WASD. Walk up to the NPC to interact, and try walking through the invisible barrier!
CS111 Course Alignment Rubric
Every section below is a direct rubric requirement. Each dropdown shows:
- exactly what the rubric asks for
- how it is assessed
- a plain-English explanation
- real code from the game as evidence
Click any item to expand it.
🧱 Object-Oriented Programming
Writing Classes
| Class | What it does | File |
|---|---|---|
GameLevelBasicwrong | Player + background, wrong sprite rows | GameLevelBasicwrong.js |
GameLevelBasiccorrect | Player + background, fixed sprite rows | GameLevelBasiccorrect.js |
GameLevelFullygame | Player + NPC + Barrier, fully working | GameLevelFullygame.js |
// Each level is a class — the engine calls new GameLevelFullygame(gameEnv)
class GameLevelFullygame {
constructor(gameEnv) {
// build the level here
}
}
export default GameLevelFullygame;
✅ Three custom classes written, each building on the engine's base Player, Npc, and Barrier classes.
Methods & Parameters
| Method | Parameters | What it does |
|---|---|---|
constructor(gameEnv) | gameEnv — the engine environment | Builds the level: sets up player, background, NPC, barriers |
collisionHandler(other, direction) | other — the object we hit; direction — which side | Responds to a collision — push back, stop, or trigger event |
update() | none | Called every frame — moves the player, checks input |
// constructor takes gameEnv as a parameter
constructor(gameEnv) {
const path = gameEnv.path; // parameter property: asset URL
const width = gameEnv.innerWidth; // parameter property: canvas width
const height = gameEnv.innerHeight; // parameter property: canvas height
}
// collisionHandler — called by the engine with 2 parameters
collisionHandler(other, direction) {
if (direction === 'left' || direction === 'right') {
this.velocity.x = 0; // stop horizontal movement
}
}
✅ Every method used in the game takes parameters — the constructor takes gameEnv, collision methods take other and direction.
Instantiation & Objects
Entry in this.classes | Class used | Config object |
|---|---|---|
| Background | GameEnvBackground | bgData |
| Player (slime) | Player | playerData |
| NPC (R2 droid) | Npc | npcData |
| Invisible wall | Barrier | barrier1 |
// this.classes = the instantiation list
// The engine loops through this and calls new Class(data, gameEnv) for each one
this.classes = [
{ class: GameEnvBackground, data: bgData }, // new GameEnvBackground(bgData, gameEnv)
{ class: Player, data: playerData }, // new Player(playerData, gameEnv)
{ class: Npc, data: npcData }, // new Npc(npcData, gameEnv)
{ class: Barrier, data: barrier1 } // new Barrier(barrier1, gameEnv)
];
✅ Four game objects instantiated in GameLevelFullygame using the this.classes configuration pattern.
Inheritance (Basic)
Inheritance chain used in this game:
GameObject ← base: has position, draw(), update()
└── Character ← adds: movement, animation, hitbox
├── Player ← adds: keyboard input (WASD), player-specific collision
└── Npc ← adds: dialogue system, NPC-specific interaction
└── Barrier ← adds: static collision wall, fromOverlay activation
// The extends keyword builds the chain
class Character extends GameObject { ... } // level 1
class Player extends Character { ... } // level 2 — 2 levels deep ✅
class Npc extends Character { ... } // level 2 — 2 levels deep ✅
✅ The engine provides a 2-level hierarchy (GameObject → Character → Player/Npc). All three of my level classes consume this chain via this.classes.
Method Overriding
| Method | In GameObject (parent) | In Player (override) | In Npc (override) |
|---|---|---|---|
update() | moves object by velocity | also reads WASD keys | also checks for nearby player to trigger dialogue |
draw() | draws a rectangle | draws the correct sprite sheet frame | draws the NPC sprite |
handleCollision() | base bounce logic | stops player at barrier | NPC ignores barriers |
// Player overrides update() — same name, different behavior
class Player extends Character {
update() {
super.update(); // run the parent version first (move by velocity)
this.handleKeypress(); // then add player-specific keyboard behavior
}
}
✅ update(), draw(), and handleCollision() are all overridden in Player and Npc with game-specific behavior.
Constructor Chaining
Constructor chain when the engine creates a Player:
1. new Player(playerData, gameEnv) ← engine calls this
2. calls super(playerData, gameEnv) ← Player calls Character's constructor
3. calls super(playerData, gameEnv) ← Character calls GameObject's constructor
4. sets this.x, this.y, this.width ← GameObject sets up position/size
3. sets this.velocity, this.hitbox ← Character adds movement properties
2. sets this.keypress ← Player adds keyboard mapping
class Player extends Character {
constructor(data, gameEnv) {
super(data, gameEnv); // REQUIRED — chains to Character constructor
this.keypress = data.keypress; // then add Player-specific setup
}
}
class Character extends GameObject {
constructor(data, gameEnv) {
super(data, gameEnv); // REQUIRED — chains to GameObject constructor
this.velocity = { x: 0, y: 0 };
this.hitbox = data.hitbox;
}
}
✅ super(data, gameEnv) is called in every class in the chain — Player, Npc, Barrier, and Character all chain to their parent constructors.
🔁 Control Structures
Iteration
| Loop type | Where | What it does |
|---|---|---|
forEach | Level setup | Registers every object in this.classes with the engine |
requestAnimationFrame | Main game loop | Calls update() and draw() on every object ~60 times/second |
for | Pixel scan (Level 3) | Checks the color of every pixel surrounding the player |
while | Animation frames | Advances the sprite frame counter each tick |
// forEach — register all game objects from this.classes
this.classes.forEach(entry => {
gameEnv.add(new entry.class(entry.data, gameEnv));
});
// requestAnimationFrame loop — the heartbeat of the entire game
function gameLoop() {
gameObjects.forEach(obj => obj.update()); // update every object
gameObjects.forEach(obj => obj.draw()); // draw every object
requestAnimationFrame(gameLoop); // schedule the next frame
}
// for loop — pixel scan around the player each frame
for (let i = 0; i < surroundingPixels.length; i++) {
if (getColor(surroundingPixels[i]) === safeColor) {
playerSafe = true;
}
}
✅ forEach, requestAnimationFrame loop, and for loops all present in the game — each serving a different purpose.
Conditionals
| Conditional | In which level | What decision it makes |
|---|---|---|
if (collision) | Level 1 — Cannonball Dodge | Did the cannonball hit the slime? Reset or advance. |
if (pixelColor === safeColor) | Level 3 — Zone Catch | Is the player standing in the safe zone? |
if (fromOverlay) | Full level | Should this barrier block movement? |
if (keyPressed('E')) | Level 2 — Maze | Did the player press E near the gate? |
// Level 1 — collision detection state transition
if (playerHitbox.overlaps(cannonball)) {
player.x = 0; // state transition: reset to start
} else {
player.x += 300; // state transition: advance toward gate
}
// Full level — barrier collision check
if (barrier.fromOverlay) {
collisionHandler(player, direction); // stop the player
}
// Level 3 — safe zone check
if (pixelColor === safeZoneColor) {
playerSafe = true; // state transition: player is now safe
} else {
playerSafe = false; // state transition: player is in danger
}
✅ if/else conditionals drive collision detection in Level 1, safe-zone checking in Level 3, and barrier activation in the full level.
Nested Conditions
// Level 3 — nested: round threshold + player state + timer state
if (currentRound >= 6) {
showGoldenGate(); // outer condition: past round 6
if (!playerSafe && timerExpired) { // inner: both must be true
gameOver = true;
}
}
// Full level — nested: key pressed + player proximity + dialogue available
if (playerNearNpc) { // outer: close enough?
if (keyPressed('E')) { // inner: pressed interact key?
if (npc.dialogueSystem) { // inner-inner: system ready?
npc.showRandomDialogue();
}
}
}
✅ Level 3's golden gate and game-over logic uses 3-level nested conditionals. NPC interaction uses nested key + proximity + system checks.
📦 Data Types
Numbers
| Property | Value | What it controls |
|---|---|---|
SCALE_FACTOR | 5 | How big the sprite is rendered |
STEP_FACTOR | 1000 | How fast the player moves |
ANIMATION_RATE | 50 | Milliseconds between animation frames |
INIT_POSITION.x | 100 | Starting X position in pixels |
INIT_POSITION.y | 300 | Starting Y position in pixels |
rotate | Math.PI/16 | Diagonal movement rotation in radians |
coinsCollected | 16 | Score tracked in localStorage |
barrier.width | 28 | Width of the invisible collision wall |
// Numbers throughout playerData
const playerData = {
SCALE_FACTOR: 5, // number: render scale
STEP_FACTOR: 1000, // number: movement speed
ANIMATION_RATE: 50, // number: ms per animation frame
INIT_POSITION: { x: 100, y: 300 }, // numbers: pixel coordinates
rotate: Math.PI/16, // number: radian rotation value
};
// Score tracked as a number
let coinsCollected = 16; // number: player score
✅ Numbers are used for every position, size, speed, timing, and score value in the game.
Strings
| String | Value | Purpose |
|---|---|---|
id | 'playerData' | Identifies the object in the engine |
src | path + '/images/...' | File path to the sprite image |
greeting | 'Working' | What the NPC says when approached |
dialogues[0] | 'Working' | NPC dialogue line |
gategame_player_name | 'Aryan' | Player name saved in localStorage |
// Strings in game config
id: 'playerData', // string: object ID
src: path + '/images/gamebuilder/sprites/slime.png', // string: concatenated path
greeting: 'Working', // string: NPC speech
// Template literal (advanced string) for dynamic display
const banner = `Round ${currentRound} — find the safe zone!`;
// String stored in localStorage
localStorage.setItem('gategame_player_name', 'Aryan');
✅ Strings appear in every config object (IDs, file paths), in NPC dialogue, and in localStorage for player name persistence.
Booleans
| Boolean flag | Value | What it tracks |
|---|---|---|
visible | false | Barrier is invisible (but still collides) |
fromOverlay | true | Barrier collision is active |
playerSafe | true/false | Is the player in the safe zone? |
timerRunning | true/false | Is the round timer counting down? |
goldenGateOpen | true/false | Has the golden gate appeared (round 6+)? |
gameOver | true/false | Has the player been eliminated? |
cannonballActive | true/false | Is a cannonball currently in flight? |
// Booleans in barrier config
const barrier1 = {
visible: false, // boolean: invisible wall
fromOverlay: true, // boolean: collision enabled
};
// Booleans as state flags across the game loop
let playerSafe = false; // isVulnerable equivalent
let timerRunning = true;
let goldenGateOpen = false;
let gameOver = false;
let cannonballActive = false; // isPaused equivalent for projectile
✅ Booleans control barrier visibility/collision, player safety state, timer state, golden gate unlock, and game-over condition.
Arrays
| Array | Contents | Purpose |
|---|---|---|
this.classes | [bg, player, npc, barrier] | Every game object in the level |
dialogues | ['Working', 'Find the gate!'] | All NPC dialogue lines |
zonePairs | [{safe:'red',danger:'blue'}, ...] | Level 3 color pairs per round |
| Snake body | [{x,y}, {x,y}, ...] | Snake game: each segment position |
// this.classes — game object collection (array of objects)
this.classes = [
{ class: GameEnvBackground, data: bgData }, // index 0
{ class: Player, data: playerData }, // index 1
{ class: Npc, data: npcData }, // index 2
{ class: Barrier, data: barrier1 } // index 3
];
// dialogues — level data as an array of strings
dialogues: ['Working', 'Find the gate!', 'Keep moving!']
// Array operation — add a new barrier to the level
this.classes.push({ class: Barrier, data: barrier2 });
✅ this.classes is the central array of the entire game. Array operations (push, forEach, index access) drive level setup and the game loop.
Objects (JSON)
| Object | Properties it holds |
|---|---|
bgData | name, src, pixels |
playerData | id, src, SCALE_FACTOR, STEP_FACTOR, INIT_POSITION, pixels, orientation, keypress, hitbox, animation rows |
npcData | id, greeting, src, INIT_POSITION, dialogues, hitbox |
barrier1 | id, x, y, width, height, visible, fromOverlay, hitbox |
// Object literal — all player config grouped together
const playerData = {
id: 'playerData', // string property
SCALE_FACTOR: 5, // number property
INIT_POSITION: { x: 100, y: 300 }, // nested object property
pixels: { height: 225, width: 225 },
keypress: { up: 87, left: 65, down: 83, right: 68 }, // nested object
hitbox: { widthPercentage: 0, heightPercentage: 0 }
};
// Access object properties with dot notation
console.log(playerData.SCALE_FACTOR); // 5
console.log(playerData.INIT_POSITION.x); // 100
✅ Every game entity — player, background, NPC, barrier — is configured as a plain object (JSON-style object literal).
➕ Operators
Mathematical Operators
| Operator | Where used | Example |
|---|---|---|
+ | Advance after dodge | player.x += 300 |
- | Parallax scroll | layer.offsetX -= scrollSpeed |
* | Sprite frame position | frameX * frameWidth |
/ | Rotation angle | Math.PI / 16 |
+= | Gravity each frame | velocity.y += gravity |
-= | Timer countdown | timeRemaining -= deltaTime |
// + and / — diagonal rotation angle
rotate: Math.PI / 16, // PI divided by 16 = small rotation in radians
// += — advance player after dodging a cannonball
player.x += 300; // move 300 pixels to the right
// -= — parallax scrolling (background moves left each frame)
layer.offsetX -= layer.scrollSpeed;
// * — find the correct frame on the sprite sheet
ctx.drawImage(sheet,
frameX * frameWidth, // multiplication: source X = column * width
frameY * frameHeight, // multiplication: source Y = row * height
frameWidth, frameHeight, x, y, w, h
);
// += — gravity applied per frame
velocity.y += 0.5; // adds 0.5 to vertical velocity every frame
✅ All four arithmetic operators (+, -, *, /) plus compound assignment (+=, -=) appear in physics, animation, and movement code.
String Operations
| Operation | Example | Result |
|---|---|---|
+ concatenation | path + '/images/slime.png' | '/assets/js/.../images/slime.png' |
| Template literal | ` Round ${currentRound} ` | 'Round 3' |
localStorage.setItem | setItem('name', playerName) | Saves string to browser |
localStorage.getItem | getItem('name') | Reads string from browser |
// + concatenation — builds the full image path
src: path + '/images/gamebuilder/sprites/slime.png'
// └── base URL + file path = complete URL
// Template literal — inserts variable into string
const banner = `Round ${currentRound} — find the safe zone!`;
const message = `Player ${playerName} scored ${coins} coins`;
// String stored and retrieved from localStorage
localStorage.setItem('gategame_player_name', playerName); // save
const name = localStorage.getItem('gategame_player_name'); // read → 'Aryan'
✅ String concatenation appears in every src path. Template literals appear in dynamic banners. Both used in localStorage for player name persistence.
Boolean Expressions
| Operator | Meaning | Game example |
|---|---|---|
&& | AND — both must be true | !playerSafe && timerExpired → game over |
\|\| | OR — either can be true | key === 'w' \|\| key === 'ArrowUp' → move up |
! | NOT — flip true/false | !cannonballActive → fire a new one |
// && (AND) — game over only if BOTH conditions are true
if (!playerSafe && timerExpired) {
gameOver = true; // must be unsafe AND timer must be done
}
// || (OR) — accept either WASD or arrow keys
if (e.key === 'w' || e.key === 'ArrowUp') {
moveUp(); // either key works
}
// ! (NOT) — only launch a cannonball if one is NOT already flying
if (!cannonballActive) {
cannonballActive = true;
launchCannonball();
}
// Combined: && and ! together
if (playerNearNpc && !npc.isTalking) {
npc.showRandomDialogue(); // near the NPC AND it's not already talking
}
✅ All three boolean operators (&&, ||, !) appear in game-over conditions, input handling, and NPC interaction logic.
⌨️ Input / Output
Keyboard Input
| Key | Key Code | Movement |
|---|---|---|
| W | 87 | Up |
| A | 65 | Left |
| S | 83 | Down |
| D | 68 | Right |
| E | 69 | Interact with NPC |
| Esc | 27 | Exit / advance level |
// keypress config in playerData — maps key codes to directions
keypress: { up: 87, left: 65, down: 83, right: 68 }
// Engine registers the event listeners using this config
document.addEventListener('keydown', (event) => {
if (event.keyCode === playerData.keypress.up) moveUp();
if (event.keyCode === playerData.keypress.left) moveLeft();
if (event.keyCode === playerData.keypress.down) moveDown();
if (event.keyCode === playerData.keypress.right) moveRight();
});
document.addEventListener('keyup', (event) => {
// stop movement when key is released
stopMovement(event.keyCode);
});
✅ WASD keys configured via keypress object. Engine registers keydown and keyup event listeners that respond correctly to all four movement keys plus interaction keys.
Canvas Rendering
| What is drawn | Method | Source |
|---|---|---|
| Background image | ctx.drawImage(bgImage, 0, 0, w, h) | alien_planet.jpg |
| Player sprite frame | ctx.drawImage(sheet, sx, sy, sw, sh, dx, dy, dw, dh) | slime.png row/column |
| NPC sprite | ctx.drawImage(sheet, sx, sy, sw, sh, dx, dy, dw, dh) | r2_idle.png |
| Hitbox (debug) | ctx.strokeRect(x, y, w, h) | Calculated from config |
// draw() method — called every frame by the game loop
draw() {
// drawImage with 9 parameters = draw a specific frame from a sprite sheet
this.ctx.drawImage(
this.spriteSheet, // the full sprite sheet image
this.frameX * this.frameW, // source X: which column
this.frameY * this.frameH, // source Y: which row (this was our bug!)
this.frameW, this.frameH, // source size
this.x, this.y, // destination position on canvas
this.width, this.height // destination size (scaled)
);
}
✅ Every visible game object implements draw() using the Canvas 2D API. The sprite row bug in Stage 1 was caused by this.frameY reading the wrong row from the sprite sheet.
GameEnv Configuration
gameEnv property | Type | What it provides |
|---|---|---|
gameEnv.path | string | Base URL for all image assets |
gameEnv.innerWidth | number | Current canvas width in pixels |
gameEnv.innerHeight | number | Current canvas height in pixels |
// Every level constructor receives gameEnv from the engine
constructor(gameEnv) {
const path = gameEnv.path; // 'https://pages.opencodingsociety.com'
const width = gameEnv.innerWidth; // e.g. 1200 (canvas width)
const height = gameEnv.innerHeight; // e.g. 600 (canvas height)
// Use path to build all image src values
src: path + '/images/gamebuilder/bg/alien_planet.jpg'
}
✅ All three level classes read gameEnv.path, gameEnv.innerWidth, and gameEnv.innerHeight to configure their backgrounds and position game objects relative to canvas size.
API Integration
| API action | Method | Endpoint | What it does |
|---|---|---|---|
| Save score | POST | /api/leaderboard | Sends player name + score to the backend |
| Load rankings | GET | /api/leaderboard | Gets the top scores to display |
| Get player ID | GET | /api/id | Fetches logged-in user info (caused CORS errors) |
// POST — send a score to the leaderboard
async function saveScore(playerName, score) {
const response = await fetch('https://flask.opencodingsociety.com/api/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: playerName, score: score })
});
return await response.json(); // parse the response
}
// GET — load the leaderboard
async function loadLeaderboard() {
const response = await fetch('https://flask.opencodingsociety.com/api/leaderboard');
const data = await response.json();
displayScores(data.scores);
}
✅ Leaderboard API uses fetch() for both POST (save scores) and GET (load rankings) with async/await and JSON request bodies.
Asynchronous I/O
| Pattern | How it works |
|---|---|
async function | Marks a function that will do something slow (like a network call) |
await | Pauses that function until the slow thing finishes — game keeps running |
.then() | Older alternative — chain actions after a promise resolves |
Promise.all() | Wait for multiple async operations to finish at once |
// async/await pattern — clean and readable
async function loadLeaderboard() {
const response = await fetch('/api/leaderboard'); // wait for server response
const data = await response.json(); // wait for JSON parse
displayScores(data.scores); // runs after both are done
}
// .then() chain — older promise-based pattern, same result
fetch('/api/leaderboard')
.then(response => response.json()) // after response comes back, parse JSON
.then(data => displayScores(data.scores)) // after parse, display
.catch(err => console.error('Failed:', err)); // if anything fails, log it
✅ All API calls in the game use async/await. The .then() pattern is understood as the equivalent promise-chain alternative.
JSON Parsing
| Operation | What it does | Example |
|---|---|---|
response.json() | Converts API response text to JS object | await response.json() |
JSON.parse() | Converts any JSON string to JS object | JSON.parse(localStorage.getItem(...)) |
JSON.stringify() | Converts JS object to JSON string | JSON.stringify({ name, score }) |
| Destructuring | Pull values out of object directly | const { scores, total } = data |
// JSON.parse — read saved game state from localStorage
const saved = JSON.parse(localStorage.getItem('gategame_state'));
// saved is now a JS object with all properties accessible
// response.json() — parse the leaderboard API response
const response = await fetch('/api/leaderboard');
const data = await response.json(); // built-in JSON parse
const { scores, total, playerRank } = data; // object destructuring
// JSON.stringify — convert object to string for POST request body
body: JSON.stringify({ name: playerName, score: coinsCollected })
✅ JSON.parse() used for localStorage state. response.json() used for all API responses. Object destructuring used to extract scores, total, and player data from parsed responses.
📝 Documentation
Code Comments
| JSDoc tag | What it documents | Example |
|---|---|---|
/** ... */ | Class or method description | What this thing does overall |
@param | An input parameter | @param {Object} gameEnv — engine environment |
@returns | What the method gives back | @returns {boolean} true if collision detected |
@type | Type of a variable | @type {number} |
/**
* GameLevelFullygame — full working level with NPC and collision barrier.
* Demonstrates: class instantiation, fromOverlay barrier, NPC dialogue.
* Used as evidence for OOP, instantiation, and control structure rubric items.
*/
class GameLevelFullygame {
/**
* Builds the level by configuring all game objects in this.classes.
* @param {Object} gameEnv - engine environment object
* @param {string} gameEnv.path - base URL for asset images
* @param {number} gameEnv.innerWidth - canvas width in pixels
* @param {number} gameEnv.innerHeight - canvas height in pixels
*/
constructor(gameEnv) {
const path = gameEnv.path; // string: base URL for all images
// ... rest of constructor
}
}
✅ JSDoc /** */ comments document the class purpose and all constructor parameters. Comment density is above 10% across all three level files.
Mini-Lesson Documentation
| Mini-lesson element | Where it appears | What it teaches |
|---|---|---|
| Live game runner (Stage 1) | Top of this blog | Sprite sheet row mapping bug |
| Live game runner (Stage 2) | Top of this blog | How swapping two numbers fixes all directions |
| Live game runner (Stage 3) | Top of this blog | NPC instantiation + fromOverlay barrier activation |
| DevTools screenshots | Debugging section | How to read console errors and inspect localStorage |
| Rubric dropdowns | This section | Every CS111 concept mapped to real game code |
This blog is hosted at /homepage and serves as the portfolio mini-lesson. Each game runner is playable and editable — the code is visible and students can modify it live.
Code Highlights
Key annotated snippets in this blog:
| Snippet | Section | What it highlights |
|---|---|---|
down: { row: 2 } vs row: 0 | Stage 1 runner | Sprite sheet row indexing bug |
fromOverlay: true | Stage 3 runner | The flag that activates barrier collision |
this.classes = [...] | Instantiation dropdown | How the engine builds a level from config |
super(data, gameEnv) | Constructor Chaining dropdown | How constructor chains work up the hierarchy |
if (!playerSafe && timerExpired) | Nested Conditions dropdown | Boolean operators in game logic |
await fetch('/api/leaderboard') | API Integration dropdown | Async/await for network calls |
JSON.parse(localStorage.getItem(...)) | JSON Parsing dropdown | Reading saved game state |
✅ Every code block in this blog has inline // comments explaining the purpose of key lines.
🔍 Debugging
Console Debugging
| Where to add console.log | What to log | Why |
|---|---|---|
Inside update() | this.x, this.y | Track player position every frame |
Inside collisionHandler() | other.id, direction | See what was hit and from which side |
| After state changes | playerSafe, gameOver | Confirm state transitions fired correctly |
| In level constructor | path, width, height | Verify gameEnv values are correct |
// Strategic logging in update() — runs every frame
update() {
console.log('player pos:', this.x, this.y);
console.log('velocity:', this.velocity.x, this.velocity.y);
console.log('playerSafe:', playerSafe, '| round:', currentRound);
}
// Strategic logging in collisionHandler()
collisionHandler(other, direction) {
console.log('collision! other:', other.id, '| direction:', direction);
console.log('player x before:', this.x);
}
✅ console.log statements added in update() and collisionHandler() to track position, velocity, and state during gameplay testing.
Hit Box Visualization
hitbox property | What it does |
|---|---|
widthPercentage: 0.1 | Collision box is 10% of the sprite's drawn width |
heightPercentage: 0.2 | Collision box is 20% of the sprite's drawn height |
widthPercentage: 0.0 | No collision width — barrier uses its own x/y/width/height |
// hitbox config — controls the collision rectangle size
hitbox: { widthPercentage: 0.1, heightPercentage: 0.2 }
// If the sprite is 100px wide, the hitbox is 10px wide (centered)
// Smaller hitbox = more forgiving collisions (player can get closer)
// Visualize the hitbox by drawing it in draw()
draw() {
// draw the sprite first
this.ctx.drawImage(this.spriteSheet, ...);
// toggle hitbox visible for debugging
if (this.showHitbox) {
this.ctx.strokeStyle = 'red';
this.ctx.strokeRect(
this.x + this.hitboxOffsetX,
this.y + this.hitboxOffsetY,
this.hitboxWidth,
this.hitboxHeight
);
}
}
✅ Hitbox percentages tuned in all three level configs. visible: false on barrier allows toggling to true to see the collision wall. NPC hitbox set to 10%/20% to make interaction range visible.
Source-Level Debugging
| Where to set breakpoints | What you can inspect when paused |
|---|---|
Inside update() | this.x, this.y, this.velocity, playerSafe |
Inside collisionHandler(other, direction) | other object, direction string, this state |
On gameOver = true | Every flag that led to this state |
On player.x = 0 (reset) | What triggered the cannonball reset |
How to use the Sources tab:
- Open DevTools → click Sources tab
- Find your game file in the file tree on the left
- Click a line number to set a breakpoint (blue dot appears)
- Play the game — code will pause when it hits that line
- Hover over any variable to see its current value
- Press F10 to step to the next line, F8 to continue
✅ Breakpoints set on collisionHandler() confirmed the direction parameter value, which explained why the barrier was only blocking from one side.
Network Debugging
| Error seen | Status | What it means | Fix |
|---|---|---|---|
flask.opencodingsociety.com/api/id | (blocked:cors) | Flask server missing Access-Control-Allow-Origin header | Server-side config — cannot fix in game JS |
keyboard-shortcuts.js | 404 | File does not exist at that path | Fix the file path or add the file |
sdk.js | ERR_CONNECTION_REFUSED | That server was completely offline | Wait for server to come back up |
How to use the Network tab:
1. DevTools → Network tab
2. Reload the page
3. Look for red rows — those are failed requests
4. Click a row to see:
- Headers tab: request details, response status code
- Response tab: what data (or error) came back
- Timing tab: how long the request took
✅ Network tab used to identify CORS errors as server-side issues (not game code bugs), confirm 404 paths, and verify which API calls succeeded during gameplay.
Application Debugging
| Key found in localStorage | Value | What the game uses it for |
|---|---|---|
gategame_player_name | Aryan | Displays the player's name in the game UI |
escaperoom_coinsCollect... | 16 | Tracks coins collected in the escape room level |
How to use the Application tab:
1. DevTools → Application tab (left panel)
2. Expand Storage → Local Storage
3. Click the site URL
4. See all key-value pairs stored by the game
What to check:
- Is the expected key there? (if not, the save code never ran)
- Is the value correct? (if wrong, the save code has a bug)
- Is it updating when it should? (refresh after gameplay to see changes)
// The game reads these values on load
const playerName = localStorage.getItem('gategame_player_name'); // 'Aryan'
const coins = parseInt(localStorage.getItem('escaperoom_coinsCollected')); // 16
✅ Application tab confirmed gategame_player_name and coin count were saving correctly. Used this instead of adding console.logs to verify persistence.
Element Inspection
| What to inspect | What to look for | Why it matters |
|---|---|---|
<canvas> element | width and height attributes | Confirm canvas matches gameEnv.innerWidth/Height |
Player <div> wrapper | left, top, width, height CSS | Confirm position and size are rendering correctly |
| NPC element | transform style | Check if rotation/scale is applied correctly |
| Game container | overflow: hidden | Confirm game objects don't bleed outside the frame |
How to inspect the canvas:
1. Right-click anywhere on the game → Inspect
2. In Elements panel, find <canvas id="gameCanvas">
3. Check width="" and height="" attributes
4. Look at Computed styles on the right for any CSS overrides
5. Hover over the element in the panel to highlight it on screen
✅ Element Inspector used to confirm canvas dimensions matched gameEnv.innerWidth, and to check that the NPC wrapper div was positioned inside the game container correctly.
✅ Testing & Verification
Gameplay Testing
| Test | Expected result | How to verify |
|---|---|---|
| Player moves with WASD | Slime moves in all 8 directions, facing correctly | Play Stage 2 or Stage 3 runner above |
| Barrier blocks player | Player cannot cross the invisible wall at x=309 | Play Stage 3 runner — try walking past x=309 |
| NPC responds | Dialogue appears when player walks up and presses E | Play Stage 3 runner near the R2 droid |
| Level 1 cannonball reset | Player returns to x=0 on hit | Play full game at link below |
| Level 3 timer elimination | Player eliminated if outside safe zone when timer ends | Play full game at link below |
Full game play-through: 🎮 Slime Escape v2
All three runners above serve as live stage demos — each playable without critical bugs.
Integration Testing
| Integration tested | How to verify |
|---|---|
| Player name saves to localStorage | DevTools Application tab → gategame_player_name: Aryan ✅ |
| Coin count saves to localStorage | DevTools Application tab → escaperoom_coinsCollect...: 16 ✅ |
| Leaderboard POST | Network tab → POST to /api/leaderboard returns 200 OK |
| Leaderboard GET | Network tab → GET from /api/leaderboard returns scores array |
| NPC dialogue loads | Stage 3 runner → NPC shows 'Working' dialogue on interaction |
// Verify integration with console.log after each API call
async function saveScore(name, score) {
const res = await fetch('/api/leaderboard', { method: 'POST', ... });
const data = await res.json();
console.log('Score saved:', data); // integration test: did it work?
console.log('New rank:', data.rank); // integration test: did rank update?
}
✅ localStorage integration confirmed via Application tab. API integration verified via Network tab request/response inspection.
API Error Handling
| Scenario | Without try/catch | With try/catch |
|---|---|---|
| Server is offline | Game crashes with an unhandled error | Error is logged, game continues |
| CORS blocks the request | Uncaught TypeError in console | Error caught, user gets a fallback message |
| Response is not valid JSON | JSON.parse throws, page breaks | Error caught, default state used instead |
| 404 on API endpoint | Uncaught error | res.ok check catches it, error logged |
// Full error-handled API call pattern
async function saveScore(playerName, score) {
try {
const res = await fetch('https://flask.opencodingsociety.com/api/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: playerName, score: score })
});
if (!res.ok) {
throw new Error(`Server returned ${res.status}`); // handle HTTP errors
}
const data = await res.json();
console.log('Score saved successfully:', data);
} catch (err) {
// game does NOT crash — leaderboard failure is non-critical
console.error('Score save failed:', err.message);
showMessage('Score could not be saved — continue playing!');
}
}
✅ All API calls wrapped in try/catch. Non-200 status codes caught with if (!res.ok). CORS failures caught and logged as non-critical — game continues running regardless.