Cs111 Blog
CS 111 Requirements — VoidStriker Code Guide
A complete walkthrough of every CS 111 learning objective with annotated examples pulled directly from GameLevelVoidStriker.js.
Object-Oriented Programming
Writing Classes
VoidStriker’s level is itself a class. The constructor receives the shared gameEnv (canvas size, asset path) and registers which game objects to build.
class GameLevelVoidStriker {
constructor(gameEnv) {
let width = gameEnv.innerWidth;
let height = gameEnv.innerHeight;
let path = gameEnv.path;
// ...
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
}
}
GameEnvBackground is a second class (imported at the top) — satisfying the 2+ custom classes requirement.
Methods & Parameters
Every major system is its own method with clear parameters. Example: spawnExplosion takes coordinates and a color string, then generates particle objects.
function spawnExplosion(x, y, color) {
for (let i = 0; i < 18; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(1, 5);
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
r: rand(1.5, 4),
alpha: 1,
color,
});
}
}
Other examples with 2+ parameters: fireDirected(sx, sy), buildBackgroundScene() (uses module-level W, H).
Instantiation & Objects
Game objects are built as plain objects (JSON-style) inside factory functions and pushed into arrays — a data-driven approach the GameBuilder pattern relies on.
enemies.push({
x: rand(40, W - 40),
y: rand(-200, -30) - i * 35,
r: 18,
speed: rand(1.0, 1.8 + wave * 0.25),
hp: 1 + Math.floor(wave / 2),
color: `hsl(${hueBase},85%,55%)`,
shootTimer: wave >= 5 ? randI(40, 120) : Infinity,
chaser: isChaser,
});
The boss object is instantiated similarly inside spawnBoss().
Inheritance (Basic)
GameLevelVoidStriker extends the engine’s level system by registering GameEnvBackground (imported base class) in this.classes. The engine then instantiates each class in the array, chaining constructors automatically.
import GameEnvBackground from '@assets/js/GameEnginev1.1/essentials/GameEnvBackground.js';
// GameEnvBackground is the parent class; this level composes it
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
Method Overriding
The IIFE module pattern gives each system its own update + draw pair, mimicking the polymorphic lifecycle the engine expects. For example, enemies and the boss each have dedicated update/draw functions that replace default behavior:
// Standard enemies scroll downward
function updateEnemies() {
enemies.forEach(e => {
e.y += e.speed * worldSpeed;
e.x += e.vx * worldSpeed;
// ...
});
}
// Boss overrides movement — it chases the player using atan2
function updateBoss() {
const dx = ship.x - boss.x;
const dy = ship.y - boss.y;
const angle = Math.atan2(dy, dx);
boss.x += Math.cos(angle) * boss.speed;
boss.y += Math.sin(angle) * boss.speed;
}
Constructor Chaining
The level constructor delegates background setup to the engine via this.classes, which causes the engine to call new GameEnvBackground(data, gameEnv) — chaining through super() internally.
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
// Engine calls: new GameEnvBackground(image_data_space, gameEnv)
// GameEnvBackground calls: super(data, gameEnv) internally
Control Structures
Iteration
Loops appear throughout. The particle, enemy, bullet, and asteroid arrays are all processed with forEach and filtered with filter every frame:
// Update every particle each frame
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.alpha -= 0.03;
p.r *= 0.97;
});
// Remove dead particles (filter = loop + condition)
particles = particles.filter(p => p.alpha > 0.02);
Wave spawning uses a for loop to create multiple enemies at once:
for (let i = 0; i < count; i++) {
enemies.push({ /* ... */ y: rand(-200, -30) - i * 35 });
}
Conditionals
State transitions, hit detection, and feature flags all use if/else:
// Game state transition
if (lives <= 0) {
gameState = 'dead';
showDeadScreen();
}
// Chaser enemies use direction-vector movement; normal enemies scroll
if (e.chaser) {
const dx = ship.x - e.x;
const dy = ship.y - e.y;
// ...
} else {
e.y += e.speed * worldSpeed;
}
Nested Conditions
Collision detection checks multiple independent layers — bullet alive, boss present, then enemy list, then asteroids:
for (let bi = bullets.length - 1; bi >= 0; bi--) {
const b = bullets[bi];
if (b.life <= 0) continue; // outer: bullet must be alive
if (boss) {
const dx = b.x - boss.x, dy = b.y - boss.y;
if (Math.sqrt(dx*dx + dy*dy) < boss.r + 4) { // inner: hit radius
boss.hp--;
b.life = 0;
if (boss.hp <= 0) killBoss(); // innermost: kill condition
}
}
// ... then check enemy array, then asteroid array
}
Data Types
Numbers
Position, velocity, health, wave index, score — nearly every game property is a number:
ship = {
x: W / 2, y: H * 0.78, // position
w: 28, h: 38, // dimensions
speed: 4.5, // velocity scalar
shootCooldown: 0, // counter
invincible: 0, // timer
};
Strings
Ship names, CSS color strings, game state flags, and DOM text are all strings:
const SHIP_CHARS = [
{ name: 'Striker', body: '#a0d8ff', cockpit: '#00eeff', thrustRgb: '0,200,255' },
{ name: 'Shadow', body: '#bb99ee', cockpit: '#cc55ff', thrustRgb: '160,0,255' },
];
let gameState = 'title'; // 'title' | 'playing' | 'dead'
// Template literal to build dynamic color strings
e.color = `hsl(${hueBase},85%,55%)`;
Booleans
Flags control flow throughout the game loop:
let consoleActive = false; // paused?
const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);
// Boolean used in guard clause
if (ship.invincible <= 0) { /* take damage */ }
Arrays
All dynamic game objects live in arrays that shrink and grow every frame:
let bullets = [], enemies = [], asteroids = [], particles = [];
let enemyBullets = [];
// Add
bullets.push({ x: ship.x, y: ship.y, vx, vy, life: 60 });
// Remove dead entries (non-destructive filter)
bullets = bullets.filter(b => b.life-- > 0);
Objects (JSON)
Configuration data is stored as plain object literals — easy to read and swap:
const image_data_space = {
id: 'VoidStriker-Background',
src: '',
pixels: { height: 570, width: 1025 }
};
const BOSS_PALETTES = [
{ glow: '255,40,80', bodyHi: '#ff7799', bodyLo: '#660022', tentacle: '#aa1144', eye: '#ffee44' },
// ...
];
Operators
Mathematical
Physics, geometry, and scoring all rely on arithmetic operators:
// Asteroid gravity system (acceleration → velocity → position)
a.verticalVelocity -= a.gravityAcceleration; // subtract
a.y += -a.verticalVelocity * worldSpeed; // multiply + add
// Distance formula for collision detection
const dist = Math.sqrt(dx * dx + dy * dy); // multiply + add + sqrt
// Score tracking
totalKills += 5; // boss is worth 5 kills
String Operations
Template literals build color and UI strings dynamically:
// Dynamic HSL color per enemy wave
`hsl(${(hueBase + randI(-15,15) + 360) % 360},85%,55%)`
// Dynamic CSS gradient from palette data
`linear-gradient(90deg, rgba(${p.glow},1), ${p.bodyHi})`
// HUD text update
s.textContent = `KILLS: ${totalKills}`;
w.textContent = `WAVE: ${wave}`;
Boolean Expressions
Compound conditions guard damage, spawning, and shooting:
// Boss spawns only when conditions both true
if (wave === nextBossWave && !boss) spawnBoss();
// Chaser spawns only in wave 2+ AND random roll passes
const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);
// Player can only shoot when cooldown expired AND a direction key is held
if (ship.shootCooldown === 0) {
let sx = 0, sy = 0;
if (keys['ArrowLeft']) sx -= 1;
if (keys['ArrowRight']) sx += 1;
if (sx !== 0 || sy !== 0) { fireDirected(sx, sy); }
}
Input / Output
Keyboard Input
attachInput() registers keydown and keyup listeners on window. The keys object acts as a real-time snapshot of held keys:
const keys = {};
function attachInput() {
window.addEventListener('keydown', e => {
if (e.key === 'p' || e.key === 'P') {
// Toggle pause/console
}
if (!consoleActive) keys[e.key] = true;
// Prevent page scroll while playing
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
}
// Consumed each frame in updateShip()
if (keys['a'] || keys['A']) ship.x -= ship.speed;
Canvas Rendering
All visuals are drawn to a <canvas> element every frame via the 2D context. Gradients, arcs, and transforms compose the ship, enemies, and particles:
// Ship body (triangle)
ctx.fillStyle = ch.body;
ctx.beginPath();
ctx.moveTo(0, -h / 2);
ctx.lineTo(w / 2, h / 2);
ctx.lineTo(-w / 2, h / 2);
ctx.closePath();
ctx.fill();
// Bullet glow (radial gradient)
const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, 5);
g.addColorStop(0, '#fff');
g.addColorStop(0.4, bColor);
g.addColorStop(1, 'transparent');
ctx.fillStyle = g;
ctx.arc(b.x, b.y, 5, 0, Math.PI * 2);
ctx.fill();
GameEnv Configuration
The constructor reads gameEnv properties to size and position everything relative to the host engine’s canvas:
constructor(gameEnv) {
let width = gameEnv.innerWidth;
let height = gameEnv.innerHeight;
let path = gameEnv.path;
const image_data_space = {
id: 'VoidStriker-Background',
pixels: { height: 570, width: 1025 }
};
// ...
}
Inside init(), the canvas is sized to match the container:
const rect = container.getBoundingClientRect();
W = rect.width || gameEnv.innerWidth || 800;
H = rect.height || gameEnv.innerHeight || 500;
canvas.width = W;
canvas.height = H;
API Integration
The game fires a custom DOM event every time the kill count changes. An external leaderboard listener can catch this and POST the score:
// Dispatched on every kill (enemies, asteroids, boss)
window.dispatchEvent(new CustomEvent('vs-kills', {
detail: { total: totalKills }
}));
// Example leaderboard integration (async/await pattern)
window.addEventListener('vs-kills', async (e) => {
try {
const res = await fetch('/api/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score: e.detail.total })
});
const data = await res.json();
console.log('Score saved:', data);
} catch (err) {
console.error('Leaderboard error:', err);
}
});
Asynchronous I/O
The setTimeout in the constructor defers init() until the engine has finished mounting its canvas — a simple async pattern ensuring DOM readiness:
constructor(gameEnv) {
// Defer init so the engine canvas exists before we query the DOM
setTimeout(() => VoidStrikerGame.init(gameEnv), 200);
}
For leaderboard or NPC AI calls, async/await handles the promise chain (see API Integration above).
JSON Parsing
API responses are parsed with JSON.parse() (or res.json() which does the same). The cheat console also parses user input:
// Typical fetch + parse pattern
const res = await fetch('/api/leaderboard');
const data = await res.json(); // parses JSON body
const topScores = data.scores; // destructure the array
Documentation
Code Comments
Every major system in the file has a purpose comment. Inline comments explain the why of non-obvious math:
// Per-frame gravity loop (lesson: Gravity System):
// 1. Subtract gravity from vertical velocity (pulls asteroid down)
a.verticalVelocity -= a.gravityAcceleration;
// 2. Clamp at terminal velocity so asteroids don't fall infinitely fast
if (-a.verticalVelocity > a.terminalVelocity) {
a.verticalVelocity = -a.terminalVelocity;
}
// 3. Convert to engine velocity (flip sign): negative verticalVelocity = downward y motion
a.y += -a.verticalVelocity * worldSpeed;
Target: comment density > 10% — count comment lines vs. total lines in your file.
Debugging
Console Debugging
Add console.log at key state transitions and collision checks:
function killBoss() {
console.log(`Boss tier ${boss.tier} killed. Total kills: ${totalKills}`);
// ...
}
function checkCollisions() {
// Temporary: log every frame to track collision state
if (boss) console.log(`Boss HP: ${boss.hp}/${boss.maxHp}`);
}
Hit Box Visualization
Toggle a debug draw mode to see collision radii:
// Add to drawEnemies() during development
if (DEBUG_HITBOX) {
ctx.strokeStyle = 'lime';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(e.x, e.y, e.r, 0, Math.PI * 2);
ctx.stroke();
}
Source-Level Debugging
Set a breakpoint inside checkCollisions() in Chrome DevTools → Sources tab. Step through the nested loops to watch dx, dy, and the distance calculation in real time.
Network Debugging
After posting a score, open DevTools → Network tab. Find the POST /api/leaderboard request and inspect:
- Status — 200 OK vs. 4xx/5xx
- Payload — confirm
{ score: 42 }was sent correctly - Response — check what the server returned
- CORS headers —
Access-Control-Allow-Originmust be present for cross-origin calls
Application Debugging
If the game stores session data (login token, saved settings), DevTools → Application → Local Storage / Cookies shows the raw key-value pairs. Use this to confirm a user token is present before making authenticated leaderboard requests.
Element Inspection
Right-click the game canvas → Inspect. In the Elements panel you can:
- Confirm
voidstriker-canvashasposition: absolute; z-index: 9999 - Check that the HUD elements (
#vs-ui,#vs-lives) overlay the canvas correctly - Live-edit CSS to test layout fixes without reloading
Testing & Verification
Gameplay Testing
Play through at least three waves to verify:
- Wave counter increments only after all enemies and asteroids are cleared
- Boss spawns on wave 3; health bar appears and fills correctly
- Death screen shows correct kill count and best score
- Retry resets wave, lives, and kill counter to starting values
Integration Testing
After adding the leaderboard listener, test end-to-end:
- Kill enough enemies to trigger a
vs-killsevent - Open Network tab — confirm the POST fires with the right payload
- Refresh the leaderboard GET endpoint — confirm the score appears
API Error Handling
Wrap every fetch in try/catch and handle both network failures and bad status codes:
try {
const res = await fetch('/api/leaderboard', { method: 'POST', body: JSON.stringify(payload) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log('Saved:', data);
} catch (err) {
// Graceful degradation — game continues even if the leaderboard is down
console.error('Leaderboard unavailable:', err.message);
}
All code examples above are drawn directly from GameLevelVoidStriker.js. Line numbers will vary as the file evolves — search by function name to locate each snippet.