Files

417 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
```sh
open Arcade85/Arcade85.xcodeproj
# pick an iPhone simulator, then Cmd-R
```
Or from the command line:
```sh
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`