Step 19.5: plan amendments and regression
CI / check (push) Successful in 6m35s
Details
CI / check (push) Successful in 6m35s
Details
CR-019 §6 amendments (plan 0.5.52, 0.5.53, 3.9, 9.4, 12.1; CR-002, CR-006, CR-007). The balance player reads boards for the qualifying fleet; e2e 12.6 learns Heavy Cargo before a Heavy job is offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b201e2841
commit
e9d5a7e463
|
|
@ -1650,3 +1650,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **"Needs Heavy Cargo 2" stays on cards**: it describes the job, as distance and mass do, and every job shown can be taken by someone. Only the rose problem pills and the red mass went.
|
||||
- **One empty-board message for both cases**: a board emptied by filtering and one whose jobs were all taken read the same, "No jobs here your fleet can take right now", with the countdown. The client can't tell the two apart, and the advice is the same.
|
||||
- **The "No idle ships here" hint above the board is unchanged**; on an empty board it still points to the Fleet screen.
|
||||
|
||||
## Step 19.5
|
||||
|
||||
- **e2e 12.6 no longer shows a Heavy job blocked by skill on the board**: under CR-019 the job isn't offered until someone has the skill, so the test checks that instead and dispatches once the skill is learned. The dispatch sheet's "Needs Heavy Cargo 1 (pilot has 0)" reason is still covered by the component tests.
|
||||
- **The balance simulator reads boards the way the engine does** (qualifying fleet, read-time filter). The new `jobsShown`/`uncompletableShown` counts make "never sees a job it can't complete" a checked invariant.
|
||||
|
|
|
|||
|
|
@ -40,38 +40,20 @@ test.describe("end to end (Phase 12)", () => {
|
|||
pilot.id,
|
||||
(s) => s.ships.find((x) => x.id === ship.id)!.locationId === "moon",
|
||||
);
|
||||
const heavyJob = async () =>
|
||||
(await board(page, "moon")).contracts.find(
|
||||
const heavyJobs = async () =>
|
||||
(await board(page, "moon")).contracts.filter(
|
||||
(c) =>
|
||||
c.requiredSkill?.skillId === "heavy-cargo" &&
|
||||
c.requiredSkill.level === 1 &&
|
||||
c.massTonnes <= 80 &&
|
||||
c.requiredLongHaulLevel === 0,
|
||||
);
|
||||
let job = await heavyJob();
|
||||
for (let k = 0; !job && k < 60; k++) {
|
||||
await advance(page, content.config.contractBoardPeriodRealMinutes * 60_000);
|
||||
job = await heavyJob();
|
||||
// Boards show only jobs the fleet can complete (CR-019): no one has Heavy Cargo yet.
|
||||
for (const contract of (await board(page, "moon")).contracts) {
|
||||
expect(contract.requiredSkill?.skillId).not.toBe("heavy-cargo");
|
||||
}
|
||||
expect(job, "a Heavy Cargo 1 job at the Moon").toBeDefined();
|
||||
await acknowledgeAll(page);
|
||||
const resting = (await state(page)).pilots.find((p) => p.id === pilot.id)!.restEndsAt;
|
||||
if (resting !== null) await advanceTo(page, resting);
|
||||
|
||||
// In the dispatch sheet the Lighter's pair is blocked by the skill...
|
||||
const pairFor = async () => {
|
||||
await page.goto("/contracts?location=moon");
|
||||
const card = page
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: content.good(job!.goodId)!.name })
|
||||
.filter({ hasText: `${job!.massTonnes} t` });
|
||||
await card.getByRole("button", { name: "Dispatch" }).first().click();
|
||||
return page.locator(`input[name="pair"][value="${ship.id}"]`);
|
||||
};
|
||||
await expect(await pairFor()).toBeDisabled();
|
||||
await expect(page.getByText("Needs Heavy Cargo 1 (pilot has 0)")).toBeVisible();
|
||||
|
||||
// ...until the pilot spends a point in Heavy Cargo.
|
||||
// The pilot spends a point in Heavy Cargo...
|
||||
await page.goto("/pilots");
|
||||
await page.getByRole("button", { name: `Raise Heavy Cargo for ${pilot.name}` }).click();
|
||||
await expect
|
||||
|
|
@ -80,7 +62,26 @@ test.describe("end to end (Phase 12)", () => {
|
|||
(await state(page)).pilots.find((p) => p.id === pilot.id)!.skills["heavy-cargo"],
|
||||
)
|
||||
.toBe(1);
|
||||
await expect(await pairFor()).toBeEnabled();
|
||||
|
||||
// ...and from a later board period, Heavy Cargo 1 jobs are offered at the Moon.
|
||||
let job = (await heavyJobs())[0];
|
||||
for (let k = 0; !job && k < 60; k++) {
|
||||
await advance(page, content.config.contractBoardPeriodRealMinutes * 60_000);
|
||||
job = (await heavyJobs())[0];
|
||||
}
|
||||
expect(job, "a Heavy Cargo 1 job at the Moon").toBeDefined();
|
||||
await acknowledgeAll(page);
|
||||
const resting = (await state(page)).pilots.find((p) => p.id === pilot.id)!.restEndsAt;
|
||||
if (resting !== null) await advanceTo(page, resting);
|
||||
|
||||
// The Lighter's pair can take it in the dispatch sheet.
|
||||
await page.goto("/contracts?location=moon");
|
||||
const card = page
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: content.good(job!.goodId)!.name })
|
||||
.filter({ hasText: `${job!.massTonnes} t` });
|
||||
await card.getByRole("button", { name: "Dispatch" }).first().click();
|
||||
await expect(page.locator(`input[name="pair"][value="${ship.id}"]`)).toBeEnabled();
|
||||
|
||||
// CR-004: Long Haul raises the duty limit.
|
||||
const limitBefore = (await state(page)).pilots.find((p) => p.id === pilot.id)!.dutyLimit;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Record these in `DECISIONS.md` and in `implementation-plan.md` §0.4 and §0.5.
|
|||
- **Deadhead** (new) → A reposition: a ship flying empty to another location. Fuel only, no payout, no XP.
|
||||
|
||||
**Replaced**
|
||||
- **0.5.5 (Contract boards are stored)** → A board exists for every **discovered location**, per player, per period. It is generated on the first request in a period and stored; later requests in that period return the stored board. Every contract expires at the end of its board period.
|
||||
- **0.5.5 (Contract boards are stored)** → A board exists for every **discovered location**, per player, per period. It is generated on the first request in a period and stored; later requests in that period return the stored board. Every contract expires at the end of its board period. *(Amended by CR-019:)* a stored location board also records the qualifying fleet it was generated for, and reads hide jobs the current qualifying fleet can't complete (Step 19.3).
|
||||
- **Step 3.14 rule (reposition only to a bay)** → An idle ship can deadhead from **any** location to **any discovered** location other than its current one, subject to the normal range and landing checks.
|
||||
|
||||
**New**
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ Record as plan §0.5.52–0.5.53.
|
|||
### Step 10F.1 — Client: weight warning
|
||||
- On location boards, the mass is shown in rose with a "Too heavy" pill when it exceeds the largest capacity among the player's ships at that location (all ships when none are there). No ships at all: no warning.
|
||||
- **Test:** a board with a 50 t job and a Courier at the location shows the warning on it and not on a 10 t job; with the Courier elsewhere and a Freighter at the location, no warning on 50 t.
|
||||
- **Replaced by CR-019:** boards show only completable jobs, so the weight warning and its test are gone (Step 19.4).
|
||||
|
||||
### Step 10F.2 — Content: board config
|
||||
- GameConfig gains `boardPreviewSlots` (default 1) and `boardOpenSlotsPerBand` (default 1), non-negative integers.
|
||||
|
|
@ -60,9 +61,11 @@ Record as plan §0.5.52–0.5.53.
|
|||
### Step 10F.3 — Sim: fleet-aware boards
|
||||
- `BoardInput.fleet` (optional): `{ modelId, skills }[]`. Implement §2.
|
||||
- **Test:** no fleet gives the same board as before (existing tests unchanged); determinism with a fleet; Courier-only fleet at Earth: board size unchanged, at least `bands' total − previews − open` fit contracts, each passing `checkRoute`, `checkPilotEligibility` and capacity for the Courier and pilot; a skilled pilot (High-Value 3) gets High-Value fit jobs at tiers ≤ 3 over many periods; a full fleet keeps the configured band counts.
|
||||
- **Amended by CR-019:** With a fleet, only completable candidates are kept (no previews, no open jobs the fleet can't take); an empty fleet gets an empty board (Step 19.2).
|
||||
|
||||
### Step 10F.4 — Server: pass the fleet
|
||||
- `locationBoard` reads the player's ships and their assigned pilots' skills and passes them as the fleet.
|
||||
- **Amended by CR-019:** it passes the location's qualifying fleet instead (decision 0.5.115, Step 19.3).
|
||||
- **Test:** a player with one Courier gets at least 5 Earth contracts that pass sim `validateDispatch` for that Courier and pilot.
|
||||
|
||||
### Step 10F.5 — Regression
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ Record these in `DECISIONS.md` and `implementation-plan.md` §0.5. *(Recorded as
|
|||
- Keep the existing class selection. After a class is chosen, pick the good, mass, Priority flag and window, and shipper per decisions 0.5.39, 0.5.41, and 0.5.43.
|
||||
- Keep everything deterministic from the existing inputs.
|
||||
- **Test:** Same inputs give identical boards. Every contract's good is produced at the origin and (except first-contact and fallbacks) demanded at the destination, and its mass is within the good's range. Over 1,000 boards from Earth with all locations discovered: the share of each cargo class is within ±3 percentage points of what the pre-change generator produces for the same inputs (record the baseline before changing the generator); Priority appears only on eligible goods and on 10–20% of eligible contracts; every contract has a shipper from its origin's list. First-contact rate is still 5–15%. *(Mass within the good's range except band fallbacks, per the implementation notes.)*
|
||||
- **Amended by CR-019:** The class-mix test keeps its fleet-free inputs: with any fleet, CR-006's fit slots pick goods the crew can carry instead of drawing from the class mix. A second test shows a full-capability fleet filters nothing (Step 19.2).
|
||||
|
||||
### Step 10E.4 — Sim: Priority validation
|
||||
- Dispatch validation gains `too_slow`: for a Priority contract, the ship's trip time (with handling time, at departure) must not exceed the delivery window.
|
||||
|
|
@ -170,6 +171,7 @@ Record these in `DECISIONS.md` and `implementation-plan.md` §0.5. *(Recorded as
|
|||
- Requirement ("No skills needed", "Expensive Cargo 1") and expiry.
|
||||
- Status badges as now, plus "TOO SLOW" when no idle ship at the origin can meet the window.
|
||||
- **Test:** Component test: a card shows good name, class and tier chip, shipper, mass, and requirement; a Priority card shows the window chip; a Priority card with only slow idle ships at the origin shows "TOO SLOW".
|
||||
- **Amended by CR-019:** The "TOO SLOW" badge is removed: boards show only jobs some pair can complete; the dispatch sheet still gives each slow pair's reason (Step 19.4).
|
||||
|
||||
### Step 10E.9 — Client: Priority in the dispatch sheet
|
||||
- For a Priority contract, each ship+pilot pair shows its trip time against the window. Pairs that don't fit are disabled with "Too slow — 7h vs 5h window".
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
|||
49. **Navigation** (CR-005). Icon tabs: a bottom bar on phones, a left rail from 768 px wide. Tab order and labels are unchanged. Attention badges are decorative (`aria-hidden`), so each tab's accessible name stays its label.
|
||||
50. **Art on screens** (CR-005). Ships and locations show their content `sprite` from the generated map art as plain images with empty `alt`; the name is always in text beside it. A missing image hides itself.
|
||||
51. **Presentation only** (CR-005). The overhaul changes no game rules, API calls, copy or accessible names.
|
||||
52. **Boards fit the fleet** (CR-006). Location boards are generated with the player's fleet (ship models and pilot skills) as input. Bands the fleet can't carry shrink to a preview; in bands it can, all but a configured number of contracts are ones some ship-and-pilot pair can take.
|
||||
53. **Weight warning** (CR-006). The board highlights a contract's mass when none of the player's ships at that location (or in the fleet, when none are there) can carry it. Display only.
|
||||
52. **Boards fit the fleet** (CR-006). Location boards are generated with the player's fleet (ship models and pilot skills) as input. Bands the fleet can't carry shrink to a preview; in bands it can, all but a configured number of contracts are ones some ship-and-pilot pair can take. *(Amended by CR-019, decision 0.5.114:)* boards keep only jobs some qualifying pair can complete; the open and preview slots now only shape the candidates.
|
||||
53. **Weight warning** (CR-006). The board highlights a contract's mass when none of the player's ships at that location (or in the fleet, when none are there) can carry it. Display only. *(Replaced by CR-019, decision 0.5.117:)* boards show only completable jobs, so the warning is gone.
|
||||
54. **Goods** (CR-007). Contracts carry a named good from the catalog; a location's produces and demands lists name goods. After the generator picks a contract's class (unchanged), it picks a good of that class produced at the origin and demanded at the destination, falling back to a Standard good on the route, else any Standard good produced at the origin. Mass comes from the slot's ship-size band, inside the good's range when they overlap.
|
||||
55. **Tier from goods** (CR-007). High-Value and Dangerous contracts take their tier from the good; Standard is tier 0; Heavy keeps the random tier.
|
||||
56. **Priority** (CR-007). A Priority-eligible good becomes Priority with the configured chance. Its delivery window is the reference ship model's trip time for the cargo and route at offer time × the window factor; a ship whose trip time exceeds it fails dispatch with `too-slow`. Priority pays the configured bonus; no skill is required.
|
||||
|
|
@ -324,6 +324,7 @@ The game has had no server since CR-014. When a later or unfinished change reque
|
|||
- **Amended by CR-002:** Input is a location, not a bay: boards are generated for any discovered location (Step 10B.1).
|
||||
- **Amended by CR-006:** `BoardInput` takes the player's fleet (ship models and pilot skills); bands the fleet can't carry shrink to a preview and flyable bands are mostly jobs the fleet can take (decision 0.5.52, Step 10F.3).
|
||||
- **Amended by CR-007:** Class selection unchanged; each contract then gets a good (mass from its band, inside the good's range when they overlap), maybe Priority with a window, and a shipper (Step 10G.3).
|
||||
- **Amended by CR-019:** With the qualifying fleet as input, the generator keeps only candidates some pair can complete (`isCompletable`), drawing slot by slot until the board is full or `boardMaxAttempts` (200) have been tried; first contacts too. A board may be smaller than the configured size (Step 19.2).
|
||||
|
||||
### Step 3.10 — Tutorial contract board
|
||||
- Add a separate generator for the tutorial "company board": Earth↔Moon only, Standard cargo only, payout reduced by the tutorial payout share, always flown with the company loaner Courier. Contracts start at the loaner's current location: Earth→Moon when it is at Earth, Moon→Earth when it is at the Moon.
|
||||
|
|
@ -572,6 +573,7 @@ The game has had no server since CR-014. When a later or unfinished change reque
|
|||
- **Amended by CR-006:** A contract's mass is highlighted, with a "Too heavy" tag, when none of the player's ships at the location (or in the fleet, when none are there) can carry it (decision 0.5.53, Step 10F.1).
|
||||
- **Amended by CR-007:** Cards headline the good with class/tier, Priority and Too slow chips, and show shipper · mass · distance (Step 10G.8).
|
||||
- **Amended by CR-008:** When a location's board changes, the screen says "New contracts posted" and tags contracts not on the last board seen there "New" (Step 10H.3).
|
||||
- **Amended by CR-019:** Cards drop the "Too heavy" and "Too slow" badges (every job shown can be taken); an empty board says "No jobs here your fleet can take right now. New contracts in mm:ss.", with Rest and Deadhead for idle ships there (Step 19.4).
|
||||
|
||||
### Step 9.5 — Dispatch flow
|
||||
- Selecting a contract opens a dispatch sheet: choose an eligible ship+pilot pair at the origin. Show trip time, fuel cost, net profit, and ETA in both game time and real time. Ineligible pairs are listed but disabled with the reason shown (out of range, can't land, skill too low, too heavy).
|
||||
|
|
@ -757,6 +759,7 @@ Steps 10M.1–10M.5 are specified in `memory-bank/change-requests/CR-013-veteran
|
|||
- **Amended by CR-012:** The script runs two second-ship policies, Lighter-first and Clipper-first; both must reach "own three ships" within 25 % of each other's time. *(Replaced by CR-013, below.)*
|
||||
- **Amended by CR-013:** The player starts with two ships (Courier and Clipper) and a 66,600 cr loan; re-check every timing target with them. Targets: first Market purchase 45–75 minutes, first discovery under 90 minutes, loan paid off in under 4 hours of active play. Run two first-purchase policies, Lighter-first and Freighter-first; both must reach "own four ships" within 25 % of each other's time.
|
||||
- **Amended by CR-018:** Ships are on Manual refueling: the simulated player taps "Refuel here first" before a leg only when the best job needs more fuel than is aboard (or to go looking for work), instead of relying on Auto top-up. The deal's ships start full. The first Market purchase target is 30–95 minutes (user decision). The balance script also prints each ship's fuel share of the median payout per route (Step 18.1).
|
||||
- **Amended by CR-019:** Boards are generated for each location's qualifying fleet and read with jobs the fleet can no longer complete hidden, as in the engine; a check confirms no run is ever shown a job its fleet can't complete. Before buying a ship, the simulated player keeps enough cash to fill every landed ship's tank.
|
||||
- **As built:** `packages/sim/balance/simulate.ts` (run with `pnpm --filter @deadhead/sim balance [seed] [hours]`), tested in `packages/sim/test/balance.test.ts` over seven world seeds: timing targets on the median of the Lighter-first runs (the default player buys the cheaper ship first), the four-ship comparison on both policies' medians, and every invariant in every run. Meeting the targets needed decisions 0.5.84 and 0.5.85 and the tuning in `DECISIONS.md` (Step 12.1).
|
||||
|
||||
### Step 12.2 — Revisit the tutorial economy test
|
||||
|
|
|
|||
|
|
@ -444,4 +444,6 @@ Spec: `memory-bank/change-requests/CR-019-completable-boards.md` (received as "C
|
|||
- [x] **Step 19.2 — Sim: generation keeps only completable jobs.** `generateContractBoard` with a fleet draws candidates slot by slot (CR-006's band plan), round and round. It keeps those `isCompletable` for the fleet, numbered in order kept, until the board is full or `config.boardMaxAttempts` (200) candidates have been tried; first contacts count only when kept. Without a fleet it is unfiltered as before (tools and the class-mix baseline). The balance simulator keeps a fuel reserve big enough to fill every landed ship before buying another; one world had stranded a Clipper at Venus after a purchase. The fuel-share check's pilot is skilled in everything: only the ship limits what fits, and now no route is above 25%, the Clipper's Mars → Earth included. Balance medians: deal 3.7 min, first purchase 36.7, discovery 36.6, loan 123.3, no violations. Tests (`packages/sim/test/board-completable.test.ts`): 1,000 boards over five assorted fleets and five origins never offer an uncompletable job; a level-0 avatar's Courier at Earth always gets 8 Standard Moon jobs it fits; a Courier with only Earth known gets short boards of Moon first contacts, without error; identical inputs give identical boards; first contacts are filtered too. The CR-006 tests now expect an empty board for an empty fleet, full Courier boards with no previews, and band counts kept for a fleet that can do everything. `board-goods.test.ts`: a full-capability fleet filters nothing. The Venus board test gives its pilot Long Haul 1.
|
||||
- [x] **Step 19.3 — Engine: boards, filtering, and migration.** `boardFleet(save, locationId, periodEndsAt)` in `packages/engine/src/game/boards.ts` (replacing `playerFleet`) builds the qualifying fleet from the save: ships in creation order with upgrades, fuel, and the destination and arrival of a flying ship, plus pilots and bays. A location board is generated for it and stores it as `fleet` on the saved board (`fleetMemberSchema`; null for tutorial and hand-made boards). Every read hides jobs the current qualifying fleet can't complete. Save version 4: the 3 → 4 migration drops stored boards. Also fixes Step 19.2's unformatted DECISIONS.md line. Tests (`packages/engine/test/completable-boards.test.ts`): a Freighter's heavy jobs vanish from Earth's board once it deadheads to the Moon, leaving the Courier's jobs unchanged; a Freighter bought mid-period adds nothing until the next period, then heavy jobs appear; the stored board carries the fleet snapshot; a version-3 save opens with no boards and regenerates the same board on view. `packages/schema/test/save.test.ts`: 3 → 4 drops boards and changes nothing else.
|
||||
- [x] **Step 19.4 — Client: cards and empty state.** Contract cards lose the "Too heavy" and "Too slow" pills and the red mass (`carryLimit` and `tooSlowForAll` removed, with the CR-006 weight-warning test); the "Needs Heavy Cargo 2" requirement line stays, as it describes the job rather than a problem. The dispatch sheet keeps its per-pair reasons. An empty board says "No jobs here your fleet can take right now. New contracts in mm:ss.", above the existing Rest and Deadhead panel for each idle ship there. Screenshots: `docs/screenshots/19-4-earth-board-desktop.png`, `19-4-earth-board-phone.png`, `19-4-empty-board-desktop.png`, `19-4-empty-board-phone.png` (Mars after the deal: no pilot has Long Haul for its legs). Tests (`apps/web/test/board-cards.test.tsx`): heavy, rushed and skilled jobs show no problem badge; an empty board shows the message with the countdown, plus Rest and Deadhead for the idle Courier. The badge tests in `contract-card.test.tsx` and `effective-stats-ui.test.tsx` are gone, and `states.test.tsx` expects the new message.
|
||||
- [ ] Step 19.5 — Plan amendments and regression
|
||||
- [x] **Step 19.5 — Plan amendments and regression.** CR-019 §6 applied: plan 0.5.52 and 0.5.53, Steps 3.9, 9.4 and 12.1; CR-002's 0.5.5 (stored boards keep the fleet snapshot); CR-007 Steps 10G.3 and 10G.8; and this repo's CR-006 Steps 10F.1, 10F.3 and 10F.4, which it supersedes. The balance simulator now generates each board for that location's qualifying fleet and hides jobs the fleet can no longer complete, as the engine does. It counts the jobs shown: none in any of the 14 runs was uncompletable. e2e 12.6 is reordered: a Heavy Cargo job now appears only after the pilot learns the skill, so the pilot raises Heavy Cargo first; the test then waits for a Heavy Cargo 1 job at the Moon and checks the Lighter's pair can take it. Before that, it checks no Heavy Cargo job is offered. Full suite: typecheck, lint, 1327 unit tests and 9 e2e tests pass. Balance (7 worlds): deal 3.7 min, first Market purchase 36.6 (30–95), discovery 36.6, loan paid off 123.0, four ships 118.4 vs 122.9, no violations.
|
||||
|
||||
**Phase 19 is complete; stopped for review.** Manual check for the user: with only the deal's Courier and Clipper, browse the Earth board and confirm every job can be dispatched with one of them; boards elsewhere (e.g. Mars, where neither pilot has Long Haul) say no job fits.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
effectiveStats,
|
||||
generateContractBoard,
|
||||
generateTutorialBoard,
|
||||
isCompletable,
|
||||
isResting,
|
||||
loanerModel,
|
||||
MS_PER_HOUR,
|
||||
|
|
@ -32,6 +33,7 @@ import {
|
|||
nextUpgradeLevel,
|
||||
pilotLevel,
|
||||
pilotShare,
|
||||
qualifyingFleet,
|
||||
resolveDispatch,
|
||||
restEndsAt,
|
||||
skillPointsAvailable,
|
||||
|
|
@ -106,6 +108,9 @@ export interface SimulationResult {
|
|||
violations: { minute: number; rule: string; detail: string }[];
|
||||
/** Ships bought, in order. */
|
||||
purchases: string[];
|
||||
/** Board jobs offered to the player (CR-019): those shown, and those no qualifying pair could complete. */
|
||||
jobsShown: number;
|
||||
uncompletableShown: number;
|
||||
}
|
||||
|
||||
interface SimShip extends ShipState {
|
||||
|
|
@ -224,17 +229,44 @@ export function simulate(options: SimulationOptions): SimulationResult {
|
|||
skills: p.skills,
|
||||
restEndsAt: p.restEndsAt,
|
||||
});
|
||||
const fleet = () =>
|
||||
ships.map((s) => ({
|
||||
modelId: s.modelId,
|
||||
skills: pilotOf(s)?.skills ?? {},
|
||||
upgrades: s.upgrades,
|
||||
}));
|
||||
/** The qualifying fleet for a location's board (decision 0.5.115), as the engine builds it. */
|
||||
const fleetFor = (locationId: string, periodEndsAt: number) =>
|
||||
qualifyingFleet({
|
||||
locationId,
|
||||
ships: ships.map((s) => {
|
||||
const flight = s.inFlight ? flights.find((f) => f.shipId === s.id) : undefined;
|
||||
return {
|
||||
id: s.id,
|
||||
modelId: s.modelId,
|
||||
upgrades: s.upgrades,
|
||||
fuelFu: s.tank.fuelFu,
|
||||
locationId: s.inFlight ? null : s.locationId,
|
||||
destinationId: flight?.record.destinationId ?? null,
|
||||
arrivalAt: flight?.record.arrivalAt ?? null,
|
||||
};
|
||||
}),
|
||||
// Every simulated pilot is hired onto a ship, so none waits at a bay.
|
||||
pilots: pilots.map((p) => ({
|
||||
id: p.id,
|
||||
skills: p.skills,
|
||||
assignedShipId: p.assignedShipId,
|
||||
locationId: null,
|
||||
})),
|
||||
bayLocationIds: bays,
|
||||
periodEndsAt,
|
||||
}).members;
|
||||
let jobsShown = 0;
|
||||
let uncompletableShown = 0;
|
||||
|
||||
/** The location board for the current period, generated once per period (as the server stores it). */
|
||||
/**
|
||||
* The location board for the current period, generated once per period for
|
||||
* the qualifying fleet, and read as the engine reads it: jobs the fleet can
|
||||
* no longer complete are hidden (CR-019).
|
||||
*/
|
||||
function board(locationId: string): Contract[] {
|
||||
const period = boardPeriodIndex(now, config);
|
||||
const key = `${locationId}:${period}`;
|
||||
const fleet = fleetFor(locationId, (period + 1) * boardPeriodMs(config));
|
||||
let contracts = boards.get(key);
|
||||
if (!contracts) {
|
||||
contracts = generateContractBoard(
|
||||
|
|
@ -243,13 +275,20 @@ export function simulate(options: SimulationOptions): SimulationResult {
|
|||
discoveredIds: discovered,
|
||||
periodIndex: period,
|
||||
worldSeed: options.worldSeed,
|
||||
fleet: fleet(),
|
||||
fleet,
|
||||
},
|
||||
content,
|
||||
);
|
||||
boards.set(key, contracts);
|
||||
// Fresh from the generator, every job must be one this fleet can complete.
|
||||
jobsShown += contracts.length;
|
||||
uncompletableShown += contracts.filter(
|
||||
(c) => !isCompletable(c, fleet, discovered, content),
|
||||
).length;
|
||||
}
|
||||
return contracts.filter((c) => !accepted.has(c.id));
|
||||
return contracts.filter(
|
||||
(c) => !accepted.has(c.id) && isCompletable(c, fleet, discovered, content),
|
||||
);
|
||||
}
|
||||
|
||||
/** Credits per real minute the company keeps from a contract (payout − fuel − pilot pay). */
|
||||
|
|
@ -793,5 +832,7 @@ export function simulate(options: SimulationOptions): SimulationResult {
|
|||
veteranContractsFirstSession,
|
||||
violations,
|
||||
purchases,
|
||||
jobsShown,
|
||||
uncompletableShown,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,13 @@ describe("balance simulation (Step 12.1)", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("never shows the player a job its fleet can't complete, in any world (CR-019)", () => {
|
||||
for (const result of [...lighterFirst, ...freighterFirst]) {
|
||||
expect(result.jobsShown).toBeGreaterThan(0);
|
||||
expect(result.uncompletableShown).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("has the veteran fly at least one contract in the first session (CR-010)", () => {
|
||||
for (const result of lighterFirst)
|
||||
expect(result.veteranContractsFirstSession).toBeGreaterThan(0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue