Step 3.11: add pilot eligibility
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a0a43a54e
commit
dfd76ba9bc
|
|
@ -130,3 +130,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **The loaner is the content's Courier,** found by archetype `courier`. `SimContent` now includes `shipModels`.
|
||||
- **Tutorial payout** = round(normal payout × tutorial share), where the normal payout is the regular rounded payout for the same contract. The contract stores this reduced amount, so resolution credits `contract.payout` directly.
|
||||
- **Tutorial board seed** is `tutorial:<loanerLocation>:<periodIndex>`, giving one Earth→Moon and one Moon→Earth board per period (decision 0.5.21). Contract ids use the same prefix.
|
||||
|
||||
## Step 3.11
|
||||
|
||||
- **Pilot skills are a `{ skillId: level }` map**; a missing skill counts as level 0.
|
||||
- **The Long Haul skill id is the constant `long-haul`** in sim, because the rule (decision 0.5.7) is specifically about that skill.
|
||||
- **The cargo skill is checked before Long Haul** when reporting a failure reason.
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
- [x] **Step 3.8 — Contract model and payout.** `src/contract.ts`: `Contract` interface and `contractPayout` (base × multiplier^tier × mass × distance × first-contact multiplier, rounded).
|
||||
- [x] **Step 3.9 — Contract board generation.** `src/board.ts`: `generateContractBoard` (seeded per world, bay and period), plus `boardPeriodIndex`, `requiredLongHaulLevel` and the `SimContent` input type. First contact ≈10% over 1,000 boards.
|
||||
- [x] **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`. `SimContent` gained `shipModels`.
|
||||
- [ ] Step 3.11 — Pilot eligibility
|
||||
- [x] **Step 3.11 — Pilot eligibility.** `src/pilot.ts`: `checkPilotEligibility(skills, contract)` returns `ok` or `cargo-skill-too-low` / `long-haul-too-low`.
|
||||
- [ ] Step 3.12 — Dispatch validation
|
||||
- [ ] Step 3.13 — Dispatch record creation
|
||||
- [ ] Step 3.14 — Reposition dispatch
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ export * from "./eligibility";
|
|||
export * from "./contract";
|
||||
export * from "./board";
|
||||
export * from "./tutorial";
|
||||
export * from "./pilot";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import type { Contract } from "./contract";
|
||||
|
||||
export const LONG_HAUL_SKILL_ID = "long-haul";
|
||||
|
||||
/** A pilot's skill levels by skill id; a missing skill is level 0. */
|
||||
export type SkillLevels = Readonly<Record<string, number>>;
|
||||
|
||||
export type PilotCheck =
|
||||
{ ok: true } | { ok: false; reason: "cargo-skill-too-low" | "long-haul-too-low" };
|
||||
|
||||
export function skillLevel(skills: SkillLevels, skillId: string): number {
|
||||
return skills[skillId] ?? 0;
|
||||
}
|
||||
|
||||
/** Whether a pilot's skills meet a contract's cargo-skill and Long Haul requirements. */
|
||||
export function checkPilotEligibility(skills: SkillLevels, contract: Contract): PilotCheck {
|
||||
const required = contract.requiredSkill;
|
||||
if (required && skillLevel(skills, required.skillId) < required.level) {
|
||||
return { ok: false, reason: "cargo-skill-too-low" };
|
||||
}
|
||||
if (skillLevel(skills, LONG_HAUL_SKILL_ID) < contract.requiredLongHaulLevel) {
|
||||
return { ok: false, reason: "long-haul-too-low" };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { checkPilotEligibility, type Contract } from "../src/index";
|
||||
|
||||
const shortStandard: Contract = {
|
||||
id: "c1",
|
||||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 1000,
|
||||
requiredSkill: null,
|
||||
requiredLongHaulLevel: 0,
|
||||
offeredAt: 0,
|
||||
expiresAt: 3_600_000,
|
||||
firstContact: false,
|
||||
};
|
||||
const heavyTier2: Contract = {
|
||||
...shortStandard,
|
||||
cargoTypeId: "heavy",
|
||||
tier: 2,
|
||||
requiredSkill: { skillId: "heavy-cargo", level: 2 },
|
||||
};
|
||||
const longRoute: Contract = {
|
||||
...shortStandard,
|
||||
destinationId: "ceres-station",
|
||||
requiredLongHaulLevel: 1,
|
||||
};
|
||||
|
||||
describe("pilot eligibility", () => {
|
||||
it("lets a level-0 pilot take a short Standard contract", () => {
|
||||
expect(checkPilotEligibility({}, shortStandard)).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("stops a level-0 pilot from taking a Heavy contract needing Heavy Cargo 2", () => {
|
||||
expect(checkPilotEligibility({}, heavyTier2)).toEqual({
|
||||
ok: false,
|
||||
reason: "cargo-skill-too-low",
|
||||
});
|
||||
expect(checkPilotEligibility({ "heavy-cargo": 1 }, heavyTier2).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("lets a pilot with Heavy Cargo 2 take it", () => {
|
||||
expect(checkPilotEligibility({ "heavy-cargo": 2 }, heavyTier2)).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("stops a level-0 pilot from taking a route needing Long Haul 1", () => {
|
||||
expect(checkPilotEligibility({}, longRoute)).toEqual({
|
||||
ok: false,
|
||||
reason: "long-haul-too-low",
|
||||
});
|
||||
expect(checkPilotEligibility({ "long-haul": 1 }, longRoute)).toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue