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 hooks —
onEndfor 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