No description
  • Kotlin 71.5%
  • Svelte 20%
  • CSS 5.1%
  • TypeScript 2.9%
  • Dockerfile 0.3%
  • Other 0.2%
Find a file
2026-06-09 00:16:49 +02:00
.forgejo/workflows Update actions/checkout action to v6 2026-04-05 22:03:29 +00:00
.mvn/wrapper Update dependency maven to v3.9.16 2026-05-17 22:02:05 +00:00
frontend Update dependency svelte to v5.56.3 2026-06-07 22:09:33 +00:00
Harena API basic frontend 2026-03-07 19:55:53 +01:00
screenshots Arenas 2026-03-08 17:45:43 +01:00
src polish(v2-p13): tuning pass + CHANGELOG 2026-04-20 16:49:19 +02:00
.gitattributes initial commit 2026-03-07 12:58:32 +01:00
.gitattributes:Zone.Identifier initial commit 2026-03-07 12:58:32 +01:00
.gitignore initial commit 2026-03-07 12:58:32 +01:00
.gitignore:Zone.Identifier initial commit 2026-03-07 12:58:32 +01:00
CHANGELOG.md polish(v2-p13): tuning pass + CHANGELOG 2026-04-20 16:49:19 +02:00
CLAUDE.md chore: upgrade to Kotlin 2.3.20 + Java 25 2026-04-20 20:15:42 +02:00
docker-compose.yml fix: restore Liquibase startup on Spring Boot 4 and fix PG18 volume mount 2026-05-22 19:58:26 +02:00
Dockerfile Merge remote-tracking branch 'origin/renovate/eclipse-temurin-26.x' into develop 2026-05-22 18:15:52 +02:00
HARENA_ARENAS.md Arenas 2026-03-08 17:45:43 +01:00
HARENA_ARENAS.md:Zone.Identifier Arenas 2026-03-08 17:45:43 +01:00
HELP.md:Zone.Identifier initial commit 2026-03-07 12:58:32 +01:00
mvnw Phase 1 setup 2026-03-07 17:10:21 +01:00
mvnw.cmd initial commit 2026-03-07 12:58:32 +01:00
playtest.md refactor(phase-0): extract DeathRollCalculator + NarrationPool; stub TraitPool + DoctorPersonalityPool 2026-04-20 10:21:44 +02:00
pom.xml Update dependency org.openapitools:openapi-generator-maven-plugin to v7.23.0 2026-06-08 22:12:31 +00:00
pom.xml:Zone.Identifier initial commit 2026-03-07 12:58:32 +01:00
README.md chore: upgrade to Kotlin 2.3.20 + Java 25 2026-04-20 20:15:42 +02:00
renovate.json Add renovate.json 2026-04-03 20:42:02 +00:00
v2-plan.md refactor(phase-0): extract DeathRollCalculator + NarrationPool; stub TraitPool + DoctorPersonalityPool 2026-04-20 10:21:44 +02:00
v2.md refactor(phase-0): extract DeathRollCalculator + NarrationPool; stub TraitPool + DoctorPersonalityPool 2026-04-20 10:21:44 +02:00

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:

  1. Run ./mvnw generate-sources
  2. Generated interfaces and models are updated in target/generated-sources/
  3. The compiler flags any controller that no longer satisfies the interface
  4. 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 12 +5 0%
NORMAL 2 days 24 +12 1%
HARD 3 days 47 +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)

  1. Day increment
  2. Per-gladiator upkeep (515 denarii/day, may go negative)
  3. Doctor salaries + Medicus salaries; loyalty decay on unpaid wages
  4. Training session tick — stat gain, exhaustion, injury roll at intensity
  5. Injury tick — recovery days drop; scars stay permanent
  6. State tick — INJURED → RECOVERING → RESTING based on HP
  7. Exhaustion recovery during RESTING (10/day)
  8. Season rollover (every 60 days) — aging (peak +1 @ 2832, decay 1 @ ≥35 with IRON_WILL halving)
  9. Rival ludi Elo simulation (2 matches/day)
  10. News feed generation
  11. Event fight resolution (scheduled for today) — prize, reputation, loot drops, crowd favour
  12. Calendar regeneration (top up to 60-day window)
  13. Market refresh (gladiators + staff + equipment — each on their own cadence)

Gladiator states

  • RESTING — ready to fight or train, exhaustion recovers
  • TRAINING — in a TrainingSession, exhaustion climbs
  • INJURED — health < 30, heals slowly
  • RECOVERING — health 3079, heals faster
  • DEAD — permanent
  • RETIRED — 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".