Skip to content

Traffic lanes: narrow passages become one-way from traffic history (draft) - #213

Closed
Giszmo wants to merge 1 commit into
feat/pathfinding-round-tripfrom
feat/pathfinding-traffic-lanes
Closed

Giszmo wants to merge 1 commit into
feat/pathfinding-round-tripfrom
feat/pathfinding-traffic-lanes

Conversation

@Giszmo

@Giszmo Giszmo commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #193 (round trip); one commit. Split out of the old #193 so it can be judged on its own evidence.

What it does. Units record the direction they step in, per cell (one Uint8 per cell and direction, halved every 1024 ticks). Building a gradient charges an extra cost, per edge, for entering a passage against its prevailing direction, in proportion to the excess of opposing over same-way units (full penalty at 16 units of excess, worth three tiles); directionByGradient sees the same cost. The first units through a gap set its direction and later units heading the other way take another gap; on open ground and beside a single wall nothing changes. Derived state, not saved; one shared traffic layer for all teams.

A passage is a cell in a channel at most two cells wide across one axis: walls on both sides, or a wall on one side and a wall right behind the free cell on the other (new since the old #193, which only knew 1-wide gaps). Each column of a 2-wide channel is its own passage, so the two take opposite directions on their own. That is the typical human layout: paired buildings that will merge after an upgrade leave 2-wide channels between them until then, and inns north of other buildings leave 1-wide ones.

Evidence. Nicowar never builds such layouts, so Nicowar-vs-Nicowar games cannot show the effect (they were flat within noise in the old #193, and are unchanged here). The fixture is a mid-game save of exactly such a base by @Giszmo (FourSquares1_testing.game, team 0 human with a base of 2-wide channels, teams 1 and 2 Nicowar), run headlessly for about 16 000 ticks after the save point with the human team working its existing orders. A loaded game is deterministic, so the synchronous random generator was reseeded after loading with 12 different seeds per build (different sidestep and AI dice, same layout and state); means ± standard error over the 12, per 1000 ticks of play. "Blocked" counts non-strict directionByGradient calls that found no step at all.

team metric master #193 (round trip) #193 + lanes lanes vs master lanes vs #193
0 (human) deliveries / 1000 ticks 64.6 ± 0.9 61.8 ± 1.1 64.1 ± 1.1 −0.6 % (t=−0.3) +3.8 % (t=+1.5)
0 (human) tiles per round trip 20.22 ± 0.14 20.71 ± 0.17 20.25 ± 0.25 +0.1 % (t=+0.1) −2.3 % (t=−1.6)
0 (human) blocked ticks / 1000 ticks 78.1 ± 2.1 70.5 ± 2.1 51.6 ± 1.5 −34 % (t=−10.5) −27 % (t=−7.4)
1 (Nicowar) deliveries / 1000 ticks 91.3 ± 2.3 102.1 ± 3.2 99.6 ± 2.5 +9.1 % (t=+2.4) −2.5 % (t=−0.6)
1 (Nicowar) tiles per round trip 22.46 ± 0.33 21.36 ± 0.19 20.65 ± 0.28 −8.0 % (t=−4.2) −3.3 % (t=−2.1)
1 (Nicowar) blocked ticks / 1000 ticks 85.0 ± 7.9 94.5 ± 6.3 73.0 ± 4.3 −14 % (t=−1.3) −23 % (t=−2.8)
2 (Nicowar) deliveries / 1000 ticks 104.8 ± 3.1 107.5 ± 1.7 106.3 ± 3.1 +1.5 % (t=+0.3) −1.1 % (t=−0.3)
2 (Nicowar) tiles per round trip 21.74 ± 0.34 21.06 ± 0.27 20.78 ± 0.20 −4.4 % (t=−2.4) −1.3 % (t=−0.8)
2 (Nicowar) blocked ticks / 1000 ticks 84.6 ± 6.3 94.3 ± 7.5 65.1 ± 4.9 −23 % (t=−2.4) −31 % (t=−3.2)

Reading: on the human base the lanes cut blocked ticks by a third against master (and by a quarter against #193), with trips and deliveries unchanged; on the two AI teams blocking drops by a quarter to a third and trips get a few percent shorter. Output does not move: the units were blocked for a fraction of a tick per trip, so unblocking them shows in smoothness, not in deliveries. The old scripted choke fixture (3-thick wall, 1/2/4 one-wide gaps) from the previous #193 description still applies: two gaps, blocked ticks −64 %, deliveries +4.5 %; one or four gaps, no change.

Cost, and why this is a draft. Whole-game wall time on top of #193 (bash time, alternated runs): 128² Mazury, 2 Nicowars, 15360 ticks: 3.97 s → 4.39 s (+11 %); 512² Mazury tiled 4x4, 8 Nicowars, 9216 ticks: 46.9 s → 57.4 s (+22 %). That is far more than the 3 to 5 % measured before #184's optimised propagation loop, because the lane check now runs per popped cell inside that loop (anyTraffic load plus up to eight gradient lookups in isPassage) for every field of every tick, and the traffic layer is 8 bytes per cell (2 MB on 512²) that the loop touches. Plan before asking for review: keep a passage bitmap per map, maintained incrementally where cases change (buildings, resources), so the loop tests one bit and one 8-byte load, and touch the traffic counters only for passage cells. If that does not bring the cost under a few percent on 512², this stays a draft.

Known limits. One traffic layer for all teams (an enemy's traffic through a gap counts). The three constants (half-life 1024 ticks, full penalty at 16 excess units, three tiles) are first values. Traffic is not saved, so a loaded game starts with no lanes and re-learns them within a few hundred ticks.

Determinism: two runs of the same seed give identical checksum sidecars (the traffic counters are simulation state derived from unit steps, so they are the same on every peer). Tests: the three GradientTest cases from the old #193 (seeds beyond the bucket window, cost bound, lane penalty) plus the existing suites and harnesses.

Units record the direction they step in, per cell (one counter per cell
and direction, halved every 1024 ticks). Building a gradient charges an
extra cost, per edge, for entering a passage against its prevailing
direction, in proportion to the excess of opposing over same-way units,
up to three tiles; directionByGradient sees the same cost. The first
units through a gap set its direction and later units heading the other
way take another gap; on open ground and beside a single wall nothing
changes. Derived state, not saved.

A passage is a cell in a channel at most two cells wide across one axis:
walls on both sides, or a wall on one side and a wall right behind the
free cell on the other. Each column of a 2-wide channel is its own
passage, so the two take opposite directions on their own.

Rebased onto the merged weighted pathfinder: the penalty is applied per
edge inside the optimised propagation loop, which otherwise shares the
two terrain costs of a cell across its eight reverse edges.

Fable 5.1 helped authoring this commit.
@Giszmo
Giszmo force-pushed the feat/pathfinding-traffic-lanes branch from 6fbf26d to dc5eb8a Compare September 10, 2026 20:39
@Giszmo

Giszmo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #244 (the round-trip successor of #193): the branch is now the single lanes commit dc5eb8a on top of 62aa2dc. On Leo's petri map (no walled passages) it measures identical to #244, as expected; the choke-point evidence from the #193 thread still stands as the only case where the lane rule fires.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant