A small, text-based single-player RPG you play in your terminal — and, more to the point, a working laboratory for the Clean DDD methodology in a domain that is rich enough to be interesting.
You walk through a connected graph of scenes (look, move north), items spawn
into the world at startup by chance, and the whole thing runs as one console
process. It's deliberately playful. The game is the excuse; the architecture is the
subject.
Toy examples make any methodology look clean. The hard, honest questions — where does a shared opening between two use cases live? when is something a subcase versus a duplicated branch? how do ports keep the domain framework-free without turning into ceremony? — only surface once a domain has enough moving parts to push back.
An RPG universe is a good source of that pressure: a player and (eventually) NPCs acting over shared, persistent state, with reads, writes, transactions, randomness, and authored content all in play. As the game grows, it keeps surfacing interesting points and genuine gaps in the doctrine — exactly what this project wants to find and document, not paper over.
So this repo is two things at once:
- a real, runnable game built strictly hexagonal / Clean DDD — a
framework-free
core(domain model + ports + use cases) wrapped by aninfrastructurelayer of adapters and Spring wiring, with the boundaries enforced by ArchUnit; - a narrated design record of how each vertical slice was reasoned about.
This repository ships its .claude/ memory directory on purpose. It isn't
incidental tooling clutter — it's the long-form reasoning behind the code:
.claude/memory/project-context.md— the current map: stack, package layout, and a slice-by-slice status of what's been built..claude/memory/design-notes.md— the why: the open threads, the trade-offs weighed, the doctrine questions each feature raised..claude/memory/spring-boot-4-notes.md— a running log of Spring Boot 4-vs-3.x differences hit along the way.
If you came for the methodology rather than the gameplay, those files are the main
attraction. (The @~/.claude/methodology/* references inside them point at the
author's local methodology library and won't resolve in a fresh clone — they're
kept as provenance, not a runnable dependency.)
Effectively all of the code here is vibe-coded in collaboration with Claude
(Anthropic's Claude Opus 4.8, driven from Claude Desktop) — but under close,
deliberate human supervision. Every slice was designed in conversation first and reviewed
before it landed; nothing was merged on autopilot. That working style is itself
part of what the project demonstrates: a methodology-disciplined human-AI pairing,
where the doctrine in .claude/ is what keeps the AI's output coherent across long
gaps between sessions.
Claude is also a co-author of the world content — the scene descriptions, item
flavour text, and the authored seed files under src/main/resources/world/ are the
kind of thing it's happy to help expand. Want a new wing of the ruined keep or a
cursed amulet with a backstory? That's a fun thing to ask it for.
Requirements: JDK 21+, Maven, and Docker (for the PostgreSQL play database).
The game persists its world to PostgreSQL, brought up via the project compose file:
docker compose up -dmvn -DskipTests packageThe UI is a JLine terminal application, and JLine needs a real terminal. The launch helper discovers a JDK 21+ runtime and runs the fat jar for you:
.claude/scripts/run-app.shThen type look to see where you are, move north (or go east, …) to travel,
and bye to quit.
Run it from an interactive shell (Git Bash, Windows Terminal, a system console). Do not launch through Maven (
mvn spring-boot:run/exec:java): Maven takes ownership of stdin/stdout and forces JLine into dumb-terminal mode, losing line editing, history, and colour. For the same reason the script runs the built jar withjava -jardirectly.
Logging goes to a file, not the console. Because JLine owns the terminal, Logback is configured (
logback-spring.xml) to write to./logs/game-clean.logwith no console appender — so logs never scribble over the game UI. If something goes wrong at startup and the console seems stuck or empty, that log file is where the story is.
Two authored worlds ship in src/main/resources/world/. Pass --world to pick one
(omit it for the default scenes.yaml):
.claude/scripts/run-app.sh --world scenes2.yamlA bare name resolves to classpath:world/<name>; a value with a scheme
(file:/abs/path.yaml) is used verbatim. Any other --game.* arguments are
forwarded to the application.
| Concern | Choice |
|---|---|
| Runtime | Spring Boot 4.0.6 on Java 21 |
| Build | Maven |
| Database | PostgreSQL (project docker-compose.yaml) |
| Migrations | Flyway — no ORM |
| Persistence access | Spring Data JDBC + MapStruct |
| UI | JLine terminal console |
| Id generation | Domain Dice rolls (core/model/id) — no port, no library |
| Tests | JUnit 5 unit tests + integration tests on ephemeral Testcontainers Postgres |
| Boundaries | ArchUnit guards keeping core framework-free and hexagonal |
core/ framework-free: the heart of the application
model/{aggregate}/ aggregate roots + value objects (scene, player, item, dice, id)
port/{operation}/ output ports (persistence, transaction, seed, …)
usecase/{goal}/ use cases + their input/presenter ports; subcases as peers
infrastructure/ adapters + Spring wiring
persistence/ Spring Data JDBC repos, DB entities, MapStruct mappers
terminal/ JLine config, console session, command parsing, presenters
world/ YAML seed reader + seeder (authored content → through the domain)
transaction/
Built so far: game initialization, the interactive terminal shell, and the look,
move, and item-spawning verticals. Not yet: NPCs, look <target> / take, and
asynchronous/event processing. See .claude/memory/project-context.md for the live
status.
A pet project, worked on sporadically and shared for the community. No delivery pressure — the point is to experiment, learn, and demonstrate the methodology in the open.