Files
TheLastGarden/src/sim/garden/growth.test.ts
T
josh e82a11b988 feat(02-02): sim/garden — types, plants table, growth state machine, plantSeed
- src/sim/garden/types.ts: Tile/PlantInstance/PlantType/PlantTypeId/GrowthStage interfaces; tileIdx(row,col) + tileCoords(idx) + emptyTiles() helpers (RESEARCH Pitfall 2 canonical row*4+col encoding)
- src/sim/garden/plants.ts: 3 Season-1 plants per D-03 (rosemary 600t / yarrow 900t / winter-rose 1500t — D-08/D-09 2–5min band) with placeholder tints (D-26)
- src/sim/garden/growth.ts: advanceGrowth() pure function — Sprout (0%) → Mature (33%) → Ready (100%); Math.max clamp on negative deltas defends Pitfall 1 system-clock rewind
- src/sim/garden/commands.ts: plantSeed (D-05 unlock-gate + occupied silent no-op + immutability) + simulateOneTick (BLOCKER 3 — writes tickCount, NEVER lastTickAt) + tileGrowthStage helper
- src/sim/garden/index.ts: barrel
- src/sim/index.ts: re-export garden barrel
- 25 new tests (11 growth + 14 commands) — all green; lint clean; build green
- ESLint sim-purity rule from Plan 02-01 confirms zero Date.now/setInterval call sites under src/sim/garden/
2026-05-09 09:32:59 -04:00

68 lines
2.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { advanceGrowth, GROWTH_THRESHOLDS } from './growth';
import { PLANT_TYPES } from './plants';
import type { PlantInstance } from './types';
const rosemary = PLANT_TYPES.rosemary;
const yarrow = PLANT_TYPES.yarrow;
const winterRose = PLANT_TYPES['winter-rose'];
function plant(plantedAtTick: number, plantTypeId: PlantInstance['plantTypeId'] = 'rosemary'): PlantInstance {
return { plantedAtTick, plantTypeId };
}
describe('advanceGrowth (D-08, D-09; pure function of currentTick + duration)', () => {
it('returns sprout at tick=plantedAtTick', () => {
expect(advanceGrowth(plant(0), rosemary, 0)).toBe('sprout');
});
it('returns sprout just below the 33% mature threshold', () => {
// 600 * 0.33 = 198. Tick 197 is below the threshold.
expect(advanceGrowth(plant(0), rosemary, 197)).toBe('sprout');
});
it('returns mature at the 33% threshold (≥, not >)', () => {
expect(advanceGrowth(plant(0), rosemary, 198)).toBe('mature');
});
it('returns mature just below the ready threshold', () => {
expect(advanceGrowth(plant(0), rosemary, 599)).toBe('mature');
});
it('returns ready at the duration boundary (100%)', () => {
expect(advanceGrowth(plant(0), rosemary, 600)).toBe('ready');
});
it('returns sprout when just planted (currentTick === plantedAtTick != 0)', () => {
expect(advanceGrowth(plant(100), rosemary, 100)).toBe('sprout');
});
it('clamps negative deltas to sprout (Pitfall 1 — system-clock rewind defense)', () => {
expect(advanceGrowth(plant(100), rosemary, 50)).toBe('sprout');
});
it('overgrowth stays at ready (no overflow stage)', () => {
expect(advanceGrowth(plant(0), rosemary, 100000)).toBe('ready');
});
it('respects per-plant duration — yarrow at 900 ticks is ready', () => {
// Yarrow 33% threshold = 297; 900 = ready.
expect(advanceGrowth(plant(0), yarrow, 296)).toBe('sprout');
expect(advanceGrowth(plant(0), yarrow, 297)).toBe('mature');
expect(advanceGrowth(plant(0), yarrow, 899)).toBe('mature');
expect(advanceGrowth(plant(0), yarrow, 900)).toBe('ready');
});
it('respects per-plant duration — winter-rose at 1500 ticks is ready', () => {
// 1500 * 0.33 = 495.
expect(advanceGrowth(plant(0), winterRose, 494)).toBe('sprout');
expect(advanceGrowth(plant(0), winterRose, 495)).toBe('mature');
expect(advanceGrowth(plant(0), winterRose, 1499)).toBe('mature');
expect(advanceGrowth(plant(0), winterRose, 1500)).toBe('ready');
});
it('GROWTH_THRESHOLDS is frozen (no accidental mutation)', () => {
expect(Object.isFrozen(GROWTH_THRESHOLDS)).toBe(true);
});
});