Chef Overflow: Agent API Reference

Build an AI agent to run your kitchen. Top scores earn automatic admission to Hack the 6ix 2026.

← Back to Game KitchenAPI v2.0.0

1. How the Game Works

Five chefs move on a grid kitchen. You send them to stations with KitchenAPI.command(chefId, stationId). Orders appear at reception stands; each order lists the exact ingredients and preparation state required (raw, chopped, or cooked).

Chefs take ingredients from bins → chop at cutting boards → cook on stoves → drop ingredients onto a plating area → pick the plate back up → deliver it to the reception stand that holds that order.

Plating areas are infinite. Drop any ingredient on one and it accumulates; picking up from an area with items gives the chef a held plate containing those items. There is no plate inventory, no dish rack, and no sink — once a customer finishes eating they simply leave.

The game ends after three expired orders. Score = delivery points + endurance-time bonus after 10 minutes. Difficulty ramps aggressively: the first minute is approachable manually; after that, automation is essentially required.

Game Phases

PhaseApprox. TimeWhat's new
Tutorial0:00 – 1:00Salad & Steak only; generous timers
Ramp1:00 – 2:30Burger unlocks; spawn rate tightens
Automation2:30 – 10:00Pizza → Deluxe Burger → Feast → Supreme Pizza in stages; timers shrink on a curve
Endurance10:00+Full recipe mix, tightest pacing, bonus score tick per second

2. Quick Start

  1. Open index.html in Chrome (or another modern browser).
  2. Open DevTools F12Console.
  3. Paste one of the example agents from Section 8 (or the minimal snippet printed in the console banner). Register it with KitchenAPI.run(fn).
  4. Click Start Game or call KitchenAPI.start().
  5. Iterate: add chefs, more recipes, bussing logic, and rush handling.
Register your agent with KitchenAPI.run(fn) — it survives start() and restarts, and re-pasting replaces the old agent. Raw onTick listeners are cleared on every start(); run() avoids that.
To load an agent from a file instead of the console, inject it via a bookmarklet or a local <script> tag — anything that runs where window.KitchenAPI exists.

3. API Reference

KitchenAPI.version

Type: string. Current value: "2.0.0".

KitchenAPI.run(fn)

Recommended entry point. Registers fn as your agent. It runs once per game frame and survives start() and restarts. Calling run() again replaces the previous agent, so re-pasting your script is always safe (no double-registration).

Signature: fn(state, api, tick), where state is a fresh getState() snapshot, api is KitchenAPI, and tick is { dt, time }. Exceptions thrown inside fn are caught and logged as "Agent error:" — the run continues.

Returns: undefined.

KitchenAPI.run((s, api) => {
  if (!s.running || s.paused) return;
  // your logic here
});
KitchenAPI.start();

KitchenAPI.stop()

Unregisters the agent set by run(). Does not stop the game.

Returns: undefined.

KitchenAPI.plan(fn, opts)

Canonical LLM entry point. Registers an async planner that runs on a cadence (default every 1000ms, clamped 100–60000ms) independent of the game's tick loop. The planner's return value is stored as the current policy and exposed on state.policy and via KitchenAPI.getPolicy(). Re-entrancy is guarded — if a planner call takes longer than the interval, subsequent ticks are skipped rather than queued.

Signature: fn(state, api) → Promise<policy>. Exceptions are caught and logged; the last good policy is preserved on error.

Use plan() together with run() for the two-tier pattern: a slow async planner sets policy, a fast sync executor dispatches commands against it. This is the recommended shape for any agent whose decision step takes longer than a frame (LLMs, MILP solvers, search-based planners).

KitchenAPI.plan(async (s) => {
  // Slow side: call an LLM, solve an LP, whatever takes >16ms.
  // Return any object — it becomes state.policy.
  const urgent = [...s.orders].sort((a, b) => a.timeLeft - b.timeLeft)[0];
  return { focusDish: urgent?.dish || null };
}, { everyMs: 2000 });

KitchenAPI.run((s, api) => {
  // Fast side: dispatch against the current policy.
  if (!s.running || s.paused || !s.policy) return;
  // ...act on s.policy.focusDish with the same per-tick guards as before.
});

KitchenAPI.unplan()

Unregisters the planner. Clears the current policy. Does not affect the run() agent.

KitchenAPI.getPolicy()

Returns the latest value the planner returned (or null if no planner is registered or it hasn't resolved yet).

KitchenAPI.clearListeners()

Removes all event listeners registered via on() / onTick() / etc. The run() agent is re-attached automatically. Useful to reset raw listeners without restarting.

Returns: undefined.

KitchenAPI.start()

Starts or restarts the game. Resets stations, chefs, orders, and score. Clears raw event listeners (those registered with onTick, on, etc.), so re-register them after each start(), or use KitchenAPI.run() instead, which survives restarts automatically.

Returns: undefined.

KitchenAPI.togglePause()

Toggles the pause state. Syncs with the in-game Pause button.

Returns: undefined.

KitchenAPI.command(chefId, stationId)

Signature: (chefId: number, stationId: string) → { success: boolean, error?: string }

Queues pathfinding to a walkable tile adjacent to the target station, then triggers the same interaction as a mouse click. Fails if: game not running or paused, invalid chef id, chef is busy, unknown station id, no adjacent walkable tile, or A* finds no path.

Chef IDs: 04. Station IDs: strings like 'bin_3', 'stove_0', 'reception_2', see Section 4.

const r = KitchenAPI.command(0, 'plating_0');
if (!r.success) console.error(r.error);

KitchenAPI.boost(chefId)

Returns: { success: boolean, error?: string }. Applies a 3.5 s movement speed boost with a 12 s cooldown. Same as pressing B in-game.

KitchenAPI.selectChef(chefId)

Visual selection only (highlights chef in UI). Does not move the chef.

Returns: undefined.

KitchenAPI.getState()

Returns a plain snapshot of the entire game state. Ingredient objects are { ingredient, state }. Plates are { type: 'plate', items: [...] }. The run ends when failedOrders >= maxFailedOrders (currently 3).

{ time: number, // seconds elapsed score: number, difficulty: number, // current multiplier (phase-based curve) phase: string, // "tutorial" | "ramp" | "automation" | "endurance" streak: number, bestStreak: number, rush: { active: boolean, timeLeft: number, cooldown: number }, failedOrders: number, maxFailedOrders: number, // 3, game ends when failedOrders >= this running: boolean, paused: boolean, gameOver: boolean, chefs: [{ id: number, // 0–4 name: string, pos: [x, y], holding: null | { ingredient, state } | { type: "plate", items }, busy: boolean, hasPath: boolean, boostActive: boolean, boostTime: number, boostCooldown: number, stall: number // seconds remaining of commitment-cost stall, 0 if free }], stations: { ingredientBins: [{ id, name, pos, ingredient }], stoves: [{ id, name, pos, cooking, cookTime, maxCookTime, ready, burnt }], cuttingBoards: [{ id, name, pos, busy, processing, processTime, maxProcessTime }], platingAreas: [{ id, name, pos, items }], receptionStands: [{ id, name, pos, order }], trashCans: [{ id, pos }], counters: [{ id, pos, items }] }, orders: [{ id, dish, timeLeft, standId, components }], upcomingOrders: [{ // next 3 orders, pre-rolled — agents can pre-stage dish: string, components: [{ ingredient, state }, ...], etaSeconds: number // estimated seconds until this becomes a live order }], policy: any, // latest value returned by KitchenAPI.plan() planner, or null recipes: object // full internal RECIPES table }

Reception order when present: { id, dish, timeLeft, components }. Stove ready / burnt mirror the UI progress thresholds.

KitchenAPI.getRecipes()

Returns: Array<{ name, icon, difficulty, components }>, static recipe metadata without internal step strings. icon is a short string code (e.g. "STK" for Steak).

Event Registration

Each method returns an unsubscribe function: const off = KitchenAPI.onTick(fn); off();

MethodAlias / event namePayload
KitchenAPI.on(eventName, fn)genericvaries
KitchenAPI.onTick(fn)'tick'{ dt, time }
KitchenAPI.onOrderSpawned(fn)'orderSpawned'{ id, dish, timeLeft, standId, components }
KitchenAPI.onOrderExpired(fn)'orderExpired'{ id, dish, standId }
KitchenAPI.onOrderDelivered(fn)'orderDelivered'{ id, dish, score, streak }
KitchenAPI.onOrderFailed(fn)'orderFailed'{ dish }
KitchenAPI.onPhaseChanged(fn)'phaseChanged'{ phase }
KitchenAPI.onRushBurst(fn)'rushBurst'{ count } — fires when rush activates and spawns a burst of orders simultaneously
KitchenAPI.onGameOver(fn)'gameOver'{ score, time, bestStreak }

4. Station ID Reference

StationID patternCountNotes
Ingredient binbin_0bin_56bin_0 tomato · bin_1 lettuce · bin_2 onion · bin_3 meat · bin_4 dough · bin_5 cheese
Stovestove_0stove_23Cooks raw meat or dough; pick up at ~80% progress; burns if left too long
Cutting boardcutting_0, cutting_12Chops raw ingredients (not dough)
Plating areaplating_0plating_34Infinite. Drop ingredients onto it; empty-handed pickup wraps them into a plate. Holding a plate + interact merges its items back onto the area.
Trashtrash_01Destroys the chef's held item permanently
Countercounter_0counter_N~15Temporary storage; check getState().stations.counters for exact IDs
Reception standreception_0reception_45Deliver matching plate. After eating, the customer leaves and the stand becomes available again.

5. Recipes

DishComponents (ingredient · state)Quick steps
🥗 Saladlettuce · chopped, tomato · choppedChop both → plate → deliver
🥩 Steakmeat · cookedCook meat → plate → deliver
🍔 Burgermeat · cooked, dough · rawCook meat, bun (dough) on plate → deliver
🍕 Pizzadough · cooked, tomato · chopped, cheese · rawCook dough, chop tomato, cheese on plate → deliver
🍔+ Deluxe Burgermeat · cooked, dough · raw, onion · choppedCook meat, chop onion, dough on plate → deliver
🍱 Feast Plattermeat · cooked, lettuce · chopped, tomato · chopped, cheese · rawCook meat, chop veg, cheese on plate → deliver
🍕+ Supreme Pizzadough · cooked, tomato · chopped, onion · chopped, cheese · rawCook dough, chop tomato & onion, cheese on plate → deliver

6. Game Mechanics for Agents

7. Agent Architecture Tips

Prove one recipe end-to-end with one chef before adding more chefs or recipes.

8. Example Agents

These are teaching examples, not optimal solutions. They omit multi-recipe routing, full bussing, boost timing, and rush handling.

Example 1: Single-chef steak bot

Bin → stove → plating area → reception. Plating areas are infinite, so picking up from one with cooked meat on it produces a held plate containing that meat.

KitchenAPI.run((s, api) => {
  if (!s.running || s.paused) return;
  const c = s.chefs[0];
  if (c.busy || c.hasPath) return;
  const steak = s.orders.find(o => o.dish === 'Steak');
  if (!steak) return;

  const h = c.holding;
  const platItems = s.stations.platingAreas[0].items || [];
  const meatReady = platItems.some(i => i.ingredient === 'meat' && i.state === 'cooked');

  if (!h) { api.command(0, meatReady ? 'plating_0' : 'bin_3'); return; }
  if (h.ingredient === 'meat' && h.state === 'raw')    { api.command(0, 'stove_0');   return; }
  if (h.ingredient === 'meat' && h.state === 'cooked') { api.command(0, 'plating_0'); return; }
  if (h.type === 'plate') { api.command(0, steak.standId); }
});

Example 2: Simple multi-chef coordinator

Chef 0 cooks meat; Chef 1 chops lettuce for Salad; Chef 2 plates and delivers the most urgent available order.

KitchenAPI.run((s, api) => {
  if (!s.running || s.paused) return;
  const go = (id, station) => {
    const c = s.chefs[id];
    if (c && !c.busy && !c.hasPath) api.command(id, station);
  };

  // Chef 0: cycle meat through stove → plating_0
  const c0 = s.chefs[0];
  if (!c0.busy && !c0.hasPath) {
    const h = c0.holding;
    if (!h) go(0, 'bin_3');
    else if (h.ingredient === 'meat' && h.state === 'raw')    go(0, 'stove_0');
    else if (h.ingredient === 'meat' && h.state === 'cooked') go(0, 'plating_0');
  }

  // Chef 1: chop lettuce whenever a Salad order exists
  const c1 = s.chefs[1];
  if (!c1.busy && !c1.hasPath && s.orders.some(o => o.dish === 'Salad')) {
    const h = c1.holding;
    if (!h) go(1, 'bin_1');
    else if (h.ingredient === 'lettuce' && h.state === 'raw')     go(1, 'cutting_0');
    else if (h.ingredient === 'lettuce' && h.state === 'chopped') go(1, 'plating_1');
  }

  // Chef 2: pick up the prepared items from plating_1 → deliver most urgent order
  const c2 = s.chefs[2];
  if (!c2 || c2.busy || c2.hasPath) return;
  const urgent = [...s.orders].sort((a, b) => a.timeLeft - b.timeLeft)[0];
  if (!urgent) return;
  const h2 = c2.holding;
  if (!h2)                                          { go(2, 'plating_1');     return; }
  if (h2.type === 'plate' &&  h2.items?.length > 0) { go(2, urgent.standId);  return; }
});

Example 3: Lookahead pre-stager (beats Example 2)

Uses upcomingOrders to start cooking before the order is live. When a Pizza is forecast within 10 seconds, Chef 0 picks up dough and stoves it now — so the dough is already cooked by the time the order spawns. The same pattern generalizes to any cook-bound recipe.

KitchenAPI.run((s, api) => {
  if (!s.running || s.paused) return;
  const c = s.chefs[0];
  if (c.busy || c.hasPath) return;

  // Live work wins over speculative work.
  const livePizza = s.orders.find(o => o.dish === 'Pizza' || o.dish === 'Supreme Pizza');
  const soonPizza = s.upcomingOrders.find(u =>
    (u.dish === 'Pizza' || u.dish === 'Supreme Pizza') && u.etaSeconds < 10
  );
  const want = livePizza || soonPizza;
  if (!want) return;

  const h = c.holding;
  const platItems = s.stations.platingAreas[0].items || [];
  const doughReady = platItems.some(i => i.ingredient === 'dough' && i.state === 'cooked');

  if (!h) { api.command(0, doughReady ? 'plating_0' : 'bin_4'); return; }
  if (h.ingredient === 'dough' && h.state === 'raw')    { api.command(0, 'stove_0');   return; }
  if (h.ingredient === 'dough' && h.state === 'cooked') { api.command(0, 'plating_0'); return; }
});
The key shift vs Example 2: this agent acts on predicted demand, not just observed orders. Combine with onRushBurst to switch into a batch-parallel mode the instant rush hits, and you'll consistently outscore role-split FSMs in endurance.

Example 4: LLM planner + executor scaffold

The canonical shape for any agent whose decision step is slower than a frame. The planner runs every 2 seconds (swap in fetch() to your model of choice); the executor runs every tick and just dispatches against the latest policy. The game loop never blocks on the LLM.

// Replace this with a real model call to Anthropic / OpenAI / your backend.
async function callLLM(state) {
  // ... await fetch('/api/plan', { method: 'POST', body: JSON.stringify(state) })
  // For this skeleton, just pick the most urgent dish.
  const urgent = [...state.orders].sort((a, b) => a.timeLeft - b.timeLeft)[0];
  return { assignments: { 0: urgent?.dish || null } };
}

KitchenAPI.plan(async (s) => await callLLM(s), { everyMs: 2000 });

KitchenAPI.run((s, api) => {
  if (!s.running || s.paused || !s.policy) return;
  const dish = s.policy.assignments?.[0];
  if (!dish) return;
  const order = s.orders.find(o => o.dish === dish);
  if (!order) return;
  const c = s.chefs[0];
  if (c.busy || c.hasPath || c.stall > 0) return;
  // ... your per-recipe dispatch logic here, e.g. for Steak:
  if (dish === 'Steak') {
    const h = c.holding;
    if (!h) api.command(0, 'bin_3');
    else if (h.ingredient === 'meat' && h.state === 'raw')    api.command(0, 'stove_0');
    else if (h.ingredient === 'meat' && h.state === 'cooked') api.command(0, 'plating_0');
    else if (h.type === 'plate') api.command(0, order.standId);
  }
});
Don't await inside run(). The executor runs every frame and must return in <16ms. All slow work belongs in plan().
The planner kicks once immediately on register, so state.policy is populated quickly. Subsequent updates happen on the everyMs cadence; if a planner call exceeds the interval, the next tick is skipped, not queued — so you can't accidentally pile up in-flight LLM calls.