Dominic Kennedy

Spacewar! From Web to Apple TV

Game DevelopmentSwiftApple TV

Porting a web game to the biggest screen in the house. From empty repo to TestFlight in one session, then the humbling reality of playtesting.

The couch multiplayer dream

Spacewar! was always a two-player game. The 1962 original had two people hunched over a . Seiler's 1985 DOS version put them side-by-side at a keyboard. My web recreation kept that same setup — two players, one screen.

Apple TV puts that screen where it belongs: the biggest one in the house. Controllers in hand, couch between you, the game finally gets the shared-screen experience it was designed for.

From Canvas to SpriteKit

The web edition runs on Canvas 2D with hand-rolled everything — physics, rendering, collision detection, particle systems. The Apple TV port rebuilds it all in Swift with SpriteKit, but the parameters stay identical.

meant converting every Canvas 2D call into SpriteKit nodes. CGPaths replaced arc/lineTo calls. SKShapeNodes replaced fillRect. The replaced keyboard events.

One constant source of friction: . Canvas has Y pointing down; SpriteKit has Y pointing up. Every coordinate from the web version needed negating. The ship designs — the Pretzel's quad curves and the Triangle's delta body — all had their Y components flipped. Easy to describe, tedious to debug.

The SKNode.copy() crash

The web version renders "edge ghosts" — when a ship nears a screen edge, a copy appears on the opposite side to create seamless toroidal space. In JavaScript, you just draw the same sprite at an offset position.

In SpriteKit, I tried SKNode.copy(). It crashed.

. Custom SKNode subclasses with init(coder:) = fatalError can't be copied — and that's the standard Swift pattern for nodes you never decode from archives. The solution: copy the entity's children (standard shape nodes that copy cleanly) instead of the entity itself.

Three hours of debugging for a three-line fix. Porting stories.

Empty repo to TestFlight

The entire first session — bootstrap through TestFlight upload — happened in one sitting. Twelve commits across all planned phases:

  1. Bootstrap — XcodeGen project, DOS VGA font, amber theme, gameplay constants ported from game.js
  2. Ships + movement — CGPath geometries, manual physics, GameController polling
  3. Weapons + collisions + energy — Torpedoes, phasers, energy/shield system with HUD
  4. Warp + AI — Three-phase hyperspace animation, two AI personalities
  5. Menu + demo cycle — Four cycling screens matching the DOS original
  6. Audio synthesis — Nine sound effects via AVAudioEngine render blocks
  7. Planet + gravity — Striped planet, tilted elliptical moon orbit, gravity well
  8. Particle effects — Explosions, shield impacts, exhaust trails
  9. Edge ghosts — The SKNode.copy() fix
  10. TestFlight assets — Layered parallax icons, top shelf images, privacy manifest

Build 1.0 (1) uploaded to App Store Connect at 22:37 on March 4, 2026.

cost more time than any game logic.

The humbling reality of playtesting

Then someone actually played it on a TV.

Everything was too small. The menu text, the HUD bars, the ship lines — all designed on a Retina display at arm's length, now viewed from across a living room. The first round of feedback was humbling:

  • All text bumped up (title 48pt to 64pt, buttons 20pt to 36pt, body 16pt to 28pt)
  • Ship and projectile line widths increased (2px to 3-4px)
  • HUD bars redesigned with proportional widths and mirror layout
  • Menu moved from bottom to top of screen
  • Torpedo and phaser damage reduced — fights ended too fast for new players

The non-robot ship was also playing as AI even with no robot toggled. An auto-assignment block in the input manager was helpfully giving Player 2 an AI when only one controller was connected. Three lines deleted.

The tutorial born from watching

The first-time player experience was brutal. No one knew what the buttons did. No one understood energy transfer. Everyone died to the planet.

So I built a nine-step interactive tutorial: rotate, thrust, navigate through checkpoints, fire torpedoes at targets, use phasers, cloak, warp, transfer energy, then defeat a nerfed AI opponent. Each step has , pulsing target zones, and progress indicators.

It auto-triggers on first launch. Returning players skip it with MENU.

CRT phosphor overlay

A TV game about a CGA monitor should look like it's running on a CGA monitor.

layers scanlines and a radial vignette over everything. It sits at z=200, always on top. The effect is subtle — you feel it more than you see it.

Two warp styles

Hyperspace got two visual treatments, togglable from the settings menu:

. The particle trail scatters points between origin and destination. The CRT effect squeezes the ship to a dot like an old TV switching off, then expands it at the destination. Both feel right for different reasons.

Polish from play

Every subsequent playtesting session surfaced something new. Gravity too weak. Stick-up accidentally mapped to thrust. Score resetting each game. Each fix came from watching real people play on real hardware:

  • (first to 3 wins)
  • Session crown — a small amber crown above the leading player's score
  • Joystick-to-dpad mapping for menu navigation
  • Energy/shield caps raised (50/50 to 100/80) for breathing room
  • Ambient soundtrack —
  • Logo scatter animation ported from the web version

Never touch Xcode GUI again

The final workflow unlock: a CLI deploy pipeline. Manual signing configured in project.yml, API key authentication for App Store Connect, and a single deploy.sh command that archives and uploads to TestFlight. Changes go from code to testers without opening the Xcode GUI.

are straightforward once you know the incantations. The hard part was discovering that Xcode silently caches signing settings even after you change project.yml.

Where it stands

Build 1.0 (12) is on TestFlight. The App Store listing is drafted: "SPACEWAR! 26 — The 1962 Classic on Apple TV." Free, no data collected, age rating 9+.

From a web game running in a browser tab to a native tvOS app on the biggest screen in the house — same physics, same amber glow, same two ships. Just better controllers and a couch between the players.