deadhead-space-freight-tycoon/packages/engine/test/fuel.test.ts

226 lines
8.5 KiB
TypeScript

import { content } from "@deadhead/content";
import { emergencyFuel, fuelPriceAt, tripFuel } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import { fullSave } from "../../schema/test/save-fixture";
import { shipState } from "../src/game/state";
import { encodeSave } from "../src/index";
import {
call,
edit,
getState,
putBoard,
readSave,
setCredits,
signUp,
signUpOwner,
testContract,
useTestApp,
} from "./support";
const testApp = useTestApp();
const tank = (id: string) => content.shipModel(id)!.tankCapacityFu;
const price = (id: string) => fuelPriceAt(content.location(id)!, content.config);
describe("fuel tanks in the engine (Step 15.4)", () => {
it("delivers a Market ship with a full tank, bought at the bay's price, on Manual (CR-018)", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, content.shipModel("freighter")!.price);
const { ship } = await app.call("buyShip", { modelId: "freighter", bayId: owner.earthBayId });
expect(ship).toMatchObject({
fuelFu: tank("freighter"),
fuelAvgPrice: price("earth"),
refuelPolicy: "manual",
});
// The tank was included in the price.
expect((await getState(app)).company.credits).toBe(0);
});
it("hands over the deal's ships full, at Earth's price, on Manual (CR-018, decisions 0.5.111, 0.5.112)", async () => {
const { app } = testApp();
await signUp(app);
await edit(app, (save) => {
save.player.tutorialRunsCompleted = content.config.tutorialRunsBeforeDeal;
save.player.loanerLocationId = "earth";
});
const state = await app.call("acceptDeal", {});
for (const ship of state.ships) {
expect(ship.fuelFu, ship.modelId).toBeCloseTo(tank(ship.modelId));
expect(ship.fuelAvgPrice).toBe(price("earth"));
expect(ship.refuelPolicy).toBe("manual");
}
});
it("burns the leg from the tank and, switched to Auto, tops up on landing at the local price", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 10_000);
await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "auto" });
const contract = testContract();
await putBoard(app, [contract]);
const { dispatch } = await app.call("createDispatch", {
kind: "contract",
contractId: contract.id,
shipId: owner.shipId,
pilotId: owner.avatarId,
});
expect((await getState(app)).company.credits).toBe(10_000);
clock.now = dispatch.arrivalAt;
const state = await getState(app);
const [summary] = state.summaries;
const bought = summary!.topUp!;
expect(bought.locationId).toBe("moon");
expect(bought.boughtFu).toBeCloseTo(dispatch.fuelBurnedFu);
expect(bought.cost).toBe(Math.round(dispatch.fuelBurnedFu * price("moon")));
expect(state.company.credits).toBe(10_000 + summary!.credits - bought.cost);
expect(state.ships[0]!.fuelFu).toBeCloseTo(tank("courier"));
});
it("leaves the tank as it landed, and spends nothing on fuel, on Manual (the default)", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 10_000);
expect((await readSave(app)).ships.find((s) => s.id === owner.shipId)!.refuelPolicy).toBe(
"manual",
);
const contract = testContract();
await putBoard(app, [contract]);
const { dispatch } = await app.call("createDispatch", {
kind: "contract",
contractId: contract.id,
shipId: owner.shipId,
pilotId: owner.avatarId,
});
clock.now = dispatch.arrivalAt;
const state = await getState(app);
expect(state.summaries[0]!.topUp).toBeNull();
expect(state.company.credits).toBe(10_000 + state.summaries[0]!.credits);
const saved = (await readSave(app)).ships.find((s) => s.id === owner.shipId)!;
expect(saved.fuelFu).toBeCloseTo(tank("courier") - dispatch.fuelBurnedFu);
});
});
/** The fixture as a version-1 save, before fuel tanks (CR-015). */
function versionOneSave() {
const old = JSON.parse(JSON.stringify(fullSave())) as {
saveVersion: number;
ships: Record<string, unknown>[];
dispatches: { record: Record<string, unknown>; resolution: Record<string, unknown> | null }[];
};
old.saveVersion = 1;
for (const ship of old.ships) {
delete ship.fuelFu;
delete ship.fuelAvgPrice;
delete ship.refuelPolicy;
}
for (const dispatch of old.dispatches) {
delete dispatch.record.fuelBurnedFu;
if (dispatch.resolution) delete dispatch.resolution.topUp;
}
return old;
}
describe("fuel commands (Step 15.6)", () => {
it("opens an older save with every ship full, on Manual (CR-018)", async () => {
const { app } = testApp();
const { state } = await app.call("importSave", {
code: await encodeSave(versionOneSave() as never),
});
for (const ship of state.ships) {
expect(ship.fuelFu, ship.name).toBeCloseTo(ship.stats.tankCapacityFu);
expect(ship.refuelPolicy).toBe("manual");
}
});
it("refuels where the ship is, charging the local price, and keeps the average", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 1_000);
await edit(app, (save) => {
Object.assign(
save.ships.find((s) => s.id === owner.shipId)!,
{
fuelFu: 10,
fuelAvgPrice: 11,
},
);
});
const { ship } = await app.call("refuelShip", { shipId: owner.shipId, amountFu: 20 });
expect(ship.fuelFu).toBeCloseTo(30);
// 10 fu bought at 11 cr/fu, then 20 fu at Earth's price.
expect(ship.fuelAvgPrice).toBeCloseTo((10 * 11 + 20 * price("earth")) / 30);
expect((await getState(app)).company.credits).toBe(1_000 - Math.round(20 * price("earth")));
const filled = await app.call("refuelShip", { shipId: owner.shipId });
expect(filled.ship.fuelFu).toBeCloseTo(tank("courier"));
await expect(app.call("refuelShip", { shipId: owner.shipId })).rejects.toMatchObject({
code: "tank-full",
});
});
it("refuses to refuel without the credits, changing nothing", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 0);
await edit(app, (save) => {
save.ships.find((s) => s.id === owner.shipId)!.fuelFu = 1;
});
const before = await readSave(app);
const response = await call(app, owner, "POST", `/api/ships/${owner.shipId}/refuel`, {});
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({ error: { code: "insufficient-funds" } });
expect(await readSave(app)).toEqual(before);
});
it("switches a ship between Auto top-up and Manual", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
const manual = await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "manual" });
expect(manual.ship.refuelPolicy).toBe("manual");
const auto = await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "auto" });
expect(auto.ship.refuelPolicy).toBe("auto");
});
it("refuses emergency fuel when it isn't needed", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await expect(app.call("orderEmergencyFuel", { shipId: owner.shipId })).rejects.toMatchObject({
code: "emergency-fuel-unavailable",
});
});
it("adds emergency fuel and exactly its cost to the loan", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 0);
await edit(app, (save) => {
save.ships.find((s) => s.id === owner.shipId)!.fuelFu = 1;
save.company.loanBalance = 5_000;
});
const save = await readSave(app);
const offer = emergencyFuel(
{
ship: shipState(save.ships.find((s) => s.id === owner.shipId)!),
credits: 0,
discoveredIds: new Set(save.discoveredLocations.map((d) => d.locationId)),
},
content,
)!;
const needed = tripFuel(
content.shipModel("courier")!,
content.location("earth")!,
content.location("moon")!,
content.config,
);
expect(offer.amountFu).toBeCloseTo(needed - 1);
expect(offer.cost).toBe(Math.round((needed - 1) * price("earth") * 2.5));
const { ship } = await app.call("orderEmergencyFuel", { shipId: owner.shipId });
expect(ship.fuelFu).toBeCloseTo(needed);
const state = await getState(app);
expect(state.company.loanBalance).toBe(5_000 + offer.cost);
expect(state.company.credits).toBe(0);
});
});