Skip to content

Hire a fetcher for one delivery at a time - #254

Draft
Giszmo wants to merge 2 commits into
masterfrom
feat/gig-hiring
Draft

Hire a fetcher for one delivery at a time#254
Giszmo wants to merge 2 commits into
masterfrom
feat/gig-hiring

Conversation

@Giszmo

@Giszmo Giszmo commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Rebased onto master now that #253 is in. Two commits: per-delivery hiring, then Leo's gate on top.

Summary

A worker stayed subscribed to its building until the building's whole wish list was satisfied. After every drop-off it picked its own next resource from attachedBuilding->computeWishedResources(), so the trip belonged to whoever had delivered there last, never to whoever was closest to it and never to another building that wanted a worker more.

First commit: release the unit after every deposit (stopAttachedForBuilding(false)), one gig at a time. Team::updateAllBuildingTasks runs later in the same tick, after every unit has stepped, and considerUnitForBuilding only accepts ACT_RANDOM units, so a unit freed during its own step is back on the market before that tick ends. The random step it starts on release is still in flight while the auction runs, so a unit that wins its own job back pays one tile for it.

Second commit (Leo, after the numbers below): release only while at least 20% of the team's living workers are idle (RELEASE_IDLE_WORKER_PERCENT, Team::idleWorkerShareAtLeast). The auction only moves work when idle workers exist that may by chance stand closer to the next job; with nobody idle it could only hand the trip back to the deliverer a tile later. Otherwise the worker keeps its building and picks its next trip as on master, market path included, so the "market fetch gap" of the first commit only exists while a fifth of the team is idle.

Simulation behaviour changes, so VERSION_MINOR → 97 and the replay floor moves with it. No save-format change.

Measurement of the first commit (release always)

Eight Nicowars on Playground, resumed from a tick-25000 save, run to 40000, both builds on top of #253, ASLR off, MALLOC_PERTURB_=0 (every cell identical across fills 0/85/170). M = master, G = release always.

seed deliveries M / G starvation deaths M / G population M / G
7 3289 / 3258 44 / 35 244 / 277
11 2839 / 2918 23 / 12 228 / 267
12 1792 / 1616 53 / 29 169 / 161
13 2669 / 2752 35 / 30 282 / 298
14 2939 / 3015 21 / 17 287 / 297
15 2987 / 2505 30 / 69 243 / 187
total 16515 / 16064 (−2.7%) 206 / 192 (−6.8%) 1453 / 1487 (+2.3%)

petri2 (four AINone colonies, ten seeds, 120000 ticks): 37559 master vs 37411 (−0.4%), flat per 2048-tick window from the first minutes on (comment below). ~25% of deliveries changed employer the same tick, ~59% re-hired at the same building: the mechanism works, it just moves no work while free workers are plentiful.

Measurement of the gated version

Running; tables follow in a comment.

Feel

Per CLAUDE.md, this changes how the economy feels: while a fifth of the colony is idle, workers no longer "belong" to a building between trips, so units switch employers more often and the "N units working" counters fluctuate. With everyone busy nothing changes.

Verification

  • gig-release-test harness (new, in CI): four of five idle → released; nobody idle → keeps the inn and heads for the next wheat; exactly one in five → released.
  • TestsRunner (187) plus the hiring harness set.

Authored by Carol (first commit) on Leo's idea; gate and rebase by Bob.

🤖 Generated with Claude Code

@Giszmo
Giszmo marked this pull request as draft September 11, 2026 11:33
@Giszmo

Giszmo commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

petri2 (AINone) measurement

Leo asked for the pathfinding bed instead of the AI one: petri2.map (four AINone colonies, the bed behind the #244/#197 numbers), 120000 ticks, seeds 1–10 via GLOB2_PERTURB_SEED, ASLR off, master 4e00c2b17 vs this branch a85c8b0, both with the same tooling counters (GLOB2_WORK deliveries= per team every 512 ticks). Deliveries per team at tick 120000:

seed master t0 / t1 / t2 / t3 gig t0 / t1 / t2 / t3 total master / gig
1 2159 / 295 / 815 / 536 2130 / 304 / 737 / 532 3805 / 3703
2 2163 / 294 / 810 / 537 2165 / 288 / 754 / 545 3804 / 3752
3 2132 / 303 / 790 / 542 2156 / 243 / 734 / 527 3767 / 3660
4 2150 / 290 / 781 / 541 2144 / 304 / 765 / 549 3762 / 3762
5 2181 / 304 / 772 / 549 2171 / 289 / 809 / 535 3806 / 3804
6 2092 / 247 / 781 / 520 2130 / 239 / 792 / 544 3640 / 3705
7 2143 / 274 / 793 / 539 2166 / 308 / 795 / 528 3749 / 3797
8 2157 / 290 / 764 / 540 2150 / 296 / 762 / 538 3751 / 3746
9 2153 / 276 / 761 / 541 2162 / 269 / 789 / 529 3731 / 3749
10 2169 / 294 / 762 / 519 2144 / 286 / 772 / 531 3744 / 3733
all 21499 / 2867 / 7829 / 5364 21518 / 2826 / 7709 / 5358 37559 / 37411 (−0.4%)

Per 20000-tick window, summed over seeds and teams: master 8432 / 5932 / 5907 / 5780 / 5775 / 5733, gig 8393 / 5888 / 5908 / 5762 / 5771 / 5689. Identical within seed-to-seed spread throughout; no window where one build pulls ahead.

Read: on an AINone bed with steady-state economies, per-gig hiring is throughput-neutral. The behavioural change (workers change employer instead of belonging to one building) is real but does not move deliveries here. Together with the Nicowar table above: this change is about the model, not throughput; a bed where buildings actually compete for scarce workers (few workers, several hungry inns) is where a difference would show and that bed does not exist yet.

A worker stayed subscribed to its building until that building's whole
wish list was satisfied. After every drop-off it picked its own next
resource from `attachedBuilding->computeWishedResources()`, so the trip
belonged to whoever happened to deliver there last -- never to whoever
was closest to it, and never to another building that wanted a worker
more.

Release it instead. `Team::updateAllBuildingTasks` runs later in the same
tick, after every unit has stepped, and `considerUnitForBuilding` only
accepts ACT_RANDOM units, so a unit freed during its own step is back on
the market before that tick ends. The random step it starts on release is
still in flight while the auction runs, so a unit that loses its own job
back pays one tile for it.

This is not a new mechanism, only a wider one: the release path already
ran whenever a building wanted nothing the unit could reach, which was
35-40% of deposits in an eight-Nicowar Playground run.

Known gap: fetching a resource out of a market -- an inn pulling fruit a
market holds -- lives only in the self-renewal block this removes, and
the building-side hiring scorer has no exchange option, so that path is
now unreachable. It never fired in six 40000-tick eight-Nicowar runs
(exchange_picked=0, exchange_taken=0), but it is a real gap and porting
the exchange option into `Building::considerUnitForResource` is the fix.
`Building::insertUnitToHarvesting` is left in place for that.

Simulation behaviour changes, so VERSION_MINOR goes to 97 and replays
older than that are refused.

(cherry picked from commit b1ab3cb65b3c2540dfa78077b459dfc78aeed383)

Fable 5.1 helped authoring this commit.
… idle

Per-delivery hiring on its own moved no work: on the petri2 and Nicowar
beds deliveries were flat to within two percent, in the early minutes
as much as later. The auction only changes anything when a building has
to wait for a worker, and with nobody idle it can only hand the trip back
to the unit that just delivered, one tile later.

So gate the release on idle hands. After a deposit the worker goes back
to the pool only when at least RELEASE_IDLE_WORKER_PERCENT (20) of the
team's living workers are idle -- those are the ones that may by chance
stand closer to the next job. Otherwise it keeps its building and picks
its next trip itself, the way master does, including the market path
for an inn's fruit.

Team::idleWorkerShareAtLeast counts workers and idle workers over the
unit table; it runs once per deposit.

The real-engine check in test/GigReleaseHarness.cpp puts a worker at an
inn's door with wheat on its back and deposits: with four of five
workers idle it is released and off the inn's crew; with nobody idle it
keeps the inn and is already heading for the next wheat; with exactly
one in five idle it is released. Built with gig-release-test and run in
CI.

Fable 5.1 helped authoring this commit.
@Giszmo

Giszmo commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Measurement of the gated version (release only while ≥20% of workers are idle)

Same beds and protocol as above, both builds on the current master (cd72d184f, #253 in). ASLR off, MALLOC_PERTURB_=0; every cell identical across fills 0/85/170 on both sides. M = master, G = this branch (f9aacfa).

Eight Nicowars on Playground, tick 25000 → 40000:

seed deliveries M / G starvation deaths M / G population M / G
7 3289 / 3307 44 / 33 244 / 271
11 2839 / 3250 23 / 5 228 / 312
12 1792 / 1735 53 / 19 169 / 180
13 2669 / 2661 35 / 22 282 / 273
14 2939 / 2977 21 / 22 287 / 275
15 2987 / 2274 30 / 43 243 / 164
total 16515 / 16204 (−1.9%) 206 / 144 (−30%) 1453 / 1475 (+1.5%)

Per 2048-tick window, summed over seeds and teams (M / G): 1341 / 1360, 2611 / 2539, 2306 / 2286, 2124 / 2212, 2267 / 2379, 2102 / 2112, 2214 / 1954, 1486 / 1308. Flat to slightly up through tick 36000, then down 12% in the last two windows, the same shape as the ungated version. Seed 11 is a clear win (+14% deliveries, starvation 23 → 5, population +37%); seed 15 a clear loss (−24% deliveries, population −33%); the other four are within a few percent. Starvation deaths fall on five of six seeds.

petri2 (four AINone colonies, ten seeds, 120000 ticks): identical to master on every seed, checksum for checksum. The gate never opened: on that bed fewer than a fifth of the workers are ever idle, so the branch is byte-for-byte master there. That also answers the earlier "why no early-minutes gain": the AINone bed runs at full employment from the first minute.

Read: with the gate the change is neutral on throughput, clearly good on starvation, and does nothing on a fully employed colony. A fire-rate count (how many deliveries were released vs kept) follows in a comment.

@Giszmo

Giszmo commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

How often the gate opens

Same six Nicowar runs, with a counter at the two branches of the post-delivery decision (bench build only). "released" = handed back to the auction because ≥20% of the team's workers were idle; "kept" = stayed with its building as on master.

seed kept released released share
7 2639 537 17%
11 2943 197 6%
12 1374 291 17%
13 2275 289 11%
14 2703 155 5%
15 1937 274 12%
all 13871 1743 11%

So on this bed about one delivery in nine ends in a release, and those are enough to move the starvation numbers above. On petri2 the count is zero.

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