- Kotlin 71.5%
- Svelte 20%
- CSS 5.1%
- TypeScript 2.9%
- Dockerfile 0.3%
- Other 0.2%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .mvn/wrapper | ||
| frontend | ||
| Harena API | ||
| screenshots | ||
| src | ||
| .gitattributes | ||
| .gitattributes:Zone.Identifier | ||
| .gitignore | ||
| .gitignore:Zone.Identifier | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| HARENA_ARENAS.md | ||
| HARENA_ARENAS.md:Zone.Identifier | ||
| HELP.md:Zone.Identifier | ||
| mvnw | ||
| mvnw.cmd | ||
| playtest.md | ||
| pom.xml | ||
| pom.xml:Zone.Identifier | ||
| README.md | ||
| renovate.json | ||
| v2-plan.md | ||
| v2.md | ||
Harena
A gladiator management game built API-first. You are a lanista — master of a gladiatorial school in ancient Rome. Train your fighters, hire doctores and medici, enter the annual games, feel every permanent loss. The API is the game.
Inspired by Top Eleven (Nordeus). Designed so humans and AI agents play through the same REST API.
Quick start
# Run in dev mode (H2 in-memory database, no setup needed)
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Open http://localhost:8080 for the game UI, or http://localhost:8080/swagger-ui.html for the raw API explorer.
Game loop
Every action is an API call. The day advances via POST /api/v1/game/tick; scheduled events resolve themselves on their day.
1. Create your ludus
POST /api/v1/ludus
{ "name": "Ludus Magnus" }
Gives you 5000 denarii, 3 random gladiators, and an initial 60-day calendar.
2. Inspect your roster
GET /api/v1/gladiators
Shows stats, health, exhaustion, crowd favour, injuries, traits, assigned Doctor, and equipped gear.
3. Hire a Doctor (required before any training)
GET /api/v1/staff/market
POST /api/v1/staff/market/{listingId}/hire
POST /api/v1/staff/doctors/{id}/assign
{ "gladiatorId": "<uuid>" }
Doctors have a style specialisation, a tier (Journeyman / Master / Grand Master) with 1/2/3 training slots, and a daily salary.
4. Train (requires an assigned Doctor)
POST /api/v1/gladiators/{id}/train
{ "stat": "STRENGTH", "intensity": "NORMAL" }
Intensity is LIGHT / NORMAL / HARD — trade duration, exhaustion cost, and injury risk for bigger gains.
5. Sign up for fights on the calendar
GET /api/v1/events
POST /api/v1/events/{id}/signup
{ "gladiatorId": "<uuid>" }
Events auto-resolve on their scheduled day when you advance the game. REGULAR, GRAND, and FESTIVAL event types differ in purse multiplier, participant slots, and reputation reward.
6. Advance the day
POST /api/v1/game/tick
Processes training, injuries, doctor salaries, market refresh, rival ludi matches, news generation, season rollovers, and resolves any event scheduled for today.
7. Buy from the market
GET /api/v1/market
POST /api/v1/market/{listingId}/purchase
8. Retire a veteran (closes the loop)
POST /api/v1/gladiators/{id}/retire
High-crowd-favour veterans can be awarded the rudis (wooden sword). They retire permanently and re-appear in your staff list as a Doctor, at a reduced salary for 180 days. Your first star becomes your future trainer.
Full snapshot (useful for AI agents)
GET /api/v1/game/state
Running with PostgreSQL
# Start the database
docker-compose up -d db
# Run the app
./mvnw spring-boot:run
Or run everything at once:
docker-compose up
Frontend
A Svelte 5 UI is included in frontend/. It embeds into the Spring Boot JAR as static files.
# Dev mode — hot-reload UI against the running backend
cd frontend
npm install
npm run dev # http://localhost:5173 (proxies /api to :8080)
# Build and embed into the JAR (requires Node on PATH)
./mvnw package -P build-frontend
The production build writes to src/main/resources/static/ so Spring Boot serves the UI at /.
Pages: Dashboard, Gladiators, Staff, Market, Fights, Calendar, Season, News, Glossary.
Development
# Run tests (181 tests, no external services needed)
./mvnw test
# After editing openapi.yaml — regenerate interfaces
./mvnw generate-sources
# Build a Docker image
docker build -t harena .
OpenAPI-first workflow
The API contract lives in src/main/resources/static/openapi.yaml. It is the single source of truth.
When you change the spec:
- Run
./mvnw generate-sources - Generated interfaces and models are updated in
target/generated-sources/ - The compiler flags any controller that no longer satisfies the interface
- Update the controller
The spec is served at http://localhost:8080/openapi.yaml and can be imported directly into Bruno or Postman.
Tech stack
| Language | Kotlin 2.3.20 (JVM 25) |
| Framework | Spring Boot 3.5.13 |
| Database | PostgreSQL (default), H2 (dev) |
| Migrations | Liquibase |
| API contract | OpenAPI 3, code-generated via openapi-generator |
| API docs | Swagger UI (/swagger-ui.html) |
| Frontend | Svelte 5 + Vite 5 (embedded in JAR) |
| Testing | JUnit 5, MockK, Testcontainers |
Game mechanics
Fighting styles (rock-paper-scissors matchup)
| Style | Beats | Loses to |
|---|---|---|
| MURMILLO | RETIARIUS | THRAEX |
| RETIARIUS | SECUTOR | MURMILLO |
| THRAEX | MURMILLO | SECUTOR |
| SECUTOR | THRAEX | RETIARIUS |
Aggression settings
| Setting | Attack | Defense | Death chance on KO |
|---|---|---|---|
| CAUTIOUS | 0.75× | 1.20× | 10% |
| BALANCED | 1.00× | 1.00× | 25% |
| AGGRESSIVE | 1.35× | 0.75× | 55% |
Home-gladiator death chance is further modified by age (≥35: +10pp), traits (IRON_WILL −10pp, FRAGILE +10pp), Medicus (−5pp), and arena deathRiskModifier. A single-gladiator roster has a pity floor of 0% to prevent single-fight wipes.
Training intensity
| Intensity | Duration | Gain/day | Exhaustion | Injury risk |
|---|---|---|---|---|
| LIGHT | 1 day | 1–2 | +5 | 0% |
| NORMAL | 2 days | 2–4 | +12 | 1% |
| HARD | 3 days | 4–7 | +25 | 5% |
Gains use a triangular bell curve. Diminishing returns kick in past the style baseline (gain halves per 20 points over baseline, floor of 1). A matching-style Doctor grants ×1.3 gains and −20% exhaustion; cross-style Doctor is ×0.8. Doctor tier adds +0 / +0.1 / +0.2 to the gain multiplier.
Injury categories
| Category | Duration | Effect | Trigger |
|---|---|---|---|
| Cut | 3 days | −5% attack | Damage ≥ 20 HP in a fight (80% roll) |
| Sprain | 5 days | −10% agility | Damage ≥ 25 HP in a fight (40% roll) |
| Fracture | 10 days | −20% all combat, blocks training | IRON_WILL-saved near-death (50%) |
| Scar | permanent | −5% crowd gain | IRON_WILL-saved near-death (25%) |
A Medicus reduces recovery time by 30% and lowers scar risk.
Seasons and annual events
A season is 60 days. Named events auto-schedule on fixed offsets:
| Event | Season day | Type | Purse × | Rep required |
|---|---|---|---|---|
| Ludi Megalenses | 8 | GRAND | 2.0 | 100 |
| Ludi Romani | 24 | FESTIVAL | 2.5 | 300 |
| Emperor's Games | 44 | FESTIVAL | 3.5 | 500 |
| Saturnalia | 58 | GRAND | 2.0 | 150 |
Tick processing (in order)
- Day increment
- Per-gladiator upkeep (5–15 denarii/day, may go negative)
- Doctor salaries + Medicus salaries; loyalty decay on unpaid wages
- Training session tick — stat gain, exhaustion, injury roll at intensity
- Injury tick — recovery days drop; scars stay permanent
- State tick — INJURED → RECOVERING → RESTING based on HP
- Exhaustion recovery during RESTING (−10/day)
- Season rollover (every 60 days) — aging (peak +1 @ 28–32, decay −1 @ ≥35 with IRON_WILL halving)
- Rival ludi Elo simulation (2 matches/day)
- News feed generation
- Event fight resolution (scheduled for today) — prize, reputation, loot drops, crowd favour
- Calendar regeneration (top up to 60-day window)
- Market refresh (gladiators + staff + equipment — each on their own cadence)
Gladiator states
RESTING— ready to fight or train, exhaustion recoversTRAINING— in a TrainingSession, exhaustion climbsINJURED— health < 30, heals slowlyRECOVERING— health 30–79, heals fasterDEAD— permanentRETIRED— awarded the rudis; candidate for re-appearance as a Doctor
Version history
See CHANGELOG.md for the release notes. The current build is v2 — "Staff, Seasons, and Legacy".