Step 2.5: add the default game config

Rescales money values so tutorial runs pay meaningful amounts, and moves
the game epoch from .env.example into GameConfig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 17:47:15 -04:00
parent 6804a7120c
commit 2c211524d3
9 changed files with 101 additions and 21 deletions

View File

@ -11,8 +11,3 @@ BETTER_AUTH_URL=http://localhost:3000
# HTTP port the API server listens on
PORT=3000
# Game clock epoch (decision 0.5.9): the real instant the game clock starts,
# and the in-game ephemeris date it maps to. Both ISO 8601, UTC.
GAME_EPOCH_REAL_START=2026-01-01T00:00:00Z
GAME_EPOCH_EPHEMERIS_DATE=2100-01-01T00:00:00Z

View File

@ -63,3 +63,12 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Ids:** cargo `standard`, `heavy`, `high-value`, `dangerous`; skills `long-haul`, `heavy-cargo`, `expensive-cargo`, `dangerous-cargo`.
- **Base pay** (credits per tonne per million km): Standard 10, Heavy 14, High-Value 18, Dangerous 20. **Tier multipliers:** 1, 1.2, 1.25, 1.3. First guesses, to be tuned in Phase 12.
- **Cargo and skills share one data file** (`src/data/cargo.ts`), since every cargo skill exists to gate a cargo type.
## Step 2.5
- **The game epoch lives in GameConfig, not in env vars.** Step 2.1 puts it in GameConfig, and rule 0.1.5 keeps game numbers in `packages/content`. The `GAME_EPOCH_*` variables added to `.env.example` in Step 1.5 are removed; the placeholder values (real start 2026-01-01, in-game 2100-01-01) are unchanged.
- **Money values scaled up.** With the Step 2.2–2.4 rates a tutorial run paid ~24 credits. Pay rates and fuel costs are ×10, and prices are set so a Courier (4,000) plus an Earth bay (1,000) takes ~21 runs at ~240 credits each. Freighter 100,000, Bulk Hauler 400,000; leases Moon 800, Mars 20,000, Ceres 40,000, Jupiter 80,000.
- **Placeholder tutorial payout in the test:** Standard pay × 0.384M km × average mass (midpoint of the contract minimum and Courier capacity, 12.5 t) × tutorial share (0.5).
- **Starting credits are 0.** The company pays fuel during the tutorial, and the ~21-run target already assumes the player starts with nothing.
- **Other defaults:** board size 8, tutorial board 40 (more than the ~30 runs that fit in a 60-minute period), first contact 10% at ×2 pay, contract mass 5–400 t, Long Haul cut-offs 100/250/400/600/800M km, ten pilot levels at 2 skill points each (enough to max all four skills), hire cost 2,000, pilot pay 100 per delivery.
- **Known balance issue for Phase 12:** pay is linear in distance, but every trip pays a fixed overhead, so short Earth–Moon runs earn far less per real hour than long runs (a Freighter on Mars earns roughly 50× a Courier on the Moon).

View File

@ -14,5 +14,5 @@
- [x] **Step 2.2 — The five locations.** `src/data/locations.ts`: Earth, Moon, Mars, Ceres Station, Jupiter Cloud Platform. Earth and Moon start discovered; Earth has the highest launch multiplier.
- [x] **Step 2.3 — The three ship models.** `src/data/ships.ts`: Courier, Freighter, Bulk Hauler, all atmospheric. Speed, capacity and price orderings tested; range rules are checked in Step 3.7.
- [x] **Step 2.4 — Cargo types and skills.** `src/data/cargo.ts`: Standard, Heavy, High-Value, Dangerous; skills Long Haul, Heavy Cargo, Expensive Cargo, Dangerous Cargo (max level 5). The location test now checks cargo ids against the real data.
- [ ] Step 2.5 — Default game config
- [x] **Step 2.5 — Default game config.** `src/data/config.ts`. Courier + Earth bay ≈ 21 tutorial runs with the placeholder average payout. Money values rescaled ×10 so tutorial runs pay meaningful amounts; epoch moved from `.env.example` into GameConfig.
- [ ] Step 2.6 — Single content bundle

View File

@ -5,28 +5,28 @@ export const cargoTypes: CargoType[] = [
{
id: "standard",
name: "Standard",
basePayPerTonnePerMillionKm: 10,
basePayPerTonnePerMillionKm: 100,
tierPayMultiplier: 1,
requiredSkillId: null,
},
{
id: "heavy",
name: "Heavy",
basePayPerTonnePerMillionKm: 14,
basePayPerTonnePerMillionKm: 140,
tierPayMultiplier: 1.2,
requiredSkillId: "heavy-cargo",
},
{
id: "high-value",
name: "High-Value",
basePayPerTonnePerMillionKm: 18,
basePayPerTonnePerMillionKm: 180,
tierPayMultiplier: 1.25,
requiredSkillId: "expensive-cargo",
},
{
id: "dangerous",
name: "Dangerous",
basePayPerTonnePerMillionKm: 20,
basePayPerTonnePerMillionKm: 200,
tierPayMultiplier: 1.3,
requiredSkillId: "dangerous-cargo",
},

View File

@ -0,0 +1,29 @@
import type { GameConfig } from "../schemas";
/** Default tuning. All numbers are first guesses until balancing in Phase 12. */
export const gameConfig: GameConfig = {
// 1 real minute = 1 game hour.
timeCompression: 60,
// Placeholders: the design docs do not fix an in-game start date yet.
epoch: {
realStart: "2026-01-01T00:00:00Z",
ephemerisDate: "2100-01-01T00:00:00Z",
},
startingCredits: 0,
tutorialPayoutShare: 0.5,
contractBoardSize: 8,
// A tutorial run takes about 2 real minutes, so one hour-long period allows ~30.
tutorialBoardSize: 40,
contractBoardPeriodRealMinutes: 60,
firstContactChance: 0.1,
firstContactPayoutMultiplier: 2,
contractMassTonnes: { min: 5, max: 400 },
// Mars at its closest needs no Long Haul; Jupiter at its farthest needs level 5.
longHaulCutoffsMillionKm: [100, 250, 400, 600, 800],
xpPerDelivery: { base: 10, perMillionKm: 0.2, perCargoTier: 5 },
pilotLevelXpThresholds: [100, 250, 450, 700, 1000, 1400, 1900, 2500, 3200, 4000],
// Ten levels × 2 points = enough to max all four skills (4 × 5 levels).
skillPointsPerLevel: 2,
pilotHireCost: 2000,
pilotPayPerDelivery: 100,
};

View File

@ -14,7 +14,7 @@ export const locations: Location[] = [
body: "earth",
launchCostMultiplier: 3,
startsDiscovered: true,
bayLeasePrice: 5000,
bayLeasePrice: 1000,
produces: ["standard", "high-value"],
demands: ["heavy", "dangerous"],
},
@ -26,7 +26,7 @@ export const locations: Location[] = [
body: "moon",
launchCostMultiplier: 0.5,
startsDiscovered: true,
bayLeasePrice: 3000,
bayLeasePrice: 800,
produces: ["standard", "heavy"],
demands: ["standard", "high-value", "dangerous"],
},
@ -38,7 +38,7 @@ export const locations: Location[] = [
body: "mars",
launchCostMultiplier: 1.5,
startsDiscovered: false,
bayLeasePrice: 8000,
bayLeasePrice: 20000,
produces: ["standard", "high-value"],
demands: ["heavy", "dangerous"],
},
@ -50,7 +50,7 @@ export const locations: Location[] = [
body: "ceres",
launchCostMultiplier: 0.3,
startsDiscovered: false,
bayLeasePrice: 10000,
bayLeasePrice: 40000,
produces: ["heavy"],
demands: ["standard", "high-value", "dangerous"],
},
@ -62,7 +62,7 @@ export const locations: Location[] = [
body: "jupiter",
launchCostMultiplier: 2,
startsDiscovered: false,
bayLeasePrice: 15000,
bayLeasePrice: 80000,
produces: ["dangerous"],
demands: ["standard", "high-value", "heavy"],
},

View File

@ -10,7 +10,7 @@ export const shipModels: ShipModel[] = [
id: "courier",
name: "Courier",
archetype: "courier",
price: 30000,
price: 4000,
cruiseSpeedKmPerGameHour: 3_000_000,
cargoCapacityTonnes: 20,
// Moon only.
@ -18,13 +18,13 @@ export const shipModels: ShipModel[] = [
hullRating: 1,
landingClass: "atmospheric",
overheadGameHours: 2,
fuelCostPerMillionKm: 20,
fuelCostPerMillionKm: 200,
},
{
id: "freighter",
name: "Freighter",
archetype: "freighter",
price: 150000,
price: 100000,
cruiseSpeedKmPerGameHour: 2_000_000,
cargoCapacityTonnes: 100,
// Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25).
@ -32,13 +32,13 @@ export const shipModels: ShipModel[] = [
hullRating: 2,
landingClass: "atmospheric",
overheadGameHours: 4,
fuelCostPerMillionKm: 40,
fuelCostPerMillionKm: 400,
},
{
id: "bulk-hauler",
name: "Bulk Hauler",
archetype: "bulk-hauler",
price: 600000,
price: 400000,
cruiseSpeedKmPerGameHour: 1_200_000,
cargoCapacityTonnes: 400,
// Jupiter at its farthest.
@ -46,6 +46,6 @@ export const shipModels: ShipModel[] = [
hullRating: 3,
landingClass: "atmospheric",
overheadGameHours: 8,
fuelCostPerMillionKm: 80,
fuelCostPerMillionKm: 800,
},
];

View File

@ -2,3 +2,4 @@ export * from "./schemas";
export * from "./data/locations";
export * from "./data/ships";
export * from "./data/cargo";
export * from "./data/config";

View File

@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import {
cargoTypes,
gameConfig,
gameConfigSchema,
locations,
shipModels,
skills,
} from "../src/index";
// Placeholder average tutorial payout until Phase 12 uses real sim numbers:
// Standard cargo over ~0.384M km (Earth–Moon), at the average mass a Courier
// can carry from the configured range, times the tutorial share.
const EARTH_MOON_MILLION_KM = 0.384;
describe("game config", () => {
it("passes the schema", () => {
expect(() => gameConfigSchema.parse(gameConfig)).not.toThrow();
});
it("uses 1 real minute = 1 game hour", () => {
expect(gameConfig.timeCompression).toBe(60);
});
it("has one Long Haul cut-off per Long Haul level", () => {
const longHaul = skills.find((s) => s.id === "long-haul")!;
expect(gameConfig.longHaulCutoffsMillionKm).toHaveLength(longHaul.maxLevel);
});
it("makes a Courier plus an Earth bay affordable after 15–25 tutorial runs", () => {
const courier = shipModels.find((s) => s.id === "courier")!;
const earth = locations.find((l) => l.id === "earth")!;
const standard = cargoTypes.find((c) => c.id === "standard")!;
const averageMass = (gameConfig.contractMassTonnes.min + courier.cargoCapacityTonnes) / 2;
const averageTutorialPayout =
standard.basePayPerTonnePerMillionKm *
averageMass *
EARTH_MOON_MILLION_KM *
gameConfig.tutorialPayoutShare;
const runs = (courier.price + earth.bayLeasePrice) / averageTutorialPayout;
expect(runs).toBeGreaterThanOrEqual(15);
expect(runs).toBeLessThanOrEqual(25);
});
});