This document defines the workspace-level architecture for Void Gladiator as a monorepo.
It answers:
- why the project should be a monorepo
- which monorepo tools to use
- what packages should exist first
- what each package is responsible for
- which dependencies are allowed between packages
- how this structure supports future LAN multiplayer cleanly
This document complements TECH_ARCHITECTURE.md.
Use:
pnpmworkspaces for package managementNxfor workspace orchestration, affected runs, caching, and project graph management
pnpm solves dependency management and workspace linking well.
Nx adds the parts that become valuable as soon as the repo has several packages:
- fast targeted builds and tests
- explicit project graph visibility
- clean project boundaries
- reusable task conventions
- room to grow into more apps and packages later
This is a better fit than a single-package setup because the project is expected to grow from:
- one single-player terminal app now
into potentially:
- a LAN host app or mode
- a LAN client app or mode
- shared protocol contracts
- networking transport packages
- replay or bot tooling later
The project has three long-term pressures:
- the simulation must stay reusable
- terminal concerns must stay isolated
- multiplayer must be addable without cutting across everything
A monorepo helps because it encourages package boundaries around exactly those concerns.
This is not about making the repo look enterprise-heavy. It is about preventing the most common failure mode for game prototypes: one growing codebase with no clear ownership boundaries.
Recommended top-level layout:
test1/
apps/
cli-game/
packages/
game-core/
engine-loop/
renderer-ansi/
terminal-input/
content/
persistence/
shared/
protocol/
network-lan/
tests/
integration/
docs/
GAME_DESIGN.md
VOID_GLADIATOR_SPEC.md
TECH_ARCHITECTURE.md
MONOREPO_ARCHITECTURE.md
package.json
pnpm-workspace.yaml
nx.json
tsconfig.base.json
eslint.config.js
.prettierrc
- runnable things go in
apps/ - reusable internal libraries go in
packages/ - cross-package integration tests go in
tests/ - design and implementation docs go in
docs/
The MVP only needs one app.
This is the actual terminal game executable.
Responsibilities:
- bootstrap the workspace runtime
- compose the engine and game packages
- start the terminal session
- run the main loop
- own process-level startup and shutdown behavior
This app should stay thin.
It should not become the place where simulation rules or renderer internals accumulate.
Each package should exist for one coherent reason.
This is the heart of the game.
Responsibilities:
- game state types
- commands and intent model
- simulation step
- player systems
- enemy systems
- collision and damage
- score and streak logic
- scene state and transitions
This package must stay free of terminal-specific APIs.
It should be the package that is easiest to test in isolation.
Responsibilities:
- fixed-tick runtime
- frame timing
- scheduler utilities
- small runtime orchestration primitives
This package is intentionally generic and should not know anything about Void Gladiator content.
Responsibilities:
- screen buffer model
- ANSI cursor movement and drawing
- frame diffing
- glyph and color output
- terminal-safe rendering helpers
This package can render public game state contracts, but it must not own gameplay rules.
Responsibilities:
- raw mode keyboard capture
- terminal key decoding
- held-key state tracking
- mapping input to normalized game commands
This package is the local input adapter.
Later, a remote input adapter can follow the same contracts.
Responsibilities:
- enemy definitions
- upgrade definitions
- wave definitions
- boss definitions
- balance constants and content metadata
Keeping content separate is useful because balance iteration is frequent and should not require touching simulation internals unnecessarily.
Responsibilities:
- save path selection
- high score storage
- config storage later
- file read and write helpers for persisted game data
Responsibilities:
- math utilities
- common types used across packages when appropriate
- small data structures
- assertions and tiny generic helpers
This package should stay tight. If it becomes a dumping ground, the workspace boundaries will weaken quickly.
Responsibilities:
- message types
- command batch contracts
- state snapshot contracts
- serialization-safe protocol models
This package may start small, but it should exist early because future multiplayer depends on it.
Responsibilities:
- LAN transport abstraction
- TCP socket implementation later
- client and host message flow helpers
- synchronization helpers once multiplayer begins
This package can stay mostly placeholder in the MVP, but the workspace should reserve the boundary now.
Recommended dependency graph:
apps/cli-gamedepends ongame-core,engine-loop,renderer-ansi,terminal-input,persistence, andcontentgame-coredepends onshared,content, andprotocolcontracts where neededrenderer-ansidepends onsharedand public state contracts onlyterminal-inputdepends onsharedand command contractsengine-loopdepends onsharedpersistencedepends onsharednetwork-landepends onprotocolandsharedprotocoldepends onsharedonly if the shared types are truly protocol-safe
game-coremust not import terminal APIsrenderer-ansimust not own gameplay rulesterminal-inputmust not mutate game state directlyapps/cli-gameshould compose packages, not absorb their logicnetwork-lanmust not become a dumping ground for game rules
The workspace should not split into dozens of packages immediately.
The goal is a small number of meaningful packages, not maximal decomposition.
- game-core
- engine-loop
- renderer-ansi
- terminal-input
- content
- persistence
- shared
- protocol
- network-lan implementation details
- replay tools
- bots or AI test harnesses
- profiling or debugging tools
This keeps the workspace structured without turning it into ceremony.
Use Nx for:
- project generation if useful
- task running across packages
- affected commands in CI or local development
- dependency graph inspection
- caching builds, tests, and linting
- build
- test
- lint
- dev
- typecheck
When the repo becomes multiplayer-aware, the number of packages and dependency edges grows. Nx will make it much easier to answer:
- what changed
- what needs rebuilding
- what can be tested in isolation
- whether package boundaries are still sane
This monorepo is for one project, not for public package publishing.
That means internal packages should be treated as workspace packages, not as independently versioned external libraries.
Keep the versioning model simple until there is a real reason to publish anything.
The package split should map directly to testing scope.
game-core: systems, simulation, collisions, scoring, transitionsengine-loop: timing behavior and scheduler helpersrenderer-ansi: buffer and diff logicterminal-input: key decoding and command generationpersistence: save and load behaviorprotocol: message validation and serialization shape
Use tests/integration/ for multi-package flows such as:
- command input flowing into simulation
- simulation state rendered into a frame buffer
- end-of-run state saved to persistence
This split keeps tests aligned with workspace boundaries.
The monorepo should make the future LAN path straightforward.
- one app:
cli-game - local input only
- no active network transport
- protocol contracts can remain thin
Likely additions or expansions:
- richer
protocolcontracts - real
network-lantransport implementation - host mode inside
cli-gameor a second dedicated app if needed - client-side synchronization flow
If simulation is already in game-core, networking can be added around it instead of through it.
That is the main architectural win.
The first implementation pass should create this minimum viable workspace:
apps/
cli-game/
packages/
game-core/
engine-loop/
renderer-ansi/
terminal-input/
content/
persistence/
shared/
protocol/
network-lan can be created immediately as a placeholder package or added right after the MVP spine is established.
It gives:
- one runnable app
- clean package seams
- clear test boundaries
- a place for future protocol and networking work
without overbuilding the workspace before gameplay exists.
The best fit for this project is a pnpm + Nx monorepo with one runnable terminal app and a small set of focused internal packages.
That gives enough engineering structure to support future LAN multiplayer cleanly while keeping the first single-player build practical and fast to iterate on.