Coroutines: Directing the Action

You can pause a computation and resume it at will. Now point that power at the oldest dream in game programming: enemy patrols and cutscenes written as straight-line stories, played one slice per frame, while the game never stops breathing.

You will be able to…

The dream, and why game loops crush it

Every game has code like this in its soul:

GUARD PATROL:  step right. step right. step right. one long stride back. repeat.
CUTSCENE:      camel walks in … dramatic pause … caption fades up.

Written as prose, trivial. Written inside a game loop, it shatters — because the loop owns time. Your step function gets one tick, must return now, and is called again a frame later with no memory beyond what you squirrelled away. So the guard's three-line story gets chopped into "where was I?" bookkeeping: a variant of phases, a counter, a match per beat. You built exactly these state machines in Phase 2. They work. And for a boss with a five-act personality, they are misery to write and worse to reread.

What we want is impossible-sounding: the script keeps its shape, and when it says wait () it freezes mid-story until the next tick. But freezing a computation mid-story is precisely what lesson 4.1 taught your code to do. This isn't a metaphor. The whole scheduler is about a dozen lines.

A one-word vocabulary: Wait

Here is the entire coroutine kit, followed by a script to drive through it. Run it and match each printed line to the code:

The output interleaves the two worlds — story lines printed by the script, status lines printed by the driver:

Piece by piece:

But hold on — resume is one line? If the script waits again, who catches it? This is 4.1's deep-handler fact doing real work: the handler travels with the continuation. When the resumed script performs Wait again, that same effect branch inside start fires again, wraps the new pause as Paused k2 — and that value arrives as the return value of continue k (). So resume always hands you the next job, with zero extra machinery. A dozen lines, full coroutines.

Break it on purpose: the one-shot law

Lesson 4.1 warned that continuations are one-shot tickets. Let's see the law enforced — run this:

The first resume j plays the ending ("encore?") and returns Done — but we threw that away and fed the stale j in again. The engine answers with the Continuation_already_resumed exception: a save file this magical can only be loaded once.

The kit is in the starter — keep it as given. Write ticks_to_finish : (unit -> unit) -> int: start the script, resume until it's Done, and return how many resume calls it took. A script that never waits finishes during start, so its answer is 0.

start script gives you the first job, with zero resumes spent so far.
A counting loop: let rec loop count job = match job with Done -> count | Paused _ -> loop (count + 1) (resume job) — then loop 0 (start script).

Scripting a believable guard

Now write behaviour the way the dream promised. A guard pacing his post: three steps out, one per tick, then one long stride home. The script owns an int ref — the piece of world it touches — and reads exactly like his orders. No phases, no counters, no "where was I?".

Keep the kit. Write patrol : int ref -> unit -> unit, a script that, three times over, steps right (x := !x + 1) and then wait ()s — and finally strides all the way back with x := !x - 3. The tests start it and step it tick by tick, checking the position after each slice: 1, 2, 3, then home at 0.

A for _ = 1 to 3 do … done loop with two statements inside: bump the ref, then wait (). After the loop, subtract 3.
Order matters: step then wait. The tests check x right after start, and the first step must already be taken by then.

Directing the whole cast

A real scene has many actors mid-story at once. Because a paused script is just a value, "many actors" is just several jobs — and the director resumes each one once per tick, round-robin. This is genuinely how effect-based schedulers work; OCaml's Eio library is this idea with a queue and better manners. One convenient detail of our kit: resume Done = Done, so you can resume everyone without checking pulses first.

Keep the kit. Write run_both : job -> job -> int. Round after round, resume every job that isn't Done once (resuming a Done job is a harmless no-op, so "resume both" is fine). Stop when both are Done and return the number of rounds. Two already-finished jobs take 0 rounds; otherwise the slowest script sets the total.

One round = resume a and resume b. Since resume Done is a no-op, no pulse-checking needed.
Match on the pair: match a, b with | Done, Done -> rounds | _ -> … — recurse with both new jobs and rounds + 1.

Lights, camera, cutscene

Grand finale: the scheduler meets the canvas. The animation callback below does two jobs — every frame it draws the stage, and every 30th frame it resumes the scene one beat. The cutscene itself is straight-line story: stride, stride, dramatic pause, caption. Count the state machines: zero. (This sample needs the in-page engine's effect support, which this site has; on localhost, the same code drops straight into your canvas-starter template from 1.3.)

Tweak it: lengthen the dramatic pause, add a second caption, make the camel moonwalk back out. Each change is one line of story — not a new state in a machine. That's the trade you made this lesson, and it only cost one effect.

Checkpoint quiz

Loot collected

Coroutines interleave beautifully — on one core, taking turns. Next lesson we stop taking turns: OCaml 5's other superpower runs your code on real cores, simultaneously. Bring a terminal; the browser sits that one out.