fix(sim): no arrivals at closing time, robust save import
This commit is contained in:
1 parent
73e5ee30b6
commit
a677b9c61c
4 files changed
+51
-5
No files matched your search
+1
-1
@@ -18,7 +18,7 @@ export function tick(state: GameState, catalog: Catalog, deltaMinutes: number):
|
||||
for (let i = 0; i < deltaMinutes && state.minutes < CLOSE_MINUTES; i++) {
|
||||
state.minutes += 1;
|
||||
if (state.minutes === OPEN_MINUTES) events.push({ type: 'shop_opened' });
|
||||
if (state.minutes > OPEN_MINUTES) {
|
||||
if (state.minutes > OPEN_MINUTES && state.minutes < CLOSE_MINUTES) {
|
||||
state.customerAccumulator += (customersPerHour(state.reputation) / 60) * (0.5 + random(state));
|
||||
while (state.customerAccumulator >= 1) {
|
||||
state.customerAccumulator -= 1;
|
||||
|
||||
+16
-3
@@ -16,12 +16,25 @@ export function serialize(state: GameState): string {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
|
||||
const OBJECT_FIELDS = ['date', 'inventory', 'loyalty', 'day', 'prices', 'pendingOrders'] as const;
|
||||
const NUMBER_FIELDS = ['cash', 'minutes', 'reputation', 'rngState', 'nextCustomerId', 'customerAccumulator'] as const;
|
||||
|
||||
export function deserialize(json: string): GameState {
|
||||
const data = JSON.parse(json);
|
||||
if (data.saveVersion !== 1) throw new Error(`Unsupported save version: ${data.saveVersion}`);
|
||||
if (typeof data.cash !== 'number' || !data.date || !data.inventory || !data.loyalty) {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch {
|
||||
throw new Error('Corrupted save');
|
||||
}
|
||||
if (typeof data !== 'object' || data === null || Array.isArray(data)) throw new Error('Corrupted save');
|
||||
const record = data as Record<string, unknown>;
|
||||
if (record.saveVersion !== 1) throw new Error(`Unsupported save version: ${record.saveVersion}`);
|
||||
for (const field of OBJECT_FIELDS) {
|
||||
if (typeof record[field] !== 'object' || record[field] === null) throw new Error('Corrupted save');
|
||||
}
|
||||
for (const field of NUMBER_FIELDS) {
|
||||
if (typeof record[field] !== 'number') throw new Error('Corrupted save');
|
||||
}
|
||||
return data as GameState;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,26 @@ describe('clock', () => {
|
||||
expect(isOpen(s)).toBe(false);
|
||||
});
|
||||
|
||||
it('never brings customers on the closing tick', () => {
|
||||
for (let seed = 1; seed <= 20; seed++) {
|
||||
const s = createNewGame(seed);
|
||||
s.reputation = 100;
|
||||
const arrivalMinutes: number[] = [];
|
||||
let opened = 0;
|
||||
let closed = 0;
|
||||
while (s.minutes < CLOSE_MINUTES) {
|
||||
const events = tick(s, catalog, 1);
|
||||
if (events.some((e) => e.type === 'customer_arrived')) arrivalMinutes.push(s.minutes);
|
||||
opened += events.filter((e) => e.type === 'shop_opened').length;
|
||||
closed += events.filter((e) => e.type === 'shop_closed').length;
|
||||
}
|
||||
expect(arrivalMinutes.length).toBeGreaterThan(0);
|
||||
expect(arrivalMinutes).not.toContain(CLOSE_MINUTES);
|
||||
expect(opened).toBe(1);
|
||||
expect(closed).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('does nothing once closed', () => {
|
||||
const s = createNewGame(1);
|
||||
tick(s, catalog, 24 * 60);
|
||||
|
||||
+14
-1
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { deserialize, loadGame, saveGame, SaveStorage, serialize } from '../../src/sim/save';
|
||||
import { createNewGame } from '../../src/sim/state';
|
||||
import { issueCard } from '../../src/sim/loyalty';
|
||||
import { STARTING_CASH_CENTS } from '../../src/sim/economy';
|
||||
|
||||
function memoryStorage(): SaveStorage {
|
||||
const data = new Map<string, string>();
|
||||
@@ -25,6 +26,18 @@ describe('save', () => {
|
||||
expect(() => deserialize('{"saveVersion":1}')).toThrow('Corrupted save');
|
||||
});
|
||||
|
||||
it('rejects invalid JSON and non-object saves', () => {
|
||||
expect(() => deserialize('{not json')).toThrow('Corrupted save');
|
||||
expect(() => deserialize('null')).toThrow('Corrupted save');
|
||||
expect(() => deserialize('[]')).toThrow('Corrupted save');
|
||||
expect(() => deserialize('42')).toThrow('Corrupted save');
|
||||
});
|
||||
|
||||
it('rejects saves with a missing field', () => {
|
||||
const { day: _day, ...rest } = createNewGame(1);
|
||||
expect(() => deserialize(JSON.stringify(rest))).toThrow('Corrupted save');
|
||||
});
|
||||
|
||||
it('keeps the current and monthly saves apart', () => {
|
||||
const storage = memoryStorage();
|
||||
const s = createNewGame(1);
|
||||
@@ -32,7 +45,7 @@ describe('save', () => {
|
||||
s.cash = 1;
|
||||
saveGame(storage, s, { monthly: false });
|
||||
expect(loadGame(storage, 'current')!.cash).toBe(1);
|
||||
expect(loadGame(storage, 'monthly')!.cash).not.toBe(1);
|
||||
expect(loadGame(storage, 'monthly')!.cash).toBe(STARTING_CASH_CENTS);
|
||||
});
|
||||
|
||||
it('returns null when nothing is saved', () => {
|
||||
|
||||
Reference in new issue
Block a user