Files
Arcade85/README.md
T

24 KiB
Raw Blame History

Arcade 85

A 5-in-1 iOS collection of faithful HD remakes of five classic TI-85 calculator games. Native SwiftUI + SpriteKit.

The games (from the original sources in this repo's sibling folders):

Game Original Status
Lunar Lander Tiger/Istar, 1997 (lander/LANDER.C) ✅ Playable
ZTetris J. Mårdell, 1998 (zetris/ZTETRIS.ASM) ✅ Playable
Phoenix P. Davidson, 2008 (phx4.0ti85/) ✅ Playable
Dope Wars J.E. Dell, 1984 · engine via J. Gannon's NerdWars, 2000 ✅ Playable
ZMercury P. Davidson, 2004 (zmercury-ti85/) ✅ Playable
Galaxian P. Davidson, 1998 (galaxian-ti85/) 💤 Ported, dormant

All five collection slots are playable. Galaxian was fully ported, then swapped out of the line-up for a trading game (too much overlap with Phoenix); its code stays in the project and swapping one GameCatalog.swift entry restores it. The trading slot first shipped as NerdWars (the TI-85's Drugwars clone), then was restored to the classic Dope Wars setup it secretly was all along — see the Dope Wars section.

Running it

Requires Xcode 16+ (developed against Xcode 26).

open Arcade85/Arcade85.xcodeproj
# pick an iPhone simulator, then Cmd-R

Or from the command line:

cd Arcade85
xcodebuild -scheme Arcade85 -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 17' build

There is no third-party dependency and no code-signing needed for the simulator. The project uses Xcode "synchronized file groups", so new files added under Arcade85/Arcade85/ are picked up automatically — no .pbxproj editing.

Project layout

Arcade85/Arcade85/
  Arcade85App.swift          @main entry point
  Shell/                     the collection UI, shared across every game
    Theme.swift              palette + fonts (SwiftUI + SpriteKit mirrors)
    Haptics.swift            feedback helpers
    HighScoreStore.swift     per-game best scores (UserDefaults)
    GameCatalog.swift        game metadata + SKScene factory
    RootView.swift           home screen (starfield + game cards)
    GameHostView.swift       full-screen SpriteView host + back button
  Games/
    SceneArt.swift           procedural gradients, starfields, symbol + sprite textures
    ComingSoonScene.swift     placeholder scene for the four unbuilt games
    Lander/
      LanderSim.swift        faithful port of LANDER.C (pure logic)
      LanderScene.swift      HD rendering, multi-touch thrusters, game flow
    ZTetris/
      TetrisSim.swift        faithful port of ZTETRIS.ASM (pure logic)
      TetrisScene.swift      HD rendering, DAS touch controls, game flow
    Phoenix/
      PhoenixData.swift      sprites (images.asm) + campaign (levels.asm), verbatim
      PhoenixSim.swift       faithful port of the Z80 game logic
      PhoenixScene.swift     HD rendering, drag-to-fly control, shop, flow
    Galaxian/                (dormant — swapped out of the catalog for Dope Wars)
      GalaxianData.swift     sprites, formation patterns + 20-level table, verbatim
      GalaxianSim.swift      faithful port of GALAXIAN.ASM
      GalaxianScene.swift    HD rendering, drag-to-fly + laser button, flow
    DopeWars/
      DopeWarsSim.swift      the NerdWars engine restored to Dell's classic setup
      DopeWarsScene.swift    night-city trading board, event cards, cop chase, flow
    ZMercury/
      MercuryData.swift      terrain tiles, map, sine table + sprites, verbatim
      MercurySim.swift       faithful port of the multi-file Z80 sources
      MercuryScene.swift     HD terrain scroller, parallax, drag-to-fly, flow
  Assets.xcassets/           app icon (procedurally generated) + accent color

Design approach

Each game is split into a pure simulation and a SpriteKit scene that renders and drives it. The simulation reproduces the original's mechanics exactly; the scene is where the HD glass goes (neon vector art, particles, haptics, touch controls). This keeps the remakes honest — the feel comes from the original numbers, not an approximation.

Lunar Lander (the reference implementation)

LanderSim ports LANDER.C line-for-line in the original integer space:

  • the Dape/Istar landscape generator (random walk + erosion smoothing passes),
  • the 10-wide landing pad carved 4 below the surrounding terrain,
  • gravity, the ±2 thruster impulses, horizontal friction, velocity clamping,
  • the 8-pixel-footprint landing/crash test (must be flat under all 8 columns with descent speed ≥ −3), and
  • the original scoring: ((90 − maxFuel) × fuel / maxFuel) × maxVel.

The sim ticks at the original's real pace — LANDER.C ends every loop pass with delay(20) ≈ 200 ms (the same delay() whose 200-unit calls are the ~2 s message pauses), i.e. ~5 loops/sec — with rendering interpolated between ticks so motion stays smooth. Taps shorter than one tick are latched so they still burn for exactly one loop, like one keypress in the original.

Deliberate HD liberties (everything else is the original's integer rules, which hold verbatim on the Moon setting):

  • The field is 128×96 instead of 128×64 — extra sky above the same 0…32 terrain for a phone-shaped playfield and a longer descent. Every rule is height-relative, so nothing else changes.
  • Soft walls. In the original the screen edge was fatal; on touch that turned a mistimed start tap into an instant "lost in space". Now the top and sides stop the lander (velocity zeroed) instead of ending the run, the lander spawns with 7 px of ceiling headroom, and a tap that starts a run is consumed by the transition — it can never double as a first-tick burn. Crashing into terrain is the only way to die.
  • A GRAVITY setting — the remake's third difficulty knob, added because the original's self-braking lateral friction leaves little to fight:
    • MOON — the original: pull 1/tick, drift damped every tick;
    • MARS — pull 1,1,1,2 (avg 1.25), damping every other tick;
    • EARTH — pull 1,2 alternating (avg 1.5), no damping — Newtonian drift you must counter-burn, landing scores ×3 (Mars ×2). The patterns are phase tables over the tick counter, so the sim stays integer-exact.

The scene renders the actual TI-85 8×8 lander bitmap as crisp merged neon pixels and adds on-screen thruster pads (with true multi-touch so you can fire lateral + main thrust together), a live V-SPEED / FUEL / SCORE HUD, and ready / landed / game-over flow. The original's adjustable FUEL (30–80) lives on the ready screen next to MAX SPEED (4–7) — the original allowed speed 3, but there terminal velocity equals the safe-touchdown limit and the game cannot be lost, so the floor is 4. The classic's own defaults (40/7) are its hardest speed setting, so the remake defaults to a gentler 60 fuel / speed 4 / Moon, remembers the player's last choice, and teaches the rule on screen (V-SPEED · SAFE ≤3 plus stepper hints). The score formula already pays more for harder settings; the gravity multiplier extends that to the new worlds. A 20-assertion standalone harness pins the spawn headroom, wall clamps, all three pull/damping patterns, the score multipliers, and that speed 4 remains losable.

ZTetris

TetrisSim ports ZTETRIS.ASM mechanically — several of its tables are embedded verbatim:

  • the 28-word BitData piece table (7 pieces × 4 rotations, four (dy,dx) offsets packed 2 bits each), decoded exactly like the ASM's Uncrunch;
  • the well geometry: 10 columns, 16 visible rows + 2 rows of hidden headroom, spawn at the original (6,16), no wall kicks, moves fail silently;
  • levelcnts gravity (level 0–20), converted at the TI-85's real interrupt timing: one main-loop iteration = 4 HALTs ≈ 20 ms, so level 0 = 0.8 s/row;
  • scoring: line clears 4/10/30/120 × (level+1), plus the hidden units counter (scoreU) — +1 per Down/Drop step including the blocked one — carried into the displayed score ÷10 on each new piece, exactly as NewB does it;
  • level = lines ÷ 10 (only increasing, capped at 20), start level 0–9;
  • "start high" trash (0–5 → ×2 rows), each row punched with 5 random gaps;
  • the 16 per-level background patterns, tiled on the rails beside the well.

The scene adds the HD glass: colored beveled blocks, a ghost landing outline, next-piece preview, DAS auto-repeat on held ◄ ► (0.167 s delay, 50 ms repeat), held soft drop, tap hard drop, both rotation directions, pause, and line-clear flash/score-pop feedback. The sim has a standalone test harness (68 assertions) covering piece decode, drop scoring, clears, leveling, trash rows and top-out.

Phoenix

The deepest port: the entire Phoenix 4.0 campaign and economy, extracted from the Z80 sources file by file:

  • Campaign (levels.asm): all 31 combat waves + 9 shop stops, with every power/movement/weapon/fire-rate value and the four pattern paths (circle, oval, figure-eight, huge oval) transcribed literally; L_GOTO chains inlined.
  • Enemy AI (emove.asm): the synchronized rectangle formation wobble (byte-timer, odd-tick moves, 17-tick phases, −60 entry descent), bosses (descend then 83/90-px sweeps), bouncers, pattern followers driven by the original's bit-reversed-timer fractional velocities (Div_A_16), rampagers with the exact trigger columns, and the 4-path swoop system.
  • Weapons: player F1–F5 with the original damage/speed/offsets (including F4's alternating sides and F5's arcing swing shots), enemy weapons from straight drops to the 7-angle-bucket aimed big/huge shots.
  • Economy: money as dmg-0 "bonus bullets" dropped every 4–11 kills, $ value by difficulty (100/50/25), cash cap 9900, and the shop's 7 items with the original prices and purchase rules (shield cap 16, one-time weapons that auto-select, companion refill).
  • Companion (helper.asm): position-queue follower, 10-damage shots every 8 frames, its own 16-point shield and explosion.
  • Scoring (score16.asm): shields×1000 + time bonus (19000 counting down every other frame) + difficulty/speed bonus + cash, awarded on finishing.
  • Sprites (images.asm): all 44 bitmaps rendered as neon pixel art, including the 16-wide bosses and the 8-frame explosion.

Touch layer: drag anywhere to fly (original ±2/±1-px-per-frame speed limits still govern the ship), always-on autofire at the original 10/4-frame cadence, tap weapon selector, pause, and full title → shop → victory/game-over flow at the original 25/29/35 fps speed options. Sim verified by a 172-assertion standalone harness (progress additions covered by a follow-up suite).

Progress scoring (remake addition). The original only scored a finished campaign (score16.asm), which on the phone meant the high score sat at 0 until you beat all 31 waves. The sim now tracks wavesCleared and totalEarned (lifetime $, unspent or not), and every run posts waves × 400 + earnings — a death shows WAVE n/31 · SCORE, and a victory adds the untouched score16 tally (shields/time/bonus/cash) on top, itemised as a PROGRESS line in the classic victory breakdown.

The 2:1 TI-85 window is drawn full-bleed with a 2× vertical stretch on entity positions while the sprites themselves keep a mild 1.25× arcade-monitor stretch — the battle fills the phone instead of sitting in a letterboxed strip, and the pixel art stays crisp. Sim geometry and collisions are untouched — the visible window itself can't widen, because the original parks and spawns entities just outside the y 32…96 band.

Galaxian

GalaxianSim ports GALAXIAN.ASM (v3.0) at its ~35 fps frame rate:

  • Formation choreography: each slot's pattern byte is both entry time and formation depth — an enemy triggers when timer & 127 == 128 - depth, descends 1 px/frame, and the 128-frame timer wrap locks it into formation exactly at its row. The whole formation then sways ±32 px on timer bit 6.
  • Swoops: the original's 8-stage dive state machine (types 5–12: up/out, arc over, dive at double speed), started by the R-register roll (R < 19/128 on every 4th frame), aimed away from the screen edge, and rejoining the entry queue at the bottom for −1 point.
  • 20 levels from levels_jump_table: plain waves, the ring formation, mixed weak/strong waves (13 power-1 + 7 of the level type), bouncing cross trainers (exactly half of which can flee, −50), aim-shooting "operator" ships, patrolling 10-hit bosses, and two 98-hit superbosses that deploy drones mid-fight (−200 each).
  • Weapons: one cannon bullet per press from a 10-slot pool (+10 per hit, +100 per kill), and 9 lasers that wipe an entire column but never hurt a boss. Enemy bullets come in 5 drift types picked by your position.
  • Scoring exactly as documented: +200 per level, +25 per boss hit, +1000 per boss kill, −16×16 per death, +5000 victory.

The scene adds drag-to-fly (4-way, original 1 px/frame speed), always-on autofire, a big LASER button with the remaining count, wave banners, and the 9-lives / 9-lasers HUD. Sim verified by a 129-assertion standalone harness.

ZMercury

MercurySim ports the multi-file 2004 sources — a fully deterministic horizontal cave-shooter (the original contains no randomness at all):

  • Terrain scroller: 1 px/frame leftward; every 8 frames a new 8-px tile column enters from a 64-column looping map (9th map row = wave triggers). The buffer's polarity quirk (bits set = open sky) and the one-byte offset between sprite space and terrain space are both preserved.
  • Waves: straight flyers, aimed-fire phoenixes, sine-bobbing spinners, turret bases mounted on their map pad tiles (ceiling and floor), spin-T sine crossers, an open-space gate that halts the map until the field is clear, and a 50-hit boss that holds the right edge firing bullet pairs.
  • Damage model: 100 shield points; every ship corner touching rock costs 1 per frame, enemy bullets cost 5, rams cost 1 (and hurt the enemy back). Kills pay +1 immediately and +20 when the explosion finishes — the original's BCD scoring, faithfully odd.
  • Aimed fire: the shallow-cone rule — bullets angle up/down only when the vertical offset beats deltaX/4 — plus per-slot fire stagger.

The scene renders the terrain from prebuilt map-column textures that slide with the sim's scroll counter — full-bleed, positions/terrain stretched 1.9× vertically (sprites 1.35×), every rock pixel facing open air lit as a glowing rim over a dark rock mass — plus drifting star layers, a slower stalactite silhouette band for cave depth, and the original ~28.6 fps (timer ≥ 7 against the ~200 Hz interrupt). Sim verified by an 81-assertion standalone harness.

Dope Wars

The genre swap for the fifth slot — and a restoration story. The engine is the verbatim port of NerdWars (J. Gannon, 2000), the TI-85's only Drugwars-style game, decoded from its .85p. Reading the BASIC revealed that Gannon had kept John E. Dell's original Drug Wars (1984) item variables C,H,A,W,S,L — Cocaine, Heroin, Acid, Weed, Speed, Ludes — and merely rescaled the price bands and renamed the nouns. DopeWarsSim reverses the re-skin: Dell's classic setup on Gannon's proven engine.

  • The month: $2,000 cash, the classic $5,500 debt to the loan shark growing 10%/day, a 6%/day bank (shark and bank live in the Bronx), a 100-pocket trench coat, and 30 subway rides between six boroughs (Bronx, Ghetto, Central Park, Manhattan, Coney Island, Brooklyn); each ride rerolls the six prices plus one 0…20 event roll.
  • Market (Dell's iconic bands): Cocaine 15000–30000 · Heroin 5000–14000 · Acid 1000–4500 · Weed 300–900 · Speed 90–250 · Ludes 10–60, bent by events: cheap heroin at 850–1999, "Cops made a big bust!" heroin at 18000–42999, $80,000+ cocaine manias, $2 pharmacy-raid ludes, weed and acid floods, a subway mugging (keep 2/3), $200-coat and $400-gun offers (Baretta / .38 Special / Saturday Night Special), free drugs on a dead dude — and the weed on the sidewalk that always ends in one last wild trip.
  • Officer Hardass: carrying 50+ units can trigger a chase — run (50/50; escaping rerolls the day, possibly straight into another event) or fight with a gun; every fight round draws return fire (+3 wounds, 50 = death), and dropping the whole squad loots Hardass's body ($750–1999) with a $1,000 doctor offer if you can afford it.
  • Scoring: your final net worth in dollars (cash + bank − debt) is the high score — "$13.4M" beats "$2.1M", the way a trading game should brag. (Dell's √(net ÷ 31.5) 0–100 rating is still computed in the sim for the curious; the old 0–100 stored best migrates to $0 once.)
  • The market grows with you (remake extension). Below $100k net worth the game is exactly the classic. Cross $100k and the ludes slot retires — any stock is bought out at yesterday's price — and METH moves in at $2,000–16,000, the widest swing on the board. Cross $1M and speed retires for CHINA WHITE at $40,000–160,000, the whale product that dwarfs even a cocaine mania. Shifts are one-way, announced as street-word cards on arrival, and bring their own events on formerly idle rolls of the 0…20 table: a Jersey meth-lab explosion spikes local product to $30–48k (the tier-2 face of the old $2-ludes roll), a superlab dump floors meth at $400–999, and the cartel flooding the docks drops China White to $8–20k. The dead-dude loot names the tier's product — yes, you can find china white on a dead dude in the subway.
  • The streets got real (round-5 extensions). Each borough now prices its own way — multipliers on the daily band rolls (Bronx neutral; event prices are absolute news and ignore the bias): heroin and speed run cheap in the Ghetto, acid and weed in the Park, everything's sleepy-cheap on Coney, Manhattan's rich buyers pay up to +30%, Brooklyn's fences pay a little over — so routing (buy in the Park, sell in Manhattan) is a real strategy, and the subway sheet captions each borough's economy. Some days word on the street names a product and a borough for tomorrow — ride there to find out if the tip was good (×3 spike or ÷3 glut) or street garbage (about 1 in 4). And the debt finally has teeth: run a $15,000+ tab past day 10 and you get one warning before the shark's two leg-breakers start jumping you — the classic chase engine with his collection money as loot, amber sodium strobe, and a nastier ending ("THE SHARK COLLECTED") if they put you down. Rumors and shark muscle roll on a separate RNG stream, so classic price/event sequences per seed stay byte-identical.
  • Month lengths: 30 / 60 / 90 days or endless on the ready screen — endless has no clock and posts no score (interest caps keep marathon runs from overflowing). Amount sheets have a MAX chip, and tapping the number opens an in-game keypad to type any amount directly (DONE trims to the limit).
  • Engine quirks kept: the dead-dude loot roll can never grant cocaine (int(rand*5) is 0…4 over the cheap-to-dear order); the shark fronts at most $5,000 per visit but unlimited visits; the final ride ends the month with no fresh roll. One NerdWars quirk was dropped with the re-theme: trading matched items by price (If F==C …), which could fill/empty several pockets at once — no longer authentic to the classic fiction, so trading is by item, the way Dell's original worked (the harness pins the new behaviour with a regression test).

The scene wraps it in a night-city board — skyline silhouettes with lit windows, sodium-lamp glow, gradient market rows with day-over-day ▲▼ movement arrows and reroll flashes (arrows sit out a tier-shift day, since yesterday's prices belong to retired products) — plus quantity-chip trade sheets, SUBWAY / SHARK / BANK / END actions, the original's Pause screens as tap-through event cards, and a RUN / FIGHT chase overlay under red/blue strobe light. The ready screen states the rules as a plain bulleted list and carries the 30/60/90/∞ month picker. Sim verified by a 133-assertion standalone harness covering the classic engine end-to-end plus every extension: tier mechanics (bands, liquidation, one-way + double promotion, tier events and their tier-1 no-ops, tier-aware loot naming), borough bias math and event-absolute pricing, rumor spawn/payout/garbage/missed/final-day gating, the shark warning → chase → loot/death paths, and month-length / endless / interest-cap behaviour.

The HD art pass

A collection-wide visual up-level (round 3 of feedback: "still a little too simple… as if they are straight ports"). Shared toolkit in SceneArt.swift: radial glow textures, gradient panel textures, programmatic particle emitters (spark bursts, thrust/trail flames, ambient dust), vignettes, screen-shake and flash actions. Per game, all render-layer only — every sim stays verbatim:

  • Lander: full-bleed field; terrain rebuilt as gradient rock with speckle grain and a lit crust hugging the skyline, over a distant ridge silhouette; particle exhaust jets + engine glow instead of triangle flames; pulsing pad light-cone and sequenced approach beacons; touchdown dust ring; crash flash + spark burst + tumbling debris + screen shake; nebula blooms, two-depth starfield, drifting dust, vignette.
  • Phoenix: full-bleed field (2× position / 1.25× sprite stretch); sprites shaded top-lit so they read as bodies, not flat clusters; engine trail and banking tilt on the ship; particle explosions; screen shake + red flash on hits; WAVE / BOSS banners; two scrolling star layers over static far stars.
  • ZMercury: full-bleed canyon (1.9× / 1.35×); terrain columns re-rendered with glowing rim pixels on every air-facing edge over dark rock; a slow stalactite/stalagmite silhouette band behind the real walls; engine trail, scrape sparks + shake, kill sparks synced to the original explosion frames, and a proper going-out blast.
  • Dope Wars: built HD from the start (see its section).
  • ZTetris: untouched — it set the bar.

Porting notes

Tip from the ZTetris port: the 1990s sources use extended-ASCII + CRLF, which makes grep treat them as binary — strip with tr -d '\r' < FILE | tr -c '[:print:]\n\t' '?' first, then the ASM tables (pieces, speeds, scoring) can be read and ported directly.

NerdWars (the Dope Wars engine) arrived as a .85p Graph Link file rather than source — but that format's ASCII mode stores TI-85 BASIC nearly as plain text, so a ~20-line decoder mapping the few special bytes (0xD6 newline, 0x1C the STO→ arrow, 0x19 ≥, 0x1A negate, 0x10 √) recovers the entire listing.

Notes

  • Portrait-only, status bar hidden, targets iOS 17+.

  • The app icon is generated by scripts-free CoreGraphics (see the git history / the 1024px asset in Assets.xcassets).

  • #if DEBUG launch hooks exist for screenshots/tests and are stripped from Release builds:

    • ARCADE_DIRECT_GAME=<id> — open straight into a game.
    • ARCADE_AUTOPLAY=1 — start the run immediately (any game).
    • ARCADE_DEMO=1 — freeze a staged mid-game pose (any game).
    • ARCADE_DEMO_VIEW=buy|chase|tier3|over|ride|keypad|sharkchase|endless — with Dope Wars' demo, pose the buy sheet, cop chase, tier-3 board (Meth + China White), net-worth game-over, borough-captioned subway sheet, amount keypad, leg-breaker chase, or an endless-mode board.
    • ARCADE_SHOP=1 — open Phoenix's shop stocked with cash.
    • ARCADE_GAMEOVER=1 — pose Phoenix's death card mid-campaign (progress score).

    e.g. SIMCTL_CHILD_ARCADE_DIRECT_GAME=lander xcrun simctl launch booted com.albanese.arcade85