Aryan Malik — CS111 Portfolio

This blog covers my full CS111 journey — how I went from not knowing how sprite sheets worked to shipping a three-level group game. Each section below shows the actual code at each stage, with live game runners where relevant.


Part 1: First Level — Moving Player (Wrong Facing)

My first working level had a background and a slime you could move with WASD. The slime moved — but it faced the wrong direction. Moving left played the down animation. Moving down played the left animation.

The bug was in playerData: I had sprite sheet rows mapped to the wrong directions.

Challenge

Move with WASD — the slime faces the wrong direction. Row 0 and row 2 are swapped!

Lines: 1 Characters: 0
Game Status: Not Started

Play the runner below and notice the slime faces the wrong way:

Challenge

Move with WASD — rows 0 and 2 are swapped. The slime now faces the correct direction!

Lines: 1 Characters: 0
Game Status: Not Started

Part 2: The Fix — Swapping Rows 0 and 2

A sprite sheet is a grid where each row is a direction and each column is one animation frame. I inspected the actual slime.png and found row 0 is LEFT-facing, row 2 is DOWN-facing — I had them backwards.

Direction Wrong Fixed
down row 0 row 2
left row 2 row 0

Swapping those two numbers fixed every direction at once:

Challenge

Move with WASD. Walk up to the NPC to interact, and try walking through the invisible barrier!

Lines: 1 Characters: 0
Game Status: Not Started

Part 3: Full Level — NPC and Collision Barrier

With movement fixed I added two new game objects: an NPC and an invisible Barrier.

The NPC uses the same config pattern as the player — position, scale, sprite sheet rows — so learning it for the player transferred directly.

For barriers, the critical thing I learned: fromOverlay: true is required or the engine ignores the barrier entirely and the player walks straight through it.

const dbarrier_1 = {
  id: 'dbarrier_1', x: 309, y: 89, width: 28, height: 252,
  visible: false,     // invisible wall
  fromOverlay: true   // required — activates collision
};

Walk up to the NPC to interact, and try to walk through the invisible wall:

%%js

// GAME_RUNNER: Move with WASD. Walk up to the NPC to interact, and try walking through the invisible barrier!

import GameControl from '/assets/js/GameEnginev1/essentials/GameControl.js';
import GameEnvBackground from '/assets/js/GameEnginev1/essentials/GameEnvBackground.js';
import Player from '/assets/js/GameEnginev1/essentials/Player.js';
import Npc from '/assets/js/GameEnginev1/essentials/Npc.js';
import Barrier from '/assets/js/GameEnginev1/essentials/Barrier.js';

class GameLevelFullygame {
  constructor(gameEnv) {
    const path = gameEnv.path;
    const bgData = {
      name: 'custom_bg',
      src: path + '/images/gamebuilder/bg/alien_planet.jpg',
      pixels: { height: 772, width: 1134 }
    };
    const playerData = {
      id: 'playerData',
      src: path + '/images/gamebuilder/sprites/slime.png',
      SCALE_FACTOR: 5,
      STEP_FACTOR: 1000,
      ANIMATION_RATE: 50,
      INIT_POSITION: { x: 100, y: 300 },
      pixels: { height: 225, width: 225 },
      orientation: { rows: 4, columns: 4 },
      down:      { row: 2, start: 0, columns: 3 },
      downRight: { row: 1, start: 0, columns: 3, rotate: Math.PI/16 },
      downLeft:  { row: 0, start: 0, columns: 3, rotate: -Math.PI/16 },
      left:      { row: 0, start: 0, columns: 3 },
      right:     { row: 1, start: 0, columns: 3 },
      up:        { row: 3, start: 0, columns: 3 },
      upLeft:    { row: 2, start: 0, columns: 3, rotate: Math.PI/16 },
      upRight:   { row: 3, start: 0, columns: 3, rotate: -Math.PI/16 },
      hitbox: { widthPercentage: 0, heightPercentage: 0 },
      keypress: { up: 87, left: 65, down: 83, right: 68 }
    };
    const npcData = {
      id: 'NPC',
      greeting: 'Working',
      src: path + '/images/gamify/r2_idle.png',
      SCALE_FACTOR: 5,
      ANIMATION_RATE: 50,
      INIT_POSITION: { x: 500, y: 300 },
      pixels: { height: 223, width: 505 },
      orientation: { rows: 1, columns: 3 },
      down:      { row: 0, start: 0, columns: 3 },
      right:     { row: 0, start: 0, columns: 3 },
      left:      { row: 0, start: 0, columns: 3 },
      up:        { row: 0, start: 0, columns: 3 },
      hitbox: { widthPercentage: 0.1, heightPercentage: 0.2 },
      dialogues: ['Working']
    };
    const barrier1 = {
      id: 'barrier1', x: 309, y: 89, width: 28, height: 252,
      visible: false,
      hitbox: { widthPercentage: 0.0, heightPercentage: 0.0 },
      fromOverlay: true
    };
    this.classes = [
      { class: GameEnvBackground, data: bgData },
      { class: Player,  data: playerData },
      { class: Npc,     data: npcData },
      { class: Barrier, data: barrier1 }
    ];
  }
}
export const gameLevelClasses = [GameLevelFullygame];
export { GameControl };

Part 4: Debugging With DevTools

Once the game was running, things still broke — just silently. The browser’s DevTools became my main diagnostic tool.

Console — Reading Errors

Console showing CORS and 404 errors

Error Meaning
404 Wrong file path or file doesn’t exist
ERR_CONNECTION_REFUSED Server is offline
CORS policy blocked Server rejected a cross-origin request — server-side fix only, can’t patch in JS
User initialization failed (non-critical) Auth failed but game still runs — not fatal

The CORS errors came from the game on gates.opencodingsociety.com calling flask.opencodingsociety.com/api/id. Different subdomains, and Flask wasn’t configured to allow it. Once I understood that, I stopped trying to fix it in game code.

Application Panel — Game State

localStorage showing player name and coin count

The Application tab shows what the game is actually storing in localStorage. Two entries in my game:

  • gategame_player_nameAryan
  • escaperoom_coinsCollect...16

If data wasn’t loading right, I could check here to see what was actually stored vs what the code expected — faster than adding console.log statements everywhere.


Part 5: The Group Game — 3-Level Slime Escape

My team built a complete three-level game on the shared class game engine. You play as a slime trying to reach the sea.

Version Link What It Is
Standalone pages.opencodingsociety.com/gategame Original self-contained build — all 3 levels in one flow
Platform gategame-integration Wired into the shared Gamify engine — required our exports to match the platform’s API

Getting the standalone game was one challenge. Getting it running inside the shared platform was harder — level class names can’t conflict, exports have to match what the engine expects, and the game loop has to hand control back cleanly when a level ends.


Level 1: Cannonball Dodge

Cannonballs fire from the right. Use W/S to dodge. Dodge = advance 300px toward the gate. Get hit = reset.

if (playerHitbox.overlaps(cannonball)) {
    player.x = 0;       // reset
} else {
    player.x += 300;    // advance
}

A single if/else runs the entire mechanic. Boolean flags tracked whether a cannonball was active mid-flight.


Level 2: Escape Room Maze

Navigate a maze with an NPC guide. Find the gate, press E then Esc to advance. Every barrier is a data-driven config object in an array — the maze layout is just data.

// All barriers live in an array — loop registers them all
barriers.forEach(b => this.classes.push({ class: Barrier, data: b }));

Changing the maze means editing the array, not rewriting logic.


Level 3: Zone Catch

Two colored zones appear. A banner says which is safe. Get inside before the timer bar empties or you’re eliminated. Round 6+ adds a golden gate for early escape.

// Pixel color scan — engine checks colors around player each frame
if (getPixelColor(player) === safeColor) { playerSafe = true; }
if (currentRound >= 6)                   { showGoldenGate(); }

This level stacked the most concepts — pixel scanning in a loop, boolean state across rounds, and a conditional that unlocks a new mechanic at a threshold.


Part 6: CS111 Concepts — How They Showed Up in Game Code

Every concept I learned in class had a direct equivalent in the game.

Concept In Class In the Game
Variables Store a value playerX, SCALE_FACTOR, coinsCollected
Data Types String, Number, Boolean, Object IDs are strings; positions are numbers; state flags are booleans; configs are objects
Conditionals if/else Hit-or-dodge check, safe zone check, golden gate unlock
Loops for, forEach Game loop runs every frame; barriers registered via forEach; pixel scan per frame
Arrays List of values this.classes holds every game object; dialogues array; zone pair list
Objects Key-value data Every playerData, bgData, npcData config is a plain object
Classes Blueprints for objects Every level is a class the engine instantiates with new
Boolean flags True/false state playerSafe, cannonballActive, gameOver
Functions Reusable logic showRandomDialogue(), launchCannonball(), advanceToNextLevel()

The thing that made it click: every mechanic in the game is just a CS concept applied to a specific problem. The hit detection is a conditional. The game loop is a loop. The level config is an object. Once I saw that, the concepts stopped being abstract.


Boolean State Management

Managing a multi-round game required tracking a lot of state with booleans:

let playerSafe     = false;  // in the safe zone?
let timerRunning   = true;   // round timer active?
let goldenGateOpen = false;  // past round 6?
let gameOver       = false;  // game done?

if (!playerSafe && timerExpired) { gameOver = true; }
if (currentRound >= 6)           { goldenGateOpen = true; }

Naming booleans clearly and updating them in the right order was one of the harder debugging lessons — especially across multiple rounds.


Final Reflections

What I Did What I Learned
Level with wrong sprite rows Read the sprite sheet before mapping rows
Fixed row mapping Debugging is reading, not guessing
Added NPC and Barrier fromOverlay: true required; config patterns transfer across objects
DevTools Console CORS is server-side; 404 means wrong path
DevTools Application localStorage shows actual state — check it first
Level 1: Cannonball Dodge Conditionals and booleans control game logic
Level 2: Escape Room Maze Data-driven arrays make levels easy to edit
Level 3: Zone Catch Pixel scanning, multi-round booleans, nested logic
Snake Arrays are the data structure under simple mechanics
Fish Parallax Loop + array + math = polished visual effect
Shipped standalone game Building a complete self-contained product
Gamify integration Writing code that fits someone else’s architecture
Networking unit The network stack explains every error I debugged

I came in knowing what variables and loops were. I’m leaving knowing how to use them to build something.