Add iOS SwiftUI emulator for TWSU DIY Gamer Kit#77
Conversation
Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new iOS/iPadOS SwiftUI-based emulator for the TWSU DIY Gamer Kit, including a hardware abstraction layer and ports of the nine built-in games, all under ios/GamerEmulator/.
Changes:
- Introduces SwiftUI app shell (launcher + views) and an
@MainActorGamerHardwareemulation layer (display/input/LED flash/sound/timing). - Ports nine games to Swift async game loops using
await gamer.delay(...). - Adds Xcode project, assets catalog, and iOS-specific persistence (UserDefaults high scores).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| ios/README.md | Documents emulator features, build requirements, architecture, and controls. |
| ios/GamerEmulator/GamerEmulator/ContentView.swift | SwiftUI app root that hosts the emulator UI. |
| ios/GamerEmulator/GamerEmulator/GamerEmulatorApp.swift | App entry point + first-launch high-score initialization. |
| ios/GamerEmulator/GamerEmulator/Info.plist | iOS app metadata and device/orientation configuration. |
| ios/GamerEmulator/GamerEmulator/Assets.xcassets/Contents.json | Asset catalog root metadata. |
| ios/GamerEmulator/GamerEmulator/Assets.xcassets/AppIcon.appiconset/Contents.json | App icon set metadata. |
| ios/GamerEmulator/GamerEmulator.xcodeproj/project.pbxproj | Xcode project configuration and build settings. |
| ios/GamerEmulator/GamerEmulator/GameAssets/GameAssets.swift | Ports bitmap frames, digit glyphs, and melody constants to Swift. |
| ios/GamerEmulator/GamerEmulator/Hardware/GamerHardware.swift | Observable hardware emulation (display/input/LED flash/sound/timing helpers). |
| ios/GamerEmulator/GamerEmulator/Hardware/SoundEngine.swift | AVAudioEngine-based tone generator matching OCR2A-derived pitch model. |
| ios/GamerEmulator/GamerEmulator/Persistence/HighScoreStore.swift | UserDefaults-backed per-game high-score storage. |
| ios/GamerEmulator/GamerEmulator/Launcher/Launcher.swift | Main launcher state machine + async tick loop + game task management. |
| ios/GamerEmulator/GamerEmulator/Games/GameProtocol.swift | Defines the shared Game protocol used by all ports. |
| ios/GamerEmulator/GamerEmulator/Games/SnakeGame.swift | Snake game port using async delay-based loop. |
| ios/GamerEmulator/GamerEmulator/Games/BreakoutGame.swift | Breakout game port including physics and block propagation logic. |
| ios/GamerEmulator/GamerEmulator/Games/SimonGame.swift | Simon game port with async playback/input waiting. |
| ios/GamerEmulator/GamerEmulator/Games/FlappyGame.swift | Flappy game port with menu/game state and tick-based difficulty. |
| ios/GamerEmulator/GamerEmulator/Games/TetrisGame.swift | Tetris port including rotation, line-clear animation, and melody playback. |
| ios/GamerEmulator/GamerEmulator/Games/AlienGame.swift | Alien game port including shoot animation and falling rows. |
| ios/GamerEmulator/GamerEmulator/Games/ConwayGame.swift | Conway’s Game of Life port + small-step preview support. |
| ios/GamerEmulator/GamerEmulator/Games/DinoGame.swift | Dino runner port with obstacle types and jump/duck physics. |
| ios/GamerEmulator/GamerEmulator/Games/BrightnessGame.swift | Brightness/settings screen port using gamer brightness/compensation flags. |
| ios/GamerEmulator/GamerEmulator/Views/ButtonsView.swift | On-screen controls (D-pad, hardware buttons, cap-touch sound toggle). |
| ios/GamerEmulator/GamerEmulator/Views/MatrixDisplayView.swift | SwiftUI renderer for the 8×8 LED matrix with brightness scaling. |
| ios/GamerEmulator/GamerEmulator/Views/GamerView.swift | Composed emulator UI (status bar + matrix + controls). |
| // Add a new item to the sequence | ||
| sequence[simonStep] = Int.random(in: 0..<Self.numDirs) | ||
|
|
There was a problem hiding this comment.
sequence[simonStep] = ... will go out of bounds once simonStep reaches 30 (the array has 30 elements, indices 0–29). Since simonStep is incremented on success without a cap, the emulator can crash late-game. Clamp simonStep to maxSeq - 1 before indexing/incrementing (or end the game / treat completion as win when simonStep == maxSeq - 1).
There was a problem hiding this comment.
Fixed in 5231605. Added a guard at the top of loop() that fires when simonStep >= maxSeq (i.e. the player completed all 30 steps): saves the high score, plays the win tune, shows the checkmark, then resets — no array access occurs. This covers the crash path where the success branch incremented simonStep to 30 between loop iterations.
| ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||
| ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||
| CODE_SIGN_STYLE = Automatic; |
There was a problem hiding this comment.
The project build settings specify ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor, but Assets.xcassets in this PR does not include an AccentColor.colorset. This can cause asset catalog build failures or missing accent color at runtime. Either add an AccentColor color set to the asset catalog, or remove the build setting so the app uses the default SwiftUI accent.
There was a problem hiding this comment.
Fixed in 38c2f32. Added AccentColor.colorset/Contents.json to Assets.xcassets (universal, no custom color — uses the system default accent). The ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor build setting now resolves correctly and won't cause asset catalog build failures.
| if gamer.display[newX][ballY - velY] == 0 { | ||
| blocks[newX][ballY + velY] = 0 | ||
| velY *= -1 | ||
| } else { | ||
| blocks[ballX + velX][ballY + velY] = 0 | ||
| velY *= -1; velX *= -1 | ||
| } | ||
| } | ||
| } else { | ||
| if gamer.display[ballX - velX][newY] == 0 { | ||
| blocks[ballX + velX][newY] = 0 | ||
| velX *= -1 | ||
| if !(newY > -1 && newY < 8) { | ||
| if gamer.display[ballX + velX][ballY - velY] == 0 { | ||
| blocks[ballX - velX][ballY - velY] = 0 |
There was a problem hiding this comment.
newY is already known to be out of bounds in this branch, but the code writes blocks[newX][ballY + velY] / blocks[ballX + velX][ballY + velY]. In Swift this will crash when ballY + velY is -1 or 8 (same condition that made newY out of bounds). Add bounds checks (only write when both indices are 0..<8) or restructure the collision handling to avoid out-of-range indexing.
| if gamer.display[newX][ballY - velY] == 0 { | |
| blocks[newX][ballY + velY] = 0 | |
| velY *= -1 | |
| } else { | |
| blocks[ballX + velX][ballY + velY] = 0 | |
| velY *= -1; velX *= -1 | |
| } | |
| } | |
| } else { | |
| if gamer.display[ballX - velX][newY] == 0 { | |
| blocks[ballX + velX][newY] = 0 | |
| velX *= -1 | |
| if !(newY > -1 && newY < 8) { | |
| if gamer.display[ballX + velX][ballY - velY] == 0 { | |
| blocks[ballX - velX][ballY - velY] = 0 | |
| let checkY = ballY - velY | |
| if checkY >= 0 && checkY < 8 && gamer.display[newX][checkY] == 0 { | |
| let targetY = ballY + velY | |
| if targetY >= 0 && targetY < 8 { | |
| blocks[newX][targetY] = 0 | |
| } | |
| velY *= -1 | |
| } else { | |
| let targetX = ballX + velX | |
| let targetY = ballY + velY | |
| if targetX >= 0 && targetX < 8 && targetY >= 0 && targetY < 8 { | |
| blocks[targetX][targetY] = 0 | |
| } | |
| velY *= -1; velX *= -1 | |
| } | |
| } | |
| } else { | |
| let checkX = ballX - velX | |
| if checkX >= 0 && checkX < 8 && newY >= 0 && newY < 8 && gamer.display[checkX][newY] == 0 { | |
| let targetX = ballX + velX | |
| if targetX >= 0 && targetX < 8 { | |
| blocks[targetX][newY] = 0 | |
| } | |
| velX *= -1 | |
| if !(newY > -1 && newY < 8) { | |
| let dispX = ballX + velX | |
| let dispY = ballY - velY | |
| let blockX = ballX - velX | |
| let blockY = ballY - velY | |
| if dispX >= 0 && dispX < 8 && dispY >= 0 && dispY < 8 && | |
| blockX >= 0 && blockX < 8 && blockY >= 0 && blockY < 8 && | |
| gamer.display[dispX][dispY] == 0 { | |
| blocks[blockX][blockY] = 0 |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com>
…log build Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com>
SimonGame: guardsimonStep >= maxSeqbeforesequence[simonStep]access — treats completing all 30 steps as an ultimate win (saves high score, plays win tune, resets) instead of crashingAccentColor.colorsettoAssets.xcassets— resolves asset catalog build failure caused byASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColorreferencing a missing color set✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.