From af10c3e8fd896cc18be53401ba94e18abf4fc30e Mon Sep 17 00:00:00 2001 From: Victor Bodinaud Date: Thu, 8 Oct 2026 12:40:10 +0200 Subject: [PATCH] feat(sim): clock tick and events --- src/sim/clock.ts | 31 +++++++++++++++++++++++++++++++ tests/sim/clock.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/sim/clock.ts create mode 100644 tests/sim/clock.test.ts diff --git a/src/sim/clock.ts b/src/sim/clock.ts new file mode 100644 index 0000000..a48e52b --- /dev/null +++ b/src/sim/clock.ts @@ -0,0 +1,31 @@ +import { Catalog } from './catalog'; +import { Customer, customersPerHour, spawnCustomer } from './customers'; +import { random } from './rng'; +import { CLOSE_MINUTES, GameState, OPEN_MINUTES } from './state'; + +export type SimEvent = + | { type: 'shop_opened' } + | { type: 'customer_arrived'; customer: Customer } + | { type: 'shop_closed' }; + +export function isOpen(state: GameState): boolean { + return state.minutes >= OPEN_MINUTES && state.minutes < CLOSE_MINUTES; +} + +/** Advances the clock minute by minute, stopping at closing time. */ +export function tick(state: GameState, catalog: Catalog, deltaMinutes: number): SimEvent[] { + const events: SimEvent[] = []; + 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) { + state.customerAccumulator += (customersPerHour(state.reputation) / 60) * (0.5 + random(state)); + while (state.customerAccumulator >= 1) { + state.customerAccumulator -= 1; + events.push({ type: 'customer_arrived', customer: spawnCustomer(state, catalog) }); + } + } + if (state.minutes === CLOSE_MINUTES) events.push({ type: 'shop_closed' }); + } + return events; +} diff --git a/tests/sim/clock.test.ts b/tests/sim/clock.test.ts new file mode 100644 index 0000000..8c95419 --- /dev/null +++ b/tests/sim/clock.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { isOpen, tick } from '../../src/sim/clock'; +import { CLOSE_MINUTES, createNewGame, OPEN_MINUTES } from '../../src/sim/state'; +import { testCatalog } from './helpers'; + +const catalog = testCatalog(); + +describe('clock', () => { + it('opens the shop at 9:00 with no customers before', () => { + const s = createNewGame(1); + const events = tick(s, catalog, 60); + expect(s.minutes).toBe(OPEN_MINUTES); + expect(events).toEqual([{ type: 'shop_opened' }]); + expect(isOpen(s)).toBe(true); + }); + + it('brings customers during opening hours and closes at 19:00', () => { + const s = createNewGame(1); + s.reputation = 100; + const events = tick(s, catalog, 24 * 60); + expect(s.minutes).toBe(CLOSE_MINUTES); + expect(events.filter((e) => e.type === 'customer_arrived').length).toBeGreaterThan(10); + expect(events[events.length - 1]).toEqual({ type: 'shop_closed' }); + expect(isOpen(s)).toBe(false); + }); + + it('does nothing once closed', () => { + const s = createNewGame(1); + tick(s, catalog, 24 * 60); + expect(tick(s, catalog, 60)).toEqual([]); + expect(s.minutes).toBe(CLOSE_MINUTES); + }); +});