A game: one careful driver against a phantom jam. Can you smooth traffic for all 22 cars?
The ring road has jammed. You drive the black car. Tick the box, then choose one cruising speed: your car holds it smoothly, and never gets closer to the car ahead than is safe. Can one driver dissolve the jam for everyone? Watch the jam meter (the spread of speeds: lower is smoother).
viewof gameOn = Inputs.toggle({label:"I'm driving the black car",value:false})viewof gameV = Inputs.range([1,8], {value:6,step:0.5,label:"My cruising speed (m/s)"})viewof gameReset = Inputs.button("Restart")
// The ring road again. Car 0 is yours: when you drive, it holds your speed smoothly (gain 0.1 per second)// but never accelerates more than the Intelligent Driver Model allows, so it cannot hit the car ahead.game = {const S = gameState, N =22, L = S.L, v0 =12, T =1.2, s0 =2, a =0.5, b =1.5, lc =4, dt =0.1, noise =0.3;const x =Float64Array.from({length: N}, (_, i) => i * L / N), v =newFloat64Array(N).fill(6), acc =newFloat64Array(N);const randn = () =>Math.sqrt(-2*Math.log(1-Math.random())) *Math.cos(2*Math.PI*Math.random());let t =0, nextRecord =0;const step = mine => {for (let i =0; i < N; i++) {const j = (i +1) % N, gap =Math.max(((x[j] - x[i]) % L + L) % L - lc,0.1);const sStar = s0 +Math.max(0, v[i] * T + v[i] * (v[i] - v[j]) / (2*Math.sqrt(a * b)));const idm = a * (1- (v[i] / v0) **4- (sStar / gap) **2); acc[i] = i ===0&& mine !==null?Math.min(idm,0.1* (mine - v[i])) : idm + noise *randn(); }for (let i =0; i < N; i++) { v[i] =Math.max(0, v[i] + acc[i] * dt); x[i] = (x[i] + v[i] * dt) % L; } t += dt; };for (let k =0; k <2000; k++) step(null);// 200 s of warm-up: the jam has formedconst t0 = t;while (true) {const mine = viewof gameOn.value? viewof gameV.value:null;for (let k =0; k <2; k++) step(mine);// 12 times real timeif (t >= nextRecord) {const mean = d3.mean(v), sd = d3.deviation(v); S.hist.push({t: t - t0, sd, mean,you: mine !==null});if (S.hist.length>400) S.hist.shift(); nextRecord = t +1; }yield {x:Array.from(x),v:Array.from(v), L,sd: d3.deviation(v),mean: d3.mean(v)}; }}
{const S = gameState;while (true) {yield Plot.plot({width: plotW,height:280,x: {label:"Time (s)"},y: {label:"Jam meter: spread of speeds (m/s)",domain: [0,4],grid:true},marks: [ Plot.areaY(S.hist.slice(), {x:"t",y:"sd",fill: d => d.you? accent :"#d7263d",fillOpacity:0.12,z:null}), Plot.line(S.hist.slice(), {x:"t",y:"sd",stroke: d => d.you? accent :"#d7263d",strokeWidth:2,z:null}) ] });await Promises.delay(500); }}
md`**Jam meter:** ${game.sd.toFixed(2)} m/s (lower is smoother) · **everyone's average speed:** ${(game.mean*3.6).toFixed(1)} km/h · ${!viewof gameOn.value?"Tick *I'm driving* to take over.": game.sd<0.8?"**You did it.** The jam is gone, and everyone is moving faster than before.":"Keep trying. Hint: drive a little *slower* than feels natural, and let the gap ahead grow."}`
How it works
This is the idea behind a real experiment in Tucson (Stern et al., Transportation Research Part C, 2018), where one automated car damped stop-and-go waves on a ring of about 20 cars. Stuck? Read the hint the game gives you. Lecture 1 shows the ring road; Lab 12 trains a reinforcement-learning car to find a strategy on its own.
theme = Generators.observe(notify => {const read = () =>notify(document.body.classList.contains("quarto-dark") ?"dark":"light");read();const watcher =newMutationObserver(read); watcher.observe(document.body, {attributes:true,attributeFilter: ["class"]});return () => watcher.disconnect();})accent = theme ==="dark"?"#c9a0ff":"#57068c"accentRange = theme ==="dark"? ["#4b3a5c","#ecdcff"] : ["#dccbee","#3a0463"]ink = theme ==="dark"?"#f2f2f2":"black"// Plot width: Plot's default 640 px, except on a phone, where a plot is drawn to fit its card// instead of being shrunk (and its text with it). Changes only when the width actually changes.plotW = Generators.observe(notify => {let last;const read = () => {const main =document.querySelector("main"), fit = main ? main.clientWidth-50:640;const w =!document.querySelector(".reveal") && fit <640?Math.max(240,Math.floor(fit)) :640;if (w !== last) notify(last = w); };read();window.addEventListener("resize", read);return () =>window.removeEventListener("resize", read);})