Voidstriker Cs111_ipynb_2_
📋 Table of Contents
🔷 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.