feat: bootstrap walking skeleton
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildContent } from '../../content/schema';
|
||||
import { createGameState, startAction } from '../game';
|
||||
import {
|
||||
applyOfflineProgress,
|
||||
createSave,
|
||||
deserializeSave,
|
||||
fromExportString,
|
||||
SAVE_VERSION,
|
||||
serializeSave,
|
||||
toExportString,
|
||||
} from '../save';
|
||||
|
||||
function testContent() {
|
||||
return buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||
actions: [
|
||||
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: { resourceId: 'gold', amount: 1 } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function sampleState() {
|
||||
const content = testContent();
|
||||
const state = createGameState(content);
|
||||
state.resources.gold = 12;
|
||||
startAction(state, content, 'forage');
|
||||
state.actionElapsedMs = 500;
|
||||
return state;
|
||||
}
|
||||
|
||||
describe('createSave()', () => {
|
||||
it('stamps the current version and timestamp around a snapshot of state', () => {
|
||||
const save = createSave(sampleState(), 1700);
|
||||
expect(save.version).toBe(SAVE_VERSION);
|
||||
expect(save.savedAt).toBe(1700);
|
||||
expect(save.state.resources.gold).toBe(12);
|
||||
expect(save.state.activeActionId).toBe('forage');
|
||||
});
|
||||
|
||||
it('snapshots state so later mutation does not affect the save', () => {
|
||||
const state = sampleState();
|
||||
const save = createSave(state, 1700);
|
||||
state.resources.gold = 999;
|
||||
expect(save.state.resources.gold).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialize / deserialize round-trip', () => {
|
||||
it('survives JSON serialization unchanged', () => {
|
||||
const save = createSave(sampleState(), 1700);
|
||||
const restored = deserializeSave(serializeSave(save));
|
||||
expect(restored).toEqual(save);
|
||||
});
|
||||
|
||||
it('survives the compressed export string unchanged', () => {
|
||||
const save = createSave(sampleState(), 1700);
|
||||
const restored = fromExportString(toExportString(save));
|
||||
expect(restored).toEqual(save);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid / tampered saves', () => {
|
||||
it('rejects a non-JSON export string cleanly', () => {
|
||||
expect(() => fromExportString('@@@not-a-valid-payload@@@')).toThrow();
|
||||
});
|
||||
|
||||
it('rejects JSON that does not match the save schema', () => {
|
||||
expect(() => deserializeSave('{"version":1,"savedAt":1,"state":{}}')).toThrow();
|
||||
});
|
||||
|
||||
it('rejects an unsupported save version', () => {
|
||||
const future = JSON.stringify({
|
||||
version: 999,
|
||||
savedAt: 1,
|
||||
state: { resources: {}, activeActionId: null, actionElapsedMs: 0 },
|
||||
});
|
||||
expect(() => deserializeSave(future)).toThrow(/version/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOfflineProgress()', () => {
|
||||
it('credits whole ticks of elapsed time to the active action', () => {
|
||||
const content = testContent();
|
||||
const state = createGameState(content);
|
||||
startAction(state, content, 'forage'); // 3000ms per gold
|
||||
const credited = applyOfflineProgress(state, content, 1000, 1000 + 9000); // 9s
|
||||
expect(credited).toBe(9000);
|
||||
expect(state.resources.gold).toBe(3); // 9000 / 3000
|
||||
});
|
||||
|
||||
it('credits nothing when the clock did not advance', () => {
|
||||
const content = testContent();
|
||||
const state = createGameState(content);
|
||||
startAction(state, content, 'forage');
|
||||
expect(applyOfflineProgress(state, content, 5000, 4000)).toBe(0);
|
||||
expect(state.resources.gold).toBe(0);
|
||||
});
|
||||
|
||||
it('clamps credited time to the offline cap', () => {
|
||||
const content = testContent();
|
||||
const state = createGameState(content);
|
||||
startAction(state, content, 'forage');
|
||||
const credited = applyOfflineProgress(state, content, 0, 10_000_000, 6000);
|
||||
expect(credited).toBe(6000);
|
||||
expect(state.resources.gold).toBe(2); // 6000 / 3000
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user