Skip to content

Turns

Turns are the action economy. Most player actions cost turns; defending and quality-of-life actions don't. Turns regenerate continuously, not as a daily reset.

✅ Live — continuous regeneration (ADR-0004). turn_service.regenerate_turns advances the pool lazily on read/spend at 1000/86400 t/s × the ARIA multiplier, capped at the stored Player.max_turns (= 1000 + rank.max_turns_bonus, rank-only — ARIA scales the rate, not the cap). Player.last_turn_regeneration and Player.max_turns columns are defined (migration a1d4f9c7b3e2); RankingService.refresh_daily_turns delegates to the regen helper. Regeneration is wired at every spend site inside the FOR UPDATE row lock (movement, combat, trading dock/undock). Proven live on dev 2026-06-18 (see FINDINGS). See SYSTEMS/turn-regeneration.md for the full spec.

Allocation

  • Default max: 1,000 turns. Player.max_turns is a canonical launch column per ADR-0004; the value is 1000 + rank.max_turns_bonus, recomputed on rank promotion (per ADR-0023). The stored column is written by turn_service.regenerate_turns (recomputed from rank on each regen via RankingService.calculate_max_turns, which is rank-only — ARIA does not scale the cap), so the stored value and the computed value agree.
  • Stored field: Player.turns — current pool, integer.
  • Regen anchor: Player.last_turn_regeneration (falls back to created_at for never-played accounts).
  • No carryover: once at max, regen stops until you spend.

Regeneration formula

From services/movement_service.py and the turn-regen pattern:

# No carryover: at-cap players don't accumulate regen credit;
# the anchor jumps to `now` and the function returns early.
if player.turns >= player.max_turns:
    player.last_turn_regeneration = now
    return

base_rate        = 1000 / 86400        # ≈ 0.01157 turns/sec
effective_rate   = base_rate * player.aria_bonus_multiplier
elapsed_seconds  = (now - player.last_turn_regeneration).total_seconds()
turns_added      = int(elapsed_seconds * effective_rate)
seconds_per_turn = 1.0 / effective_rate
consumed_seconds = int(turns_added * seconds_per_turn)
player.turns     = min(player.max_turns, player.turns + turns_added)
# Advance the anchor by the integer-turn-equivalent seconds, NOT to `now`,
# so the sub-turn remainder carries across polls and the steady-state regen
# rate holds to spec. See SYSTEMS/turn-regeneration.md for the canonical
# anchor-advance model.
player.last_turn_regeneration = (
    player.last_turn_regeneration + timedelta(seconds=consumed_seconds)
)

In hourly terms: ~41.67 turns/hour at 1.0×; up to 62.5 turns/hour at the top ARIA tier (1.5×).

aria_bonus_multiplier is set by the consciousness-level table updated on combat wins (see combat.md):

ARIA interactions Consciousness Multiplier
0–49 1 1.0×
50+ 2 1.1×
150+ 3 1.2×
400+ 4 1.35×
1,000+ 5 1.5×

Maximum-cap bonuses also come from rank (see ranking.md) — Fleet Admiral grants +120 to max_turns.

Action cost matrix

Sources: services/movement_service.py, services/combat_service.py, services/genesis_service.py, services/trading_service.py.

Action Turns Notes
Sector travel (direct warp) ship.turn_cost (≈1, ship-modified) Locked while docked/landed
Travel via natural warp tunnel 1–3 Distance-based
Travel via player warp gate 0 Major incentive over natural tunnels
Dock at station 1
Undock 1
Buy / sell while docked 0
Land on planet 1
Leave planet 1
Attack player ship min 2; defender's attack_turn_cost Escape pods set very high cost intentionally
Attack sector drones 2
Attack planet 3
Attack port 3
Defend (any) 0 Always free
Ship upgrade 0 Time-based, not turn-based
Quantum Jump commit (Warp Jumper) 50 + tow surcharge if any 24-hour real-time cooldown; consumes 1 Quantum Charge. See ../galaxy/sectors.md#quantum-jump-warp-jumper.
Long-range quantum scan (Warp Jumper) 5 per scan 4-hour scan cooldown (independent of jump cooldown); Far-band additionally consumes 1 Quantum Shard; results expire after 10 minutes. 📐 Design-only
Scan adjacent sectors 2 📐 Design-only — currently free / passive map fill
Tractor Beam tow (per-move surcharge) +1 / +2 / +3 / +5 by towed ship_size (tiny / small / medium / large) Through natural warp tunnels. 📐 Design-only
Tractor Beam tow through player warp gate +2 flat 📐 Design-only
Tractor Beam tow through Quantum Jump +5 flat (Warp Jumper hauler only, medium-max towed) 📐 Design-only — only the WJ's own Tractor operates through QJ
Deploy drone squadron 3 📐 Design-only — currently 0; tracked for future scarcity
Genesis sequence reduced action cap during 48h sequence Solo Genesis does not lock the player to the sector (per ADR-0015); team Genesis locks all participating players

Concurrency safety

MovementService.move_player_to_sector opens a row lock (with_for_update) on the player to prevent two simultaneous movement requests from double-spending turns. Combat services use the same pattern.

Player-facing affordances

  • Player state endpoints expose turns, max_turns, last regen timestamp; clients render time-to-full.
  • Low-turn warning UI hints when the pool is below thresholds (design: <50).
  • Out-of-turns players can still defend.

Source map

Topic Path
Turn fields & defaults services/gameserver/src/models/player.py
Movement (turn spend) services/gameserver/src/services/movement_service.py
Combat turn costs services/gameserver/src/services/combat_service.py
ARIA multiplier writes services/gameserver/src/services/combat_service.py (consciousness thresholds)
Rank-based max-turn bonus services/gameserver/src/services/ranking_service.py
Player API services/gameserver/src/api/routes/player.py

Planned (not in code)

📐 Design-only — dynamic regeneration boosts (sector-based, event-based). Turns are strictly per-player and capped at the rank-derived max_turns, with no cross-player or temporal accumulation paths.