🔷 1 — 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. GameEnvBackground is a second class (imported at the top) — satisfying the 2+ custom classes requirement.

# Writing Classes
# GameLevelVoidStriker is the main level class.
# GameEnvBackground is the second custom class (imported).

class GameLevelVoidStriker:
    def __init__(self, gameEnv):
        width  = gameEnv['innerWidth']
        height = gameEnv['innerHeight']
        path   = gameEnv['path']

        self.classes = [
            { 'class': 'GameEnvBackground', 'data': 'image_data_space' },
        ]

# JS original:
# class GameLevelVoidStriker {
#   constructor(gameEnv) {
#     let width  = gameEnv.innerWidth;
#     let height = gameEnv.innerHeight;
#     let path   = gameEnv.path;
#     this.classes = [
#       { class: GameEnvBackground, data: image_data_space },
#     ];
#   }
# }

⚙️ Methods & Parameters

Every major system is its own method with clear parameters. spawnExplosion takes coordinates and a color string, then generates particle objects. Other examples with 2+ parameters: fireDirected(sx, sy), buildBackgroundScene().

import math, random

particles = []

def rand(lo, hi):
    return random.uniform(lo, hi)

def spawnExplosion(x, y, color):
    """Generate 18 particle objects at (x, y) in the given color."""
    for i in range(18):
        angle = rand(0, math.pi * 2)
        speed = rand(1, 5)
        particles.append({
            'x': x, 'y': y,
            'vx': math.cos(angle) * speed,
            'vy': math.sin(angle) * speed,
            'r':  rand(1.5, 4),
            'alpha': 1,
            'color': color,
        })

print(f"spawnExplosion created {len(particles)} particles")

🧱 Instantiation & Objects

Game objects are built as plain objects (JSON-style) inside factory functions and pushed into arrays. The boss object is instantiated similarly inside spawnBoss().

import random, math

def rand(lo, hi): return random.uniform(lo, hi)
def randI(lo, hi): return random.randint(lo, hi)

W, H = 800, 500
wave = 1
enemies = []
count = 5
hueBase = randI(0, 360)

for i in range(count):
    isChaser = wave >= 2 and random.random() < min(0.45, 0.08 * wave)
    enemies.append({
        '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':     f'hsl({hueBase},85%,55%)',
        'shootTimer': randI(40, 120) if wave >= 5 else float('inf'),
        'chaser':    isChaser,
    })

print(f"Spawned {len(enemies)} enemies. First enemy: {enemies[0]}")

🧬 Inheritance

GameLevelVoidStriker extends the engine's level system by registering GameEnvBackground in this.classes. The engine instantiates each class in the array, chaining constructors automatically via super().

# Inheritance / Constructor Chaining demo

class GameEnvBackground:
    """Base class provided by the engine."""
    def __init__(self, data, gameEnv):
        self.data    = data
        self.gameEnv = gameEnv
        print(f"GameEnvBackground initialized with id: {data['id']}")

class GameLevelVoidStriker(GameEnvBackground):
    """Level subclass — calls super().__init__() to chain constructors."""
    def __init__(self, data, gameEnv):
        super().__init__(data, gameEnv)   # constructor chaining
        print("GameLevelVoidStriker ready!")

env  = {'innerWidth': 800, 'innerHeight': 500, 'path': '/assets'}
data = {'id': 'VoidStriker-Background', 'pixels': {'height': 570, 'width': 1025}}
level = GameLevelVoidStriker(data, env)

🔁 Method Overriding

Enemies and the boss each have dedicated update/draw functions. Normal enemies scroll downward; the boss overrides movement — it chases the player using atan2.

import math

ship = {'x': 400, 'y': 390}

# Standard enemies scroll downward
def updateEnemy(e, worldSpeed=1.0):
    e['y'] += e['speed'] * worldSpeed
    return e

# Boss OVERRIDES movement — chases the player using atan2
def updateBoss(boss):
    dx = ship['x'] - boss['x']
    dy = ship['y'] - boss['y']
    angle = math.atan2(dy, dx)
    boss['x'] += math.cos(angle) * boss['speed']
    boss['y'] += math.sin(angle) * boss['speed']
    return boss

enemy = {'x': 100, 'y': 0, 'speed': 1.5, 'vx': 0}
boss  = {'x': 400, 'y': 50, 'speed': 2.0, 'hp': 20, 'maxHp': 20, 'r': 40, 'tier': 1}

print("Enemy before:", enemy['y'])
updateEnemy(enemy)
print("Enemy after: ", enemy['y'])

print("Boss before: ", (round(boss['x'],2), round(boss['y'],2)))
updateBoss(boss)
print("Boss after:  ", (round(boss['x'],2), round(boss['y'],2)))

🔷 2 — Control Structures

🔄 Iteration

Particle, enemy, bullet, and asteroid arrays are processed and filtered every frame. Wave spawning uses a for loop to create multiple enemies at once.

# Iteration — particle update + filter

particles = [
    {'x': 100, 'y': 200, 'vx': 0.5, 'vy': -1.0, 'alpha': 1.0, 'r': 3.0}
    for _ in range(5)
]

# Update every particle each frame
for p in particles:
    p['x']     += p['vx']
    p['y']     += p['vy']
    p['alpha'] -= 0.03
    p['r']     *= 0.97

# Remove dead particles (filter = loop + condition)
particles = [p for p in particles if p['alpha'] > 0.02]
print(f"Living particles after 1 frame: {len(particles)}")

# Wave spawning: for loop creates multiple enemies
count = 6
new_enemies = []
for i in range(count):
    new_enemies.append({'y': -200 - i * 35})
print(f"Spawned {len(new_enemies)} enemies at y positions: {[e['y'] for e in new_enemies]}")

❓ Conditionals & Nested Conditions

State transitions, hit detection, and feature flags all use if/else. Collision detection checks multiple independent layers — bullet alive, boss present, then enemy list, then asteroids.

import math

# --- Simple conditional: game state transition ---
lives = 0
gameState = 'playing'

if lives <= 0:
    gameState = 'dead'
    print("Game Over! Showing dead screen.")

# --- Nested conditions: bullet vs. boss collision ---
bullets = [{'x': 405, 'y': 80, 'life': 10}]
boss = {'x': 400, 'y': 80, 'r': 40, 'hp': 10, 'maxHp': 10}

def killBoss():
    print("BOSS KILLED!")

for bi in range(len(bullets) - 1, -1, -1):
    b = bullets[bi]
    if b['life'] <= 0:
        continue                       # outer: bullet must be alive
    if boss:
        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'] -= 1
            b['life'] = 0
            print(f"Bullet hit boss! Boss HP now: {boss['hp']}")
            if boss['hp'] <= 0:        # innermost: kill condition
                killBoss()

🔷 3 — Data Types

# --- Numbers ---
ship = {
    'x': 800 / 2, 'y': 500 * 0.78,   # position
    'w': 28, 'h': 38,                  # dimensions
    'speed': 4.5,                      # velocity scalar
    'shootCooldown': 0,                # counter
    'invincible': 0,                   # timer
}
print("Numbers — ship:", ship)

# --- Strings ---
SHIP_CHARS = [
    {'name': 'Striker',  'body': '#a0d8ff', 'cockpit': '#00eeff', 'thrustRgb': '0,200,255'},
    {'name': 'Shadow',   'body': '#bb99ee', 'cockpit': '#cc55ff', 'thrustRgb': '160,0,255'},
]
gameState = 'title'   # 'title' | 'playing' | 'dead'
hueBase = 200
e_color = f'hsl({hueBase},85%,55%)'   # template literal → dynamic color string
print("Strings — gameState:", gameState, "| enemy color:", e_color)

# --- Booleans ---
import random
wave = 3
consoleActive = False
isChaser = wave >= 2 and random.random() < min(0.45, 0.08 * wave)
print("Booleans — consoleActive:", consoleActive, "| isChaser:", isChaser)

# --- Arrays ---
bullets, enemies, asteroids, particles = [], [], [], []
bullets.append({'x': ship['x'], 'y': ship['y'], 'vx': 0, 'vy': -8, 'life': 60})
bullets = [b for b in bullets if b['life'] > 0]   # filter removes dead bullets
print("Arrays — active bullets:", len(bullets))

# --- Objects (JSON) ---
image_data_space = {
    'id':     'VoidStriker-Background',
    'src':    '',
    'pixels': {'height': 570, 'width': 1025}
}
BOSS_PALETTES = [
    {'glow': '255,40,80', 'bodyHi': '#ff7799', 'bodyLo': '#660022',
     'tentacle': '#aa1144', 'eye': '#ffee44'},
]
print("Objects — background data:", image_data_space)

🔷 4 — Operators

➕ Mathematical, String & Boolean Operators

Physics, geometry, and scoring use arithmetic. Template literals build color and UI strings dynamically. Compound conditions guard damage, spawning, and shooting.

import math, random

# --- Mathematical Operators ---
a = {'verticalVelocity': 0.5, 'gravityAcceleration': 0.05, 'terminalVelocity': 8, 'y': 100}
worldSpeed = 1.0

a['verticalVelocity'] -= a['gravityAcceleration']       # subtract
a['y'] += -a['verticalVelocity'] * worldSpeed            # multiply + add

dx, dy = 30, 40
dist = math.sqrt(dx * dx + dy * dy)                      # distance formula
totalKills = 10
totalKills += 5                                           # boss worth 5 kills
print(f"Math ops — dist: {dist}, totalKills: {totalKills}, asteroid y: {a['y']:.2f}")

# --- String Operations (template literals / f-strings) ---
hueBase = 200
def randI(lo, hi): return random.randint(lo, hi)

enemy_color = f'hsl({(hueBase + randI(-15,15) + 360) % 360},85%,55%)'
kills_hud   = f'KILLS: {totalKills}'
wave_hud    = f'WAVE: 3'
print(f"Strings — color: {enemy_color} | HUD: '{kills_hud}' | '{wave_hud}'")

# --- Boolean Expressions ---
wave = 3
boss = None
nextBossWave = 3

if wave == nextBossWave and not boss:
    print("Boolean — both conditions met: spawning boss!")

keys = {'ArrowLeft': False, 'ArrowRight': True}
shootCooldown = 0
if shootCooldown == 0:
    sx = 0
    if keys['ArrowLeft']:  sx -= 1
    if keys['ArrowRight']: sx += 1
    if sx != 0:
        print(f"Boolean — fireDirected called with sx={sx}")

🔷 5 — Input / Output

⌨️ Keyboard Input

attachInput() registers keydown and keyup listeners on window. The keys object acts as a real-time snapshot of held keys, consumed each frame in updateShip().

# Simulated keyboard input (Python equivalent of JS keydown/keyup listeners)

keys = {}
consoleActive = False

def on_keydown(key):
    if key in ('p', 'P'):
        print("[P] Toggling pause/console")
    if not consoleActive:
        keys[key] = True
    if key in ('ArrowUp', 'ArrowDown'):
        print(f"[{key}] Default scroll prevented")

def on_keyup(key):
    keys[key] = False

ship = {'x': 400, 'y': 390, 'speed': 4.5}

def updateShip():
    if keys.get('a') or keys.get('A'): ship['x'] -= ship['speed']
    if keys.get('d') or keys.get('D'): ship['x'] += ship['speed']

# Simulate pressing 'd' (right)
on_keydown('d')
updateShip()
print(f"Ship moved right: x = {ship['x']}")
on_keyup('d')
updateShip()
print(f"Key released, ship stays: x = {ship['x']}")

🌐 API Integration & Async I/O

The game fires a custom DOM event on every kill. An external leaderboard listener catches it and POSTs the score. async/await handles the promise chain. setTimeout in the constructor defers init() until the canvas is mounted.

import asyncio, json

# Simulated async fetch (mirrors the JS async/await leaderboard pattern)
async def post_score(total_kills: int):
    payload = json.dumps({'score': total_kills})
    print(f"[POST] /api/leaderboard  payload={payload}")

    # Simulate a successful server response
    await asyncio.sleep(0.05)          # mimic network latency
    response_data = {'status': 'ok', 'rank': 3}
    print(f"[RESPONSE] {response_data}")
    return response_data

async def on_vs_kills(event_detail):
    try:
        data = await post_score(event_detail['total'])
        print(f"Score saved: {data}")
    except Exception as err:
        print(f"Leaderboard unavailable: {err}")

# Trigger the event (wave kill)
asyncio.run(on_vs_kills({'total': 42}))

# --- JSON Parsing ---
raw_json = '{"scores": [99, 87, 42]}'
parsed   = json.loads(raw_json)
print(f"JSON parsed top score: {parsed['scores'][0]}")

🔷 6 — Documentation

📝 Code Comments

Every major system has a purpose comment. Inline comments explain the why of non-obvious math. Target: comment density > 10% — count comment lines vs. total lines in your file.

# Per-frame gravity loop (lesson: Gravity System)

def update_asteroid(a, worldSpeed=1.0):
    # 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
    return a

# Test the gravity system
asteroid = {
    'y': 0,
    'verticalVelocity': 0.0,
    'gravityAcceleration': 0.05,
    'terminalVelocity': 8.0,
}

for frame in range(5):
    update_asteroid(asteroid)
    print(f"Frame {frame+1}: y={asteroid['y']:.3f}, vVel={asteroid['verticalVelocity']:.3f}")

🔷 7 — Debugging

Technique How
Console Debugging console.log at state transitions and collision checks
Hitbox Visualization Toggle DEBUG_HITBOX to draw collision radii on canvas
Source-Level Breakpoint in checkCollisions() → step through loops in DevTools
Network DevTools → Network → inspect POST payload, status, CORS headers
Application DevTools → Application → Local Storage to check session tokens
import math

DEBUG_HITBOX = True   # Toggle to see collision radii

boss = {'x': 400, 'y': 100, 'hp': 5, 'maxHp': 20, 'r': 40, 'tier': 1}
totalKills = 15

# Console Debugging — log at key state transitions
def killBoss():
    print(f"[DEBUG] Boss tier {boss['tier']} killed. Total kills: {totalKills}")

# Hitbox Visualization (Python text representation)
def debugHitbox(entity, label):
    if DEBUG_HITBOX:
        print(f"[HITBOX] {label} — center: ({entity['x']}, {entity['y']}), radius: {entity['r']}")

# Network Debugging — inspect POST structure before sending
import json
def debugNetworkPayload(score):
    payload = {'score': score}
    print(f"[NETWORK] POST /api/leaderboard  →  {json.dumps(payload)}")
    print("[NETWORK] Expected response: 200 OK with Access-Control-Allow-Origin header")

debugHitbox(boss, 'Boss')
debugNetworkPayload(totalKills)

# Simulate boss death trigger
boss['hp'] -= 5
if boss['hp'] <= 0:
    killBoss()

🔷 8 — Testing & Verification

import math, json

print("=" * 55)
print("  VoidStriker — Automated Test Suite")
print("=" * 55)

passed = 0
failed = 0

def check(label, condition):
    global passed, failed
    if condition:
        print(f"  ✅ PASS  {label}")
        passed += 1
    else:
        print(f"  ❌ FAIL  {label}")
        failed += 1

# --- Gameplay Tests ---
print("\n[Gameplay]")
wave = 1
enemies_cleared = True
if enemies_cleared:
    wave += 1
check("Wave increments only after all enemies cleared", wave == 2)

nextBossWave = 3
wave = 3
boss_spawned = (wave == nextBossWave)
check("Boss spawns on wave 3", boss_spawned)

lives = 0
gameState = 'dead' if lives <= 0 else 'playing'
check("Death screen shown when lives reach 0", gameState == 'dead')

# Retry resets
wave, lives, totalKills = 1, 3, 0
check("Retry resets wave to 1",       wave == 1)
check("Retry resets lives to 3",      lives == 3)
check("Retry resets kills to 0",      totalKills == 0)

# --- Integration Test: leaderboard POST ---
print("\n[Integration]")
score = 42
payload = json.dumps({'score': score})
parsed  = json.loads(payload)
check("Leaderboard payload serializes correctly",    parsed['score'] == score)
check("POST payload contains 'score' key",           'score' in parsed)

# --- API Error Handling ---
print("\n[Error Handling]")
def safe_fetch(ok):
    try:
        if not ok:
            raise RuntimeError("HTTP 500")
        return {'status': 'ok'}
    except Exception as err:
        return {'error': str(err)}

check("Successful response handled",  'status' in safe_fetch(True))
check("Failed response caught gracefully", 'error' in safe_fetch(False))

print("\n" + "=" * 55)
print(f"  Results: {passed} passed, {failed} failed")
print("=" * 55)

✅ All CS 111 requirements demonstrated using VoidStriker

All code examples drawn from GameLevelVoidStriker.js — search by function name to locate each snippet.