12 KiB
12 KiB
Progress
Phase 1 — Repository Setup
- Step 1.1 — Create the monorepo. pnpm workspace with
apps/web,apps/server,packages/sim,packages/schema,packages/content(all@deadhead/*), stricttsconfig.base.jsonextended by each package.pnpm install+pnpm typecheckpass. Repo moved toC:\Projects\Deadhead Space Freight Tycoon(G: is exFAT, no symlinks). - Step 1.2 — Linting and formatting. ESLint 10 + typescript-eslint + Prettier at the root (
pnpm lint,pnpm format). Purity rule bansMath.random,Date.nowand argument-lessnew Date()inpackages/simandpackages/content; verified it fails on a plantedMath.random(). - Step 1.3 — Test runner. Vitest 5 with one project per workspace package (
pnpm test). Trivial smoke test inpackages/sim/test/smoke.test.ts; 1 passing. - Step 1.4 — Continuous integration. Repo pushed to Gitea;
.gitea/workflows/ci.ymlruns install, typecheck, lint and test on every push (runnerhomelab-runner). Run #1 failed because Prettier scanned the runner's in-workspace.pnpm-store/; ignored it, and run #2 is green. - Step 1.5 — Local Postgres.
docker compose up -dstarts Postgres 18 withdeadhead(dev) anddeadhead_testdatabases; connected to both with psql..env.examplelists the server's env vars.
Phase 2 — Content Package
- Step 2.1 — Content schemas. Zod 4 schemas in
packages/content/src/schemas.tsfor Location, ShipModel, CargoType, Skill and GameConfig, with inferred types. Tests check that each schema accepts a valid example and rejects it with any one field missing. - 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. - 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. - 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.
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.exampleinto GameConfig. - Step 2.6 — Single content bundle.
contentinsrc/bundle.tsholds all data pluslocation,shipModel,cargoTypeandskilllookups that returnundefinedfor unknown ids.
Phase 3 — Simulation Package
- Step 3.1 — Game clock conversion.
src/clock.ts:realToGame/gameToRealusing the config epoch and compression; times are Unix-ms numbers. - Step 3.2 — Seeded RNG.
src/rng.ts:createRng(seed)(mulberry32) withnext,int,pick;deriveSeed(parent, label)for child seeds. - Step 3.3 — Location positions.
src/positions.ts: heliocentric ecliptic positions (million km) fromastronomy-enginefor Earth, Mars and Jupiter; Moon = Earth + fixed offset; Ceres on a circular orbit. - Step 3.4 — Route distance.
src/distance.ts:routeDistance(from, to, gameTime), straight-line million km. - Step 3.5 — Travel time.
src/travel.ts:travelTimeGameHours(ship, distance)= cruise time + overhead. - Step 3.6 — Fuel cost.
src/fuel.ts:fuelCost(ship, origin, distance)= distance × cost per million km × origin launch multiplier. - Step 3.7 — Range and landing eligibility.
src/eligibility.ts:checkRoutereturnsokorcannot-land/out-of-range;canLandAtranks vacuum < atmospheric < hardened. Step 2.3 ranges confirmed against sampled real distances. - Step 3.8 — Contract model and payout.
src/contract.ts:Contractinterface andcontractPayout(base × multiplier^tier × mass × distance × first-contact multiplier, rounded). - Step 3.9 — Contract board generation.
src/board.ts:generateContractBoard(seeded per world, bay and period), plusboardPeriodIndex,requiredLongHaulLeveland theSimContentinput type. First contact ≈10% over 1,000 boards. - Step 3.10 — Tutorial contract board.
src/tutorial.ts:generateTutorialBoard(Earth↔Moon from the loaner's location, Standard cargo capped at Courier capacity, payout × tutorial share);loanerModel.SimContentgainedshipModels. - Step 3.11 — Pilot eligibility.
src/pilot.ts:checkPilotEligibility(skills, contract)returnsokorcargo-skill-too-low/long-haul-too-low. - Step 3.12 — Dispatch validation.
src/dispatch.ts:ShipState,PilotState,validateDispatch(first failing reason, plan order),tripFuelCost(rounded). One test per failure reason plus tutorial cases. - Step 3.13 — Dispatch record creation.
createDispatchRecordinsrc/dispatch.ts:DispatchRecordwith arrival from travel time, fuel cost, departure distance, atutorialflag and a per-dispatch seed. - Step 3.14 — Reposition dispatch.
validateRepositionandcreateRepositionRecordinsrc/dispatch.ts: empty flight to a player bay, range/landing/fuel only, zero payout. Zero XP is covered by resolution (Step 3.17). - Step 3.15 — In-flight position and progress.
src/flight.ts:dispatchProgress(clamped 0–1) anddispatchPosition(straight line from origin at departure to destination at arrival). - Step 3.16 — XP and leveling.
src/xp.ts:deliveryXp,pilotLevel,skillPointsAvailable. - Step 3.17 — Dispatch resolution.
src/resolution.ts:resolveDispatchreturnsin-flightor aResolution(credits after pilot pay, XP, new ship location, discovery). - Step 3.18 — Public API and coverage.
src/index.tsexports an explicit list (content lookups and contract building stay internal).pnpm coverage(V8,packages/sim/src, 90% line threshold) reports 100% lines; CI now runspnpm coverageinstead ofpnpm test.
Phase 4 — Shared Schema Package
- Step 4.1 — API data shapes.
packages/schema/src: entity schemas (entities.ts), error format and codes (errors.ts), request/response bodies (requests.ts). Error codes include every sim validation reason; sim now exportsDISPATCH_FAILURES/REPOSITION_FAILURES.architecture.mdstarted with the API data model. - Step 4.2 — API route catalog.
packages/schema/src/routes.ts: 13 typed endpoints under/apiwith request/response schemas and anauthflag, plusRouteRequest/RouteResponsehelper types. Route table added toarchitecture.md.
Phase 5 — Server Foundation
- Step 5.1 — Fastify skeleton.
apps/server: Zod-validated env config (src/config.ts),buildAppwith pino JSON logging andGET /api/health(src/app.ts), entry pointsrc/main.ts(loads repo-root.envif present).pnpm --filter @deadhead/server dev|startrun it with tsx. Integration test starts the app on an ephemeral port and calls health over HTTP. - Step 5.2 — Server time endpoint.
GET /api/timereturns{ realTime, gameTime }from an injectableClock(src/clock.ts, passed viabuildApp(config, { clock })). - Step 5.3 — Database schema and migrations. Drizzle schema (
apps/server/src/db/schema.ts, copied intoarchitecture.md), first migrationdrizzle/0000_init.sql,db:generate/db:migratescripts. Migration test runs against an emptied test database twice. CI now has a Postgres 18 service; server tests run one file at a time. - Step 5.4 — Test database harness.
apps/server/test/db.ts:useTestDatabase()(migrate once, truncate all tables before each test) andseedPlayer(db, "tutorial" | "owner"). New-player creation lives insrc/game/new-player.ts(createPlayer), shared with guest sign-up. - Step 5.5 — Guest accounts. Better Auth with the anonymous plugin (
src/auth.ts, tables insrc/db/auth-schema.ts, migration0001_auth).POST /api/guestcreates a guest session and a tutorial player;GET /api/statereturns player state for the session or401 unauthorized. Error handler sends the documented error format. - Step 5.6 — Request validation from the route catalog.
registerRoute(src/http/route.ts) validates merged params/query/body before auth, checks sessions forauthroutes, and checks responses outside production. Malformed JSON and unexpected fields return400 validation-error.
Phase 6 — Server Game Logic
- Step 6.1 — Lazy resolution on read.
resolveArrivedDispatches(src/game/resolve.ts) resolves arrived dispatches in arrival order insidewithPlayerLock(src/game/lock.ts, a transaction holding the company row lock); a conditional status update makes each resolve exactly once.GET /api/stateruns it before reading. Routes moved tosrc/routes/. - Step 6.2 — Get full player state. Endpoint from Step 5.5 plus lazy resolution; test covers a player with one ship in flight and one idle (crewed by a hired pilot), checking ships, pilots (XP, level, skills, points, assignment, status, location), bays, dispatches, summaries and schema validity.
- Step 6.3 — Tutorial board endpoint.
GET /api/tutorial/board(src/routes/boards.ts,src/game/boards.ts): stored per (period, loaner location) on first request, accepted contracts hidden,409 not-in-tutorialafter the tutorial. Every game route now runs throughwithPlayer(lock + lazy resolution). - Step 6.4 — Tutorial dispatch.
POST /api/dispatcheswithkind: "tutorial"(src/game/dispatch.ts): avatar + loaner, sim validation, no fuel, one run at a time; contract marked accepted. Arrival pays the stored tutorial payout and moves loaner and avatar. - Step 6.5 — Buy a ship.
POST /api/ships(src/game/fleet.ts,src/routes/fleet.ts): deducts the price, adds an idle uncrewed ship at Earth named from the world seed (src/game/names.ts);409 insufficient-fundschanges nothing. - Step 6.6 — Lease a bay.
POST /api/bays(leaseBayinsrc/game/fleet.ts): one-time lease price at a discovered location;409 location-undiscovered,409 bay-already-leased,409 insufficient-funds,404 not-found. - Step 6.7 — End tutorial.
maybeEndTutorial(src/game/tutorial.ts) runs after each resolution pass and after every game action: with a ship, an Earth bay and no tutorial run in flight, tutorial mode turns off, the loaner is removed, and the avatar becomes an idle, unassigned pilot at Earth. - Step 6.8 — Hire and assign pilots.
POST /api/pilots,/api/pilots/:pilotId/assign,/api/pilots/:pilotId/unassign(src/game/pilots.ts): hire at a bay for the fee with a seeded name; assign to an idle, uncrewed ship at the same location; unassign only at a bay. New error codeavatar-in-tutorial. - Step 6.9 — Bay contract board endpoint.
GET /api/bays/:locationId/board(bayBoardinsrc/game/boards.ts): generated from the discovered set on first request in a period and stored; accepted contracts hidden;409 no-bay-at-locationwithout a bay. The expired-old-board dispatch check is tested in Step 6.10. - Step 6.10 — Contract dispatch.
POST /api/dispatcheswithkind: "contract"(dispatchContract): sim validation, fuel deducted, contract accepted, ship and pilot in flight. One integration test per sim reason (cannot-landvia a test-only vacuum ship in mocked content), each checking nothing changed; also covers the expired-old-board case from Step 6.9. - Step 6.11 — Reposition dispatch.
POST /api/dispatcheswithkind: "reposition"(dispatchReposition): sim reposition validation against the player's bays, fuel only, no payout or XP; ship and pilot arrive at the destination. - Step 6.12 — Spend skill points.
POST /api/pilots/:pilotId/skills(spendSkillPoint): +1 skill level, +1 point spent;409 no-skill-points,409 skill-maxed,404 unknown-skill. - Step 6.13 — Acknowledge summaries
- Step 6.14 — Concurrency and money safety