Fold the phone. Fold space.
A space-exploration game where the iPhone Duo's hinge is the ship.
Close the phone and you fold space to the next star. Squeeze it, snap it open, and a planet shatters in the crease.
Landing page · Quick start · Controls · Architecture · Pitch deck (PDF) · Deck & talk · FAQ
Built in one day at Bitrig Hacks: iPhone Duo Edition · Y Combinator · 26 September 2026
- A flat iPhone has no angle. There is nothing to close, squeeze, snap or pump. FOLDSPACE reads the hinge as a continuous joystick axis, with velocity and rhythm.
- A flat iPhone has one plane. Two screens meeting at a known angle are a 3D corner, like Seoul's wave billboard. The planet hologram stands in the fold.
- A flat iPhone has one face. When the phone is closed the outer display tells the world
FOLDING SPACE · 4.24 lywhile the ship is in transit.
Sagittarius A*, ray-traced along Schwarzschild null geodesics (RK4). The far side of the disc is bent over and under the shadow; the photon ring sits on the 2.598 rs critical curve. Seamless loop from the 360-frame render that ships in the app as HEVC-with-alpha.
Every hinge posture is a flight state. Every hinge motion is a verb. No mode switch, no button.
| Posture | Angle | What you see | What it means | |
|---|---|---|---|---|
| ▁ | Closed | 0–12° | Outer 5.4" display | Docked, or in transit (FOLDING SPACE · 4.24 ly) |
| ◢ | Squeezed | 12–45° | The crease glows | Nova Lance charging |
| ⌐ | Cockpit | 45–135° | Windshield above the fold, console below | Flying. The planet hologram sits in the fold |
| ⟋ | Open | 135–170° | Wide cockpit | Flying |
| ━ | Flat | 170–180° | The whole 7.6" inner display | Galaxy map |
| Gesture | Detected as | Game verb |
|---|---|---|
| Dip the lid and bring it back | .hop |
Hop to the next planet in the system |
| Close the phone smoothly (60–160°/s) | .warpClose(quality:) |
Warp: fold space to the targeted star. Slam it or crawl and the bubble collapses |
| Squeeze below 45° and hold | .squeezeBegan |
Charge the Nova Lance |
| Snap it open (> 250°/s) | .snapOpen |
Fire. The planet shatters in 3D in the corner |
| Pump open/shut four times, then slam | .pump(count:) → .pumpComplete |
Charge the Fold Core for the 2.5-million-light-year jump |
| Close slowly while orbiting the Sun | continuous angle | Dive: depth = fold amount. Open the phone to climb out |
| Lay it flat | .laidFlat |
Galaxy map |
The recogniser is HingeEngine.swift: a 2.5-second ring of (angle, velocity, t) samples turned into discrete gestures with hysteresis, so a squeeze never reads as a warp and a hop never reads as a close. Exact thresholds are in the gesture reference.
Corner billboards draw one scene across two surfaces that meet at an angle. iPhone Duo is exactly that, and it knows its own angle. SpaceScene pitches the SceneKit camera rig from the live hinge angle, so the planet stands on the console half and rises into the windshield half. Open or close the phone and the planet stays put while the screens move around it. The only control allowed to cross the fold is the probe: drag it up into the planet to plant a beacon.
Every image below was rendered in Blender 5.2 from equations and procedural shaders: no photographs, no downloaded reference imagery. Method, assumptions and timings are in assets/NOTES.md.
Six of 26 procedural 2048 × 1024 equirectangular worlds. Zonal bands and the Great Red Spot on the giants, craters and ice on rocky worlds. Atmosphere rims and lighting are added at runtime.
App screenshots, 01–07 (generated by CI through the FOLDSPACE_DEMO launch hook)
The build workflow launches the app once per demo state on an iPhone 17 Pro simulator and saves these files as a CI artifact. They are committed to docs/screenshots/ after each capture run. Any that are not yet committed will show as missing images below. To capture them locally, see Demo states.
| Cockpit (half-open) | Galaxy map (flat) | Outer display (closed, warping) |
|---|---|---|
![]() |
![]() |
![]() |
| Sun dive | Nova Lance | Sagittarius A* | Andromeda |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Act | You can… | You need… |
|---|---|---|
| I · Stranded | Hop between Solar System planets on normal engines. Drag the probe across the fold into the hologram to plant a beacon. | The three warp-drive parts: Exotic Matter, the Field Coil and the Navigation Core, hidden on Mars, Jupiter and Neptune |
| II · Warp | Pick a star on the galaxy map, close the phone, fold space (Alcubierre-style: squeeze space ahead, stretch it behind, ride the bubble in the crease). Explore or destroy each world. Destroying pays more energy, but whatever you might have found there is gone. | Dive into the Sun for the Stellar Core Sample, which unlocks the Nova Lance |
| III · The Core | Slingshot around Sagittarius A* (fold = gravity; Earth's clock races while yours crawls). Pump the hinge to charge the Fold Core, slam it shut, open it: the Milky Way behind you, Andromeda rising out of the corner. | A charged Fold Core |
| Destination | Distance | Warp cost | What's there |
|---|---|---|---|
| Solar System | home | — | Mercury → Neptune. Drive parts on Mars, Jupiter and Neptune. The Sun is divable |
| Alpha Centauri | 4.24 ly | 30 | Proxima b (1.07 M⊕, 11.2-day orbit, habitable zone) and Proxima d |
| Barnard's Star | 5.96 ly | 35 | Four sub-Earth planets confirmed 2024–2025 |
| Wolf 359 | 7.86 ly | 40 | A flare star with a candidate planet |
| Sirius | 8.6 ly | 45 | The brightest star in our sky and its white-dwarf companion |
| Epsilon Eridani | 10.5 ly | 50 | A young system with a gas giant and debris rings |
| Tau Ceti | 11.9 ly | 55 | Super-Earths e and f |
| TRAPPIST-1 | 40.7 ly | 80 | Seven Earth-sized planets, three in the habitable zone |
| Sagittarius A* | 26,670 ly | 150 | The Milky Way's 4.3-million-solar-mass black hole. Fire the Nova Lance at it and the beam simply vanishes |
| Andromeda | 2,537,000 ly | Fold Core | The finale |
Every body carries a real blurb and real facts in UniverseData.swift. The Sun dive uses real layer temperatures: corona ~1,000,000 K, photosphere 5,800 K, core 15,000,000 K. Radii of non-transiting worlds are mass–radius estimates, used for render scale only.
| Duo capability | API | Where it's used |
|---|---|---|
| Continuous hinge angle | onHingeChange { old, new in … } → DeviceHingeContext.hinge: DeviceHinge? with angle: Angle and status: .closed / .partiallyOpen / .fullyOpen (iOS 27.1) |
HingeSourceModifier.swift feeds hinge.angle.degrees into HingeEngine. Per Apple's guidance the angle drives interactions and effects, never layout |
| The fold and the camera | proxy.reservedRegions(kind: .division) and reservedRegions(kind: .occlusion) |
FoldGeometry.seamRect(in:proxy:) + FoldSeam: nothing interactive crosses the crease, except the probe drag, on purpose. .occlusion rects keep HUD readouts clear of the camera |
| Outer ↔ inner display continuity | posture .closed ↔ .laptop |
OuterDisplayView shows transit while closed; the cockpit resumes on open |
| Adaptive layout, no orientation lock | geometry-driven split; portrait and both landscapes enabled | RootView and CockpitView split at the fold from GeometryReader, never at a fixed pixel. Size-class variants and Apple's ArrangementView are on the roadmap |
| Split View / multiple windows | UIApplicationSupportsMultipleScenes |
On in project.yml; the galaxy map works as a second window |
Note
The DUO_SDK flag. The Duo APIs ship in the Xcode 27.1 beta SDK, which needs macOS 26.6+. The hackathon build machine was on 26.5, so the native hinge path sits behind the DUO_SDK compilation condition. The same engine runs from three sources: the Duo hinge, an on-screen hinge control in the simulator, or CoreMotion tilt on a flat iPhone. scripts/build-duo.sh builds with the flag on. The Bitrig Mac app's iPhone Duo simulator (folding + rotation) can also run the generated project.
All three are optional. With no keys the game runs fully offline and every feature still works. The registry reports OFFLINE, the ship computer speaks with a deterministic voice built from the same real facts, and telemetry is a no-op.
| Tool | What it does in FOLDSPACE | Code |
|---|---|---|
| Supabase (Postgres + PostgREST) | The Galactic Registry. Every beacon, destroyed world, warp, core sample, slingshot and Andromeda arrival is POSTed to public.events. The HUD and the live dashboard read it back. Row-level security allows anon INSERT + SELECT and nothing else. Fire-and-forget, with an in-memory retry queue and an optimistic local echo. |
GalacticRegistry.swift · supabase/ |
OpenAI (chat completions, gpt-5-mini by default) |
The ship computer: NARRATE the arrival, ASK about the current body, and write the Commander's Log at Andromeda. Strict system prompt (≤ 2 sentences, never invent numbers), fed only the body's real blurb and facts. 12 s timeout, then the offline voice. | ShipComputer.swift · ConsoleView.swift |
| Sentry (sentry-cocoa via SwiftPM) | Crash and error monitoring with automatic performance tracing. Every GameStore.log line becomes a breadcrumb, so a crash report carries the last 60 game events. OpenAI errors are captured as handled errors. |
Telemetry.swift · FoldspaceApp.swift |
Setting up keys: Foldspace/Secrets.plist
cp Foldspace/Secrets.example.plist Foldspace/Secrets.plist # gitignored, never committed
open Foldspace/Secrets.plist # fill in what you have
xcodegen generate # once, so Xcode bundles the new resource| Key | Where to get it | Empty means |
|---|---|---|
SUPABASE_URL |
Supabase → Project Settings → API → Project URL | registry offline |
SUPABASE_ANON_KEY |
same page → anon public key |
registry offline |
OPENAI_API_KEY |
platform.openai.com → API keys (API credits, not a ChatGPT plan) | ship computer uses the offline voice |
OPENAI_MODEL |
any chat-completions model id | gpt-5-mini |
SENTRY_DSN |
sentry.io → Project → Settings → Client Keys (DSN) | Sentry never starts |
Secrets.swift reads Secrets.plist, falls back to Secrets.example.plist (all empty), and lets a FOLDSPACE_<KEY> environment variable override either, which is handy in the simulator. Schema, RLS policies, seed rows and curl examples are in supabase/README.md.
Requirements: macOS 26+, Xcode 26.3+ (Xcode 27.1 beta for the iPhone Duo simulator), XcodeGen.
brew install xcodegen
git clone https://github.com/vnmoorthy/foldspace.git && cd foldspaceAny iPhone simulator (on-screen hinge control):
scripts/build-sim.sh # iPhone 17 Pro
DEVICE="iPhone 17" scripts/build-sim.sh
DEMO=blackhole scripts/build-sim.sh # jump straight to a demo stateiPhone Duo simulator (native hinge, DUO_SDK on):
scripts/build-duo.sh # auto-detects Xcode-beta.app / Xcode_27*.app
XCODE=/Applications/Xcode-beta.app scripts/build-duo.shbuild-duo.sh checks for Xcode 27.x, the iPhone Duo device type and an iOS 27 runtime, creates an iPhone Duo simulator if there is none, then builds with SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) DUO_SDK'. Fold the device from Device Hub; the on-screen control hides itself.
In Xcode: xcodegen generate && open Foldspace.xcodeproj, pick any iPhone simulator and run. Drive the hinge from the control at the bottom of the screen: CLOSE, SLAM, HOP, SQUEEZE, SNAP, PUMP ×4, or drag the lid yourself.
Plain xcodebuild
xcodegen generate
xcodebuild -project Foldspace.xcodeproj -scheme Foldspace \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' CODE_SIGNING_ALLOWED=NO buildFor stage demos and screenshots, the app reads FOLDSPACE_DEMO at launch and jumps straight to a moment:
SIMCTL_CHILD_FOLDSPACE_DEMO=blackhole xcrun simctl launch booted com.vnmoorthy.foldspacecockpit · galaxy · outer · warp · sun · weapon · blackhole · andromeda. The console's gear menu also has Demo → Act II / Act III.
.github/workflows/build.yml runs on every push to main that touches code, on GitHub's macos-26 runner with Xcode 26.6. It generates the project, builds for the iPhone 17 Pro simulator, installs the app, launches all seven demo states, and uploads the screenshots, build log, crash triage log and a zipped Foldspace.app as artifacts.
Three objects live for the whole app: one HingeEngine, one GameStore, one FlightController (created in FoldspaceApp.swift). Views read the first two from the SwiftUI environment. State only changes through GameStore methods, and only two things call them: FlightController (the hinge) and the console buttons (touch). The full walkthrough is in docs/ARCHITECTURE.md.
flowchart LR
subgraph Input["Hinge sources"]
DUO["iPhone Duo<br/>onHingeChange, DUO_SDK"]
SIM["HingeSimulatorControl<br/>slider · drag pad · macros"]
MOT["CoreMotion tilt<br/>pitch to 0…180°"]
end
DUO --> HE
SIM --> HE
MOT --> HE
HE["HingeEngine<br/>angle → velocity → posture<br/>→ HingeGesture"] --> FC["FlightController<br/>gesture map + 60 Hz tick"]
FC --> GS["GameStore, Observable<br/>phase · act · energy · hull<br/>claimed · destroyed · SaveData"]
UD["UniverseData<br/>10 real systems"] --> GS
GS --> RV["RootView<br/>posture + phase → screen"]
RV --> OD["OuterDisplayView<br/>closed"]
RV --> CK["CockpitView<br/>SceneViewport + HUD · FoldSeam · Console"]
RV --> GM["GalaxyMapView<br/>flat"]
RV --> SQ["Sequences<br/>Warp · SunDive · Weapon<br/>BlackHole · Andromeda · ShipLost"]
CK --> SC["SpaceScene, SceneKit<br/>hologram camera rig<br/>warp particles · shatter"]
HE -. "raw angle every frame" .-> SC
GS --> GR["GalacticRegistry<br/>URLSession · retry queue"] --> SB[("Supabase<br/>public.events · RLS anon")]
SB --> DASH["docs/registry.html<br/>live dashboard"]
CK --> SHIP["ShipComputer<br/>narrate · ask · log"] --> OAI[("OpenAI<br/>chat completions")]
GS --> TM["Telemetry<br/>breadcrumbs"] --> SEN[("Sentry<br/>crashes · performance")]
SEC["Secrets.plist<br/>gitignored"] -.-> GR
SEC -.-> SHIP
SEC -.-> TM
Five detectors run on every sample of the 2.5 s ring. They share the ring but not state, which is why a squeeze that keeps going becomes a warp close without ever emitting a snap.
stateDiagram-v2
direction LR
state "HOP" as Hop {
[*] --> hIdle
hIdle --> hDipping : posture laptop or open and angle drops below 70°
hDipping --> hIdle : angle below 30° or over 1.5 s, became a close or squeeze
hDipping --> hIdle : back above 85° with minimum below 65°, emit hop
}
state "SQUEEZE and SNAP" as Squeeze {
[*] --> sIdle
sIdle --> sActive : enters peek zone below 45° while closing, pump mode off, emit squeezeBegan
sActive --> sIdle : reaches closed below 12°, silent, now a warp close
sActive --> sSnapPending : back to 45° or more with peak velocity above 250°/s in last 250 ms
sActive --> sIdle : back to 45° or more slowly, emit squeezeCancelled
sSnapPending --> sIdle : passes 100°, emit snapOpen
sSnapPending --> sIdle : 0.6 s elapsed or closing again, emit squeezeCancelled
}
state "WARP CLOSE" as Close {
[*] --> cIdle
cIdle --> cClosing : velocity below -5°/s at 60° or more, start angle recorded
cClosing --> cIdle : reopens above +5°/s while above 20°
cClosing --> cIdle : 6 s elapsed
cClosing --> cIdle : reaches closed below 12°, emit warpClose with quality
}
state "PUMP, Fold Core phase only" as Pump {
[*] --> pWaiting
pWaiting --> pTracking : first direction with speed above 15°/s
pTracking --> pTracking : reversal after a swing of 40° or more, emit pump count
pTracking --> pDone : count reaches pumpTarget 4, emit pumpComplete
}
state "FLAT" as Flat {
[*] --> fLifted
fLifted --> fSettled : angle 170° or more and speed below 40°/s, emit laidFlat
fSettled --> fLifted : angle below 160°, emit liftedFromFlat
}
warpQuality(avgSpeed:) grades a close by its average speed from the start angle to closed:
| Average close speed | < 20°/s (crawl) | 20–60°/s | 60–160°/s (ideal) | 160–320°/s | > 320°/s (slam) |
|---|---|---|---|---|---|
| Quality | 0.3 | 0.3 → 1.0 | 1.0 | 1.0 → 0.2 | 0.2 |
Below 0.45 the bubble collapses: you lose 15 hull (floor 20) and drop out short. Sagittarius A* is the exception: you arrive regardless.
sequenceDiagram
autonumber
actor Cmdr as Commander
participant HE as HingeEngine
participant RV as RootView
participant GM as GalaxyMapView
participant FC as FlightController
participant GS as GameStore
participant OD as OuterDisplayView
participant GR as GalacticRegistry
participant SB as Supabase
Cmdr->>HE: lay flat, 170° or more
HE-->>RV: posture flat
RV->>GM: galaxy map on the full inner display
Cmdr->>GM: tap Alpha Centauri
GM->>GS: targetSystemID = alpha-centauri
Cmdr->>HE: lift to cockpit, then close from 60° or more at about 100°/s
HE-->>FC: warpClose, quality 1.0
FC->>GS: beginWarp(quality:)
Note over GS: guards: drive assembled, target unlocked,<br/>energy at least warpCost 30, Andromeda needs a charged Fold Core
GS->>GS: energy -= 30, phase = warping, duration about 2.8 s
GS->>GR: record warped
GR-)SB: POST /rest/v1/events
HE-->>RV: posture closed
RV->>OD: FOLDING SPACE · 4.24 ly
loop every 33 ms
GS-->>OD: transitProgress, light-year counter, bubble stability
end
GS->>GS: completeWarp()
alt quality 0.45 or more
GS->>GS: arrive, phase = orbit, log lore
else quality below 0.45
GS->>GS: hull -15, drop out short
end
Cmdr->>HE: open the phone
HE-->>RV: posture laptop, CockpitView shows Proxima b
Transit time is min(5, 2.2 + 0.9 × log10(distance)) seconds, or 7 s for the intergalactic jump.
sequenceDiagram
autonumber
actor Cmdr as Commander
participant CV as ConsoleView
participant HE as HingeEngine
participant FC as FlightController
participant GS as GameStore
participant SL as SunLayer
participant RV as RootView
Cmdr->>CV: tap STAR DIVE in orbit around the Sun
CV->>GS: beginSunDive()
GS->>GS: phase = sunDive, diveDepth = 0
RV->>RV: SunDiveView full screen, closing now means deeper
loop every 16 ms tick
Cmdr->>HE: close the phone slowly
HE-->>FC: angle
FC->>FC: depth = clamp of 120 minus angle over 108
FC->>GS: updateSunDive(depth:dt:)
GS->>GS: hull -= depth^2.2 × 26 per s, plus 1.5 below 15% depth
GS->>SL: at(depth:)
SL-->>GS: corona 1,000,000 K ... core 15,000,000 K
end
alt depth above 0.97, first time
GS->>GS: solarCoreSample, +120 energy, Nova Lance unlocked
else hull reaches 0
GS->>GS: phase = shipLost
end
Cmdr->>HE: open past 130°
FC->>GS: endSunDive()
GS->>GS: phase = orbit
sequenceDiagram
autonumber
actor Cmdr as Commander
participant HE as HingeEngine
participant FC as FlightController
participant GS as GameStore
participant SV as SceneViewport
participant SC as SpaceScene
participant GR as GalacticRegistry
Cmdr->>HE: squeeze below 45° and hold
HE-->>FC: squeezeBegan
FC->>GS: beginWeaponCharge()
Note over GS: needs hasWeapon, orbit, a planet not yet destroyed
loop every 16 ms tick
FC->>GS: updateWeaponCharge(hingeAngle:dt:)
GS->>GS: charge += dt × 0.35 + 0.9 × squeeze
GS-->>SV: weaponCharge
SV->>SC: setWeaponCharge
end
Cmdr->>HE: snap open above 250°/s past 100°
HE-->>FC: snapOpen
FC->>GS: fireWeapon()
alt charge 0.6 or more
GS->>GS: phase = weaponFiring, shatterEvent
SV->>SC: shatter(bodyID:)
SC->>SC: beam, flash, about 40 fragments
GS->>GS: after 2.4 s, destroyed, +destroyYield energy
GS->>GR: record destroyed
else charge too low
GS->>GS: cancelWeaponCharge()
end
classDiagram
direction LR
class Universe {
+StarSystem[] systems
+system(id) StarSystem
+body(id) StarSystem and CelestialBody
+distance(from, to) Double
}
class StarSystem {
+String id
+String name
+Double distanceLY
+MapPoint map
+CelestialBody primary
+CelestialBody[] bodies
+Int act
+String lore
+Int warpCost
}
class CelestialBody {
+String id
+String name
+BodyKind kind
+PlanetClass planetClass
+String blurb
+String[] facts
+Double radiusEarths
+Int claimYield
+Int destroyYield
+DrivePart drivePart
+Bool habitable
+Bool divable
}
class DrivePart {
<<enumeration>>
exoticMatter
fieldCoil
navigationCore
}
class SaveData {
+String systemID
+String bodyID
+Int energy
+Set~DrivePart~ driveParts
+Set~String~ claimed
+Set~String~ destroyed
+Bool hasWeapon
+Bool solarCoreSample
+Bool passedSagittariusA
+Bool reachedAndromeda
+Double earthYearsElapsed
+String callsign
+LogEntry[] log
}
class FlightPhase {
<<enumeration>>
docked
orbit
hopping
warping
sunDive
weaponCharging
weaponFiring
blackHole
foldCoreCharging
intergalacticJump
andromeda
shipLost
}
class GameStore {
+Universe universe
+SaveData save
+FlightPhase phase
+Double hull
+Double weaponCharge
+Double foldCoreCharge
+Double diveDepth
}
Universe "1" *-- "10" StarSystem
StarSystem "1" *-- "many" CelestialBody
CelestialBody --> DrivePart
GameStore --> Universe
GameStore *-- SaveData
GameStore --> FlightPhase
SaveData --> DrivePart
Every gesture: trigger, emitted case, game verb
| Gesture | Exact trigger in HingeEngine.detectGestures |
FlightController → GameStore |
|---|---|---|
.hop |
Posture .laptop/.open, angle drops below 70°, then rises above 85° with a minimum below 65°, within 1.5 s and never below 30° |
hopToNextBody() (0.9 s hop), haptic tap |
.warpClose(quality:) |
Velocity below −5°/s starting at ≥ 60°, reaches .closed (< 12°) within 6 s without reopening (> +5°/s above 20°). Quality from warpQuality(avgSpeed:) |
beginWarp(quality:); ignored during a sun dive; heavy haptic |
.squeezeBegan |
Enters .peek (12–45°) with velocity ≤ 0, pump mode off |
beginWeaponCharge() |
.snapOpen |
Leaves the squeeze at ≥ 45° with peak velocity > 250°/s in the last 250 ms, then passes 100° within 0.6 s | fireWeapon(); heavy haptic on a hit, warning if charge < 0.6 |
.squeezeCancelled |
Leaves the squeeze slowly, or a pending snap stalls or reverses | cancelWeaponCharge() |
.pump(count:) |
Pump mode only: direction reversal (speed above 15°/s) after a swing ≥ 40° | recordPump(count:target:), charge = count / 4 |
.pumpComplete |
pumpCount reaches pumpTarget (4) |
foldCoreCharged(), success haptic |
.laidFlat |
Angle ≥ 170° and speed below 40°/s | haptic only; RootView shows the map from posture |
.liftedFromFlat |
Angle drops below 160° after settling flat | haptic only |
Pump mode is owned by FlightController: its tick sets hinge.pumpModeEnabled = (phase == .foldCoreCharging). That is why the squeeze detector checks !pumpModeEnabled: four pumps swing through the peek zone and must not charge the lance.
HingeEngine API
| Member | Purpose |
|---|---|
ingest(angle:timestamp:) |
Feed an absolute angle (0…180°). Clamps, smooths velocity (0.35 × prev + 0.65 × instant), updates posture, runs detectors |
nudge(by:) |
Relative change, used by the drag pad |
animate(to:duration:) |
Eased sweep at 60 steps/s, used by the simulator macros and demos |
resetPumps() |
Zero the pump counter and direction |
static warpQuality(avgSpeed:) |
Close-speed → 0.2…1.0 grade |
angle, velocity, posture, lastGesture, pumpCount |
Read-only observable state |
fold |
0 flat … 1 closed, for shaders |
source |
.simulated, .motion (starts CoreMotion) or .duo |
pumpModeEnabled, pumpTarget, onGesture |
Pump switch, pumps required (4), gesture sink |
Every public method on GameStore
| Method | What it does |
|---|---|
log(_:_:) |
Append a log line (kept to 60), emit a Sentry breadcrumb, show a toast, persist |
claim(_:) |
Plant a beacon: +claimYield energy, may recover a drive part; records claimed or driveAssembled |
hopToNextBody() |
From orbit, hop to the next living body in the system |
hop(to:) |
0.9 s in-system transit to a specific body |
beginWarp(quality:) |
Guarded fold to targetSystem: spends warpCost, runs the transit, records warped |
completeWarp() |
Arrive, or collapse if quality < 0.45; enters .blackHole at Sagittarius A*, .andromeda after the jump |
beginWeaponCharge() |
Enter .weaponCharging if the lance is unlocked and the body is a planet |
updateWeaponCharge(hingeAngle:dt:) |
Charge faster the tighter the squeeze |
cancelWeaponCharge() |
Back to orbit, charge zeroed |
fireWeapon() |
At charge ≥ 0.6: shatter, then after 2.4 s destroy for +destroyYield. At Sagittarius A* the beam vanishes |
beginSunDive() |
Enter .sunDive around a divable star |
updateSunDive(depth:dt:) |
Burn hull by depth; core sample at depth > 0.97; .shipLost at 0 hull |
endSunDive() |
Climb out to orbit |
blackHoleEscaped() |
Mark the slingshot passed, unlock the Fold Core, record passedBlackHole |
blackHoleConsumed() |
Ship lost to the event horizon |
addEarthYears(_:) |
Accumulate time-dilation years at Sagittarius A* |
beginFoldCoreCharge() |
Enter .foldCoreCharging, target Andromeda |
recordPump(count:target:) |
Fold Core charge = count / target |
foldCoreCharged() |
Fold Core at 100% |
cancelFoldCoreCharge() |
Abort the charge |
respawn() |
Backup ship: hull 100, back in orbit |
repair() |
−20 energy, hull back to 100 |
reset() |
New commander, fresh save |
demoSkip(to:) |
Jump to Act I, II or III for stage demos |
persist() |
JSON-encode SaveData into UserDefaults (foldspace.save.v1) |
isClaimed(_:), isDestroyed(_:), isUnlocked(_:) |
Queries |
Derived state: currentSystem, currentBody, targetSystem, hasWarpDrive, act, energy, livingBodies, reachableSystems.
| Path | Purpose |
|---|---|
Foldspace/ |
The app: Swift sources, Info.plist, Secrets.example.plist |
Foldspace.xcodeproj/ |
Generated Xcode project (regenerate with xcodegen generate) |
project.yml |
XcodeGen spec: iOS 26 target, Sentry package, DUO_SDK hook, bundled assets/ |
scripts/ |
build-sim.sh (any iPhone simulator), build-duo.sh (iPhone Duo simulator with DUO_SDK) |
assets/ |
Blender renders bundled into the app: textures/, sprites/, video/, manifest.json, NOTES.md |
blender/ |
Headless Blender generators; blender/codex/ holds the final asset pipeline and validator |
supabase/ |
Galactic Registry: migrations/, seed.sql, setup README |
docs/ |
Landing page, live dashboard, hero, architecture, deck, script, screenshots/, visuals/, site/ media, tools/ |
.github/ |
CI workflow and issue templates |
CONTRIBUTING.md |
Build, layout, adding a star system, PR expectations |
LICENSE |
MIT |
Source tree
Foldspace/
├── App/ FoldspaceApp (Sentry start · environment wiring · demo hook), RootView
├── Config/ Secrets (Secrets.plist loader), Telemetry (Sentry breadcrumbs / captures)
├── Hinge/ HingeState, HingeEngine, HingeSourceModifier (DUO_SDK), HingeSimulatorControl
├── Game/ GameStore, FlightController
├── Universe/ Universe (models), UniverseData (catalog)
├── Scene/ SpaceScene, PlanetMaterials, SceneViewport
├── AI/ ShipComputer (OpenAI chat completions + deterministic offline voice)
├── Network/ GalacticRegistry (Supabase PostgREST client, retry queue)
├── UI/
│ ├── Cockpit/ CockpitView, ConsoleView (SHIP COMPUTER panel), HUDOverlay
│ ├── GalaxyMap/ GalaxyMapView
│ ├── Outer/ OuterDisplayView
│ ├── Sequences/ WarpSequenceView, SunDiveView, WeaponView, BlackHoleView,
│ │ AndromedaFinaleView (Commander's Log), ShipLostView
│ └── Components/ Theme, Haptics, FoldSeam, TransparentLoopingMovie, VisualAssets
├── Secrets.example.plist template: copy to Secrets.plist (gitignored)
└── Info.plist
Every visual is generated headlessly in Blender 5.2 and checked by a validator before it ships.
docs/BLENDER-VISUALS-PLAN.md: the plan: physics, look-dev, who built what.docs/BLENDER-CODEX-BRIEF.md: the asset contract: file names, sizes, formats, the physics each image must obey.blender/README.md: how to run the scripts (--quick/--final, device notes, output naming).blender/codex/: the final generators (blackhole.py,planets.py,sun.py,galaxies.py,warp.py,shatter.py) andvalidate_assets.py --final.assets/NOTES.md: the delivered set: 42 PNGs + one HEVC-alpha movie, with method, approximations and SHA-256 hashes inmanifest.json.
docs/FOLDSPACE-Deck.pptx |
10-slide deck with speaker notes and real Blender renders |
docs/FOLDSPACE-Deck.pdf |
The same deck as a PDF (opens in the browser, no PowerPoint needed) |
docs/PRESENTATION-SCRIPT.md |
The 3-minute script: every spoken line, every hinge beat, pre-show checklist |
docs/ARCHITECTURE.md |
Code walkthrough: recogniser, flight loop, store, scene, sponsor stack |
docs/BLENDER-VISUALS-PLAN.md · docs/BLENDER-CODEX-BRIEF.md · blender/README.md |
Visuals plan, asset brief, Blender how-to |
supabase/README.md |
Registry setup: project, migration, RLS, curl |
docs/registry.html |
Live Galactic Registry dashboard |
docs/index.html |
Landing page source, live on GitHub Pages at vnmoorthy.github.io/foldspace |
- Run on iPhone Duo hardware with
DUO_SDKon by default once the SDK reaches the stable Xcode - Size-class layout variants and Apple's
ArrangementViewfor the flat and Split View layouts - Commit CI screenshots automatically to
docs/screenshots/ - Kerr (spinning) black hole: frame dragging around Sagittarius A*
- More systems from the NASA Exoplanet Archive
- Shared multiplayer registry views: see other commanders' beacons on the galaxy map
Why is the hinge simulated?
The iPhone Duo APIs (
onHingeChange, reserved regions) ship in the Xcode 27.1 beta SDK, which needs macOS 26.6+. The hackathon build machine was on 26.5. So the native path is behind the DUO_SDK flag, and the same HingeEngine is fed by an on-screen hinge control or CoreMotion tilt. On Xcode 27.1, scripts/build-duo.sh turns the flag on and the Duo simulator drives the game directly.
Does it run on a normal iPhone?
Yes. Every hinge verb is also a console button, the on-screen control has CLOSE / SLAM / HOP / SQUEEZE / SNAP / PUMP ×4 macros, and the Motion source lets you tilt a flat iPhone like a lid. The two-screen corner and outer display only make full sense on a Duo.
Is the physics real?
The data is: distances, masses, orbits and temperatures come from the NASA Exoplanet Archive, the Event Horizon Telescope collaboration and NASA's Sun fact sheets. The black hole is a numerical Schwarzschild ray trace. The game mechanics are playful: the warp is Alcubierre taken literally, not a working drive, and the black-hole disc is a cinematic teaching model.
assets/NOTES.md lists every approximation.
Why no Star Wars names?
It is an original game with real astronomy. The weapon is the Nova Lance, the jump engine is the Fold Core, and every destination is a real place.
Can I add planets?
Yes. Add a
StarSystem to UniverseData.swift with real facts, a map point, an act and a warp cost. Procedural materials cover any PlanetClass with no texture needed. See CONTRIBUTING.md.
Issues and PRs are welcome. Read CONTRIBUTING.md for building, the code layout and how to add a star system. Bug reports and feature ideas use the issue templates.
Built by vnmoorthy at Bitrig Hacks with Claude Code. Astronomy data from the NASA Exoplanet Archive, the Event Horizon Telescope collaboration and NASA's Sun fact sheets. Black-hole method after Luminet (1979) and James, von Tunzelmann, Franklin & Thorne (2015). Sponsor stack: Supabase, OpenAI, Sentry.














