e82a11b988
- 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/
68 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|