Turns¶
Status: ✅ Shipped — turn economy and regeneration rules documented against live turn-regeneration service. (re-verified 2026-08-21 vs Sectorwars2102
46bce720.)
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_turnsadvances the pool lazily on read/spend at1000/86400t/s × the ARIA multiplier, capped at the storedPlayer.max_turns(=1000 + rank.max_turns_bonus, rank-only — ARIA scales the rate, not the cap).Player.last_turn_regenerationandPlayer.max_turnscolumns are defined (migrationa1d4f9c7b3e2);RankingService.refresh_daily_turnsdelegates to the regen helper. Regeneration is wired at every spend site inside theFOR UPDATErow lock (movement, combat, trading dock/undock). Proven live on dev 2026-06-18 (see FINDINGS). SeeSYSTEMS/turn-regeneration.mdfor the full spec.
Allocation¶
- Default max: 1,000 turns.
Player.max_turnsis a canonical launch column per ADR-0004; the value is1000 + rank.max_turns_bonus, recomputed on rank promotion (per ADR-0023). The stored column is written byturn_service.regenerate_turns(recomputed from rank on each regen viaRankingService.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 tocreated_atfor 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
# WO-CG: the summed, capped medal `turn_regen` bonus (from active medals,
# `medal_service.get_active_medal_bonuses`) stacks additively onto the ARIA
# multiplier before the same hard clamp — see turn_service.py:181-183.
combined_multiplier = min(1.5, player.aria_bonus_multiplier + medal_turn_regen_bonus)
effective_rate = base_rate * combined_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.
Medal regen bonus stacks additively, same cap. Active medals with a turn_regen bonus (medal_service.get_active_medal_bonuses, summed and pre-clamped to the blessed +0.05 ceiling) add directly onto the ARIA multiplier before the shared 1.5× hard clamp — turn_service._medal_turn_regen_bonus / turn_service.py:181-183 (and mirrored in the display-only effective_regen_per_hour helper at turn_service.py:96-98). The combined aria + medal multiplier still cannot exceed 1.5×, so medal regen can push a sub-max-tier player faster but never past the ADR-0004 ceiling. It never touches max_turns — the cap stays rank-only per ADR-0023.
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. ✅ Shipped — quantum_service.py SCAN_TURN_COST=5, SCAN_COOLDOWN_HOURS=4, SCAN_RESULT_TTL_MINUTES=10 + spend_turns / Far-band shard spend. |
| Scan adjacent sectors | 2 | ✅ Shipped — player.py::scan_adjacent_sector + movement_service.py's scanner-range scaling ladder (WO-PROG-SECTOR-SCAN-1). Resource/hazard disclosure band magnitudes are [NO-CANON], pending a DECISIONS.md ratification. |
| Tractor Beam tow (per-move surcharge) | +1 / +2 / +3 / +5 by towed ship_size (tiny / small / medium / large) |
Through natural warp tunnels. ✅ Shipped — tow_service.py's TOW_SURCHARGE table via tow_surcharge_for(). |
| Tractor Beam tow through player warp gate | +2 flat | ✅ Shipped — tow_service.py::GATE_TOW_SURCHARGE_FLAT = 2. |
| Tractor Beam tow through Quantum Jump | +5 flat (Warp Jumper hauler only, medium-max towed) | ✅ Shipped — tow_service.py::QJ_TOW_SURCHARGE_FLAT = 5. Only the WJ's own Tractor operates through QJ. |
| Deploy drone squadron | 3 | ✅ Shipped — drone_service.py DRONE_DEPLOY_TURN_COST = 3 + spend_turns on deploy (supersedes the old "currently 0" Design-only mark). |
| 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;StatusBar.tsxhover title renders time-to-full (Turns — {h}h {m}m to full) on origin/feat46bce720. - ✅ Low-turn warning:
lowTurns = turnsNow < 50appliessb-turns-low(StatusBar.lowTurns.test.tsx— class at 49, not at 50). - Out-of-turns players can still defend (GS combat cost table — not a separate HUD banner).
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) |
| Medal regen bonus | services/gameserver/src/services/turn_service.py:_medal_turn_regen_bonus (reads services/medal_service.py:get_active_medal_bonuses) |
| 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.