Skip to Content

Coffee Shop

A realistic coffee shop simulation with multiple baristas, different drink types, and customer patience. This is the most comprehensive example, combining several simloop features.

What it demonstrates

  • Resource primitive — baristas as a shared resource with seize/delay/release
  • Event cancellation — patience timeouts cancelled when service starts
  • Entity management — customers as stateful entities
  • Statistics collection — wait times, service times, drink counts
  • Lifecycle hooksonEnd for a formatted report

Configuration

const CONFIG = { numBaristas: 2, avgArrivalInterval: 2.5, // avg minutes between customers maxQueuePatience: 8, // minutes a customer is willing to wait simulationDuration: 480, // 8-hour shift in minutes seed: 12345, }; const DRINKS = [ { name: 'espresso', prepTime: { min: 1, max: 2 }, popularity: 4 }, { name: 'cappuccino', prepTime: { min: 2, max: 4 }, popularity: 3 }, { name: 'latte', prepTime: { min: 2.5, max: 4.5 }, popularity: 2 }, { name: 'cold-brew', prepTime: { min: 0.5, max: 1.5 }, popularity: 1 }, ];

Code

import { SimulationEngine, Resource } from 'simloop'; import type { SimEvent, RequestHandle } from 'simloop'; type DrinkType = 'espresso' | 'cappuccino' | 'latte' | 'cold-brew'; interface DrinkSpec { name: DrinkType; prepTime: { min: number; max: number }; popularity: number; } const DRINKS: DrinkSpec[] = [ { name: 'espresso', prepTime: { min: 1, max: 2 }, popularity: 4 }, { name: 'cappuccino', prepTime: { min: 2, max: 4 }, popularity: 3 }, { name: 'latte', prepTime: { min: 2.5, max: 4.5 }, popularity: 2 }, { name: 'cold-brew', prepTime: { min: 0.5, max: 1.5 }, popularity: 1 }, ]; const CONFIG = { numBaristas: 2, avgArrivalInterval: 2.5, maxQueuePatience: 8, simulationDuration: 480, seed: 12345, }; type CoffeeShopEvents = { 'customer:arrive': { customerId: string }; 'customer:leave': { customerId: string }; 'order:complete': { customerId: string; drink: DrinkType }; }; interface CustomerState { arrivedAt: number; drink: DrinkType; baristaHandle: RequestHandle | null; patienceTimeout: SimEvent | null; } interface CoffeeShopStore { customersInService: number; } function pickDrink(u: number): DrinkSpec { const total = DRINKS.reduce((s, d) => s + d.popularity, 0); let t = u * total; for (const d of DRINKS) { t -= d.popularity; if (t <= 0) return d; } return DRINKS[DRINKS.length - 1]; } const sim = new SimulationEngine<CoffeeShopEvents, CoffeeShopStore>({ seed: CONFIG.seed, maxTime: CONFIG.simulationDuration, logLevel: 'info', name: 'CoffeeShop', store: { customersInService: 0 }, }); const baristas = new Resource<CoffeeShopEvents, CoffeeShopStore>('baristas', { capacity: CONFIG.numBaristas, }); // --- Customer arrives --- sim.on('customer:arrive', (event, ctx) => { const { customerId } = event.payload; const drink = pickDrink(ctx.random()); ctx.stats.increment('totalArrivals'); ctx.log('debug', `${customerId} arrives, wants a ${drink.name}`); ctx.addEntity<CustomerState>({ id: customerId, state: { arrivedAt: ctx.clock, drink: drink.name, baristaHandle: null, patienceTimeout: null }, }); // Patience timeout const patience = ctx.dist.uniform(CONFIG.maxQueuePatience * 0.5, CONFIG.maxQueuePatience)(); const patienceTimeout = ctx.schedule('customer:leave', ctx.clock + patience, { customerId }); ctx.getEntity<CustomerState>(customerId)!.state.patienceTimeout = patienceTimeout; // SEIZE barista const handle = baristas.request(ctx, (ctx) => { const customer = ctx.getEntity<CustomerState>(customerId); if (!customer) { baristas.release(ctx); return; } // Cancel patience timeout if (customer.state.patienceTimeout) { ctx.cancelEvent(customer.state.patienceTimeout); } ctx.store.customersInService++; const waitTime = ctx.clock - customer.state.arrivedAt; ctx.stats.record('waitTimeServed', waitTime); const drinkSpec = DRINKS.find(d => d.name === customer.state.drink)!; const prepTime = ctx.dist.uniform(drinkSpec.prepTime.min, drinkSpec.prepTime.max)(); ctx.schedule('order:complete', ctx.clock + prepTime, { customerId, drink: customer.state.drink, }); }); ctx.getEntity<CustomerState>(customerId)!.state.baristaHandle = handle; // Schedule next arrival const nextArrival = ctx.dist.exponential(1 / CONFIG.avgArrivalInterval)(); const nextId = `customer-${ctx.stats.get('totalArrivals').count + 1}`; ctx.schedule('customer:arrive', ctx.clock + nextArrival, { customerId: nextId }); }); // --- Customer leaves (impatient) --- sim.on('customer:leave', (event, ctx) => { const { customerId } = event.payload; const customer = ctx.getEntity<CustomerState>(customerId); if (!customer) return; const waitTime = ctx.clock - customer.state.arrivedAt; ctx.stats.increment('customersLeftQueue'); ctx.stats.record('waitTimeLeftQueue', waitTime); if (customer.state.baristaHandle) { baristas.cancel(customer.state.baristaHandle); } ctx.removeEntity(customerId); }); // --- Order complete --- sim.on('order:complete', (event, ctx) => { const { customerId, drink } = event.payload; const customer = ctx.getEntity<CustomerState>(customerId); // RELEASE barista baristas.release(ctx); if (customer) { ctx.store.customersInService--; const totalTime = ctx.clock - customer.state.arrivedAt; ctx.stats.increment('customersServed'); ctx.stats.record('totalServiceTime', totalTime); ctx.stats.increment(`drinks:${drink}`); ctx.removeEntity(customerId); } }); // --- Initialize and run --- sim.init((ctx) => { ctx.schedule('customer:arrive', 0, { customerId: 'customer-1' }); }); sim.onEnd((ctx) => { const arrivals = ctx.stats.get('totalArrivals'); const served = ctx.stats.get('customersServed'); const left = ctx.stats.get('customersLeftQueue'); const waitServed = ctx.stats.get('waitTimeServed'); const serviceTime = ctx.stats.get('totalServiceTime'); const util = ctx.stats.get('resource.baristas.utilization'); const queueLen = ctx.stats.get('resource.baristas.queueLength'); console.log(`Simulation time: ${ctx.clock.toFixed(0)} minutes`); console.log(`Arrivals: ${arrivals.count}, Served: ${served.count}, Left: ${left.count}`); console.log(`Avg wait (served): ${waitServed.mean.toFixed(2)} min`); console.log(`Avg service time: ${serviceTime.mean.toFixed(2)} min`); console.log(`Barista utilization: ${(util.mean * 100).toFixed(1)}%`); console.log(`Max queue length: ${queueLen.max}`); }); const result = sim.run();

Key patterns

  • Resource replaces manual queue management — no need for custom barista entities, queue state, or assignment helpers
  • Patience timeout + cancellation — a timeout event is scheduled on arrival; if service starts first, the timeout is cancelled via ctx.cancelEvent()
  • Barista queue withdrawal — if the customer leaves before being served, baristas.cancel(handle) removes them from the waiting queue
  • Resource auto-stats — utilization, queue length, wait times are all collected automatically
Last updated on