Space Invaders++

Classic Space Invaders reimplemented with procedural polygon aliens, pixel-perfect collision, destructible barriers, lives system, and a high score system. All rendering is done client-side via the Canvas 2D API — no server-side images.

Play

Arrow keys to move left/right, Space to fire. P to pause, Esc for settings. Key bindings and colours are customisable via the settings panel. Visit /invaders.

Architecture

The entire game runs in the browser. The server provides four things:

  1. The template (templates/invaders.html) — loads the external game script, defines the page layout (HTML + CSS, ~200 lines), and includes configurable game metadata.
  2. The game script (static/js/invaders.js) — ~1250 lines of client-side JavaScript handling physics, rendering, collision, scoring, settings persistence, audio synthesis, and high score submission. Served as a static asset via FastAPI's StaticFiles mount; also exports its pure functions for Node.js testing.
  3. The game route (src/routers/invaders.py) — serves the template.
  4. The high score API (/api/highscores, POST /api/highscores) — accepts score submissions from localhost only.

Everything else — alien movement, collision detection, rendering — is client-side JavaScript.

Alien Design

Three distinct alien types, each with a hand-crafted polygon skeleton and inner detail polygons for eyes and highlights:

Type 1 — Squid (top row, 100 pts)

Red (#e74c3c), 18 vertices. Pointed antenna at the top, bulbous dome, narrow waist, flared hips, and two tentacles with a centre spike between them.

Type 2 — Crab (middle row, 50 pts)

Blue (#3498db), 20 vertices. Wide silhouette with upward-pointing claw tips on both shoulders, sharp middle claw on each side, and three downward leg points on each side with a centre bottom spike.

Type 3 — Humanoid (bottom row, 20 pts)

Gold (#f1c40f), 20 vertices. Taller with a pointed helmet peak, rounded helmet sides, arms extended outward with elbows, clear leg separation, and a centre crotch point.

Inner details

Each alien renders small triangular detail polygons on top of its body — two triangular eyes near the top of the head and either a mouth line (squid), claw highlights (crab), or arm highlights + mouth dot (humanoid). Details are white on dark themes and dark on light themes at 85% opacity, optionally glowed if the Glow setting is on.

Spawning

Each round generates a grid of aliens. The grid grows with each round:

Round Columns Rows
1 7 3
2 8 3
3 9 4
N 7 + (N-1) 3 + min(N-1, 2)

Columns cap at 15, rows at 5. The three alien tiers map to rows (row 0 = squid, row 1 = crab, rows 2+ = humanoid).

Round Progression

When all aliens are cleared, a new round begins. The ship respawns centre-screen with 1.5 seconds of invincibility (indicated by flashing). Alien speed increases, fire rate increases, and the alien count grows each round.

Collision Detection

Player bullet vs alien

The bullet's center point is tested against the alien's transformed polygon using the ray-casting algorithm (pointInPolygon). A hit kills the alien, awards points, triggers a floating +N score popup (coloured to the alien tier), spawns explosion particles, and applies screen shake.

Alien bullet vs player

Simple AABB overlap check. Alien bullets move straight down and are removed when they leave the screen or hit the player. Player has 0.25s fire cooldown.

Swept collision (bullets vs barriers)

Both player and alien bullets use a two-phase barrier check: 1. pointInPolygon at the bullet's current position. 2. If that misses, a segment intersection check along the bullet's path from its previous frame position to its current position. This prevents fast-moving bullets from tunneling through thin barrier polygons between frames.

Lives System

The player starts with 3 lives. On death: - Explosion particles spawn (gated by the Particles toggle). - Screen shake triggers (gated by the Shake toggle). - Lives decrement. - If lives reach 0, the game ends and the high score check API is called. - Otherwise, the ship respawns centre-screen with 1.5 seconds of invincibility (flashing).

Game Over Detection

The game ends when: - The player runs out of lives. - Any alien reaches the bottom of the screen (y > H - 60).

Page Layout

The game uses a three-column flex layout that collapses vertically on small screens, matching the Asteroids++ layout pattern:

<div class="row justify-content-center">
  <div class="game-layout">
    <div class="game-layout-inner">
      <div class="controls-sidebar">          <!-- width: 160px; flex-shrink: 0 -->
        Controls panel (key hints + settings toggles)
      </div>
      <div class="game-canvas-col">           <!-- flex: 1 1 0; min-width: 0 -->
        <div class="canvas-wrap">             <!-- aspect-ratio: 1024/768 -->
          <canvas id="game-canvas"></canvas>
        </div>
        <div class="d-flex justify-content-between mt-2">
          Score / focus / settings links.
        </div>
      </div>
      <div class="highscore-sidebar">         <!-- width: 200px; flex-shrink: 0 -->
        High score list.
      </div>
    </div>
  </div>
</div>

Behaviour

  • Wide viewports (≥992px): Three columns sit side by side. The canvas-col grows to fill available space (flex: 1 1 0) between the two fixed sidebars.
  • Short viewports (landscape mobile): The calc(min(100%, (100vh - 18rem) * 1024/768)) height constraint is overridden to width: 100% to prevent the canvas from scrunching.
  • Narrow viewports (<992px): The flex-direction: column media query stacks all three items vertically at full width.

Code Architecture

All game state, rendering, and logic live inside an IIFE in static/js/invaders.js, which keeps internals private while exporting 10 pure functions via module.exports when loaded in Node.js.

CONFIG object

All tunable game constants are defined in a single CONFIG object at module load time:

CONFIG.player          — speed, invincibleDuration, invincibleBlinkRate
CONFIG.bullet          — speed, life, width, height, fireCooldown
CONFIG.alien           — baseSpeed, speedPerRound, descentAmount, fireInterval, fireIntervalMin, cols, rows, colsPerRound, rowsPerRound
CONFIG.stars           — count
CONFIG.shake           — hitIntensity, deathIntensity, decay

Game state object

var game = {
    player: null, aliens: [], bullets: [], alienBullets: [],
    stars: [], particles: [], floatingTexts: [],
    score: 0, round: 0, running: false, paused: false,
    fireCooldown: 0, shake: 0, age: 0,
    alienDir: 1, alienSpeed: 0, alienFireTimer: 0,
    submitToken: '', countdownTimer: null, roundFlash: 0,
};

Core functions

Function Role
initGame() Resets player, score, round; calls startRound()
gameLoop(timestamp) requestAnimationFrame loop with delta-time capping at 50ms
update(dt) Star drift, floating texts, particles, player input, bullet movement, alien group movement, firing, collisions, win/loss checks
draw() Background, stars, round flash, aliens (body + inner details), barriers (removed), bullets, player ship, particles, HUD, floating texts, overlays
createAlien(templateIndex, x, y) Copies the template polygon, computes boundRadius, applies tier size multiplier
playerDie() Decrements lives, checks game over, optionally triggers high score modal
alienFire() Picks a random living alien and fires a bullet downward

Event-loop robustness

  • Visibility API: the game auto-pauses when the browser tab is hidden and resumes when it becomes visible again (unless a settings or name modal is open).
  • AudioContext on gesture: the Web Audio context is resumed on the first click or keydown event, satisfying browser autoplay policies.
  • Fetch error handling: all three fetch() calls have .catch() handlers.
  • rAF ID stored: the requestAnimationFrame ID is stored in rAFId.

Testing

The exported static/js/invaders.js provides 10 pure functions:

  • polygonArea, pointInPolygon, cross2, segmentsIntersect
  • getWorldPoints, polygonCollision
  • rand, randInt, keyCodeToLabel, escHtml

These are tested by 26 assertions in tests/js/invaders.test.js using Node.js's built-in test runner (node --test). All 26 tests run as part of the project's test.sh script alongside the Python/pytest suite.

Rendering

Theme awareness

The game reads CSS custom properties from the active Bootswatch theme for element colours:

  • Ship--bs-primary
  • Bullets--bs-danger
  • Aliens — tier colours (red, blue, gold)
  • Stars — semi-transparent dark/light
  • Background--bs-body-bg

Each element can be overridden via the settings panel. Users can create and save named colour themes in their browser's localStorage.

Visual effects

Each effect can be independently toggled via the sidebar settings:

  • Glow (glowFX): All polygons and alien inner details render with a shadowBlur in their own colour, creating a soft neon glow.
  • Screen shake (shake): Alien hits trigger a 3px screen shake; ship death triggers 8px. The shake decays exponentially.
  • Score popups (hits): When an alien is destroyed, a floating +N text rises from the impact point and fades out over 1 second, coloured to match the alien tier (red, blue, gold).
  • Explosion particles (particles): Alien hits and player death release a burst of coloured particles that scatter and fade.
  • Starfield (stars): 80 stars with varied sizes, twinkle (sine-based opacity oscillation), and slow downward parallax drift.
  • Background vignette: A radial gradient overlay adds subtle depth — brighter at the centre, darker at the edges. Not user-toggleable.

HiDPI support

Canvas buffer is scaled by window.devicePixelRatio while keeping CSS display size at 1024×768 logical pixels, ensuring crisp rendering on Retina displays.

High Scores

Submission flow

  1. When the player dies with no lives remaining, the client calls GET /api/highscores/check?score=N. If the score is a new high score, a name-entry modal appears.
  2. The player enters their name (or defaults to "Unknown").
  3. The client submits name, score, and round via POST /api/highscores.
  4. The server records the score and returns the player's rank.
  5. The high score sidebar refreshes immediately.

Anti-tampering

Score submissions require a single-use HMAC-signed token obtained from GET /api/highscores/check. Tokens are valid for 10 seconds and can only be used once. The name-entry modal shows a live countdown and auto-dismisses when the token expires.

Audio

All sound effects are synthesized at runtime using the Web Audio API — no audio files are loaded:

  • Fire: Square-wave oscillator, frequency sweeps from 660Hz → 220Hz over 0.12s, low gain.
  • Explosion: 0.2s white noise buffer with exponential gain decay.
  • Alien death: Square-wave oscillator, frequency sweeps from 440Hz → 880Hz over 0.1s (rising pitch, distinct from the descending fire sound).

The AudioContext is created on the first sound trigger (browser policy). All audio can be disabled via the Sound effects toggle in settings.

In-memory store

src/store.py keeps the top 10 scores sorted by value, protected by a threading lock. The cache is process-local — restarting the server resets scores.

Controls

Key Action
Arrow Left / Right Move
Space Fire
Enter (game over) Restart
Escape Open / close settings
P Pause / resume
Click eye icon Toggle focus mode (dims sidebars, keeps canvas)

All key bindings are customisable via the settings panel. P and Escape cannot be rebound. A controls sidebar on the left of the canvas shows the current key bindings at all times (with line-height: 1.9 for comfortable reading); it is dimmed in focus mode.

Settings

Click the cog icon or press Escape to open the settings panel. Settings are persisted in browser localStorage.

Key Mappings

Each action (move left, move right, fire) can be rebound to any keyboard key. Click a bind button, then press the desired key. Binding a key that is already in use will reassign it (the old action reverts to its default). P and Escape are reserved and cannot be bound.

Game Options

Six toggles are available in the left sidebar, each persisted in browser localStorage:

Toggle Setting Effect when off
Sound sound Mutes all synthesized audio
Shake shake Suppresses camera shake on hits and death
Stars stars Hides the entire starfield
Glow glowFX Removes the neon shadowBlur from all polygons and inner details
Hit FX hits Suppresses floating +N score popups
Particles particles Suppresses explosion particle bursts from alien kills and player death

The Hit FX and Particles toggles have distinct purposes: Hit FX controls score-feedback text, while Particles controls the visual debris burst. All toggles default to on and can be changed mid-game without restarting.

Colour Themes

The Default (bootswatch) mode inherits colours from the active Bootswatch theme and is not editable. Create a named theme via the + button to save custom colours — colour pickers become editable and changes are saved in real time. Themes can be switched, renamed, or deleted. Background colour is included alongside ship, bullets, aliens, and stars.

Files

File Role
templates/invaders.html Game template (layout, styles, script include)
static/js/invaders.js Game engine (all client-side logic)
tests/js/invaders.test.js 26 unit tests for exported pure functions
src/routers/invaders.py GET /invaders route
src/routers/highscores.py GET/POST /api/highscores, GET /api/highscores/check
src/store.py Thread-safe in-memory high score store