This file is the original greenfield project plan that kicked off Codexitma.
It is no longer an exact description of the shipped codebase. The project moved well beyond this terminal-first proposal into a graphics-first RPG engine with:
- a native macOS frontend
- an SDL3 cross-platform frontend and Win64 release build
- built-in audio, screenshot automation, and a headless bridge
- a first-class adventure editor
- external JSON adventure packs and overrides
- multiple adventures, visual themes, and a
Depth 3Drenderer
The historical design intent here is still useful, but the implementation details below should be read as archival planning, not current architecture.
For the current project surface, see:
README.mdSCRATCHPAD.md
Build a greenfield, single-player 2D top-down RPG adventure game in Swift 6 as a terminal-first macOS app using Swift Package Manager. The game will present like a hybrid of Apple II-era exploration and MS-DOS text-mode adventure games: fixed-grid rendering, low-color palette, tile-based movement, menu-driven interactions, and strict retro-era constraints.
I am choosing:
- Terminal UI over AppKit/SpriteKit
- ASCII / box-drawing presentation over bitmap graphics
- Single executable Swift package
- No external dependencies by default
This keeps the implementation focused, portable across macOS terminals, and faithful to the retro constraint brief.
The player explores a fallen valley, uncovers why the old beacon towers went dark, solves environmental puzzles, fights hostile creatures, and restores a final tower to end the blight.
- Melancholic but adventurous
- Sparse, atmospheric writing
- Low-fi text presentation with strong readability
- Retro constraints treated as a feature, not a limitation
- Top-down overworld exploration on a tile grid
- Small handcrafted world split into connected zones
- Turn-based movement and combat
- Simple inventory and quest flags
- 4-6 hours of total playtime target
- Save/load support
- Playable terminal game loop
- World exploration
- NPC dialogue
- Turn-based encounters on the same map
- Items, keys, healing, one ranged tool
- Environmental puzzles
- Story progression with an ending
- Save/load to local JSON file
- Procedural generation
- Multiplayer
- Modding
- Audio
- Networking
- Pixel art / native window rendering
- Real-time combat
- Language: Swift
- Package manager: Swift Package Manager
- Target: macOS command-line executable
- Minimum target assumption: macOS 14+
- Entry point:
Sources/Game/main.swift
Use ANSI terminal control sequences for:
- Clear screen
- Cursor repositioning
- Color changes
- Hiding/showing cursor
Fallback behavior:
- If ANSI color is unsupported, run in monochrome mode
- If terminal size is too small, show a resize prompt and pause until valid
- Visible playfield: 60x22
- Status + log area: 20x22 side panel, yielding an 80x24 expected terminal
- Fixed update cadence: input-driven turns, not frame-driven animation
- Tile glyphs restricted to ASCII plus common box characters when available
- Palette limited to 8 base ANSI colors
Use 6 connected regions:
- Village of Merrow (safe hub)
- South Fields (intro/tutorial area)
- Sunken Orchard (first puzzle zone)
- Hollow Barrows (combat-heavy dungeon)
- Black Fen (navigation hazard area)
- Beacon Spire (final dungeon / ending)
Each region is a hand-authored tilemap loaded from local data files.
- Wake in Merrow after the beacon fails
- Receive first objective from village elder
- Restore power to two outer relay shrines
- Gain access to the Black Fen route
- Recover the Lens Core from the Barrows
- Reach Beacon Spire and reactivate the beacon
- Final encounter and ending scene
- Movement: 4-directional, one tile per turn
- Collision: walls, water, locked gates, interactables
- Interaction:
SpaceorE - Inventory: small fixed-capacity list
- Combat: bump-to-engage, still resolved in-map turn mode
- Dialogue: typewriter-style text reveal can be skipped
- Quests: hidden flag-based progression, optional journal page
- Save points: manual save at beacons and beds; optional quick-save on quit
- Health
- Stamina
- Attack
- Defense
- Lantern charge (used in dark areas / ranged tool gating)
- Healing tonic
- Iron key / shrine keys
- Lantern oil
- Charm fragments (optional upgrades)
- Lens Core (main quest artifact)
- Crows
- Root Hounds
- Mire Wraiths
- Barrow Sentinels
- Final boss: The Shaded Keeper
- Pressure plates
- Switch ordering
- Light-routing with beacon mirrors
- Key-and-gate traversal
- One “darkness” visibility restriction in Black Fen
Sources/Game/main.swiftSources/Game/App/GameApp.swiftSources/Game/Engine/Sources/Game/Model/Sources/Game/Systems/Sources/Game/Rendering/Sources/Game/Input/Sources/Game/Content/Sources/Game/Persistence/Tests/GameTests/
- App: process startup, terminal setup/teardown, main loop
- Engine: state machine, scene transitions, turn resolution
- Rendering: terminal buffer, ANSI output, HUD/log drawing
- Input: key parsing and command mapping
- Model: core data types
- Systems: combat, interactions, AI, quests
- Content: maps, dialogue, item definitions, NPCs
- Persistence: save/load serialization
These are the core interfaces the implementation should expose internally and stabilize early.
enum GameMode { case title, exploration, dialogue, inventory, combat, pause, gameOver, ending }enum TileType { case floor, wall, water, brush, doorLocked, doorOpen, shrine, stairs, beacon }enum Direction { case up, down, left, right }enum ActionCommand { case move(Direction), interact, openInventory, confirm, cancel, help, quit }
struct Position { var x: Int; var y: Int }struct Tile { var type: TileType; var glyph: Character; var walkable: Bool; var color: ANSIColor }struct Item { let id: ItemID; let name: String; let kind: ItemKind; let value: Int }struct PlayerState { ...stats, inventory, position, currentMapID... }struct NPCState { let id: NPCID; var position: Position; var dialogueState: Int }struct EnemyState { let id: EnemyID; var position: Position; var hp: Int; var ai: AIKind }struct QuestState { var flags: Set<QuestFlag> }struct SaveGame: Codable { var player: PlayerState; var world: WorldState; var quests: QuestState; var playTimeSeconds: Int }
protocol Scene { func render(into: inout ScreenBuffer, state: GameState); mutating func handle(_ command: ActionCommand, engine: inout GameEngine) }protocol RenderableEntity { var position: Position { get }; var glyph: Character { get }; var color: ANSIColor { get } }
struct GameStatefinal class GameEnginestruct ScreenBufferfinal class TerminalRendererfinal class InputReaderfinal class SaveRepository
struct MapDefinition: Decodablestruct DialogueNode: Decodablestruct EncounterDefinition: Decodable
Store maps as local JSON or compact text assets. Chosen default:
- ASCII map files for layout
- JSON sidecar for metadata (spawns, exits, triggers)
Reason: easier to author, inspect, and patch.
- Path:
~/Library/Application Support/AshesOfMerrow/savegame.json - Single save slot in initial implementation
- Atomic write via temp file + replace
All game content loads at startup and is validated before the title screen. Fatal content errors should fail fast with a human-readable terminal message.
WASDor arrow keys: movementEorSpace: interact / confirmI: inventoryJ: journal/helpEscorQ: pause / backS: save when allowedL: load from title screen
- Single-key input, no Enter required
- If raw terminal mode cannot be enabled, fallback to line-input mode with a warning and alternate command prompt
- Left: playfield
- Right: HUD
- Bottom lines (within side panel or reserved row): rolling event log
- Player name
- HP / max HP
- Stamina
- Lantern charge
- Current region
- Active objective
- Key item count
- Player:
@ - Friendly NPC:
& - Hostile enemy:
g,h,w, etc. - Doors/gates:
+ - Water:
~ - Brush:
" - Shrine/goal:
*
- Every player action consumes one turn
- Enemy AI updates after each valid player turn
- Invalid movement does not consume a turn but writes a short log message
- Triggered when attacking or colliding into hostile target
- Damage formula is deterministic with a small random range
- Enemy telegraphs via log line before special actions
- Player can attack, use item, or retreat if adjacent path exists
- No separate battle screen; combat remains in-map
- On defeat, return to last save point
- Lose 10% carried consumables rounded down, never quest items
- Log a clear recovery message to avoid frustration
- Single default difficulty
- Tuned for low frustration and steady progress
- Optional hidden “Iron Lantern” mode is out of scope for v1
- Map parser validates rectangular bounds and legal glyphs
- Collision tests for all blocking tile types
- Combat formula tests for deterministic min/max output
- Save/load round-trip tests
- Quest flag transition tests
- Input parser tests for ANSI escape sequences and fallback keys
- Terminal buffer diff tests to ensure stable rendering writes
- New game starts, title renders correctly in 80x24 terminal
- Player can move, collide, interact, and open inventory
- First shrine can be activated only after obtaining its required item
- Enemy turn order behaves consistently
- Death returns player to last valid save point
- Save persists across app restart
- Final quest chain can be completed end-to-end without soft lock
- Game remains usable in monochrome fallback mode
- Full playable beginning-to-end path with no blockers
- No crashes during normal play path
- Terminal restored correctly on exit, including interrupt/quit paths
- Save file corruption is handled gracefully with a recovery message
- All six regions load and connect correctly
These phase notes are historical. In practice, the project has already moved through and past them, with major scope additions not captured in this original plan.
- Initialize Swift package executable
- Implement terminal raw mode handling and cleanup
- Build ANSI renderer and double-buffered
ScreenBuffer - Add title screen and resize gate
- Add
GameState,GameMode, and main loop - Add input reader and command mapping
- Add scene switching and event log
- Implement map loading
- Add tile collision, region transitions, interactables
- Render world, entities, and HUD
- Add inventory, NPC dialogue, quest flags
- Add enemy AI and in-map combat
- Add item use and stat effects
- Author six regions
- Add story text, NPCs, enemies, puzzles
- Tune progression to avoid soft locks
- Add save/load
- Add fallback modes and terminal capability checks
- Add test coverage and balancing passes
- Terminal portability issues: keep ANSI usage conservative and provide monochrome fallback
- Raw input edge cases: isolate terminal mode setup/teardown and test abnormal exits
- Content soft locks: use explicit quest flags and validation checks for required items and triggers
- Unreadable text UI: enforce fixed layout and minimum terminal size
- The repo is currently empty; this is a new project.
- No external libraries will be used unless raw terminal input proves impractical.
- The game will target modern macOS terminals, primarily Terminal.app and iTerm2.
- The chosen default presentation is terminal retro text mode, not a native graphical app.
- The chosen aesthetic is a DOS/Apple II-inspired hybrid, but implemented with modern Swift and ANSI terminal control.
- Single save slot is sufficient for v1.
- All story, naming, and art direction are fully delegated, so this plan locks them without further approval.