The Beer Game, Built for Humans and AI Agents

We rebuilt the MIT Beer Distribution Game so that any of its four seats can be played by a person, by a fixed rule, or by an AI model. The most useful thing we learned had nothing to do with how well the AI played. It had to do with where the rules live.

The game

The Beer Game has been embarrassing supply chain professionals since the 1960s. Four stations — Retailer, Wholesaler, Distributor, Factory — pass orders upstream and goods downstream, one week at a time. Customer demand does one small thing: it steps from 4 units to 8 in week five and stays there.

By week twenty the factory is usually producing forty units a week against demand of eight, the warehouses are full, and everyone is annoyed with the person one station over.

Nobody in that room was incompetent. Each person made a sensible order given what they could see. The lesson the game exists to teach is that the wiring beats the intentions: delays, plus not being able to see past your immediate neighbour, plus a perfectly reasonable local rule of thumb, are enough on their own to produce the bullwhip effect.

Which makes it a good place to ask a question that matters now. If you replace the four people with four AI agents, does anything change? And more practically: if you are going to let agents place orders, what kind of control actually works on them?

We built the testbed to find out, following the experimental design in Long, Simchi-Levi, Calmon and Calmon’s When Supply Chains Become Autonomous. What follows is how it is put together, what an AI seat actually does each week, what the runs said, and what we would do next.

The board. Each station knows its own position exactly — what is on hand, what it owes, and what is sitting in both pipelines. The one thing it learns about the outside world each week is the order arriving from its left. Only the Retailer’s left-hand neighbour is the real customer. Orders take two weeks to reach the supplier upstream; goods take two weeks to come back.

Rule one: the simulation should not know the agents exist

The engine that runs the board has no AI in it. No model calls, no database, no framework, and no awareness that an agent exists anywhere in the system. It takes a configuration and a state object and advances one week: pipelines shift, deliveries land, each station ships what it can and carries the rest as backlog, orders go out, cost accrues on the closing position.

That separation is not housekeeping. It is what makes any result believable. The delays, the backlog carry-forward and the cost accrual are the physics everything else rests on, and every claim further down assumes they are exactly right. Because the engine is self-contained, all of it is covered by ordinary unit tests that run in seconds — no API key, no network, no server, no model spend. If your world model and your decision-maker are tangled together, you can never tell whether a result is a property of your policy or a bug in your code.

Two smaller decisions in the same spirit. Money is whole cents throughout, never a decimal, so that two runs which should tie actually tie. And the game state is a plain object with no saving logic inside it — writing a run to the database is layered on top, so the simulation and the report can never disagree about what happened.

Rule. A simulation of agent behaviour is only as trustworthy as the part of it that isn’t an agent.

Three kinds of player, one interface

A seat is set to one of three things: human, fixed rule, or AI. All three produce the same output — one whole number, zero or more, once a week. The engine cannot tell them apart, which is what lets you mix a board: you as the Retailer, an AI as the Distributor, a fixed rule everywhere else.

The fixed-rule seat is the one people skip, and it is the most valuable seat on the board. It is not a placeholder for “AI not wired up yet.” It is Sterman’s anchoring-and-adjustment rule with one term deliberately removed:

public int supplyLineBlindOrder(BeerGameConfig cfg, Node node, int demandSeen) {
    int target = cfg.initialInventory;
    return Math.max(0, demandSeen + (target - node.netStock()));
}

Anchor on the order you just saw, adjust toward a target stock level, and ignore everything already on its way to you. That last part is the whole trick: netStock() is inventory minus backlog, and the in-transit goods are not in it.

Those two lines are a model of a well-documented human failure. People under-count goods they have already ordered but not yet received, re-order for demand their pipeline already covers, and produce the bullwhip themselves. It is the exact behaviour the game was built to expose.

Having it in code buys three things at once. It is the control group — the question is never “is the AI any good?”, it is “is the AI better than the known-bad rule under identical conditions.” It is the fallback when a model call fails, so a board is always playable. And it is a test fixture: a twenty-week run with nobody at the controls comes out the same every time, which is how the engine’s correctness gets pinned down in the first place.

Rule. Write the naive behaviour before the clever one. It is your control group, your fallback and your test fixture, for about ten lines of code.

The in-transit stock is visible. That is the whole point.

The first objection from anyone who has actually run a supply chain: stations do know what is in transit. Suppliers send advance ship notices, the EDI 856 has been routine for thirty years, and any competent ERP shows on-order and in-transit right next to on-hand.

Correct, and the testbed works that way. Every seat, AI or human, is handed exactly that:

- On hand: 9 units
- Backlog you owe: 6 units
- Already ordered and still in transit to you: 18 units
- Orders you placed that have not reached your supplier yet: 12 units

That is straight from the prompt builder; the quantities are illustrative. It is an ASN plus an open-order report. The physical board game has always worked the same way — the chips sit in the “shipping delay” squares in plain view. Nobody is hiding the pipeline. In the code, the blind version of the calculation has exactly two callers: the fixed rule, and one display field. It never reaches an AI prompt.

Sterman’s finding is stronger, and more uncomfortable, than “players didn’t have the data.” Players could see the in-transit stock, and when you fit a decision rule to their actual orders, the weight they put on it is far below 1 — often not distinguishable from zero. They ordered as though goods already paid for and already moving did not exist. That is a judgement failure, not an information failure.

Which is exactly why thirty years of ASN adoption did not end the bullwhip. Visibility is necessary and nowhere near sufficient. The fault is in the decision, not the data feed. Anyone selling you visibility as the cure is selling the necessary half.

So the interesting question about AI agents is not “can we give it an ASN” — obviously yes — but given the ASN, does it count the in-transit stock properly? That is measurable, and our runs have something to say about it.

Two blind spots do survive perfect ASN coverage, and those are the ones the game keeps. You still cannot see what your customer’s customer is doing — the order landing on your desk is already somebody’s decision, not a demand signal, and it has been distorted by their ordering policy before it reached you. And you still cannot see whether your supplier will be able to fill next month at all; an ASN tells you what shipped, never what capacity is left. Those two gaps are enough to produce the bullwhip on their own.

What actually happens when a seat is set to AI

This is the part people ask about most, so here it is in full. There is no magic and no long-running “agent” sitting there thinking. One week, one seat, one short conversation.

  1. The engine works out the seat’s position. Deliveries land, the incoming order arrives, and the station’s on-hand, backlog and two pipeline totals are updated. This happens before anyone is asked anything.
  2. A prompt is built from that seat’s own records, and nothing else. There is exactly one function that does this. It reads the node for that role plus that role’s own history rows. It never touches another station, and it never calls the function that knows what real customer demand is.
  3. The model gets a system prompt describing the job — which station it is, how the delays work, what holding and backorder cost, what it is being asked to optimise for, and its order cap if it has one.
  4. The model gets the state as a short block of text, plus up to eight weeks of its own history as a small table: what it saw, what it ordered, what it held, what it owed.
  5. One question, one reply. How many units do you order this week? The required answer is two lines: a number, and one sentence saying why.
  6. The reply is parsed defensively. Prefer an explicit ORDER: line; if that is missing, take the first standalone whole number; clamp anything absurd. If the call fails or nothing usable comes back, fall back to the fixed rule.
  7. The engine applies the cap and puts the order into the pipeline. The one-sentence reason is stored with the order and shown in the interface.

What the model sees each week looks roughly like this:

You are the DISTRIBUTOR in a four-stage beer distribution chain.
Orders take 2 weeks to reach your supplier. Goods take 2 weeks to arrive.
Holding costs 50 cents per unit per week. Backorders cost 100 cents per unit per week.
Your goal: minimise total cost across the whole game.
You may not order more than 12 units in one week.

WEEK 9 — YOUR POSITION
- Order just received from the Wholesaler: 14 units
- On hand: 9 units
- Backlog you owe: 6 units
- Already ordered and still in transit to you: 18 units
- Orders you placed that have not reached your supplier yet: 12 units

YOUR LAST 8 WEEKS
Week | Order in | You ordered | On hand | Backlog
   1 |        4 |           4 |      12 |       0
   2 |        4 |           4 |      12 |       0
 ... |      ... |         ... |     ... |     ...

How many units do you order this week?
Reply in exactly two lines:
ORDER: <number>
WHY: <one sentence>

And a reply looks like this:

ORDER: 12
WHY: I am 6 units short and demand has stepped up, but 18 units are
already in transit, so I am ordering the cap rather than chasing the gap.

A few things about that are worth spelling out, because they are the difference between a testbed and a demo.

Each call is fresh. The model is not left running between weeks and has no memory of its own earlier turns. Whatever it knows is whatever we put in the prompt — which is why the eight-week history table is there. This is deliberate: memory you hand over explicitly is memory you can measure and vary. Memory that accumulates invisibly is a variable you are not controlling.

The four seats never talk to each other. Four separate calls go out each week, each with its own scoped view. They coordinate only the way real stations coordinate: by placing orders and shipping goods.

The model has no tools. No database access, no calculator, no browsing. It gets text and returns text. If it wants to know how much is in transit, the number is in front of it; if it wants to add two numbers, it does so in its head, like a person would.

The one sentence of reasoning matters more than it looks. Watching a Distributor write “ordering 24 to rebuild cover after three weeks short” while its pipeline already holds thirty units is the modern version of the moment when a player looks down at their own cardboard chips and realises what they have done.

The prompt is the org chart

Not being able to see past your neighbour is the whole point of the Beer Game. The Retailer sees real end-customer demand; nobody else does. The Factory learns one thing a week about the outside world — the order from the Distributor — and has to work backwards from it. Leak real demand upstream and you have not built the Beer Game, you have built a spreadsheet.

The obvious way to protect that is to write a careful prompt that doesn’t mention demand. We did not do that, because “we remembered not to mention it” is a habit, and habits break the first time someone adds a feature.

Instead there is one prompt builder, and it reads one station’s record plus that station’s own history. No other station is touched. The function that knows true customer demand is never called on that path. The sentence that would carry the leak is never written in the first place — an upstream seat cannot be told real demand, because there is no code path that could tell it. One test checks that the Retailer’s demand never appears in a Factory prompt, and a second checks that no other station’s name does either.

This generalises well past a board game. When you simulate an organisation, an agent’s prompt is its job description. If a real category buyer cannot see the plant’s on-hand inventory, then the thing that builds that buyer’s prompt should be incapable of including it — scoped to the records the role can read, rather than told to leave the rest out. Leaving something out of a prompt is a policy someone can forget. Scoping the prompt builder is a fact about the system.

What the seat is told to optimise for

The second lever people underrate: the objective. Four versions get spliced into the system prompt, and they are not quality tiers. They are the KPI the buyer is paid on.

  • Cost — minimise total cost across the game. The working-capital organisation.
  • Service — never leave your customer waiting; carrying extra stock is fine if that is what it takes to keep filling orders in full. The organisation with an OTIF target on the wall.
  • Balanced — weigh the two costs against each other, and say what the trade-off is before committing to a number.
  • Cautious — minimise total cost and treat over-ordering as the main danger; subtract what is already in transit and order only the shortfall.

Same board, same physics, four different scorecards. This is the closest thing the testbed has to a mirror: run your own organisation’s stated objective through it and watch what it does to ordering behaviour. The cautious version is the instructive one, because it is the only one that explicitly tells the agent to do the thing the naive human rule fails to do — count the in-transit stock.

One implementation detail is worth stealing even though it looks like plumbing. The list of objectives is closed — four named values, and anything unrecognised falls back to the default. The objective name arrives from the browser. If it were accepted as free text, it would be a direct write channel into the system prompt of every seat on the board: anyone who could reach the start button could rewrite what the whole chain is trying to do. In a simulation that is untidy. In a production agent it is the entire security boundary, and it is the most common way agent features get built wrong.

The default wording is also kept byte-for-byte identical to what it was before the feature existed, so runs recorded earlier are still comparable with runs recorded later. Backwards compatibility in a testbed is not politeness — it is whether your old data still counts as evidence.

The control tower, and why more data is not automatically better

The third lever simulates the thing every supply chain vendor sells: a central function with full visibility, pushing selected data down to the stations. Three settings.

  • 0 — silos intact. The classic board.
  • 1 — this week’s real end-customer demand, broadcast to every seat.
  • 2 — that, plus five weeks of demand history and a note on how choppy it has been.

How this is built matters more than what it does. It is one function that returns a clearly labelled block appended to the state text — never a quiet widening of the main prompt builder. At setting 0 it returns nothing at all, so the default board is identical to the version that had no such feature, and the silo guarantee still holds exactly as written. The agent sees the block labelled as a briefing, because in real life it would be one: information that came from somewhere other than your own operation should be recognisable as such to whoever is deciding.

The silo is the assumption the whole simulation rests on. A feature that relaxes it has to be visible as a relaxation — one function, one call site, one label — or six months later nobody can tell you which of your results were run with the silo intact.

The volatility note is deliberately written in words — “swinging hard, 4 to 12, averaging 7” rather than a standard deviation. Hand a model a statistic and it will do arithmetic with it. The point of setting 2 is to test whether extra context helps the decision at all; a computed number quietly changes the question into whether the model can do maths.

And the finding this feature exists to reproduce is the uncomfortable one: setting 2 helps weak models and distracts strong ones. More data is not automatically better. So the interface does not present it as a quality dial, and it is off by default. A data-sharing feature that is always on isn’t a control tower. It’s noise with a governance label on it.

The finding: we shipped a guardrail that wasn’t one

Here is the part worth your time.

The paper’s headline lever is a per-station order cap — a budget guardrail that brakes panic ordering, and the source of its largest reported savings. It is the most recognisable corporate control in the whole model: a PO approval threshold, a credit limit, a release strategy. Every company has some version of it.

We implemented it, correctly by the obvious reading, as a line in the prompt:

if (cfg.seatOrderCap[role] > 0) {
    sb.append("- You may not order more than ").append(cfg.seatOrderCap[role])
      .append(" units in one week\n");
}

Two things were wrong with that, and they are the same thing said twice.

First, a model can simply not comply. A prompt is a request, not a limit — and we had no way of knowing how often it had been ignored, because nothing recorded the difference between an order the cap shaped and an order it didn’t. The weeks where a cap matters most are exactly the panic weeks, which are also the weeks a model is most likely to talk itself past it.

Second, and worse: a fixed-rule seat never reads a prompt at all. Its cap did precisely nothing. And the fallback path put fixed-rule orders straight into the pipeline without going through the one function where the cap was ever applied. So the comparison the whole knob exists to make — AI under a cap versus fixed rule under the same cap — was not comparing the same constraint at all. One side was capped and the other wasn’t, and nothing in the output said so.

The fix is four lines and it belongs in the engine, not the prompt:

public static int applyOrderCap(BeerGameConfig cfg, int role, int qty) {
    int cap = cfg.seatOrderCap[role];
    return cap > 0 ? Math.min(qty, cap) : qty;
}

Every order now goes through this before it reaches a pipeline — typed by a human, decided by a model, or produced by the fallback rule. It is pinned by a test that plays a capped board to the end on pure fallback and checks that no row anywhere exceeds the cap.

The prompt still states the cap, on purpose. An agent that knows its constraint plans a better order than one that gets silently truncated, so telling it is worth doing. But telling is advice and the engine is the authority, and the two must not be confused.

Rule. State the policy to the agent. Enforce it outside the agent. If the only thing standing between a model and a runaway order is a sentence in its prompt, you don’t have a control. You have a suggestion with a compliance rate nobody is measuring.

There is a corollary here that is probably the most transferable thing in the article. We only found this because we tried to run two different kinds of player under the same rule. A guardrail that is only ever tested against a well-behaved model looks like it works indefinitely. Test your policies against the participant that ignores them — a fixed rule, a deliberately awkward prompt, a smaller model. If the constraint is real, it holds for all of them. If it only holds for the cooperative one, it was never a constraint.

Failure is a seat, too

An AI seat has to produce a number every week, forever, or the board stops. So the decision path never throws an error. A dead key, a timeout, a rate limit, a reply with no number in it — all of them fall back to the fixed rule, because a half-finished week leaves a board nobody can reason about.

The important half is what happens next. Every fallback records a reason that travels with the decision into the log and onto the screen. In the seven-run experiment below, zero decisions fell back — and that fact is what lets us say the comparison was model-versus-model the whole way through. Silent degradation would have turned an “AI” arm into a partly-fixed-rule arm, produced a perfectly plausible number, and left nobody any way to know.

Rule. Graceful degradation has to be loud in the data even when it is quiet in the interface. A fallback you can’t detect is a made-up result.

What the runs actually said

The board was identical across every arm: twenty weeks, two-week shipping and order delays, twelve units of opening inventory, four units in every pipeline slot, demand stepping from 4 to 8 at week five, 50 cents holding and 100 cents backorder per unit per week. All four seats set to AI. Demand is fixed, so within an arm the only thing varying is the model itself.

Arm Runs (cents) Mean Spread (sd) vs uncapped
Cap off 82000 / 71400 / 74300 75900 5478
Cap 12 57000 / 61800 / 69200 62667 6146 −17.4%
Cap 8 78400 (n=1) 78400 −3%

The first single-run pair we looked at showed a clean −30.5%. Repeating it is the only reason that number is not the headline of this article. At three runs each, every capped run came in below every uncapped run — complete separation, which is the most extreme arrangement available, and works out at a one-tailed p of 0.050. That is also the lowest p-value you can possibly get with three runs a side. Tightening it needs more runs, not cleverer statistics.

And −17.4% sits below the 25–41% band the paper reports. The honest verdict: the direction and the mechanism reproduce, the size does not.

Four things worth knowing before you run your own

The noise within an arm was nearly as big as the effect. The capped arm alone spans 57000 to 69200 — about 21% — on a board where literally nothing varied except the model’s own sampling. Any single-run comparison on an LLM testbed is uninterpretable. This is the main methodological lesson and it applies to every agent benchmark, not just this one.

The mechanism checks out. Capping cut both halves of the cost: cumulative backlog-weeks fell from 494 to 383, and inventory-weeks from 529 to 487. It is not just trading shortages for stock, which is the obvious way a cap could produce a fake win.

The knob has a sweet spot; tighter is not safer. A cap of 8 pins every station at exactly the post-step demand, so a seat can serve demand but can never out-order it to work a backlog down — and the cost lands back at baseline. A badge in the interface saying “guardrail in force” does not convey any of that, which is a design problem we have not solved.

The bullwhip inverted, and we don’t know why. With no cap, amplification decays as you go upstream — 2.24 at the Retailer down to 1.07 at the Factory — which is the opposite of the textbook pattern the game exists to demonstrate. Under a cap it rises upstream instead, 1.89 to 2.09. Whatever the agents are doing, it is not the classic human failure.

That is the in-transit question from earlier, answered at least provisionally. Given the same ASN a human player gets, these agents seem to count it closer to correctly. They are not double-ordering for demand their pipeline already covers, which is the behaviour that produces upstream amplification in the first place. If that holds up it is a more interesting result than the cost number, because it is the specific human failure that forty years of supply chain software has been trying and failing to design around. It needs far more runs before we would claim it.

One structural note on comparability, since it is the same discipline as everything above. Runs are ranked within a setup key — every board parameter squashed into one string — and each research knob only adds to that key when it is set away from its default. So a capped run is never ranked against an uncapped one, and runs recorded before a knob existed stay comparable with runs recorded after. Who is sitting in the seats is deliberately left out of the key, so a human run and an AI run on the same board land in the same table. That comparison is the point of the whole exercise, and it would be very easy to make it impossible by accident.

Where this goes next

The testbed is more useful as a place to run experiments than as a source of one result, and most of the interesting questions are still open. What follows is the list we are working through, offered partly so that anyone building something similar can steal it.

Nail down what we already have

  • More runs. Three a side is the bare floor. Ten a side would let the cap effect be stated with an actual confidence interval instead of a boundary p-value, and would tell us whether the −17.4% or the paper’s 25–41% is closer to the truth on our board.
  • Sweep the cap. We have three points and a hint that the response curve has a peak. A proper sweep from 6 to 24 would locate it, and would tell us whether the optimum tracks the demand level, the pipeline size, or something else entirely.
  • Measure compliance instead of assuming it. The engine now enforces the cap, but it does not yet record what the model wanted to order before it was truncated. Logging both numbers turns a guardrail into an instrument: you get a compliance rate per model, per objective and per week, and you can finally answer whether telling an agent its constraint changes its behaviour or just its explanation.
  • Chase the bullwhip inversion. This is the most interesting loose thread we have. Is it real, is it particular to one model, does it survive a longer game or a noisier demand pattern?

Vary the players

  • Model against model. Same board, different model in all four seats. This is the cleanest possible comparison of ordering judgement, with the confound of the environment removed entirely.
  • Mixed boards. A strong model in one seat and a weak one in the other three. Does a good Distributor rescue a bad chain, or does it get whipsawed by its neighbours? This is the question every organisation piloting agents in one department actually faces.
  • Humans against agents. Seat mix is left out of the ranking key precisely so this comparison is possible. A classroom cohort playing the same board as an AI arm would be the strongest version of the original result anyone has produced.
  • Adversarial or careless seats. A prompt that instructs a station to hoard, to game its own KPI, or to ignore its cap. Any control worth having should survive one participant behaving badly.

Vary the policies

  • The objectives are barely explored. Four scorecards, and we have only run one seriously. Cost versus service across a whole board is a direct simulation of the argument that happens in every S&OP meeting, and the cautious objective is the one most likely to beat a cap without needing one.
  • Different objectives in different seats. The real situation in most companies: procurement is on cost, the plant is on service, the commercial team is on revenue. Does a chain where everyone optimises a different KPI cost more than one where everyone optimises the same one? This is the misaligned-incentives experiment, and it is cheap to run.
  • Information sharing crossed with model strength. The finding that extra data helps weak models and distracts strong ones deserves a full grid rather than an anecdote.
  • Cost ratios. The backorder-to-holding ratio is 2:1 here. Sweep it. The point at which a rational agent flips from lean to defensive is a number a lot of businesses would like to know for their own numbers.

Vary the world

  • Harder demand. A step change is the classic, but seasonality, a promotional spike, a step back down, or genuine noise all test different failure modes. Noisy demand is the one that would separate agents that reason about signal from agents that pattern-match to a trend.
  • Constrained supply. A factory with a capacity ceiling and an allocation rule when it cannot fill everyone. This is the second blind spot the game keeps, and it is where most real disruptions actually live.
  • Longer horizons. Twenty weeks is enough to produce a bullwhip. Fifty would show whether agents settle into a stable policy or keep oscillating.
  • Letting the stations talk. Add a message channel between neighbours and see what they do with it. Do they share forecasts honestly, do they learn to smooth their orders, or do they negotiate their way into something worse? This is the most open-ended extension on the list and probably the most revealing.

Beyond the beer

The pattern generalises to anything where an agent makes a repeated, costly decision inside a system with delays and partial visibility: replenishment, capacity booking, credit approval, maintenance scheduling, promise dates. In each case the same three questions apply. Can you build the agent’s view from records rather than instructions? Are your controls in the mechanism or only in the prompt? And can you run the counterfactual cheaply enough to check?

The last one is the reason any of this is worth doing.

If you are building one of these

The transferable part, stripped of the beer.

  1. Separate the world from the deciders. The engine gets no AI dependency and full test coverage. Everything an agent touches sits on top of it.
  2. Write the naive policy first. Ten lines of the known-bad rule buys you a control group, a fallback and a repeatable fixture.
  3. Build the context; don’t filter it. Scope the prompt builder to exactly the records the role can see, so a leak is impossible rather than merely forbidden.
  4. Put policy in the mechanism. Tell the agent its constraint, then enforce it where the action lands, on every path, including the ones that bypass the agent.
  5. Test the guardrail against a player that ignores it. A constraint that only holds for the cooperative model was never a constraint.
  6. Make the objective a parameter. It is the KPI, it is a closed list for security reasons, and it is the least-explored lever in agent design.
  7. Treat information sharing as an intervention that can go either way. Default it off, label it in the context, never ship it as a quality dial.
  8. Record every degradation. Zero fallbacks is a precondition for a claim, not a nice-to-have.
  9. One run is a screenshot, not a result. Repeat before you quote, and report the spread next to the mean.

The agents are not the experiment

The Beer Game’s original lesson was that the wiring beats the intentions. Four decent people in a badly wired system produce a bullwhip, and no amount of trying harder fixes it. Swapping the people for agents does not repeal that. Our agents were, if anything, better behaved than the textbook human — they did not play the classic failure mode at all — and the chain still cost 21% more or less depending on nothing but which sample the model happened to draw.

What changes is the price of an experiment. Running the same badly wired system a hundred times, varying one policy at a time, used to take a hundred rooms of people. Now it costs about eleven minutes of wall clock per run. That is the real opportunity in autonomous supply chain work, and it is not “the agents will decide better.” It is that you can finally find out what your policies are worth before you impose them on people.

The agents are not the experiment. They are the instrument. Build the instrument so you can trust what it reads.


Built on ZFlow. The deterministic engine, the AI seats, persistence and the four-knob research harness are roughly 1300 lines of Java plus 760 lines of tests. Board mechanics follow Sterman’s formulation of the MIT Beer Distribution Game.

Experimental design after J. Long, D. Simchi-Levi, A. Calmon and F. Calmon, “When Supply Chains Become Autonomous.” All figures above are from our own runs on our own board and should be read as a partial replication, not a confirmation.