Step 10L.4: reach table tuning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
60e29f7b1d
commit
776a890757
|
|
@ -1148,3 +1148,12 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **`discoveredIds` is optional on a contract proposal**; absent means every location counts. Tutorial runs and older callers keep working, and the client's eligibility (which doesn't pass it yet) is lenient until 10L.9; the server's check is the one that counts.
|
- **`discoveredIds` is optional on a contract proposal**; absent means every location counts. Tutorial runs and older callers keep working, and the client's eligibility (which doesn't pass it yet) is lenient until 10L.9; the server's check is the one that counts.
|
||||||
- **The rule only looks at arrival plus margin**, as the CR says. A ship may land somewhere it can't leave at once but can within the margin: with today's ranges a Clipper flying Moon→Mars can land on a date when it can't leave Mars right away but can 48 hours later. The exhaustive test checks the margin time, not the arrival.
|
- **The rule only looks at arrival plus margin**, as the CR says. A ship may land somewhere it can't leave at once but can within the margin: with today's ranges a Clipper flying Moon→Mars can land on a date when it can't leave Mars right away but can 48 hours later. The exhaustive test checks the margin time, not the arrival.
|
||||||
- **Tests use a fixture** (a Moon with factor 0.5 and a "moon-station" sharing its orbit) because no real ship and date strands cleanly enough to also show "passes once another location is discovered".
|
- **Tests use a fixture** (a Moon with factor 0.5 and a "moon-station" sharing its orbit) because no real ship and date strands cleanly enough to also show "passes once another location is discovered".
|
||||||
|
|
||||||
|
## Step 10L.4
|
||||||
|
|
||||||
|
- **Ranges come from the measured leg distances over 16 game years** (monthly): Earth–Mars 58.9–399.6, Moon–Mars 58.5–400.0, Earth–Ceres 262.7–565.8, Moon–Ceres 262.8–565.9, Mars–Ceres 175.5–662.8, Ceres–Jupiter 378.9–1,216.8, Earth–Jupiter 592.8–964.8, Moon–Jupiter 592.4–965.1, Mars–Jupiter 526.9–1,029.8 M km.
|
||||||
|
- **Freighter 650 → 450.** It must cover Earth→Mars at its farthest (≥ 400), but never Moon→Jupiter (450 × 1.3 = 585 < 592.4) or Mars→Jupiter (450 × 1.15 = 517.5 < 526.9); from Ceres (585) it reaches Jupiter on some dates. 450 also clears Earth→Ceres on most dates.
|
||||||
|
- **Bulk Hauler 1000 → 1250**, so every leg is always in range, including Jupiter→Ceres at its farthest (1,216.8) with a factor of 1.
|
||||||
|
- **Clipper stays at 198**: Earth→Mars is in range on just over 30 % of dates, and Moon→Ceres (198 × 1.3 = 257.4) stays below Ceres's closest approach (262.8). The margins are small, so the Clipper's range shouldn't move without re-running this table.
|
||||||
|
- **Decision 0.5.25 is superseded** (noted in the plan): the Freighter no longer reaches Jupiter from Earth at all.
|
||||||
|
- **No other stats changed.** The existing upgrade-cap tests (0.5.67) and the Courier-to-Mars test (0.5.68) still pass at these values; 10L.5 adds the new caps.
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
||||||
22. **Pilot endpoints.** The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at.
|
22. **Pilot endpoints.** The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at.
|
||||||
23. **Avatar during tutorial.** The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends.
|
23. **Avatar during tutorial.** The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends.
|
||||||
24. **Repositions need no skills.** A reposition checks range and landing only; no Long Haul or cargo skill is required.
|
24. **Repositions need no skills.** A reposition checks range and landing only; no Long Haul or cargo skill is required.
|
||||||
25. **Freighter range** covers Ceres at its farthest from Earth (≈600M km), so it can also reach Jupiter at its closest. A ship left out of range of every bay waits until distances shrink; this is intended.
|
25. **Freighter range** covers Ceres at its farthest from Earth (≈600M km), so it can also reach Jupiter at its closest. *(Superseded by CR-012, decision 0.5.81: a Freighter reaches Ceres from Earth on most dates, not all, and never reaches Jupiter from Earth; it gets there by hopping via Ceres.)* A ship left out of range of every bay waits until distances shrink; this is intended.
|
||||||
26. **Loaner in player state.** Player state has a `tutorial` object with the loaner's location and whether a run is in flight. The loaner is not in the ships list.
|
26. **Loaner in player state.** Player state has a `tutorial` object with the loaner's location and whether a run is in flight. The loaner is not in the ships list.
|
||||||
27. **Pilot location storage.** Only unassigned pilots store a location; an assigned pilot's location is derived from their ship.
|
27. **Pilot location storage.** Only unassigned pilots store a location; an assigned pilot's location is derived from their ship.
|
||||||
28. **Git hosting.** The repo is on a self-hosted Gitea instance: `http://192.168.1.107:3000/rory/deadhead-space-freight-tycoon`. CI uses Gitea Actions.
|
28. **Git hosting.** The repo is on a self-hosted Gitea instance: `http://192.168.1.107:3000/rory/deadhead-space-freight-tycoon`. CI uses Gitea Actions.
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ Spec: `memory-bank/change-requests/CR-012-lighter-clipper-range.md` (received as
|
||||||
- [x] **Step 10L.1 — Content: range factors and new ships.** `Location.launchRangeFactor` (Earth 1, Moon 1.3, Mars 1.15, Ceres 1.3, Jupiter 1). `ShipModel.role` (all six) and optional `ShipModel.handlingGameHoursPerTonne` (the sim's effective stats now start from it). New models: **Lighter** (42,400 cr, 1.6M km/h, 80 t, range 10, overhead 2 h, handling 0.012 h/t, fuel 350/M km) and **Clipper** (61,600 cr, 6.5M km/h, 30 t, range 198, overhead 2 h, fuel 600/M km); archetypes `lighter`, `clipper`. Config `noStrandMarginGameHours` 48. Manifest lists `ship-lighter` / `ship-clipper` as pending art. Tests: the CR's orderings replace Step 2.3's (`ships.test.ts`), range factors (`locations.test.ts`), Lighter handling (`effective-stats.test.ts`).
|
- [x] **Step 10L.1 — Content: range factors and new ships.** `Location.launchRangeFactor` (Earth 1, Moon 1.3, Mars 1.15, Ceres 1.3, Jupiter 1). `ShipModel.role` (all six) and optional `ShipModel.handlingGameHoursPerTonne` (the sim's effective stats now start from it). New models: **Lighter** (42,400 cr, 1.6M km/h, 80 t, range 10, overhead 2 h, handling 0.012 h/t, fuel 350/M km) and **Clipper** (61,600 cr, 6.5M km/h, 30 t, range 198, overhead 2 h, fuel 600/M km); archetypes `lighter`, `clipper`. Config `noStrandMarginGameHours` 48. Manifest lists `ship-lighter` / `ship-clipper` as pending art. Tests: the CR's orderings replace Step 2.3's (`ships.test.ts`), range factors (`locations.test.ts`), Lighter handling (`effective-stats.test.ts`).
|
||||||
- [x] **Step 10L.2 — Sim: effective range from origin.** `rangeFrom(ship, from)` = range × origin's `launchRangeFactor`; `checkRoute` compares the leg against it (dispatch, reposition, board eligibility and the map all go through it). The dispatch sheet quotes range from the origin. Tests (`range-from.test.ts`): 100 → 130 from the Moon and 115 from Mars; a leg between 1× and 1.3× fails from Earth and passes from the Moon; with every factor 1 the result matches the old check over 16 years of samples.
|
- [x] **Step 10L.2 — Sim: effective range from origin.** `rangeFrom(ship, from)` = range × origin's `launchRangeFactor`; `checkRoute` compares the leg against it (dispatch, reposition, board eligibility and the map all go through it). The dispatch sheet quotes range from the origin. Tests (`range-from.test.ts`): 100 → 130 from the Moon and 115 from Mars; a leg between 1× and 1.3× fails from Earth and passes from the Moon; with every factor 1 the result matches the old check over 16 years of samples.
|
||||||
- [x] **Step 10L.3 — Sim: no-strand rule.** `wouldStrand(stats, origin, destination, mass, departure, discoveredIds, content)`: at arrival plus `noStrandMarginGameHours` (48), can the ship reach any other discovered location from the destination with its range from there? `validateDispatch` (new optional `discoveredIds`; absent = all) and `validateReposition` fail with `would-strand` after range and landing. The server passes the player's discovered ids for contract dispatches; server and web reason maps gained the code (full wording in 10L.6/10L.9). Tests (`no-strand.test.ts`): a fixture heavy-Moon hop is refused, then passes once a reachable location is discovered; the same for a contract; across all six models, every ordered pair and monthly samples over 16 years, an allowed trip always leaves somewhere to fly at arrival plus margin.
|
- [x] **Step 10L.3 — Sim: no-strand rule.** `wouldStrand(stats, origin, destination, mass, departure, discoveredIds, content)`: at arrival plus `noStrandMarginGameHours` (48), can the ship reach any other discovered location from the destination with its range from there? `validateDispatch` (new optional `discoveredIds`; absent = all) and `validateReposition` fail with `would-strand` after range and landing. The server passes the player's discovered ids for contract dispatches; server and web reason maps gained the code (full wording in 10L.6/10L.9). Tests (`no-strand.test.ts`): a fixture heavy-Moon hop is refused, then passes once a reachable location is discovered; the same for a contract; across all six models, every ordered pair and monthly samples over 16 years, an allowed trip always leaves somewhere to fly at arrival plus margin.
|
||||||
- [ ] Step 10L.4 — Content tuning: reach table
|
- [x] **Step 10L.4 — Content tuning: reach table.** Ranges: Courier 5, Lighter 10, Clipper 198 (unchanged from 10L.1), **Freighter 650 → 450**, **Bulk Hauler 1000 → 1250**. Step 3.7's range tests (`eligibility.test.ts`) now name their origin; the Freighter-to-Ceres-at-farthest and Freighter-to-Jupiter-at-closest tests become "reaches Ceres at its closest, not its farthest" and "never reaches Jupiter from Earth". Tests (`reach-table.test.ts`): one per §3 cell over monthly samples for 16 game years, plus the two hop tests.
|
||||||
- [ ] Step 10L.5 — Upgrade caps for the new lineup
|
- [ ] Step 10L.5 — Upgrade caps for the new lineup
|
||||||
- [ ] Step 10L.6 — Schema and server
|
- [ ] Step 10L.6 — Schema and server
|
||||||
- [ ] Step 10L.7 — Client: Market shows roles and reach
|
- [ ] Step 10L.7 — Client: Market shows roles and reach
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export const shipModels: ShipModel[] = [
|
||||||
cruiseSpeedKmPerGameHour: 2_000_000,
|
cruiseSpeedKmPerGameHour: 2_000_000,
|
||||||
cargoCapacityTonnes: 100,
|
cargoCapacityTonnes: 100,
|
||||||
// Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25).
|
// Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25).
|
||||||
fuelRangeMillionKm: 650,
|
fuelRangeMillionKm: 450,
|
||||||
hullRating: 2,
|
hullRating: 2,
|
||||||
landingClass: "atmospheric",
|
landingClass: "atmospheric",
|
||||||
overheadGameHours: 4,
|
overheadGameHours: 4,
|
||||||
|
|
@ -89,7 +89,7 @@ export const shipModels: ShipModel[] = [
|
||||||
cruiseSpeedKmPerGameHour: 1_200_000,
|
cruiseSpeedKmPerGameHour: 1_200_000,
|
||||||
cargoCapacityTonnes: 400,
|
cargoCapacityTonnes: 400,
|
||||||
// Jupiter at its farthest.
|
// Jupiter at its farthest.
|
||||||
fuelRangeMillionKm: 1000,
|
fuelRangeMillionKm: 1250,
|
||||||
hullRating: 3,
|
hullRating: 3,
|
||||||
landingClass: "atmospheric",
|
landingClass: "atmospheric",
|
||||||
overheadGameHours: 8,
|
overheadGameHours: 8,
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,14 @@ function extremeTime(to: typeof mars, pick: "closest" | "farthest"): number {
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Origins are named in every test (decision 0.5.77: range is per leg, from its origin).
|
||||||
describe("route eligibility", () => {
|
describe("route eligibility", () => {
|
||||||
it("lets a Courier fly Earth→Moon and Moon→Earth", () => {
|
it("lets a Courier fly Earth→Moon and Moon→Earth", () => {
|
||||||
expect(checkRoute(courier, earth, moon, start)).toEqual({ ok: true });
|
expect(checkRoute(courier, earth, moon, start)).toEqual({ ok: true });
|
||||||
expect(checkRoute(courier, moon, earth, start)).toEqual({ ok: true });
|
expect(checkRoute(courier, moon, earth, start)).toEqual({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps a Courier out of range of Mars, Ceres and Jupiter even at their closest", () => {
|
it("keeps a Courier from Earth out of range of Mars, Ceres and Jupiter even at their closest", () => {
|
||||||
for (const to of [mars, ceres, jupiter]) {
|
for (const to of [mars, ceres, jupiter]) {
|
||||||
expect(checkRoute(courier, earth, to, extremeTime(to, "closest"))).toEqual({
|
expect(checkRoute(courier, earth, to, extremeTime(to, "closest"))).toEqual({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -55,22 +56,28 @@ describe("route eligibility", () => {
|
||||||
expect(checkRoute(vacuumShip, earth, moon, start)).toEqual({ ok: true });
|
expect(checkRoute(vacuumShip, earth, moon, start)).toEqual({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets a Freighter reach Mars and Ceres even at their farthest", () => {
|
it("lets a Freighter from Earth reach Mars even at its farthest", () => {
|
||||||
for (const to of [mars, ceres]) {
|
expect(checkRoute(freighter, earth, mars, extremeTime(mars, "farthest"))).toEqual({ ok: true });
|
||||||
expect(checkRoute(freighter, earth, to, extremeTime(to, "farthest"))).toEqual({ ok: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets only the Bulk Hauler reach Jupiter at its farthest", () => {
|
it("lets a Freighter from Earth reach Ceres at its closest but not its farthest (CR-012)", () => {
|
||||||
|
expect(checkRoute(freighter, earth, ceres, extremeTime(ceres, "closest"))).toEqual({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
expect(checkRoute(freighter, earth, ceres, extremeTime(ceres, "farthest")).ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets only the Bulk Hauler reach Jupiter from Earth at its farthest", () => {
|
||||||
const t = extremeTime(jupiter, "farthest");
|
const t = extremeTime(jupiter, "farthest");
|
||||||
expect(checkRoute(bulkHauler, earth, jupiter, t)).toEqual({ ok: true });
|
expect(checkRoute(bulkHauler, earth, jupiter, t)).toEqual({ ok: true });
|
||||||
expect(checkRoute(freighter, earth, jupiter, t).ok).toBe(false);
|
expect(checkRoute(freighter, earth, jupiter, t).ok).toBe(false);
|
||||||
expect(checkRoute(courier, earth, jupiter, t).ok).toBe(false);
|
expect(checkRoute(courier, earth, jupiter, t).ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets a Freighter reach Jupiter at its closest (decision 0.5.25)", () => {
|
it("keeps a Freighter from Earth out of Jupiter's range even at its closest (CR-012 amends 0.5.25)", () => {
|
||||||
expect(checkRoute(freighter, earth, jupiter, extremeTime(jupiter, "closest"))).toEqual({
|
expect(checkRoute(freighter, earth, jupiter, extremeTime(jupiter, "closest"))).toEqual({
|
||||||
ok: true,
|
ok: false,
|
||||||
|
reason: "out-of-range",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { content } from "@deadhead/content";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { checkRoute, realToGame } from "../src/index";
|
||||||
|
|
||||||
|
/** CR-012 §3: reach targets for base ships, monthly over 16 game years (decision 0.5.81). */
|
||||||
|
const start = realToGame(Date.parse(content.config.epoch.realStart), content.config);
|
||||||
|
const MONTH = 30.44 * 24 * 3_600_000;
|
||||||
|
const samples = Array.from({ length: 16 * 12 + 1 }, (_, i) => start + i * MONTH);
|
||||||
|
const ids = content.locations.map((l) => l.id);
|
||||||
|
|
||||||
|
/** Share of sampled dates on which the leg is in range for the base ship. */
|
||||||
|
function share(modelId: string, fromId: string, toId: string): number {
|
||||||
|
const ship = content.shipModel(modelId)!;
|
||||||
|
const from = content.location(fromId)!;
|
||||||
|
const to = content.location(toId)!;
|
||||||
|
return samples.filter((t) => checkRoute(ship, from, to, t).ok).length / samples.length;
|
||||||
|
}
|
||||||
|
const always = (m: string, from: string, to: string) => expect(share(m, from, to)).toBe(1);
|
||||||
|
const never = (m: string, from: string, to: string) => expect(share(m, from, to)).toBe(0);
|
||||||
|
const some = (m: string, from: string, to: string) => {
|
||||||
|
const s = share(m, from, to);
|
||||||
|
expect(s).toBeGreaterThan(0);
|
||||||
|
expect(s).toBeLessThan(1);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("reach table (Step 10L.4)", () => {
|
||||||
|
for (const model of ["courier", "lighter"]) {
|
||||||
|
describe(model, () => {
|
||||||
|
it("flies Earth↔Moon always", () => {
|
||||||
|
always(model, "earth", "moon");
|
||||||
|
always(model, "moon", "earth");
|
||||||
|
});
|
||||||
|
it("never reaches Mars from Earth", () => never(model, "earth", "mars"));
|
||||||
|
it("never reaches Mars from the Moon", () => never(model, "moon", "mars"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("clipper", () => {
|
||||||
|
it("reaches Mars from Earth on some dates, at least 30 %", () => {
|
||||||
|
expect(some("clipper", "earth", "mars")).toBeGreaterThanOrEqual(0.3);
|
||||||
|
});
|
||||||
|
it("reaches Mars from the Moon on some dates, more than from Earth", () => {
|
||||||
|
expect(some("clipper", "moon", "mars")).toBeGreaterThan(share("clipper", "earth", "mars"));
|
||||||
|
});
|
||||||
|
it("reaches Ceres from Mars on some dates", () => some("clipper", "mars", "ceres-station"));
|
||||||
|
it("never reaches Ceres from Earth", () => never("clipper", "earth", "ceres-station"));
|
||||||
|
it("never reaches Ceres from the Moon", () => never("clipper", "moon", "ceres-station"));
|
||||||
|
it("never reaches Jupiter from anywhere", () => {
|
||||||
|
for (const from of ids)
|
||||||
|
if (from !== "jupiter-cloud-platform") {
|
||||||
|
never("clipper", from, "jupiter-cloud-platform");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("freighter", () => {
|
||||||
|
it("reaches Mars from Earth always", () => always("freighter", "earth", "mars"));
|
||||||
|
it("reaches Ceres from Earth on most dates", () => {
|
||||||
|
expect(some("freighter", "earth", "ceres-station")).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
it("reaches Jupiter from Ceres on some dates", () =>
|
||||||
|
some("freighter", "ceres-station", "jupiter-cloud-platform"));
|
||||||
|
it("never reaches Jupiter from Earth", () =>
|
||||||
|
never("freighter", "earth", "jupiter-cloud-platform"));
|
||||||
|
it("never reaches Jupiter from the Moon", () =>
|
||||||
|
never("freighter", "moon", "jupiter-cloud-platform"));
|
||||||
|
it("never reaches Jupiter from Mars", () =>
|
||||||
|
never("freighter", "mars", "jupiter-cloud-platform"));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bulk-hauler", () => {
|
||||||
|
it("reaches Jupiter from Earth always", () =>
|
||||||
|
always("bulk-hauler", "earth", "jupiter-cloud-platform"));
|
||||||
|
it("flies every other leg between the five locations always", () => {
|
||||||
|
for (const from of ids)
|
||||||
|
for (const to of ids) if (from !== to) always("bulk-hauler", from, to);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hops", () => {
|
||||||
|
it("a Clipper can't reach Ceres from Earth on any date but can from Mars on some", () => {
|
||||||
|
never("clipper", "earth", "ceres-station");
|
||||||
|
some("clipper", "mars", "ceres-station");
|
||||||
|
});
|
||||||
|
it("a Freighter can reach Jupiter from Ceres on some dates but never directly from Earth", () => {
|
||||||
|
some("freighter", "ceres-station", "jupiter-cloud-platform");
|
||||||
|
never("freighter", "earth", "jupiter-cloud-platform");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue