Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .ai/docs/balance-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -4210,3 +4210,68 @@ Building curves did not change in this round. This refresh supersedes the old 0.

Daily metal at levels 1, 5, 10, 15: 2,160, 5,232, 15,912, 48,480.
Starting stock: 500 metal, 300 crystal.

## Round 37 — larger fleets on fewer worlds (2026-09-26, 0.29.3)

Follow-up to the merged 0.29.2 round. Davide's play feedback:

> "The change feels way, way better. The difficult planets give you a lot of resources, but it also seems like my ships can hold way more resources, and it feels unbalanced."
>
> "The idea of increasing the resources that a planet can give you was not mostly to gather more resources, but to be able to open the game and send ships on a few planets."
>
> "I open the game, and I'm still able to deploy ships to 20 to 30 planets, and I don't like the process. I want the users to be able to manage fewer planets."
>
> "I would imagine any planet to be refilled within a week, maybe, no matter how many resources it has in total."

He clarified the mid-game target as "2 or 3 planets" and approved option A: keep larger reserves,
remove the adaptation multiplier from extraction, refill within seven days, and measure fleet
concentration without imposing a mission limit. Targets per check-in: one planet early, two or
three mid-game, at most about twelve late-game.

Round 36 multiplied both capacity and extraction. Their ratio stayed fixed, so a small fleet could
empty a difficult world as quickly as an easy world. This round retains
`1 + (T² + G² + A²) / 12` on capacity only. Richness, danger and Prospecting keep their existing
extraction effects. A Gravitic-12 world at richness 1 and danger 0 still holds 75,400 metal, but one
basic skiff extracts 60 metal per station-hour rather than 780. Four basic skiffs take 18,850
station-minutes to empty it instead of 1,450. Dispatch already suggests enough ships to empty the
deposit, or the available fleet when it cannot.

Refill changes from 5% of capacity per day (twenty days to full) to capacity divided by seven days,
linear and rounded once in fine units. It reaches full at seven days even when capacity is not
divisible by seven. Partial deposits keep their stored amounts and refill against the same cap;
flights already dispatched keep their recorded cargo and deadlines. Gates and probe timings stay
as in round 36. No fleet-size, mission-count or income target is enforced by a hidden cap.

The expected feel is that a few worthwhile planets occupy the fleet and remain useful on later
check-ins. The destination targets are acceptance goals for play, not guaranteed counts for every
fleet size, research mix and surveyed map.

### Measured concentration

From `./gradlew :sim:run --quiet`, 2026-09-26, successful exit. Three fixed galaxy seeds,
four check-ins per day, fourteen days. Both policies use the same cheapest-first progression,
probing and purchasing. The control sends one skiff per dispatch; the grouped policy uses the
dispatch sheet's real rule: the fewest available hulls that empty the chosen planet, or every
available hull when none can. Counts include check-ins with no dispatch.

| Policy | Bot age | planets/check-in median · max | dispatches median · max | metal returned | crystal returned |
|---|---|---|---|---|---|
| one hull per dispatch | days 1–3 | 0 · 1 | 0 · 1 | 3,089 | 0 |
| one hull per dispatch | days 4–7 | 1 · 3 | 4 · 30 | 88,958 | 38,063 |
| one hull per dispatch | days 8–14 | 10 · 58 | 155 · 510 | 5,772,638 | 1,970,882 |
| fewest hulls that empty one planet | days 1–3 | 0 · 1 | 0 · 1 | 3,089 | 0 |
| fewest hulls that empty one planet | days 4–7 | 1 · 3 | 1 · 3 | 89,463 | 37,665 |
| fewest hulls that empty one planet | days 8–14 | 10 · 54 | 10 · 54 | 5,225,929 | 2,088,535 |

Option A meets the typical target in this run: no more than one destination early, one at the
middle band, and ten in the last band. It also removes the hundreds of repeated one-hull dispatches.
It does not guarantee the target at every check-in: the last band's maximum is 54 destinations.
The existing flat hull price lets this bot own about 400 skiffs by day 14, so the worst row is also
evidence that deposit tuning alone cannot bound a runaway fleet. This round deliberately adds no
mission cap or hull-price change; play should decide whether the typical improvement is enough.

Verification: full `./gradlew build --quiet`, unfiltered `./gradlew verifyRoborazziDesktop --quiet`
and `./gradlew :sim:run --quiet` passed. Twenty-nine changed tuning and changelog frames were reviewed
before recording, including the 393dp mixed-fleet clamp. The five-pass local coverage job passed
against merged `main`: all ten gated line and branch values held or improved; unit branch moved from
89.13% to 89.16% and behaviour branch from 74.61% to 74.62%.
16 changes: 16 additions & 0 deletions .ai/docs/deposit-sheet.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Deposit decision sheet — 0.9

## Current ruling — fleet concentration (2026-09-26)

After playing 0.29.2, Davide clarified the goal: about one harvesting destination per check-in
early, two or three mid-game, and at most a dozen late-game. More difficult planets should sustain
larger fleets or repeat visits, rather than multiplying each ship's haul. He approved option A:
keep the adaptation-scaled deposit capacities, remove adaptation from extraction speed, and refill
an empty deposit linearly in seven days regardless of capacity. There is no new mission limit.

This supersedes the five-percent daily refill and the claim that all worlds must take the same
time to empty. Richness and danger still scale both capacity and extraction; adaptation scales
only capacity. Prospecting still improves extraction. Saved flights keep their recorded cargo and
return times. The earlier decisions below remain as history; balance-log.md records measurements.

The destination counts are play targets, not enforced caps. Simulation must count distinct
destinations per check-in, repeated visits and income; dispatch count alone is not that measure.

Written by the build, 2026-08-13, on Davide's report after playing 0.8.0:

> *"I noticed planets never exhaust resources, and that seems like OP, and makes the game boring, as
Expand Down
13 changes: 13 additions & 0 deletions .ai/docs/harvesting-balance-proposal.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Adaptation harvesting: verification and proposed balance

## Approved follow-up after 0.29.2

Davide's play feedback, 2026-09-26: difficult planets now feel worthwhile, but ships bring back too
much and managing 20–30 destinations still takes too many actions. His targets are one destination
early, two or three mid-game, and at most a dozen late-game. Every emptied planet should refill
within a week regardless of its capacity. He approved option A: larger reserves, the previous
per-ship extraction curve, and a seven-day refill, without a new simultaneous-mission limit.

The first round below multiplied both capacity and extraction, preserving depletion time. That
increased income without improving how many ships a world can occupy. The follow-up separates
those two curves and measures distinct destinations as well as fleet income. The destination
targets remain hypotheses to test across seeds and progression, not results guaranteed by tuning.

2026-09-26. Davide approved the numerical proposal with "lets try". The gating correction and
the tuning below are implemented on the branch based on 0.29.1. Measured results and remaining
play questions are recorded in balance-log.md, round 36.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ Edit `art/icon/*.svg`, rerun, commit the result — never hand-edit generated PN

## Changelog

### 0.29.3 — 2026-09-26

- **Rich planets support larger fleets.** Difficult worlds keep their deeper deposits, while each
ship harvests at its previous rate. Send more ships together or return to the same world.
- **Deposits refill within a week.** An empty deposit refills over seven days, whatever its size.
Cargo and return times of ships already in flight stay unchanged.

### 0.29.2 — 2026-09-26

- **Difficult worlds pay for the research.** Planets that need more adaptation now hold far more
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ package dev.fardavide.oltre.client.changelog.presentation
object EnglishChangelog : ChangelogText {

override val releases: List<Release> = listOf(
release(
"0.29.3", "2026-09-26", "Larger fleets on richer worlds",
"Deep deposits stay. Ships harvest at their previous rate: send more together or return.",
"Every empty deposit refills in seven days, whatever its size.",
"Ships already in flight keep their cargo and return times.",
),
release(
"0.29.2", "2026-09-26", "Difficult worlds pay more",
"More adaptation means deeper deposits and faster harvesting. Gravitic 12 alone pays 13×.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ package dev.fardavide.oltre.client.changelog.presentation
object ItalianChangelog : ChangelogText {

override val releases: List<Release> = listOf(
release(
"0.29.3", "2026-09-26", "Flotte più grandi sui mondi ricchi",
"Depositi ancora ricchi, raccolta al ritmo precedente: manda più navi insieme o ritorna.",
"Ogni deposito vuoto torna pieno in sette giorni, qualunque sia la sua capienza.",
"Le navi già in volo mantengono carico e orario di ritorno.",
),
release(
"0.29.2", "2026-09-26", "I mondi difficili rendono di più",
"Più adattamento: più risorse, raccolta più rapida. Il solo Gravitico 12 rende 13×.",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ class DispatchUiStateTest {
val state = withFleet(haulers = 1, skiffs = 2)
val whole = state.galaxy.remaining(target, ResourceKind.METAL, EPOCH)
val worked = state.copy(
galaxy = state.galaxy.withTaken(target, ResourceKind.METAL, whole - whole / 12, EPOCH),
galaxy = state.galaxy.withTaken(target, ResourceKind.METAL, whole - 500, EPOCH),
)

val offer = assertIs<DispatchUiState.Offer>(
Expand Down Expand Up @@ -687,8 +687,7 @@ class DispatchUiStateTest {

@Test
fun `the working leg is the clamp and is absent when nothing clamps`() {
// The invariant made visible with no copy at all — `working` reads the same everywhere on the
// map, because the vein and the rate carry one multiplier.
// Larger adaptation deposits need more working time at the same fleet extraction rate.
//
// **Its presence is the clamp**, which is Design's own rule for it and was not what this
// test asserted: it asked for the leg on an *unclamped* run, and got it because the leg used
Expand All @@ -697,7 +696,7 @@ class DispatchUiStateTest {
val target = runnable()
val unclamped = assertIs<DispatchUiState.Offer>(dispatchAt(target))
val clamped = assertIs<DispatchUiState.Offer>(
dispatchAt(target, state = withSkiffs(8), selection = selection(target).copy(ships = 8, window = 24.hours)),
dispatchAt(target, state = withSkiffs(40), selection = selection(target).copy(ships = 40, window = 24.hours)),
)

assertTrue(!English.resolve(unclamped.legs).contains("· working "), English.resolve(unclamped.legs))
Expand All @@ -712,7 +711,7 @@ class DispatchUiStateTest {
// the count blank would be asserting about the suggestion instead of about the clamp.
val target = runnable()
val offer = assertIs<DispatchUiState.Offer>(
dispatchAt(target, state = withSkiffs(8), selection = selection(target).copy(ships = 8, window = 24.hours)),
dispatchAt(target, state = withSkiffs(40), selection = selection(target).copy(ships = 40, window = 24.hours)),
)

assertEquals("the whole deposit", English.resolve(checkNotNull(offer.vein)))
Expand All @@ -724,7 +723,7 @@ class DispatchUiStateTest {
fun `a clamped run names the hulls that bring nothing`() {
val target = runnable()
val offer = assertIs<DispatchUiState.Offer>(
dispatchAt(target, state = withSkiffs(8), selection = selection(target).copy(ships = 8, window = 24.hours)),
dispatchAt(target, state = withSkiffs(40), selection = selection(target).copy(ships = 40, window = 24.hours)),
)

val note = assertNotNull(offer.clampNote)
Expand Down Expand Up @@ -797,15 +796,15 @@ class DispatchUiStateTest {
val target = runnable()
val cap = state.galaxy.depositCap(target, ResourceKind.METAL)!!
val stripped = state.copy(
ships = Ships.of(ShipType.SKIFF, 8),
ships = Ships.of(ShipType.SKIFF, 40),
galaxy = state.galaxy.withTaken(target, ResourceKind.METAL, cap, at = EPOCH),
)

val big = assertIs<DispatchUiState.Waiting>(
dispatchAt(
target,
state = stripped,
selection = selection(target).copy(gathering = ResourceKind.METAL, ships = 8, window = 24.hours),
selection = selection(target).copy(gathering = ResourceKind.METAL, ships = 40, window = 24.hours),
),
)
val small = assertIs<DispatchUiState.Waiting>(
Expand All @@ -827,10 +826,10 @@ class DispatchUiStateTest {
// suffix wrong. Two hulls at a world one can empty is the smallest state that reaches it.
val target = runnable()
val cap = state.galaxy.depositCap(target, ResourceKind.METAL)!!
// A world holding just over what one hull lifts on this window, so the second is the spare.
// Five hundred metal fits in one hull on this window, so the second is the spare.
val nearlyEmptied = state.copy(
ships = Ships.of(ShipType.SKIFF, 2),
galaxy = state.galaxy.withTaken(target, ResourceKind.METAL, cap - cap / 8, at = EPOCH),
galaxy = state.galaxy.withTaken(target, ResourceKind.METAL, cap - 500, at = EPOCH),
)

val offer = assertIs<DispatchUiState.Offer>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ internal val dispatchPickerClampedFrame: SkyFrame = TWO_HULL_STATE.let { state -
target = RUNNABLE,
gathering = ResourceKind.METAL,
// All but a sliver, so a single hauler's hold is more than the ground can supply.
taken = whole - whole / 12,
taken = whole - 500,
at = FIXTURE_NOW,
)
sheet(state = state.copy(galaxy = worked), gathering = ResourceKind.METAL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,7 @@ class DispatchSheetBehaviourTest {

@Test
fun `the working leg is the clamp, so it is absent when nothing clamps`() {
// The invariant made visible with no copy at all: because the vein and the rate carry one
// multiplier, this segment reads the same on the doorstep as in the next galaxy.
// Larger adaptation deposits need more working time at the same fleet extraction rate.
//
// **Its presence *is* the clamp** — Design: *"the working leg keeps its 0.9 rule… its
// absence needs no words."* On a run nothing stops, the fleet works the whole station and the
Expand Down Expand Up @@ -659,13 +658,13 @@ class DispatchSheetBehaviourTest {
@Test
fun `the sheet opens on the fleet that empties the vein rather than on every hull you own`() {
// **A suggestion rather than a cap**, which is what the pool line beside the label is for:
// the number opens where the arithmetic is and the `+` still reaches all 55.
// the number opens where the arithmetic is and the `+` still reaches all 550.
galaxyScreen(state = bigFleetState) {
raiseTheSheetOn(RUNNABLE)

assertTheSheetReads(English.resolve(suggestionFor().ships))
assertTheSheetReads("of 55 idle")
assertTheSheetDoesNotRead("55 skiffs")
assertTheSheetReads("of 550 idle")
assertTheSheetDoesNotRead("550 skiffs")
}
}

Expand Down Expand Up @@ -738,8 +737,8 @@ class DispatchSheetBehaviourTest {
private companion object {

// Big enough that the vein is what stops the run, which is the state the default was wrong
// in: 55 idle hulls at a world three of them can empty.
val bigFleetState: GameState = testGameState.adaptedTo(RUNNABLE).copy(ships = Ships.of(ShipType.SKIFF, 55))
// in: an idle pool larger than the fleet needed to empty the adaptation deposit.
val bigFleetState: GameState = testGameState.adaptedTo(RUNNABLE).copy(ships = Ships.of(ShipType.SKIFF, 550))

// What the mapper would suggest, read off the mapper rather than written down — the number
// is `FleetBalance.hullsToLift` to the hull, and a figure typed here would be this test
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading