Step 10A.5: add onboarding goal evaluation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
401914e76b
commit
b3ed6ef71d
|
|
@ -510,3 +510,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **The lender's cut is rounded down to whole credits:** repayment = `min(balance, floor(credits × share))`, or 0 when the balance is 0 or the credits after pilot pay are not positive. Credits are whole numbers everywhere, and rounding down favours the player by under one credit.
|
- **The lender's cut is rounded down to whole credits:** repayment = `min(balance, floor(credits × share))`, or 0 when the balance is 0 or the credits after pilot pay are not positive. Credits are whole numbers everywhere, and rounding down favours the player by under one credit.
|
||||||
- **`Resolution.credits` is now the net amount** (after pilot pay and repayment); `loanRepayment` is reported separately for the server to apply and the summary to show.
|
- **`Resolution.credits` is now the net amount** (after pilot pay and repayment); `loanRepayment` is reported separately for the server to apply and the summary to show.
|
||||||
- **Temporary:** the server resolves with `loanBalance: 0` until the balance column exists (Steps 10A.6 and 10A.9).
|
- **Temporary:** the server resolves with `loanBalance: 0` until the balance column exists (Steps 10A.6 and 10A.9).
|
||||||
|
|
||||||
|
## Step 10A.5
|
||||||
|
|
||||||
|
- **Goals are evaluated against a small `GoalProgress` summary** (own-ship deliveries, skill points spent, ships owned, pilots hired, extra locations discovered, deal accepted, loan balance) rather than full player state. The server builds it from the database, and the evaluator stays independent of storage.
|
||||||
|
- **"Pay off your loan" requires the deal to have been accepted**, so a player who never had a loan does not complete it by default.
|
||||||
|
- **The evaluator takes the goal list as an argument** (not part of `SimContent`), since only goal code needs it.
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ Spec: `memory-bank/change-requests/CR-001-faster-start.md`. Decisions recorded a
|
||||||
- [x] **Step 10A.2 — Sim: handling time.** `travelTimeGameHours(ship, distance, massTonnes, config)` adds `mass × handlingGameHoursPerTonne`; dispatch records pass the contract mass (0 for repositions); client predictions pass contract mass. Step 3.5 hand-calculated test updated.
|
- [x] **Step 10A.2 — Sim: handling time.** `travelTimeGameHours(ship, distance, massTonnes, config)` adds `mass × handlingGameHoursPerTonne`; dispatch records pass the contract mass (0 for repositions); client predictions pass contract mass. Step 3.5 hand-calculated test updated.
|
||||||
- [x] **Step 10A.3 — Sim: tutorial board v2.** `generateTutorialBoard({ loanerLocationId, runsCompleted, periodIndex })`: light/medium/full Standard contracts at the tutorial share; a scripted contract for the upcoming run replaces the medium slot (`scriptedId` on the contract, at its pay multiplier). Loaner is `config.tutorialLoanerShipModelId`. Step 3.10 tests replaced. Server passes `runsCompleted: 0` until Step 10A.7.
|
- [x] **Step 10A.3 — Sim: tutorial board v2.** `generateTutorialBoard({ loanerLocationId, runsCompleted, periodIndex })`: light/medium/full Standard contracts at the tutorial share; a scripted contract for the upcoming run replaces the medium slot (`scriptedId` on the contract, at its pay multiplier). Loaner is `config.tutorialLoanerShipModelId`. Step 3.10 tests replaced. Server passes `runsCompleted: 0` until Step 10A.7.
|
||||||
- [x] **Step 10A.4 — Sim: loan repayment.** `splitLoanRepayment(credits, balance, share)` (`src/loan.ts`); `resolveDispatch` takes `loanBalance` and returns `loanRepayment` with net `credits`. Server passes a zero balance until Step 10A.9.
|
- [x] **Step 10A.4 — Sim: loan repayment.** `splitLoanRepayment(credits, balance, share)` (`src/loan.ts`); `resolveDispatch` takes `loanBalance` and returns `loanRepayment` with net `credits`. Server passes a zero balance until Step 10A.9.
|
||||||
- [ ] Step 10A.5 — Sim: onboarding goal evaluation
|
- [x] **Step 10A.5 — Sim: onboarding goal evaluation.** `completedGoals(goals, progress)` / `isGoalConditionMet` (`src/goals.ts`) over a `GoalProgress` summary; tests check each goal just short of and at its condition. Sim line coverage still 100%.
|
||||||
- [ ] Step 10A.6 — Schema and database
|
- [ ] Step 10A.6 — Schema and database
|
||||||
- [ ] Step 10A.7 — Server: tutorial flow v2
|
- [ ] Step 10A.7 — Server: tutorial flow v2
|
||||||
- [ ] Step 10A.8 — Server: accept the deal
|
- [ ] Step 10A.8 — Server: accept the deal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { GoalCondition, OnboardingGoal } from "@deadhead/content";
|
||||||
|
|
||||||
|
/** The facts about a player that onboarding goals are checked against. */
|
||||||
|
export interface GoalProgress {
|
||||||
|
/** Contracts delivered by the player's own ships (tutorial runs excluded). */
|
||||||
|
ownShipDeliveries: number;
|
||||||
|
/** Skill points spent across all pilots. */
|
||||||
|
skillPointsSpent: number;
|
||||||
|
shipsOwned: number;
|
||||||
|
/** Pilots hired (the avatar excluded). */
|
||||||
|
pilotsHired: number;
|
||||||
|
/** Locations discovered beyond the ones that start discovered. */
|
||||||
|
locationsDiscovered: number;
|
||||||
|
dealAccepted: boolean;
|
||||||
|
loanBalance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGoalConditionMet(condition: GoalCondition, progress: GoalProgress): boolean {
|
||||||
|
switch (condition.kind) {
|
||||||
|
case "own-ship-deliveries":
|
||||||
|
return progress.ownShipDeliveries >= condition.count;
|
||||||
|
case "skill-points-spent":
|
||||||
|
return progress.skillPointsSpent >= condition.count;
|
||||||
|
case "ships-owned":
|
||||||
|
return progress.shipsOwned >= condition.count;
|
||||||
|
case "pilots-hired":
|
||||||
|
return progress.pilotsHired >= condition.count;
|
||||||
|
case "locations-discovered":
|
||||||
|
return progress.locationsDiscovered >= condition.count;
|
||||||
|
case "loan-paid-off":
|
||||||
|
// Only a loan that was taken can be paid off.
|
||||||
|
return progress.dealAccepted && progress.loanBalance <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ids of the goals whose conditions are met, in the goals' order (decision 0.5.34). */
|
||||||
|
export function completedGoals(goals: readonly OnboardingGoal[], progress: GoalProgress): string[] {
|
||||||
|
return goals.filter((goal) => isGoalConditionMet(goal.condition, progress)).map((g) => g.id);
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,9 @@ export { dispatchPosition, dispatchProgress } from "./flight";
|
||||||
|
|
||||||
export { deliveryXp, pilotLevel, skillPointsAvailable } from "./xp";
|
export { deliveryXp, pilotLevel, skillPointsAvailable } from "./xp";
|
||||||
|
|
||||||
|
export { completedGoals, isGoalConditionMet } from "./goals";
|
||||||
|
export type { GoalProgress } from "./goals";
|
||||||
|
|
||||||
export { splitLoanRepayment } from "./loan";
|
export { splitLoanRepayment } from "./loan";
|
||||||
export type { LoanSplit } from "./loan";
|
export type { LoanSplit } from "./loan";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { content } from "@deadhead/content";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { completedGoals, type GoalProgress } from "../src/index";
|
||||||
|
|
||||||
|
const goals = content.onboardingGoals;
|
||||||
|
|
||||||
|
/** Just after the deal: a ship, an Earth bay and the full loan. */
|
||||||
|
const fresh: GoalProgress = {
|
||||||
|
ownShipDeliveries: 0,
|
||||||
|
skillPointsSpent: 0,
|
||||||
|
shipsOwned: 1,
|
||||||
|
pilotsHired: 0,
|
||||||
|
locationsDiscovered: 0,
|
||||||
|
dealAccepted: true,
|
||||||
|
loanBalance: content.config.dealLoanAmount,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isComplete = (id: string, progress: GoalProgress) =>
|
||||||
|
completedGoals(goals, progress).includes(id);
|
||||||
|
|
||||||
|
/** For each goal: a summary just short of its condition, and one that meets it. */
|
||||||
|
const cases: [string, Partial<GoalProgress>, Partial<GoalProgress>][] = [
|
||||||
|
["first-own-delivery", { ownShipDeliveries: 0 }, { ownShipDeliveries: 1 }],
|
||||||
|
["first-skill-point", { skillPointsSpent: 0 }, { skillPointsSpent: 1 }],
|
||||||
|
["second-ship", { shipsOwned: 1 }, { shipsOwned: 2 }],
|
||||||
|
["first-hire", { pilotsHired: 0 }, { pilotsHired: 1 }],
|
||||||
|
["first-discovery", { locationsDiscovered: 0 }, { locationsDiscovered: 1 }],
|
||||||
|
["loan-paid-off", { loanBalance: 1 }, { loanBalance: 0 }],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("onboarding goal evaluation", () => {
|
||||||
|
it("covers every goal in content, in order", () => {
|
||||||
|
expect(cases.map(([id]) => id)).toEqual(goals.map((g) => g.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(cases)(
|
||||||
|
"%s: incomplete just short of the condition, complete once met",
|
||||||
|
(id, short, met) => {
|
||||||
|
expect(isComplete(id, { ...fresh, ...short })).toBe(false);
|
||||||
|
expect(isComplete(id, { ...fresh, ...met })).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("does not count a loan as paid off before the deal", () => {
|
||||||
|
expect(isComplete("loan-paid-off", { ...fresh, dealAccepted: false, loanBalance: 0 })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns completed goals in content order, identically for identical inputs", () => {
|
||||||
|
const progress: GoalProgress = { ...fresh, pilotsHired: 3, ownShipDeliveries: 5 };
|
||||||
|
expect(completedGoals(goals, progress)).toEqual(["first-own-delivery", "first-hire"]);
|
||||||
|
expect(completedGoals(goals, progress)).toEqual(completedGoals(goals, { ...progress }));
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue