deadhead-space-freight-tycoon/packages/schema/test/schemas.test.ts

345 lines
9.5 KiB
TypeScript

import {
DISPATCH_FAILURES,
REPOSITION_FAILURES,
type Contract as SimContract,
type DispatchRecord,
} from "@deadhead/sim";
import { describe, expect, expectTypeOf, it } from "vitest";
import type { z } from "zod";
import {
acknowledgeSummariesRequestSchema,
acknowledgeSummariesResponseSchema,
startUpgradeRequestSchema,
assignPilotRequestSchema,
baySchema,
claimGoalRequestSchema,
companyResponseSchema,
goalStateSchema,
repayLoanRequestSchema,
bayResponseSchema,
buyShipRequestSchema,
companySchema,
contractBoardRequestSchema,
contractBoardResponseSchema,
contractSchema,
createDispatchRequestSchema,
createGuestResponseSchema,
dispatchResponseSchema,
dispatchSchema,
emptyRequestSchema,
ERROR_CODES,
errorResponseSchema,
hirePilotRequestSchema,
leaseBayRequestSchema,
locationStateSchema,
pilotResponseSchema,
pilotSchema,
restPilotRequestSchema,
playerStateSchema,
resolutionSummarySchema,
serverTimeSchema,
shipResponseSchema,
shipSchema,
spendSkillPointRequestSchema,
tutorialStateSchema,
unassignPilotRequestSchema,
type Contract,
type Dispatch,
} from "../src/index";
const company = { id: "co1", name: "Deadhead Freight", credits: 5000, loanBalance: 3500 };
const ship = {
id: "s1",
modelId: "courier",
name: "Dusty Mule",
status: "idle",
locationId: "earth",
dispatchId: null,
pilotId: "p1",
lastArrivalAt: 4_102_444_800_000,
upgrades: { "cargo-bay": 1 },
stats: {
cruiseSpeedKmPerGameHour: 3_000_000,
cargoCapacityTonnes: 23,
fuelRangeMillionKm: 5,
overheadGameHours: 2,
handlingGameHoursPerTonne: 0.02,
},
inShopUntil: null,
shop: { upgradeId: "cargo-bay", level: 1, until: 4_102_444_000_000 },
};
const pilot = {
id: "p1",
name: "Avery Kade",
isAvatar: true,
xp: 120,
level: 1,
skills: { "long-haul": 1 },
skillPointsAvailable: 1,
assignedShipId: "s1",
status: "idle",
locationId: "earth",
dutyHours: 7.5,
dutyLimit: 14,
resting: false,
restEndsAt: null,
};
const bay = { id: "b1", locationId: "earth" };
const location = { id: "mars", discovered: false };
const contract = {
id: "earth:1:0",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 12,
payout: 480,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: 3_600_000,
expiresAt: 7_200_000,
firstContact: false,
};
const dispatch = {
id: "d1",
shipId: "s1",
pilotId: "p1",
contract,
originId: "earth",
destinationId: "moon",
tutorial: false,
departureAt: 4_000_000,
arrivalAt: 4_130_000,
distanceMillionKm: 0.39,
fuelCost: 234,
expectedPayout: 480,
seed: 123456,
status: "in-flight",
resolvedAt: null,
};
const summary = {
dispatchId: "d0",
shipId: "s1",
pilotId: "p1",
originId: "moon",
destinationId: "earth",
tutorial: true,
credits: 240,
deadhead: false,
fuelCost: 0,
goodId: "consumer-goods",
massTonnes: 12,
shipper: "Pacific Orbital Logistics",
loanRepayment: 0,
pilotXp: 10,
discoveredLocationId: null,
scriptedId: "unmarked-crates",
restEndsAt: null,
resolvedAt: 3_900_000,
};
const tutorial = {
loanerLocationId: "earth",
runInFlight: false,
runsCompleted: 2,
dealAvailable: false,
};
const goal = { id: "first-hire", status: "ready" };
const playerState = {
company,
ships: [ship],
pilots: [pilot],
bays: [bay],
locations: [location],
tutorial,
activeDispatches: [dispatch],
summaries: [summary],
goals: [goal],
};
const cases: [string, z.ZodType, unknown, unknown][] = [
[
"server time",
serverTimeSchema,
{ realTime: 1, gameTime: 60 },
{ realTime: "now", gameTime: 60 },
],
["company", companySchema, company, { ...company, credits: 1.5 }],
["ship", shipSchema, ship, { ...ship, status: "lost" }],
[
"ship in the shop",
shipSchema,
{ ...ship, inShopUntil: 4_102_445_000_000 },
{ ...ship, upgrades: { engines: 0 } },
],
[
"ship stats",
shipSchema,
{ ...ship, shop: null },
{ ...ship, stats: { ...ship.stats, cargoCapacityTonnes: 0 } },
],
[
"start upgrade request",
startUpgradeRequestSchema,
{ shipId: "s1", upgradeId: "engines" },
{ shipId: "s1" },
],
["pilot", pilotSchema, pilot, { ...pilot, xp: -1 }],
[
"resting pilot",
pilotSchema,
{ ...pilot, dutyHours: 0, resting: true, restEndsAt: 4_000_000 },
{ ...pilot, dutyHours: -1 },
],
["pilot duty limit", pilotSchema, { ...pilot, dutyLimit: 12 }, { ...pilot, dutyLimit: 0 }],
["rest pilot request", restPilotRequestSchema, { pilotId: "p1" }, { pilotId: "" }],
["bay", baySchema, bay, { id: "b1" }],
["location", locationStateSchema, location, { id: "mars", discovered: "no" }],
["contract", contractSchema, contract, { ...contract, tier: -1 }],
[
"priority contract (CR-007)",
contractSchema,
{ ...contract, priority: true, deliveryWindowGameHours: 4.5 },
{ ...contract, priority: true, deliveryWindowGameHours: -1 },
],
["contract without a good", contractSchema, contract, { ...contract, goodId: "" }],
[
"delivery summary goods (CR-007)",
resolutionSummarySchema,
{ ...summary, goodId: null, massTonnes: null, shipper: null },
{ ...summary, massTonnes: 0 },
],
["dispatch", dispatchSchema, dispatch, { ...dispatch, status: "lost" }],
["resolution summary", resolutionSummarySchema, summary, { ...summary, pilotXp: -5 }],
[
"summary with rest started",
resolutionSummarySchema,
{ ...summary, restEndsAt: 7_500_000 },
{ ...summary, restEndsAt: -1 },
],
["tutorial", tutorialStateSchema, tutorial, { ...tutorial, loanerLocationId: "mars" }],
["goal state", goalStateSchema, goal, { ...goal, status: "done" }],
["repay loan request", repayLoanRequestSchema, { amount: 500 }, { amount: 0 }],
["claim goal request", claimGoalRequestSchema, { goalId: "first-hire" }, {}],
[
"company response",
companyResponseSchema,
{ company },
{ company: { ...company, loanBalance: -1 } },
],
["player state", playerStateSchema, playerState, { ...playerState, ships: null }],
[
"error",
errorResponseSchema,
{ error: { code: "out-of-range", message: "Destination is out of range." } },
{ error: { code: "made-up", message: "?" } },
],
["empty request", emptyRequestSchema, {}, { extra: 1 }],
[
"create guest response",
createGuestResponseSchema,
{ playerId: "pl1", state: playerState },
{ playerId: "pl1" },
],
["contract board request", contractBoardRequestSchema, { locationId: "earth" }, {}],
[
"contract board response",
contractBoardResponseSchema,
{ contracts: [contract], periodEndsAt: 7_200_000 },
{ contracts: [contract] },
],
[
"create dispatch request",
createDispatchRequestSchema,
{ kind: "contract", contractId: "c1", shipId: "s1", pilotId: "p1" },
{ kind: "contract", contractId: "c1" },
],
["dispatch response", dispatchResponseSchema, { dispatch }, { dispatch: {} }],
[
"buy ship request",
buyShipRequestSchema,
{ modelId: "courier", bayId: "b1" },
{ modelId: "courier" },
],
["ship response", shipResponseSchema, { ship }, {}],
["lease bay request", leaseBayRequestSchema, { locationId: "moon" }, { locationId: 3 }],
["bay response", bayResponseSchema, { bay }, { bay: null }],
["hire pilot request", hirePilotRequestSchema, { locationId: "earth" }, {}],
[
"assign pilot request",
assignPilotRequestSchema,
{ pilotId: "p1", shipId: "s1" },
{ pilotId: "p1" },
],
["unassign pilot request", unassignPilotRequestSchema, { pilotId: "p1" }, {}],
[
"spend skill point request",
spendSkillPointRequestSchema,
{ pilotId: "p1", skillId: "heavy-cargo" },
{ pilotId: "p1" },
],
["pilot response", pilotResponseSchema, { pilot }, { pilot: {} }],
[
"acknowledge summaries request",
acknowledgeSummariesRequestSchema,
{ dispatchIds: ["d0"] },
{ dispatchIds: [] },
],
[
"acknowledge a finished upgrade",
acknowledgeSummariesRequestSchema,
{ dispatchIds: [], shipIds: ["s1"] },
{ dispatchIds: [], shipIds: [] },
],
["acknowledge summaries response", acknowledgeSummariesResponseSchema, { acknowledged: 1 }, {}],
];
describe.each(cases)("%s schema", (_name, schema, valid, invalid) => {
it("accepts a valid example", () => {
expect(schema.safeParse(valid).success).toBe(true);
});
it("rejects an invalid example", () => {
expect(schema.safeParse(invalid).success).toBe(false);
});
});
describe("create dispatch request", () => {
it("accepts tutorial and reposition variants", () => {
expect(
createDispatchRequestSchema.safeParse({ kind: "tutorial", contractId: "c1" }).success,
).toBe(true);
expect(
createDispatchRequestSchema.safeParse({
kind: "reposition",
shipId: "s1",
pilotId: "p1",
destinationId: "moon",
}).success,
).toBe(true);
});
});
describe("error codes", () => {
it("include every sim dispatch and reposition validation reason", () => {
for (const reason of [...DISPATCH_FAILURES, ...REPOSITION_FAILURES]) {
expect(ERROR_CODES).toContain(reason);
}
});
it("are unique", () => {
expect(new Set(ERROR_CODES).size).toBe(ERROR_CODES.length);
});
});
describe("sim compatibility", () => {
it("keeps contracts and dispatches assignable to and from the sim types", () => {
expectTypeOf<SimContract>().toExtend<Contract>();
expectTypeOf<Contract>().toExtend<SimContract>();
expectTypeOf<DispatchRecord>().toExtend<Omit<Dispatch, "status" | "resolvedAt">>();
});
});