Moving the claim onto the junction
A driver cannot remember the state of a shared crossing for everyone else. Moving admission to the junction cut collision episodes by roughly a third, at a cost of four to five percent of throughput.
Moving the claim onto the junction
Part six of a series, and a direct sequel to the promise only one driver could read. Read that first, because this article starts from its conclusion: a claim that lives inside one driver's forward view is not there when the other driver needs it.
So the claim has to live somewhere else. This is the story of moving it, in four steps, each of which cost something real.
Before any of it, one note on method, because otherwise the numbers below mean nothing. Every result here is measured over sixteen random seeds of the same busy town, and then checked against the rest of the suite: a clean town, a dense town, junctions with priority signs, junctions with signals, and every one of those mirrored into left hand traffic. A change is not evaluated on the run you happen to be staring at. That rule was learned the expensive way and it appears again halfway down this article.
Step one: stop asking what drivers intend, start asking where they are
The first design that moved any collision number at all was also the simplest, and it works for exactly the reason the three failed shapes did not.
A car that is on a junction connector has already passed the last point where it could have stopped. So every crossing further along that connector, that its rear bumper has not yet cleared, is spoken for. Not because the driver says so. Because of where the car is.
The rule reads only car positions, which every driver already has. It does not depend on lookahead or a remembered promise. Both drivers get the same answer from the same physical state.
It has four guards, and every one of them earns its place:
- The car holding the reservation must still be moving. A stopped car inside a junction is an obstacle, and obstacles are already handled by a different rule.
- It must not itself be stuck behind something. A car that cannot move cannot arrive, so its reservation would never clear.
- If it has already promised this crossing to the driver asking, the promise wins, unless it is too far in to keep it. A driver deep inside a long connector can still stop short of a crossing further along, and its promise says it will.
- The reservation expires after the deadlock timeout, measured at the holder's real speed. Without the bound, a crawling car closes a crossing for as long as it takes to creep there.
Later I counted every refusal across more than two hundred thousand driver and crossing pairs. Two guards do almost all the refusing: the holder's tail is already past the crossing, or it is too far away in time. The other two fire a handful of times between them. That count is what stopped me adding a fifth guard.
The result, and the trade nobody can avoid
Over the sixteen seed sweep, against the previous state:
| before | with reservations | |
|---|---|---|
| two cars overlapping | 2228 | 1658 |
| two cars in one crossing | 5395 | 3937 |
| junction throughput | 270.7 | 258.8 |
| completed trips | 216.4 | 205.0 |
| share of time standing still | 0.097 | 0.128 |
Counted as episodes rather than as samples, so that one long incident does not read as hundreds: crossing episodes fell from 231 to 167, overlap episodes from 170 to 113. The crashes really fell, it is not an artefact of how they are counted.
And the traffic got worse. Four to five percent less throughput, a third more standing around, more trips that ran over their time budget, more waits that were not bounded.
There is no version of this where you get the first column without the second. Fewer crashes means more waiting, because a crash and a wait are two answers to the same question. So this stopped being a technical decision and became a product one: fewer collisions, slower and stickier traffic.
The old liveness numbers are no longer a target. Restoring them by removing the reservations would also restore the crashes. Future changes have to improve the new baseline without losing the safety result.
I think this is the most useful thing in the whole article for anyone building something similar. A simulation of agents in conflict has a safety and liveness trade built into it. You do not get to tune your way out. What you get to do is choose the point deliberately, write it down, and stop pretending that later improvements are free.
Step two: the rule that keeps a jammed town from locking, and how it fooled me
Beside the reservation there is a much older rule: do not enter a junction unless there is room on the other side. A car that enters a full junction stops inside it, and a car stopped inside a junction blocks every movement that crosses its position.
I measured eight variations of that rule. All eight were reverted. Some loosened it, crediting a car ahead with the road it will have cleared by the time the driver gets there. Some tightened it, making drivers count each other in queue order so two approaches cannot both take the last slot.
The loosest useful shape looked like a clear win. On the busy sweep: throughput up five percent, trips up, standing time down almost a fifth, uncomfortable braking down a quarter, unreachable targets down a quarter. Three separate measurements in a row said yes.
Then it ran against the rest of the suite, and the jammed run, left hand traffic with priority signs, fell apart: throughput down nearly nine percent, standing up by over forty percent, deadlock ring breaks up fivefold, unbounded waits up nearly fourfold, trips over budget up nearly threefold. The dense town lost five percent of its throughput and started standing cars inside junctions. The clean town broke a collision rule it had never broken.
The conclusion, which took eight shapes to be sure of:
Loosening the rule buys you a free town and wrecks a jammed one. Every shape that raised throughput where traffic already flowed cost throughput where traffic was already struggling.
Tightening it raises collisions too. Making drivers wait longer at junction mouths did not monotonically reduce crashes. Two of the tightest shapes raised them, one by half.
So the current threshold is a local optimum in both directions, and the rule is what stops a congested town from gridlocking. The standing time it costs a free town is the premium on that insurance.
The method lesson stands on its own: judge a change on the whole suite, never on the run you are looking at. A shape that wins on the busy sweep and loses on the jammed one is not an improvement, it is a change of which regime you are optimising for, and you should at least know that is what you are doing.
Step three: claiming from the approach, which is as far as drivers can go
Next attempt: let a car claim the space it will need from the moment it is committed to entering, not from the moment it is physically inside. Claims are counted in queue order, so two drivers on two approaches cannot both take the last remaining slot.
Before building it, I checked what actually boxes cars into junctions, because the assumption in my own notes was that a low priority driver waits behind a queue that grows until it backs through the junction. That assumption was wrong. Walking the chain of stopped cars in front of every car stuck inside a junction showed that not one of those chains was headed by a driver waiting on a priority rule. They were headed by ordinary waits at junction mouths, and by the exit room rule itself. Give way waits existed, hundreds of them, and the longest was around eight seconds.
Checking the assumption took an afternoon. Building on it would have taken a week and produced nothing.
The claim was built anyway, in three variants, and the pattern was the same as step two. The run with priority signs, which is the one that complains most, got clearly better: its collision family fell by a tenth to a fifth and cars stuck inside junctions roughly halved. The jammed runs paid for it in ring breaks, unbounded waits, and trips over budget. The dense town paid too.
A claim held from the approach, ordered by queue position, is the strongest form of this idea that drivers can implement between themselves. It is not enough, and the reason is now familiar. Every driver is still rebuilding its picture from scratch every tick. Nothing remembers.
Step four: the junction hands out admission
So the junction owns it.
A car approaching a junction submits a request. The junction answers: granted, or wait, and if wait, why. The decision is made once, for the junction, by something that remembers what it decided last tick.
What that let us build, which drivers could not:
A grant holds its space until the car has actually left it. Not until the car enters, not until its front bumper is past. Until its rear is clear. A single crossing conflict inside the junction releases as soon as the rear passes that specific crossing, so a long vehicle does not lock the whole intersection for its entire transit.
Queue order is stable. Each request records an absolute arrival estimate when it becomes ready, and that estimate is what orders the queue afterwards. It does not drift as the car slows down, which means a car that slows because it is waiting does not lose its place because it is waiting.
An exit that is full passes its oldest waiter's priority backwards through cars that are physically stopped, so a queue does not reset its own fairness every time it stops moving.
Priority is a finite advantage, not an infinite one. A car on the main road goes first by a margin measured in time, not by absolute right. Without a bound, a busy main road starves a side road forever, which is realistic and unplayable.
A blocked older waiter lets one compatible car past. If the car at the head of the queue cannot move because something downstream is in its way, holding everyone behind it achieves nothing. One car that does not conflict with it is allowed through, and that permission is withdrawn as soon as the original blocker is no longer the problem.
A car can also meet the same junction twice on one route, which sounds exotic and happens all the time in small towns with loops. Requests are ordered along the route and only the nearest visit is live, with the connector the car is physically on taking precedence until its rear clears. Without that, the far visit replaced the near grant and produced a whole class of phantom holds.
Things that were measured and rejected on the way: reserving an extra empty slot for safety, granting one car per movement, several wider scanning and time window variants, extra ageing in the queue, and two more generous versions of the bypass rule. Each of them either produced circular waits or made waits worse. None of them is obviously wrong on paper, which is why they were measured instead of argued about.
And sometimes the answer is that the town is too small
One result from this work is my favourite, because nothing was built.
The suite kept reporting overlong waits in one particular town. The chains were traced through every seed that produced them, and they always ended on the same three roads. Those roads are about seven and a half, fourteen, and seventeen and a half metres long. They safely hold one, two and three stopped cars respectively. At the moment of the worst wait, one of them had under two metres of free road against a slot requirement of a bit over six.
There was no stale grant, no waiting ring, no scheduling defect. The town was full. The head of each chain kept alternating between physical capacity and ordinary yielding, and every chain ended at a car that was moving.
That is congestion, not starvation. The allowed wait budget for that scenario was raised to match reality, no code changed, and the investigation was written down so nobody, including me in three months, tries to fix it again.
Knowing the difference between a defect and a limit is worth as much as any rule in the system. A rule that "fixes" a capacity limit is a rule that lets cars into spaces they do not fit in.
The driver-only designs worked with two cars and failed when several cars wanted the same space. The junction now owns that space and remembers its grants. Reservations use car positions, not claims about what a driver intends to do.
The change is safer and slower. That is not a loose end hidden by tuning. The measured cost is part of the design, and future changes have to improve on the real baseline.
The next article is about a different kind of wrongness: a symptom I fixed successfully, several times, without once touching the thing that was causing it.
If a claim cannot live in a driver, it has to live on the road. Four steps, each with a real cost. Every number below is a sweep of sixteen seeds, then checked against the whole suite.
Reservations read position, not intent. A car on a connector has passed its last stopping point, so every crossing ahead that its rear has not cleared is spoken for. Both drivers read the same physical fact. Collision episodes fell by roughly a third. Throughput fell four to five percent and standing time rose by a third.
There is no version where you get the first column without the second. Fewer crashes means more waiting, because a crash and a wait are two answers to the same question. That made it a product decision, and the old liveness numbers were rewritten rather than kept as a target to win back.
The exit room rule. Eight variations were measured and all eight reverted. Loosening it buys a free town and wrecks a jammed one. Tightening it raised collisions too. Judge a change on the whole suite, never on the run you happen to be watching.
Claiming from the approach. The strongest form drivers can manage between themselves, and still not enough, because every driver rebuilds its picture from scratch every tick. Nothing remembers.
The junction hands out admission. A car submits a request, the junction grants or says why not, and it remembers what it decided last tick. A grant holds until the car's rear is clear. Queue order is stable. Priority is a finite advantage, not an infinite one, or a busy main road starves a side road forever.
One favourite result: a town that kept reporting overlong waits was simply full. That is congestion, not a defect, and no code changed.
This is the short version. The full article has the reasoning, the numbers, and the parts that went wrong.