1. Method
Read: source/treadcore-brain/src (8,458 lines on 18 October: commander.rs 2,966, lib.rs 2,150, hop.rs 766, fleet.rs 719, tactics.rs 562, fortify.rs 538, crew.rs 486, rotation.rs 271) and the sim's observe and routing. The key claims were checked against the source. Nothing was run for the report: every number in sections 2 and 6 to 9 is a code constant or a starting guess, never a measurement. The measurement is step 12a.
2. The brain as it stood on 17 October (archived)
Archived 18 October 2026: the baseline. This is the brain step 12 started from, kept as the development record and as the baseline the census measures against. The brain now is section 20; what replaced each method here, and what is still current, is
archive/BRAIN-METHODS.md. Findings L1 to L8 keep their numbers, since the rest of the report cites them.
Who runs, how often
| Level | Code | Rate | Output |
|---|---|---|---|
| Fleet commander | crew::FleetCommand in the Leader's or Builder's Commander | 20 Hz; musters every 30 s | Request{Muster, Build, Fetch, Haul} events, pings |
| Commander, one per tank | commander.rs think_map | 20 Hz | Vec<Command> |
| Auto ladder | auto(): DefendBase > Fortify > Turrets > Hunt > Expand > Guard | 20 Hz | a goal |
| Wingman | fly_wing: Defend > Ahead > Pillbox > Depot > Help > Regroup | 20 Hz, 3 s commit | a Move to a task or wedge slot |
| Doctrine, the inner RuleBot | lib.rs think_map | 4 Hz (rookies 2 Hz) | drones, bubble, gun; its Moves dropped by keep_inner |
| Tactician | tactics.rs | each doctrine think | target score, Lanchester odds (ours squared over theirs squared), Mass/Engage/Press, kite |
| Hop | hop.rs | each doctrine think | bridge (at most BRIDGE_MAX 20 water tiles), boat, landfall |
| Plan, Rotation, Crew | fortify.rs, rotation.rs, crew.rs | 20 Hz | next build item, push and defence points, the job |
| Sim router | Map::route_by, A* on 128 x 128 | per Move | a route kept until arrival or Stop |
Fleet commander (Leader or Builder)
| one-tick events: Request, Pinged (L5)
v
Commander, each tank, 20 Hz, ~17 branches, first match wins (L1)
| <-- observation: a full world copy per bot per tick -- (L2)
| --> commands --> sim router, A* per Move
+-- Auto ladder (6 rungs)
+-- Wingman (3 s commit) (L7)
+-- Crew job (Build / Fetch / Haul)
+-- Rotation, fortify plan
+-- Doctrine, 4 Hz (its Moves dropped)
+-- Tactician (22 weights)
+-- Hop (bridge <= 20, failures not cached) (L4)
+-- Dodge (indirect shells only)The Commander's branches, in order, first return wins: downed, dodge, revive, bridge help, shore gather, afloat, chain help, pings and rally, fleet, crew job, leader takeover, scoot, needs_home, goal dispatch, doctrine guns and drones, alliances.
Findings
- L1. Decisions are first-match ladders. Weights exist only in
Tactics(22 coefficients); the Auto ladder and the wingman's priorities are fixed orders with on/off rungs. - L2. Every bot gets a fresh copy of the world every tick.
World::observeclones every depot, pillbox, structure, player name and visible tank (routeVecand all) per bot, 20 times a second, and runs two 41 x 41 terrain scans with sorts; RuleBot and Defender think at 4 Hz. - L3. Repeated work.
auto()cloned the wholePlanand ranadd_turretson the copy every tick for two yes/no answers;Tacticianbuilt aVec<String>of reasons per candidate; sixwhystrings and adoingstring were formatted every tick. - L4. A failed bridge search was never cached.
Map::crossingis a Dijkstra with no heuristic over 65,536 states and 768 KB of buffers per call, rerun at 4 Hz while a crossing is over 20 tiles or impossible. - L5. No shared team memory. Push points, odds and assignments are recomputed per tank; coordination rides on one-tick events, and a missed event is a missed job.
- L6. Lists that never forget.
given_up,depots_given_up,revive_failed, Hop'sbad_*andno_bridge, the fleet'sfails, the Defender's*_triesgrow for the match and are searched linearly; what they hold is never retried. - L7. Uneven commitment.
go()re-issues a Move only on a 1-tile shift, 4 s or a stop, but targets that follow a moving thing (wedge slots, rallies, kite step-ins, chain help) re-route nearly every tick; hysteresis exists in patches (WING_COMMIT3 s,w_stick0.4, landfall 6 points over 2 s,ALARM_HOLD6 s) with no common rule. - L8. The economy is reactive. Trees are felled only when a build is short, the squad holds 8; nothing fills up ahead of need, so a starved plan turns Fortify off. Rotation balances one tank (refill to 90%, at most 25 s), never the team.
Also: two resupply latches that disagree (doctrine under 6 shells, Commander at 4 shells or 35 armour) and the doctrine's can take an Auto tank over through hop.send; direct fire is never dodged before it lands; a Hunt target given up after 12 bearings is given up for good; partial_cmp().unwrap() would panic on a NaN.
3. Research review
Every source here was opened and read (the page, the paper or the source code); what could not be confirmed is marked unverified.
| Source | Takeaway | For us |
|---|---|---|
| Baritone ActionCosts, Settings | Costs in ticks; placing a block costs blockPlacementPenalty 20 (about 4 blocks of walking); re-used path costs half, so routes don't flip-flop | bridge or detour as one priced search (6.2); discount the old path (7) |
| mineflayer-pathfinder | remainingBlocks in each search node; a place-move impossible at 0; pluggable cost adders; 40 ms per tick | carry material in the crossing search |
| Voyager (2023) | LLM-written skills, curriculum, skill library | too heavy at runtime; the curriculum suits offline tuning |
| Terraria Fighter AI | ~132 hand-written styles, no planning; stuck means back up and retry | only a stuck detector, which route refusals give us |
| Robertson & Watson, A Review of RTS Game AI (2014) | multi-scale control is the open problem; fixes are a blackboard and a commander-manager-unit chain | the TeamBoard and one arbiter for trees (L5) |
| Straatman et al., Killzone 3 multiplayer bots | commander, squad, individual over an area graph; a plan replaced only by a higher one; influence smoothed in space and time | the template for fleets; islands as areas (9) |
| Lewis, Choosing Effective Utility-Based Considerations | the Infinite Axis Utility System: normalised inputs, curves, multiplied, early exit on zero; commitment bonus, runtime and cooldown against oscillation | the core of section 4 |
| utilityworlds | nine considerations at 0.9 multiply to 0.39, so compensate; momentum 1.1 to 1.25 (exact compensation formula unverified) | the geometric mean in 4; m in 7 |
| Graham, Introduction to Utility Theory | curve families; priority buckets | survival above combat above economy |
| Humphreys, Exploring HTN Planners | compound tasks; re-plan only for a higher method | multi-step jobs (6.1) |
| Orkin, Three States and a Plan (2006) | costed actions, A* planning; squads re-clustered by nearness | re-cluster fleets; full GOAP is more than we need |
| Hagelbäck & Johansson, Multi-agent Potential Fields (2008) | tank battles; a field peaks at the attacker's maximum shooting distance; own units repel | local stand-off points, as our kite ring |
| E323AI (Spring) | income and use as EMAs; stall under 10% of storage, request under 50%, exceed over 90%; toggles at 30/70; build spot = distance + 1000 x threat; threat per domain | trees as a flow; 30/70 bands; land and water threat apart |
| FAF Supreme Commander AI | base and platoon templates; builders with priority and conditions, re-checked every 13 s | a mission launches when its platoon is full |
| Shard | each behaviour reports a priority, the highest wins | the component pattern |
| Pottinger, Coordinated Unit Movement, Implementing (1999) | unit priorities, the lower yields; break, pass and re-form past an obstacle | column on a bridge (6.3); the explicit column is game practice, not named there |
| Emerson, Flow Field Tiles | A* over sector portals, a flow field per sector | sector A* for the strategy layer; flow fields not needed at 16 tanks |
| Churchill, Saffidine & Buro (2012) | NOK-AV focus fire; LTD2 = sum of sqrt(hp) x dpf | focus fire; spread damage is worth more, the case for team rotation (9) |
| Uriarte & Ontañón, Combat Models | Lanchester's square law as a cheap evaluation | confirms our odds |
| UAlbertaBot Squad.cpp | simulate, retreat if losing, 100 frames before re-attacking | confirmation for the squad (7) |
| Liu, Louis & Ballinger, Evolving Effective Micro (2014) | 14 parameters, GA population 20 over 30 generations beat UAlbertaBot at kiting | a small weight vector evolves within a census budget (8) |
| Avery & Louis, Coevolving Team Tactics (2010) | naval capture-the-flag on islands; the team as one chromosome | fleet role sets as one genome |
| Lucas et al., Planet Wars tuning | win/loss tuning is very noisy | fixed seed sets, many matches |
| TrueSkill | mu 25, sigma 25/3, rank by mu - 3 sigma | rate genomes in leagues |
Not opened: the Ontañón et al. 2013 survey (blocked, so its time-scale split is unverified), the Spring wiki AI pages, KAIK, BA AI, the Killzone 2 GDC paper, CLOP, MineRL.
4. Design: a weighted decision graph of components
One scoring pattern at the team and unit levels:
Team brain, 1 Hz (posture, jobs, defence points) Weights and Caps
| posture, threat (sliders, bits)
v |
Features, per tank --> Considerations --> Action scores, 4 Hz <-+
(32 values, 0..1) (curves C(x)) U = W x prod C, masked by Caps
|
Intent (held) <------ Selector (beat by 15%
| for 2 thinks)
v
Survival bypass ====> Reflex, 20 Hz -------> Commands to the sim
(dodge, retreat) (desire vectors, | match result
fire gate) v
Ledger and tuner --> better weightsThe score of an action a:
U(a) = W[a] * cap[a] * P[posture][a] * (prod_i C_i(x_i))^(1/n) * (1 + m * [a is current])W[a] is the action's slider (0 to 2, default 1); cap[a] is 0 or 1 from Caps; P is the team posture's multiplier for the action's component (section 9); the geometric mean keeps a five-consideration action comparable with a two-consideration one (compensation in its simplest form); m is the commitment bonus of section 7. A curve C(x) is linear, quadratic or logistic with four parameters, and those are sliders too, so one flat Weights array holds every number a tuner may touch.
A component is a trait object that does not allocate:
trait Component {
const CAP: Cap;
fn actions(&self, f: &Features, board: &TeamBoard, out: &mut ArrayVec<Action, 16>);
fn score(&self, a: &Action, f: &Features, w: &Weights) -> f32;
fn emit(&self, a: &Action, mind: &mut TankMind, out: &mut Vec<Command>);
}| Component | Caps bit | Actions | Main considerations | From |
|---|---|---|---|---|
| Combat | KITE, FOCUS | attack, kite, mass, press | odds, target score, range, shells | Tactics, its 22 weights as they are |
| Survival | always | dodge, retreat, revive | eta, armour, shells | Dodge, needs_home |
| Economy | FILL | fill, give item, give improvement | material, plan gap, forest near | Plan::next, fell |
| Roads and crossings | ROADS, BRIDGES, BOATS | lay road, bridge, boat | priced crossing (6.2), supply distance | hop.rs |
| Fortify | FORTIFY, TURRETS | castle item, turret slot, yard | plan state, quiet | fortify.rs |
| Haul | HAULS | fetch, tow, escort | road to pillbox, crews free | crew.rs, help_chain |
| Formation | WING | wedge slot, column, wing task | leader distance, bridge ahead | fly_wing |
| Rotation | ROTATE | push point, defence point, swap | team armour spread, point threat | rotation.rs |
The team level uses the same pattern with postures for actions. The reflex layer is not scored: fixed rules at 20 Hz are cheaper, and survival should not hang on a slider.
5. Step one: memory, parameters, compute
Work each fact out once per team per think, keep it in a fixed-size board, and lend it to every tank by reference.
| Memory | Lives | Holds | Size |
|---|---|---|---|
MapFacts | the match; updated when a tile changes | island id per tile (u16); the island table (area, forest, shore, pads); the crossing cache (from, to) -> Option<tiles>, failures included (L4) | ~40 KB |
TeamBoard | per team, rebuilt at 1 Hz | 16 x 16 sector grids of 8 x 8 tiles (threat, strength, territory, forest left); the job board (persistent jobs, L5); the defence-point roster; the road graph | ~8 KB |
TankMind | per tank | the intent and its score, desire vectors, confirmation counters, recent memories with expiry ticks (L6) | under 1 KB |
Parameters: Weights([f32; N]) named by a W enum and lent as &Weights; Caps(u64), one bit per capability, a capability off is never scored and costs nothing; Features([f32; 32]) per tank per think, read by pure scorers fn(&Features, &Weights) -> f32. The Observation stops being a per-bot copy of the world: one fog-filtered snapshot per team per tick, or a borrowed view with a vision mask.
Scratch: each bot reuses its buffers (A*, crossing); status strings become codes formatted only when the HUD asks; the plan answers turret_slot and yard_left without a clone (L3).
| Layer | Rate | Staggered by | Work |
|---|---|---|---|
| Reflex | 20 Hz | none | dodge, retreat trigger, fire gate, desire vectors, hold the command |
| Unit | 4 Hz | tick % 5 == seat % 5 | score ~12 actions, choose the intent, at most one Move |
| Team | 1 Hz | tick % 20 == team * 6 % 20 | rebuild the board, post jobs, assign roles and points, pick a posture |
| Strategy | 0.2 Hz | one team a second | sites, road plans, the front, crossings (cached) |
Measure first: per-layer timers and counters in the census runs (thinks, Moves, A* calls, crossing calls, allocations), reported as microseconds per bot per tick, before and after each change.
6. Behaviours to add
6.1 The base cycle (L8)
- Fill. Shells and armour at the depot; the squad to its 8 material from the forest nearest the base, from the board's forest grid.
- Give one item. One plan item at the base, chosen by utility, so the plan never starves while the fleet is out.
- Give one improvement. Mend the worst pillbox, a mine on the moat, works on a pad; 11g's upgrades fill this slot when decided.
- Connect. Road toward the nearest established friendly base or pillbox not yet joined to ours, planned once by the strategy layer (grass 1, forest 1 and a fell, water a bridge's price), kept on the board as road jobs any tank can lay.
- Mission. When a road reaches an outlying pillbox, an escort job: two haulers on the
Towchain, the rest in column screening the threat side; the pillbox comes home along the road and is set in the castle while it has fewer than its two inside and one on the road, else at a post along a road between bases, never at a junction (section 10, the castle rule).
Weights w_fill, w_give_item, w_give_upgrade, w_connect, w_escort. A threat at base skips the cycle, as today.
6.2 Islands, boats and bridges
A base site's score:
S = w_forest * min(1, F_island / F_need) + w_pads * P + w_choke * K
- w_small * max(0, 1 - A_island / A_min)F_need is what the plan will ask for (castle, turrets, yard, roads out) in trees at 4 material each; A_min a tunable area. A small island with few trees scores low.
A crossing is priced both ways, with scarcity s = F_need / (F_reachable + 1):
C_bridge(L) = L * (1 + s) + k_long * max(0, L - 6)^2
C_boat = c_boat * (1 + s) + t_launchUp to 6 tiles the long term is zero and a short bridge wins; from 7 to 20 the square grows fast, and a long bridge wins only for a road the network will use; BRIDGE_MAX stays 20. On a small island the high s makes the boat (BOAT_COST 5) beat a long bridge. Starting guesses, for the tuner: k_long 0.5, c_boat 5, t_launch 3.
A part-laid bridge is a job on the board, picked up by the next tank with material, dropped only when the strategy layer drops the crossing.
6.3 Column on a bridge
When the leader is on a bridge or narrow tile, or its next 3 route tiles are: the leader drives straight at full speed (no gathering, no formation); slot k is k x 2 tiles behind along the bridge axis, no side offsets; wingmen still on land wait at the bridge head in slot order; the wedge returns when the last tank is 2 tiles past the far end.
7. Decision smoothing
A tank keeps doing its last command until a new one is confirmed over several thoughts; only the two survival reflexes break in at once.
- Hold the last command. The reflex layer replays the intent every tick between thinks; a think that finds nothing better emits nothing; a new Move only when the intent changes or its target moves more than a tile.
- Desire vectors. D = (1 - alpha) D + alpha d, A = (1 - beta) A + beta a, with alpha 0.4, beta 0.3 at 4 Hz: a full swing in 3 or 4 thinks.
- Confirmation with hysteresis. A challenger b replaces a when U(b) > U(a) (1 + h) for n thinks in a row, with U(a) raised by (1 + m) while held: h 0.15, n 2 (3 for a turret target), m 0.1. Advance above A = +0.35, fall back below -0.35, hold between. The aim point is led, then smoothed (EMA 0.5).
- Survival bypass. Dodge (a shell with eta 4 s or less landing within 3.3 tiles) and retreat (one threshold pair,
retreat_armorandretreat_shells) skip smoothing and confirmation at full speed; the old intent comes back after, unchanged. - Brain power per system. The cadences of section 5 decide how often a system thinks; the weights decide how much it counts; both tunable, both measured by section 8.
8. Weight seeds and the ledger
One record per seat per match, JSON lines in reports/ledger/:
| Field | Note |
|---|---|
genome | a hash of the fields below, the key |
weights | every slider: posture, actions, the Tactics 22, the smoothing constants |
caps | the components that were on |
seed | the per-seat seed |
role | Leader, Guard, Ranger, Wing, Builder |
parent | lineage |
| result | won, kills, deaths, revives, territory-seconds, plan items built, road tiles, bridges finished and started, pillboxes hauled, time to first castle, starved-seconds |
score = 10 win + k_kill kills - k_death deaths + k_terr territory_s / T
+ k_build items + k_road tiles - k_starve starved_sOffline only, on the ignored census tests, headless at 4x: a hall of fame of 16 per role ranked by a rating (Elo or TrueSkill, mu - 3 sigma); a regression of score on each weight's offset, to show which sliders matter; a (1 + lambda) evolution strategy, CMA-ES past about 20 weights, on fixed seed sets; a genome becomes the default only by beating the default over a fixed census with a confidence interval clear of zero. Role sets are genomes too, so the fair fleet start is measured.
9. Territory, fronts, logistics, forts, rotation
| Map (16 x 16 sectors, 1 Hz) | Built from | Used for |
|---|---|---|
| Influence = ours - theirs | pillbox coverage and tank worth, decayed per sector | the front where it crosses zero; territory where positive |
| Supply distance | road distance to a friendly depot | logistics cost |
| Forest left | trees per sector | felling; site scarcity |
| Favourites | forest, pads, chokes, distance to the front | where forts go |
Forts are the castle plan and the yard on a favourite site; a second fort when the front passes a sector beyond the first's guns. Roads between forts are the attack lines. Defence points are the chokes of the road network (bridge heads, gates, junctions near the front), two to four of them.
Rotation across the team: each second rank the points by threat and the tanks by readiness (armour x shells, time to supply); swap when a tank at a hot point is more than 0.3 below one at a cold point, confirmed over two team thinks; each tank keeps DWELL 6 s and the 90% refill as a floor.
| Posture | When it scores high |
|---|---|
| Economy | low material, a starved plan, no threat near |
| Fortify | an unfinished fort on a favourite site |
| Connect | road gaps between forts or to pillboxes |
| Expand / Assault | front odds at least press_odds 1.5 and an attack line |
| Defend / Rotate | threat in our territory, or a wide armour spread |
10. The plan
Step 12 of ROADMAP.md, in this order, each part behind a Caps bit so the brain of 17 October stays the baseline, each judged by a census before it is the default: 12a measure, 12b memory, 12c weights and caps, 12d intent and smoothing, 12e the team board and team brain, 12f behaviours, 12g ledger and tuner.
Paul's answers, 18 October: every either/or is a scalar ("should it x or y, we choose a scalar of how much x, and how much y is weighted"). Each question became a weight whose default is his answer and whose best value the ledger and the tuner find (ROADMAP 12h):
| Question | Answer | Weights |
|---|---|---|
| The improvement slot before 11g | mend, mines and works by weight; upgrades take a share once 11g exists | w_mend 1, w_mine 0.5, w_works 0.5, upgrade_vs_mend 0.5 |
| Sliders for players or the host | the host's file is every bot's default; a player's sliders move only their own team | (not a blend) |
| A hauled pillbox: castle or junction | the castle until it holds 2 inside and 1 outside on its road; then posts along the roads between bases and castles, beside the road, never at a junction | castle_inside 2, castle_road 1, post_spacing 10, posts_max 4, junction_clear 3, capability castle_cap |
| Road out to what | bases and castles before lone pillboxes | connect_bases 0.75 |
| The score | halfway between elimination and holding ground | long_game 0.5 |
| The same roles for every team | a person's team keeps its builder for now | assist 1 (0 deals it as a bots' team) |
Upgrades and the ways to buy them, which 11g left open, are designed in ROADMAP goal 5: in-match, for supply earned from what a side holds, installed by the squad at the base; twelve base upgrades and four for the tank; bought at a depot from the coordination screen, or by a fleet commander's bot through the improvement slot, its team's posture leaning the choice.
11. Implementation and first measurement, 18 October
Built the same day, in treadcore-brain, each part behind its bit of Caps and off by default (ROADMAP goal 4, the status list):
| Part | Module | Bit | Proof |
|---|---|---|---|
| 12a measure | meter.rs | always | the_meter_counts_and_resets; the census line |
| 12b memory | facts.rs, memory.rs, fortify.rs turret_preview, tactics.rs value | forget for expiry; the rest always | the_islands_are_counted, a_memory_runs_out_unless_kept_for_ever |
| 12c weights | weights.rs | the existing parts' bits on | default_weights_are_todays_constants, a_weights_file_round_trips_and_names_its_mistakes, the_geometric_mean_compensates_and_vetoes |
| 12d smoothing | intent.rs, commander.rs go, tactics.rs pick | smooth | a_challenger_needs_two_thinks, a_dodge_skips_confirmation, the_advance_holds_between_its_bands |
| 12e the board | board.rs, crew.rs adopt, fleet.rs board_for | board, team_rotate | the_front_is_where_influence_crosses_zero, a_hurt_tank_swaps_out, a_missed_job_is_still_on_the_board |
| 12f behaviours | cycle.rs, facts.rs prices, hop.rs, commander.rs slot_point | cycle, priced_crossing, island_site, column | a_full_tank_gives_the_base_an_item, a_small_island_takes_a_boat, a_fleet_crosses_a_bridge_in_file |
| 12g ledger | ledger.rs, fleet.rs brain_census, scripts/tune.sh | n/a | a_ledger_line_round_trips_and_rates |
Two departures from the design above. The ledger's "territory-seconds" is held_s, the pillboxes and depots a team held, summed each second: the board's territory needs the board on, and the census compares teams with it off. And the turret's confirmation needs three picks by the margin, the move's desire vector applies only to shifts under jitter (3 tiles), and the commitment bonus is in utility for the components still to be ported.
The first census, naval.map, three seeds of 900 s, respawns 3:
| Trial | Trial team, mean score a seat | Baseline teams | Crossings searched / cached | Plan copies | Cost |
|---|---|---|---|---|---|
| the baseline itself | 13.95 | 8.93 | 213 / 956 | 10 | 28.0 us a seat-tick |
| every new bit on | 10.29 | 11.53 | 207 / 1,368 | 11 | 28.6 us a seat-tick |
The baseline against itself differs by five points a seat, so three seeds measure the dice, not the parts: nothing is promoted. Over the same matches the trial team spent 0 to 44 seat-seconds a match in the base cycle and 2 to 36 relieving a teammate, and laid fewer bridge tiles, 4.7 a match against the baseline teams' 9.3 (0 to 22), which is what the crossing price asks, though not yet more than the dice allow.
Later the same day, Paul's answers (section 10) went in as step 12h: twelve more weights (68 in all) and the castle_cap bit (14 bits). The capped castle draws castle_inside pillboxes inside its wall and, once it stands, puts castle_road slots beside its road where the bastions were; past that, the Commander adds road posts every post_spacing tiles on the line to each other depot of the side, beside the road and junction_clear tiles from any junction, and the turrets rung carries outlying pillboxes out to them. In a match alone with three outlying pillboxes and a second depot, the castle stood with two inside, and the road slot and the first post were filled by 371 s (a_capped_castle_fills_then_lines_the_road). Brain tests: 55 passed, 8 ignored.
12. The capped castle made the default, 18 October
Paul made castle_cap the default the same evening ("make castle_cap the default"). The bit's default turned on in weights.rs, so every Commander, a fleet's or a person's autopilot, draws the castle of 12h: two pillboxes inside, one outside beside its road, then road posts. The castle of 11b and 11h (three towers, two bastions) stays one bit away: caps = baseline, -castle_cap in a weights file, or CENSUS_CAPS=-castle_cap in the census. The two proofs written for it, a_commander_fortifies_its_depot and a_commander_climbs_the_ladder, now set that bit off and prove the old castle still works; a_capped_castle_fills_then_lines_the_road proves the default, and default_weights_are_todays_constants says the cap is on. Brain tests: 55 passed, 8 ignored.
The measurement, and what it showed about the census
Naval.map, six seeds of 900 s, respawns 3, the trial team rotated over the three starts:
| Run | Trial team | Baseline teams | Trial a seat, by seed |
|---|---|---|---|
| the default against itself (the null run) | 12.66 | 6.44 | 6.39, 13.35, 12.51, 21.01, 11.54, 11.15 |
the old castle (-castle_cap) against the default | 11.70 | 7.67 | 6.96, 11.84, 10.48, 14.87, 21.58, 4.45 |
The null run plays the same settings on every team, yet its trial team scored six points more a seat than the rest: with the seeds fixed, the seat the trial team draws on each seed is a good one more often than not. So the census's trial-against-baseline figure is biased, and the only fair reading is paired, the trial run against the null run on the same seeds. Read that way, the old castle scored 0.96 a seat less than the capped one, inside the spread of either (4.45 to 21.58). The capped castle costs nothing measurable, and is the default by Paul's choice, not by the census. The first seed's castles were not finished in 900 s under either rule, and that seed's numbers barely moved.
What this changes in the method (ROADMAP goal 4): a part is judged against a null run on the same seeds, not against the baseline teams of its own matches; the tuner's fitness (trial less baseline, over fixed seeds) still ranks children fairly against one another, since every child meets the same bias.
13. The upgrades proposal, 18 October
Paul asked for the proposal the same evening, and it is PROPOSAL-UPGRADES.md. It is in GOVERNANCE.md's form, because it changes the rules and the wire: Change, Why, Must not change, Proof, the questions left for discussion, and the traces it needs before it can be accepted. In short:
- Supply. A side earns a second count from what it holds: 1 every 10 s a pillbox, 2 a depot, 1 every 20 s a powered structure, 10 a kill, 5 a capture. Over the naval censuses (section 12) that is about 12 to 50 a minute.
- The upgrades. Twelve for a base or the whole side, and four for the tank, each with its levels, supply price, material and how it is installed. They are hardened walls, pillbox calibre and plating, a watchtower (11j's flag), a radar mast, a gatehouse, depot stores, a yard workshop, a minefield, a dock, bridge works and a supply road; and for the tank, plating, bay, squad and hopper.
- Buying.
- The world checks every purchase made with
Command::Buy. - Upgrades that need building are installed by the squad
(
PillboyTask::Install).- A works tab on the coordination screen lists them.
- A fleet commander's bot buys through the base cycle's
improvement slot, by
upgrade_vs_mend, with its team's posture leaning the choice and aw_up_<name>weight for each upgrade.- Allies can hand each other supply (11e).
- The world checks every purchase made with
- Must not change. The world stays deterministic and hashed. Old replays check under
Scenario::upgradesfalse. A match where nobody buys plays as today. Nothing is carried between matches.
For this report, the proposal ties the brain to the game's economy. The improvement slot of 12h, and the weight upgrade_vs_mend that Paul's answer created, are where the bots will spend supply. The team's posture (12e) chooses what they buy. Each upgrade's worth is a weight in the flat array (12c), so the tuner (12g) can learn the bots' buying as it learns their fighting. Until the sim step lands, upgrade_vs_mend has nothing to weigh, and the brain buys nothing.
14. The fair census, 18 October
Section 12 found the census biased: the trial team's seat on each fixed seed is a good one more often than not. The cure, backlog item 16, is in brain_census: each seed is played three times, every team in the trial seat once (CENSUS_FAIR, on by default; CENSUS_FAIR=0 keeps the old rotation, a third of the matches). With every team the same, the three matches of a seed are the same match, so a null run reads exactly even; one seed of 120 s read 13.93 against 13.93.
The capped castle measured again, fairly, on naval.map, three seeds of 900 s, nine matches:
| Run | Old castle (trial) | Capped castle (baseline) | Difference a seat |
|---|---|---|---|
| paired against a null run, six seeds (section 12) | 11.70 | 12.66 | -0.96 |
| fair, three seeds | 8.36 | 9.08 | -0.72 |
The two methods agree: the old castle is a little behind and inside the noise, and the capped castle stays the default. A fair census costs three times the matches, so make tune now defaults to two seeds (six matches a child).
15. Smoothing finished: the ladder and the advance, 18 October
The rest of 12d, under smooth:
- The Auto ladder confirms a change of rung. A new rung must win
confirmthinks in a row while the old one still holds; a think is a quarter second. A threat to the base (DefendBase), or the old rung failing its own condition, changes the rung at once (Commander::rung_challenger). - The decision to close is an averaged advance scalar. The odds are read as -1 (at
engage_oddsor under) to +1 (atpress_oddsor over). The reading is averaged once a think bybetaand held between thinks. The tank presses only aboveadvance_band(0.35, a new weight), falls back to mass only below minus it, and engages in between (Tactician::smooth). Proof: inthe_square_law, one think of two-to-one odds is not yet a press, and two are.
The fair census, naval.map, two seeds of 900 s, six matches:
| Trial | Trial team | Baseline teams | Moves a match, all seats |
|---|---|---|---|
smooth | 7.55 | 9.47 | 4,661 |
Smoothing cost 1.9 a seat over six matches; the spread of a team's score across them was 3.28 to 14.29. It did not cut the Moves asked of the world either (the old castle's nine matches asked 4,391 a match). So smooth stays off. The likely reasons to look at next: holding a rung for half a second delays leaving a rung whose work is done, and a smoothed stance arrives at a fight a think or two late. Its weights (confirm, beta, advance_band, margin) are the tuner's to try before the bit is judged again.
16. The escort, 18 October
Paul's first message asked that the tanks "go out on a mission with the team to start moving the pillbox along the newly created road leading back to the base". The pieces existed: the turrets rung fetches an outlying pillbox, the wingmen fly the leader's wedge, and the world's router already prefers road (a road tile costs 0.6 against grass's 1.0), so a pillbox carried home comes along the road where there is one. What was missing was the leaving together. Under the new bit escort, a fleet leader at its base whose fetch trip runs farther than fetch_gather (20 tiles, a new weight) gathers its wingmen first, with the same gathering the hunt and the expand use. The fleet then goes out in its wedge and screens the carry home. Proof: a_far_fetch_gathers_the_fleet. With the wingman 45 tiles off and the pillboxes 26 and 30 out, the leader gathered for the fetch and its squad lifted the first pillbox by 324 s. Brain tests: 56 passed.
The fair census (two naval seeds, six matches of 900 s) read 7.70 against 7.70 exactly. No team reached a far fetch in 900 s, so the bit never acted and the matches were the null run's. escort stays off until a longer census (goal 5's longer matches) can see it work.
17. The sliders, 18 October
Paul's first message asked for "a slider for each coefficient weights". The weights page is the coordination screen's second page: U opens it (a new rebindable action, tune). It has a row for each of the 15 capabilities, a switch, and a row for each of the 70 weights, a slider (client::tune).
- Up and down pick a row; left and right move it one step.
- Delete puts the row back to its default; Enter saves;
Uor Esc goes back to the ladder. - Each row shows its value and a bar from nothing to twice the default, the default at the middle. A
*marks a row that differs from the default. Under the rows is the doc line of the picked row, with its default and its step.
A step is one for a rounded weight (the doc says "(rounded)"; five docs gained the word: focus_cap, answer_cap, kite, confirm, confirm_turret). Otherwise it is a tenth of the default, taken to 1, 2 or 5 of its power of ten: alpha 0.05, near_scale 2, forget_for 10 (W::step). Settings::nudge never goes under nothing and keeps four places, so three steps up and three down is the number it was.
Whose bots, the table of section 10 decided: the host's --weights file stays every bot's, and a player's sliders are their own team's. The page gives its settings to the tank under the goal at once, and, when the window hosts, to the bots of the player's side, again at each new match (Brain::set_settings, new, a no-op for a brain without weights). A remote player's page reaches only their own tank, and it says so. Enter keeps the team's settings in ~/.config/treadcore/weights, beside the keys file, and the next start reads them for the team only. Without that file the page starts from --weights or the defaults.
Two changes in the brain made a mid-match change safe:
Commander::set_settingsupdates the tactician in place, keeping its target and the team's calls.- The four switches it used to turn off for good (dodge, pings, rotate, bridges) now put away the role's own setting when turned off and give it back when turned on again (
CapHeld).
Before this, no settings ever reached the window's pilot. It now has the page's, so --weights or the team file steer the person's own tank on auto too.
Some switches are read only when a match is dealt: board gives a team its board in fleet::equip. Set on the page, such a switch takes hold at the next match.
Proof:
a_slider_steps_and_steps_back;a_switch_comes_back_and_a_slider_keeps_the_target;the_page_moves_a_weight_and_puts_it_back,the_page_switches_a_capability_and_wraps,a_saved_page_reads_back,the_bar_marks_the_default_in_the_middle.- Brain tests: 58 passed; client: 31.
- The window, driven with
hyprctl dispatch sendshortcutand shot withgrim(journal 0043): Space, thenU, then a switch and a slider moved, Delete, then Esc back to the ladder with the fleet listed.
The page measures nothing by itself, so it has no census. What it changes is who can tune: a player can now try a weight in the match in front of them, and a saved file is a genome the ledger can read.
18. Bridge jobs on the team board, 18 October
Section 6.2 planned it: "A part-laid bridge is a job on the board, picked up by the next tank with material, dropped only when the strategy layer drops the crossing." Before this, a bridge given up (stalled 40 s, no material in reach, the layer dead) stayed in the water half laid. Its layer went on by boat and wrote the target off.
Under the new bit bridge_jobs, which needs board:
- Every member laying a bridge posts it to its team board each think: the water tiles still to lay, in order from the laid end, and what it crosses to (
Hop::bridge_job,TeamBoard::post_bridge). The same bridge is known by a tile or the target in common. - Tiles laid by anyone come off; a bridge laid, or untouched for
bridge_keep(180 s, new), comes off the board (prune_bridges). That is the "strategy layer drops the crossing" of the plan, as a time. - A bridge whose layer has not posted for 3 s is free. A teammate free of more pressing work takes the nearest whose next tile is within
bridge_reach(40 tiles, new): the squad aboard with material, not resupplying, no trip under way, a bridge it has not given up itself (take_bridge,Hop::bridge_refused). - The taker's hop lays the rest from the laid end and stops there (
Hop::adopt_bridge). The crossing was the other tank's errand, so a bridge finished or given up ends the trip; a given-up one stays on the board for another.
Who is free of more pressing work was the step's finding. First it was a tank on auto whose rung was expand or guard. A temporary counter in one census match showed no such tank: the free bridges were the leader's, given up as it sailed on, and the only teammates near them with material were its wingmen. So who takes became a scalar, per the either/or rule: bridge_wing (rounded, default 1) lets a wingman with material take a free bridge, leaving the wedge to do it.
The census gained CENSUS_BASE_CAPS, the baseline teams' capabilities, so a part that needs another is tried against that one on: here board for every team and bridge_jobs for the trial team. It also counts the trial team's seat-seconds "for the team".
Proof:
a_part_laid_bridge_waits_for_the_next_tank: posted, not free while its layer lays it, not taken from too far or by a tank that gave it up, taken by the nearest, laid tiles struck off, dropped untouched and when laid.a_teammate_finishes_a_part_laid_bridge: a channel four tiles wide across the map, its first tile laid; a tank on auto with material took the other three and laid them by 9 s, and the bridge came off the board. With the bit off nobody touched it in 240 s.- Brain tests: 60 passed.
The fair census (naval.map, two seeds, six matches of 900 s, respawns 3) read 7.31 a seat for the trial team against 8.01. The bit acted in five of the six matches, 69 to 783 seat-seconds. One trial team laid 36 bridge tiles against 15 to 20 in the others. The 0.70 a seat it cost is about the noise of six matches; bridge_jobs stays off, and bridge_wing, bridge_reach and bridge_keep are for the tuner.
19. A dodge for direct fire, measured first, 18 October
Backlog 8 and finding "direct fire is never dodged before it lands" asked for a dodge of direct fire beside the dodge of indirect shells. Before designing one, the step measured whether a tank that sees a direct shell launched can get out of its way at all.
The numbers are against it:
- Direct range is 10 tiles and a direct shell flies at 40 tiles a second, so it is in the air five ticks at most (0.25 s).
- It hits if the tank is within
DIRECT_HIT_RADIUS(0.8 tiles) of where it was aimed when it lands, and a bot aims where the tank will be at the speed and heading it has now (lead). - In 0.25 s a standing tank accelerating at 4 tiles a second squared covers 0.125 tiles. A tank at full speed turning at its 3 radians a second strays about v w t^2 / 2 = 4 x 3 x 0.0625 / 2, roughly 0.4 tiles, off the line the gunner led; braking gains about 0.125.
Measured (a_led_direct_shot_cannot_be_dodged, lib.rs): a tank crossing at full speed, a gunner square to it at 4, 7 and 9.5 tiles firing a led shot, and the target answering the launch five ways (carry on, reverse, turn away, turn toward, stop). Every one was hit at every range. The test asserts it, so a change of the rules that made a dodge possible would show.
A weave before the shot is no better against a bot, because it leads from the speed and heading at the moment it fires, and only what changes during the flight counts.
So the only defence against direct fire is denying the shot: not being inside an enemy gun's range, in its line, when that gun is ready before one's own. Every tank's reload is in the observation. Kiting already does this against the one enemy a tank is fighting; a builder, a hauler or a bridge-layer shot by a tank it is not fighting has nothing. Three ways on, for Paul to choose (ROADMAP backlog 17):
- a "duck" reflex behind its own switch: out of range or line when a hostile gun ready before one's own covers the tank and the exchange is not one it wins, with a weight for how far it outranks the errand; measured by the fair census;
- close the item as impossible, as this section records;
- change the rules (a slower direct shell, a smaller hit radius) so a dodge is possible, which changes how every match plays.
20. The brain now: the improved prototype, 18 October
What runs after step 12. Every row of section 2's tables is still there, and around it are the parts step 12 added; those marked (off) are built, proven and measured but wait for a census to promote them.
| Level | Code | Rate | Output |
|---|---|---|---|
| Settings | weights.rs: 91 weights (the dodge's four since LR 27, the landfall's three since LR 31 to 36, w_smooth since LR 46), 17 capabilities; --weights FILE for every bot, the weights page (U) for a player's team | read at the deal and on a change | the numbers every part below reads |
| Team board (off) | board.rs, one per team, shared | rebuilt 1 Hz by the first member due | influence, front, defence points, posture (five scores, LR 25), kept jobs, swaps, part-laid bridges |
| Fleet commander | crew::FleetCommand | 20 Hz; musters every 30 s | jobs, as before |
| Commander, one per tank | commander.rs | 20 Hz | Vec<Command> |
| Auto ladder | DefendBase > Fortify > Turrets > Hunt > Expand > Guard, the rung confirmed over two thinks above w_smooth 0.5 (0.05 by default, LR 46), leaned by the team's posture under posture (off, LR 24) | 20 Hz | a goal |
| Base cycle (off) | cycle.rs | after a fill | an item, an improvement by weight (mend, a mine, works), a road out |
| Doctrine and tactician | lib.rs, tactics.rs, its weights from Settings | 4 Hz | guns, drones, target, odds, stance (smoothed above w_smooth 0.5) |
| Hop | hop.rs, facts.rs: the crossing cache, priced crossings (off), adopted bridges (off) | each doctrine think | bridge, boat, landfall |
| Memory | memory.rs Forget (expiry off by default), intent.rs | per use | what was given up, smoothed goals |
| Ledger and meter | ledger.rs, meter.rs, brain_census, make tune | per match | a line per tank: genome, seed, score; the cost a seat-tick |
Settings (weights.rs: 91 weights, 17 switches) <-- --weights FILE, weights page (U)
|
Team board, one per team, 1 Hz (board) (L5)
| influence, front, points, posture, jobs kept,
| swaps (team_rotate), part-laid bridges (bridge_jobs)
v
Commander, each tank, 20 Hz, first match wins (L1)
+-- survival: downed, dodge (indirect shells; its numbers
| weights and `dodge_hold`, LR 27), duck (direct
| fire denied before it is fired: duck, LR 21), revive, home
+-- bridges: help the leader's; take a part-laid one (bridge_jobs)
+-- afloat: the landfall, over the guns seen and remembered
| (landfall_memory 30 s), pricing the run (landfall_shy)
| and standing off a covered beach (landfall_wait); on
| since LR 38. Shore gather, column on a bridge (column)
+-- board: post, rebuild, adopt a missed job, relieve or fall back
+-- road posts once the capped castle stands (castle_cap, on)
+-- pings, rally, fleet, crew job
+-- Auto ladder (6 rungs; confirmed above w_smooth 0.5)
| Fortify: the capped castle; Turrets: the fetch, gathered
| first when far (escort)
+-- base cycle after a fill (cycle)
+-- go(): desire vector, by w_smooth (0.05) (L7)
+-- Doctrine, 4 Hz
+-- Tactician (its 22 weights from Settings; smoothed
| stance above w_smooth 0.5)
+-- Hop (crossing cache with failures; priced under
priced_crossing; island sites under island_site) (L4)The findings of section 2, now:
| Finding | Now |
|---|---|
| L1 first-match ladders | every number is a named weight with a slider; the ladder and the wingman's priorities are still fixed orders |
| L2 a full world copy per bot per tick | unchanged: the Observation shared per team is 12b's remainder |
| L3 repeated work | the plan copy and the reasons per candidate gone; the status as codes (LR 22): the why column formatted only when the screen shows it, logic and census reading codes; the doing words still written, measured as costing nothing |
| L4 failed searches not cached | answered: the crossing cache keeps failures 60 s |
| L5 no shared team memory | the team board (off); read by team_rotate, bridge_jobs and, since LR 24, posture; its influence, front and territory still read by nothing (LR 24) |
| L6 lists that never forget | Forget with forget_for (off) |
| L7 uneven commitment | desire vectors and confirmation, as smooth measured worse in four readings, and since LR 46 a weight, w_smooth, 0.05 by default |
| L8 a reactive economy | the base cycle (cycle, off) |
partial_cmp().unwrap() on a NaN | answered (LR 23): every float comparison a total_cmp, in the brain, the client and the sim |
The numbers, 18 October evening:
- 12,566 lines in 17 files (8,458 in 8 on 18 October morning).
- Brain tests: 64 passed, 8 ignored; client: 31; sim: 50.
- 76 weights and 17 switches, with the duck (LR 21); 77 and 18 with the posture (LR 24); 83 with its scores (LR 25).
- 23 to 33 us a seat-tick in the census.
- On by default: kite, pings, dodge, bridges, rotate, and since 18 October
castle_cap; since 19 October the landfall's three weights (landfall_memory30,landfall_shy1,landfall_wait10, LR 38). - Measured fairly: the capped castle against the old, +0.72 a seat;
smooth-1.92;escorteven (never reached);bridge_jobs-0.70 withboardon for every team;duck-0.51 (LR 21). At twelve seeds each, with the error (LR 24): none outside two errors. - 19 October (LR 27): 13,193 lines; 349 functions, 122 undocumented; brain tests 69 passed, 8 ignored; 87 weights; 24.8 us a seat-tick. Where it dies: the dodge (43% of deaths in 4.5% of the time) and the run for the beach,
landfall(20% in 3.2%); the ladder, rotation and the fleet a small share. - 19 October evening (LR 38): 13,566 lines; 360 functions, 121 undocumented; brain tests 73 passed, 8 ignored; 90 weights. The first part since the dodge measured outside the noise: guns remembered for the landfall, +2.31 +/- 0.58 a seat over 48 seeds with its two companions, landfall deaths halved; on by default. The dodge itself measured: off, -2.45 +/- 0.67 (LR 29).
What the prototype has not shown yet: that any of its new parts makes the bots win more. The fair census can now tell a real difference from the starts, but six matches of 900 s are not enough to see the parts that pay late (the escort, bridges, the base cycle), and none has had the twelve seeds the roadmap asks for. That is the next measurement, and goal 5's longer matches the one after.
21. The duck: direct fire denied, 18 October
Paul chose the first of section 19's three ways: "build the duck reflex". A shot that cannot be dodged once fired can still be denied, by not being in reach when the gun is ready.
The reflex (Commander::duck, the bit duck, off):
- Which guns. A hostile tank with shells, not afloat, within 12 tiles (direct range plus 2, since its lead reaches past its range by the target's own motion) and in line. Its gun is ready within
duck_ready(1.5 s), and before mine, or I have no shells. - Seen or remembered. Sight and direct range are both 10 tiles, so a gunner just past sight leads its shot into it unseen. The first duel showed exactly that: the gun that fired was not in the observation. So a gun is remembered for
duck_memory(5 s) where it was last seen, its reload counted down. A direct shell in flight refreshes the memory, seen or not, because it carries its gun's position (from) and says the gun has just reloaded. - When. Only with the tank's armour share at or under
w_duck. This is the scalar between safety and the errand: 1 always ducks, 0 never. - Where to. It looks at sixteen points around it, at the distance that clears the gun with a margin (13 tiles out, 3 to 8 tiles away). Of those out of its reach or line, it takes the one nearest the errand (the last Move's goal); if none, it backs straight away.
- It holds the point a second at a time. The guns stay free while it ducks, as in a dodge. It never ducks afloat or while its squad mends a wreck. Once the gun is no longer a threat, the errand plans again.
The census counts the trial team's seat-seconds "ducking".
Proof: a_tank_ducks_a_gun_ready_before_its_own.
- The rules. It ducks out of reach when the gun will fire first, and not when its own gun is ready first, not with
w_duck0, and not with the bit off. - The duel. A gunner standing 7 tiles off fires a led shot whenever it can; the tank holds its post with its gun held reloading, never able to answer. Kiting is off in both arms, since kiting already holds a fighting tank past the gun's reach; the duck is for the tank that does not kite. Over 40 s: hit 2 times ducking, 3 standing. The hit that remains comes when the memory lapses and the post, 7 tiles from the gun, draws the tank back into its fire. Holding a post under a waiting gun is the trade
w_duckis there to weigh. - Brain tests: 62 passed, 8 ignored.
Building it took four duels, each of which found something before the reflex did:
- Three direct hits down a tank, so counting hits needed a fair cap.
- A tank whose gun is ready does not duck, by the rule, and a duel that silenced its gun hid that.
- Return fire and strike drones ended the match in four seconds.
- The unseen gun at 10.3 tiles, the real finding.
Fair census (naval.map, two seeds, six matches of 900 s, respawns 3): 7.01 a seat for the trial team against 7.52. It ducked in all six matches, 2 to 16 seat-seconds in five and 567 in one. The 0.51 is inside the noise; duck stays off, and its four weights are for the tuner.
22. The status as codes, 18 October
The last of 12b's string work. Finding L3 counted "six why strings and a doing string formatted every tick" as repeated work. The step measured the cost first, and found the case for codes was not speed.
The cost. One census match (naval.map, 300 s), as it was and with the why formatting stubbed out: 14.9 and 15.0 us a seat-tick as it was, 15.3 and 15.7 without. The difference is noise. why is built only for tanks on auto. A doing string is one or two formats a think, about 0.1 us against a think of 15.
The case. The brain's own logic and its measurements read the sentences back:
- a guard counted as idle if its words began "on guard" or "to the post", and the rotation looked for exactly "on guard";
- a focus call went out if the target's reason said "called", "on it" or "one shot";
- the census counted its parts by substring, the quad census its kiting steps by phrase and its revives by "REVIVE:";
- the tests read the words.
Rewording any of those sentences would have changed play or a count, silently.
The change (status.rs, new):
Act. What a Commander is doing, 48 codes, three of them carrying aKiteStepand two a step of the hop or the base cycle. It is set beside the words at all 81 places the Commander writes them: 76 throughset_doing, and 5 that hold a borrow of the fort plan set the two fields directly.HopAct. The island hop's 23 steps, set at its 28 writes (Hop::say).CycleAct. The base cycle's steps;Cycle::improvementreturns the code, not words.KiteStep. The tactician's eight kiting steps.Kite::wordbecameKite::step, and its words come fromstep.word().Tags. The tactician keeps bits beside its reason (one shot, called, on it), and the focus call reads them.Why. The ladder's reasons, a code and its numbers; the coordination screen puts them in words withwhy_text.Brain::act. The census and the quad census count by code, the ledger can carry one, andcode()is a stable dotted name (kite.step_in.reloading,hop.bridging).- The words stay, exactly as they were, for people: the status line, the coordination screen, the logs.
The quad census's diagnostic histograms (the status shapes, what a seat was doing when it went down, the stuck detector) still read the words. They want detail the codes do not carry: one hop code covers "bridging, the squad is laying it" and "bridging, fetching material".
Proof:
every_code_is_its_own: no two codes the same.- The tests that checked logic by words now check codes: the duck, the escort, the base under threat, and the tactician's three kiting tests.
- The equivalence. The codes must change nothing a tank does. One census match (600 s,
board, cycle, escort, duck, bridge_jobson for the trial team,boardfor all) before and after: identical, in score, kills, deaths, thinks, moves and crossings. - Brain tests: 63 passed, 8 ignored; client 31.
What is left of 12b is the Observation shared per team, a sim change that must keep the world hash.
23. No panic on a NaN, 18 October
Backlog 10 and 19, from the review's "also" (section 2): partial_cmp().unwrap() would panic on a NaN. Comparing two floats with partial_cmp gives None when either is NaN, and .unwrap() on that ends the process. In the brain that was 74 calls: picking the nearest pillbox, the closest depot, the least hurt tank, the best landing. One NaN in a position, from a bug anywhere or a malformed observation, would have stopped every bot of a host at once.
The change. Every one is now a total_cmp, which orders every float, NaN included, and never fails:
- 74 in the brain, in commander.rs (26), lib.rs (25), hop.rs (10), fortify.rs (8), crew.rs (4) and rotation.rs (1);
- 19 more in the window's code (input.rs 5, goal.rs 1) and the simulation (lib.rs 11, map.rs 2), where a NaN panic is as bad.
A one-off script rewrote each X.partial_cmp(Y).unwrap() as X.total_cmp(Y), matching parentheses. One comparison was of a tuple (material short, distance), which has no total_cmp; it now compares the parts in turn. Two in the map's route search already fell back to "equal" and could not panic; they stay as they were.
total_cmp orders the real numbers exactly as partial_cmp did. It differs only by putting -0.0 before +0.0, and the values compared here are distances, health and scores, never a signed zero.
Proof:
- The bug, shown.
a_nan_does_not_panic_the_brainputs a hostile tank, a pillbox and a depot at NaN in an observation and lets a Commander think forty times under each of seven goals. Put into the code of the commit before, it panics on the first goal (Auto) at apartial_cmp().unwrap(). On the fixed code it passes. - Nothing else changed. One census match (600 s, five new switches on) before and after: identical, in score, kills, deaths, thinks, moves and crossings, and the same numbers as LR 22's run. Every crate's tests pass: brain 64, client 31, net 14, proto 3, sim 50, the sim's replay and world-hash tests among them.
One wrong turn on the way. The first run of the new test sat for ten minutes and looked like a NaN driving something into a loop. It was the build: changing the sim rebuilds every crate above it. Run by itself under a hard timeout, the test passed in an instant.
24. Each switch alone, twelve seeds; the posture read, 18 October
Backlog 18 and the roadmap's "next": every capability that is off, measured alone over the fair census at twelve seeds. Until now a part was judged "inside the noise" by eye, from two or three seeds, with no number for the noise.
The noise as a number. The census now prints the difference a seat seed by seed, and its standard error over the seeds (ledger::mean_se, the_standard_error_of_a_sample). A seed's three matches share its starts, so a seed is one paired reading; within two standard errors of zero is "inside the noise". A null run reads 0 with an error of 0 on every seed, as it must. make switches (scripts/switches.sh) runs the census once for each switch and prints the table below; a switch that needs the board is tried with the board on for every team.
The measurement (naval.map, respawns 3, 900 s, 12 seeds, every team in the trial seat: 36 matches a switch, 396 in all; run three at a time, so the microseconds read high):
| Switch | With | Trial | Baseline | Difference a seat | Error | Acted (trial team, seat-seconds) |
|---|---|---|---|---|---|---|
smooth | 7.98 | 9.96 | -1.98 | 1.25 | always (it smooths every move) | |
team_rotate | board | 8.25 | 9.09 | -0.83 | 0.87 | relieving 879 |
forget | 8.54 | 8.90 | -0.36 | 0.23 | (retries are not counted) | |
column | 8.98 | 9.05 | -0.06 | 0.41 | on bridges | |
board | 8.75 | 8.75 | 0.00 | 0.00 | never: the same matches as the baseline | |
island_site | 8.75 | 8.75 | 0.00 | 0.00 | never | |
escort | 8.76 | 8.75 | +0.01 | 0.01 | far fetches, one seed | |
priced_crossing | 9.20 | 9.09 | +0.11 | 0.56 | cheaper by boat 0; road posts 92 against 30 | |
cycle | 9.08 | 8.77 | +0.31 | 0.43 | base cycle 243 | |
duck | 8.93 | 8.54 | +0.39 | 1.04 | ducking 911 | |
bridge_jobs | board | 9.11 | 8.67 | +0.44 | 0.91 | for the team 4,438 |
None is outside two standard errors. smooth comes nearest (1.6 errors below), where LR 15 put it with six matches (-1.92). duck and bridge_jobs, slightly behind at two seeds, are slightly ahead at twelve: the sign of a difference under one error is the dice's.
What twelve seeds can and cannot tell. A seed's difference spreads by 0.8 to 4.3 a seat (smooth 4.3, duck 3.6, team_rotate 3.0, forget 0.8). To put a real half point a seat outside two errors takes (2 x spread / 0.5)^2 seeds: 10 for forget, 140 for team_rotate, 200 for duck; a whole point, a quarter of that. The 396 matches took 22 minutes three at a time, about 1,000 an hour, so 200 seeds (600 matches) is a 35-minute run: every switch at a half point is one night's census. The tuner's two seeds (make tune) can only move weights whose effect is several points.
Three switches never acted, and the census could not have found that by score alone, only by the identical matches:
island_sitechooses among the side's depots only when a tank has no base yet. In a fleet every tank's base is its leader's depot, found first, so the island rule is never asked. It needs to stand where a naval start actually chooses: which neutral depot the leader takes first.escortacts on a far fetch, and a 900 s naval match has about one (LR 16 said so).boardalone changes no decision. It draws influence, the front, the defence points and a posture, and onlyteam_rotateandbridge_jobsread any of it; its own act, adopting a job a crew missed, happened in none of 36 matches. The team's picture was drawn and nobody looked at it.
The last is the brain's structure, not a tuning: goal 4's team board was to be the one picture the team decides on (LR 5, LR 9). The step that follows makes the posture read.
The posture read (posture, off; needs board). The board's posture is what the team should mostly be doing; the Auto ladder is what a tank does, six rungs taken first-match, each a yes or no (L1). The ladder now reads the posture by a blend, as every either/or is a scalar (Rung::pick):
score(rung) = (1 - w) order(rung) + w lean(rung, posture)
order: 1 for DefendBase down to 0 for Guard, in steps of 0.2
lean: 1 for a rung the posture leans toward, else 0The rung taken is the best of those switched on that hold. w is w_posture; at 0 the pick is the first that holds, as the ladder always was, and a leaned rung climbs past fewer than 5 w / (1 - w) rungs above it, so from 0.5 the posture decides among the rungs that hold. DefendBase, holding, is never passed. The leanings (Rung::leaned): Defend to Guard; Fortify to Fortify and Turrets; Connect to Turrets and Expand; Expand to Expand and Hunt; Economy, short of material, to Expand (depots).
What the first match found. On the first census match the trial team scored 3.64 against 11.02. The meter (new: the postures read and the picks they changed) said why: the posture was Defend in 94% of the picks, and the lean sent the tanks home to Guard. The posture had never been read, so it had never been checked. Two faults:
- Defend's test was the sector threat at the base over 0.5. Each hostile tank adds 1 / (1 + d / 8) to every sector of the map, so eight tanks 80 tiles off add 0.73 between them. The base was always under threat.
- Defend was also taken when the members' readiness spread past 0.6: any team with one tank low on shells.
The fix: Defend is hostile worth within the base's alarm (BASE_ALARM, 16 tiles), a full tank 1, over 0.5; the spread is left to rotation (team_rotate), which is where it belongs. the_team_defends_a_threat_at_the_base_not_far_off puts eight hostile tanks 80 tiles off and then one at the gate. On the old rule it fails; on the new it passes. After the fix, over 48 seeds, the posture read Fortify 47%, Connect 19%, Expand 18%, Economy 16% and Defend 0.4%.
The measurement (as above, with board on for every team):
w_posture | Seeds | Picks changed | Difference a seat | Error |
|---|---|---|---|---|
| 0.25 | 1 | 0 | +0.08 | (one seed) |
| 0.35 | 12 | 5,122 | +0.39 | 0.36 |
| 0.5 (= 1) | 12 | 50,677 | +1.21 | 1.31 |
| 0.5 | 48 | 164,606 | -0.81 | 0.65 |
At 0.25 a lean climbs one rung, and the posture's leanings sit two below the rung that holds, so nothing changed. At 0.5 and at 1 the matches are identical, as the formula says they must be. The first twelve seeds read +1.21, which is promising. Thirty-six more seeds, run three ranges at a time (CENSUS_SEED0), read -1.30, -2.16 and -1.01; over all 48 the difference is -0.81 with an error of 0.65, 21 seeds ahead and 27 behind. The twelve were the dice. posture stays off, and w_posture's default is 0.5, the value measured.
What the step shows about the method. A part that is never read is never tested; the first match it is read in finds what was wrong. A reading of +1.2 on twelve seeds with an error of 1.3 is no reading. A part is promising only outside two errors, and confirmed only on seeds it was not chosen on.
Proof:
the_posture_leans_the_ladder: at weight 0 or with no posture, the pick is the first rung that holds for all 4,096 combinations of rungs on and holding; DefendBase is never passed; 0.25 climbs one rung and 0.5 every rung; a switched-off rung is never picked.the_team_defends_a_threat_at_the_base_not_far_off, failing on the old rule.the_standard_error_of_a_sample.make test: brain 67 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
25. The posture as scores, 18 October
Backlog 21, from LR 24 and CI-001 finding A3: the board's posture was itself a first-match ladder on thresholds, where LR 9 designed five scores ("when it scores high"). By Paul's rule, every either/or is a scalar.
The change (board.rs, fortify.rs, weights.rs). Each posture has a score from 0 to 1, from what the board and the rebuilding member know (Home: the plan starved, Plan::castle_left, Plan::slots_left; each member now posts its squad's material):
| Posture | Score | LR 9's "when it scores high" |
|---|---|---|
| Defend | hostile worth within the base's alarm, half a tank full | threat in our territory |
| Economy | 1 if the plan is starved, else the share of the team's material spent | low material, a starved plan |
| Fortify | the share of the castle not yet built | an unfinished fort |
| Connect | the share of pillbox slots and road posts still empty | road gaps between forts or to pillboxes |
| Expand | the team's odds against the hostiles it sees: 0 at even, 1 at press_odds; none seen is no front, even | front odds at least press_odds |
Each score has its weight (w_pos_defend, w_pos_economy, w_pos_fortify, w_pos_connect, w_pos_expand, all 1). The ladder of 12e is kept, and the posture is the blend:
value(p) = (1 - k) [p is the ladder's pick] + k w_p score_pThe highest value wins; ties go in the ladder's order (Defend, Economy, Expand, Connect, Fortify); if nothing scores, Fortify, as the ladder had it. k is posture_scored, 1 by default: at 0 the posture is the ladder's, exactly. The board keeps both picks and the scores, for the screen and the tests. The weights now number 83.
Proof: each_posture_scores_its_own_case builds the five cases. A castle just planned at even odds is Fortify; the castle up with half its slots empty is Connect; the squads spent is Economy; a starved plan is Economy; eight of ours against two of theirs in sight is Expand; a hostile at the gate is Defend over a planned castle. At posture_scored 0 the pick is the ladder's in every case. The ladder alone would have said Fortify in three of the cases, since it has no term for empty slots, spent material or odds.
The measurement (the fair census as in LR 24, posture at w_posture 0.5, board on for every team, 48 seeds in three ranges of 16, the same seeds as LR 24's):
| Posture | d a seat | Error | Postures read (econ / fort / conn / exp / def) | Picks changed |
|---|---|---|---|---|
| the ladder's (LR 24) | -0.81 | 0.65 | 16% / 47% / 19% / 18% / 0.4% | 164,606 |
scored (posture_scored 1) | -1.04 | 0.58 | 37% / 43% / 6% / 15% / 0.5% | 131,278 |
| scored less the ladder's, seed by seed | -0.23 | 0.74 | ||
| the two averaged, seed by seed | -0.93 | 0.49 |
The ranges read +0.40, -1.46 and -2.07. The last 16 seeds alone were outside two errors, and the 48 are not. That is one more case of a range reading its own luck.
What it shows. How the posture is worked out makes no measurable difference: scored and ladder's are -0.23 ± 0.74 apart. What both have in common is that they override the ladder's own order, and together they read about a point a seat behind (-0.93 ± 0.49, 1.9 errors). The lean is the suspect, not the posture. The likely mechanism is in the leanings. Economy is now read 37% of the time, from material spent on building, and it leans to Expand, so tanks leave Fortify and Turrets for depots. That is a hypothesis; the census does not yet count which rungs gained. Backlog 27 adds that count, and the leanings themselves are weights for the tuner once it can see a point (backlog 23).
posture stays off. The scored posture is the board's default, since it is LR 9's design and measures no worse; the ladder is posture_scored = 0.
make test: brain 68 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
26. Which rungs the lean moves, and where the time goes, 18 October
Backlog 27, from LR 25: leaning the Auto ladder by the team's posture reads about a point a seat behind in both forms of the posture. The hypothesis: Economy, read 37% of the time from material spent, leans tanks off Fortify and Turrets.
The instruments (no change to play):
Brain::rung: the Auto rung a think took, or None if the think did not reach the ladder (a reflex, a fleet slot, a job, another goal). The Commander keepsrung_now, cleared at each think.- The census counts the trial team's seat-seconds on each rung.
- The meter counts each pick the posture changed, by the rung that held first and the rung taken (
Meter::moved, 6 x 6). CENSUS_ACTS=1: the trial team's every seat-second by status code (Act::code), dead seats apart, ladder thinks marked.
Which rungs (48 seeds, the same as LR 24 and 25; trial team seat-seconds; "no lean" is the null run, board on for all):
| Rung | No lean | Lean | Lean, Economy off (w_pos_economy 0) |
|---|---|---|---|
| DefendBase | 609 | 381 | 422 |
| Fortify | 14,262 | 9,967 | 10,609 |
| Turrets | 8,427 | 1,026 | 3,961 |
| Hunt | 14 | 245 | 151 |
| Expand | 9,197 | 11,034 | 8,928 |
| Guard | 0 | 31 | 34 |
| Picks changed | 0 | 131,278 | 44,975 |
| d a seat | 0 | -1.04 +/- 0.58 | -0.92 +/- 0.54 |
With the lean, 84% of the changed picks were Fortify to Expand and 12% Turrets to Expand. The castle stands later, so the Turrets rung, which fills it with pillboxes, nearly vanishes (-88%). Economy caused two thirds of the changed picks: building spends material, spent material reads as Economy, and Economy leans away from building, a feedback. With Economy off, Turrets recovers about half. The score does not move: Economy off against Economy on, seed by seed, is +0.13 +/- 0.47. The hypothesis is not confirmed. The lean does move tanks off the castle, but the census cannot see that cost, if it is one.
Where the time goes (CENSUS_ACTS, 16 seeds, null run, the trial team's 205,537 seat-seconds, the 40 commonest codes):
| Share | What |
|---|---|
| 46.7% | dead (waiting to respawn, or out of respawns) |
| 18.9% | the island hop: sailing 5.5%, bridging 4.4%, by sea 2.1%, idle 1.5%, to the boat 1.5%, holding the launch 1.2%, and the rest |
| 7.3% | rotation (push and defence points) |
| 5.6% | the Auto ladder: fortify 2.1%, turrets 1.2%, take a depot 0.9%, no way to the post 0.7%, gather 0.6% |
| 4.5% | dodging |
| 4.2% | the fleet: wing 2.7%, wing at sea 1.5% |
| 3.9% | rallies and kiting toward a ping |
| 2.4% | taking depots and pillboxes outside the ladder |
| 1.7% | crew jobs |
| 0.7% to 1.9% each | gather 1.9%, revive 1.0%, resupply 0.9%, escort 0.7% |
Three things follow:
- The ladder is a small lever. It decides 5.6% of a team's seat-seconds, about 11% of its living time. A change to the ladder's pick can only move that share. The posture's lean changed most ladder picks and moved the score by a point at most.
- Half the time is dead. A seat dies 3.4 times a match on average with 3 respawns; a respawn waits 20 s, so most of the dead time is seats out of respawns. The census, as set, measures attrition. A part that pays late (the castle's turrets, the escort, bridge jobs, the cycle) is cut off before it can.
- The island hop is the largest living share (about 36% of living time), on naval.map. Speeding a crossing is worth more than any rung.
What this changes. Backlog 28: survival, since what kills half the team is the largest lever there is. The quad census's "what a seat was doing when it went down" can now be read by code. Backlog 29: a census with unlimited respawns, or longer matches (goal 5), so the late parts are measured at all. The leanings (a 5 x 6 table of 0s and 1s) stay as they are until the census can see a point.
Proof: no behaviour changed. The lean run of LR 25 run again gives the same per-seed differences to the hundredth, and the null run reads 0 on every seed. make test: brain 68 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
27. What kills half the team, 19 October
Backlog 28, from LR 26: 46.7% of the trial team's seat-seconds are dead. The hypothesis: what a seat was doing when it went down, read by code, names the two or three situations that kill most.
The instruments (no change to play):
CENSUS_ACTSnow also keeps each trial seat's last five codes, one a second, and at eachTankDestroyedcounts the code held last, every code held in the 5 s before (once a death), and what took the tank (killed_by: the weapon whose hit names it on that tick, whether it sank, and whether the shooter was the lone RuleBot or no one).- The meter counts the Commander's shell dodges, ashore and afloat (
Dodge::metered), and those hit, those hit by the dodged shell itself (on its landing tick), and those killed, between the dodge's start and its shell's landing (Dodge::watch,meter::dodge,meter::dodge_hit). A dead seat still reads its dodge (Brain::dead, called each tick in place of a think). Act::Landfall: afloat with no hop under way, the doctrine's run for the beach (RuleBot::landfall_order). It readhop.idlebefore.- The dodge's four constants are weights (
dodge_horizon4 s,dodge_margin0.8,dodge_clear2.5 tiles) and one new one,dodge_hold(0): the share of the shell's flight a tank stays at its dodge point once there. At 0 it goes back to its errand on arrival, as before; at 1 it stays out until the shell lands. The default numbers pick the same point as the constants (a_dodge_holds_out_of_the_splash_by_its_weight).
The reading (16 seeds, null run, naval.map, 900 s, respawns 3; the trial team's 819 deaths, 17.1 a match):
| Code held last | Deaths | Share of deaths | Share of time | Held in the 5 s before |
|---|---|---|---|---|
| dodge | 354 | 43.2% | 4.5% | 67.3% |
| landfall | 165 | 20.1% | 3.2% | 30.2% |
| hop.sailing | 65 | 7.9% | 4.7% | 15.4% |
| resupply | 32 | 3.9% | 0.9% | 6.3% |
| hop.holding_launch | 28 | 3.4% | 1.1% | 8.1% |
| hop.bridging | 24 | 2.9% | 4.3% | 4.3% |
| hop.by_sea | 22 | 2.7% | 2.0% | 4.9% |
| everything else living | 129 | 15.8% | 32.9% |
What took them (the twelve commonest causes, 791 of the 819): direct fire 253, indirect 275, strikes 246, pillboxes 17; 399 of the 791 sank in a boat. The commonest pairs: direct fire on a boat at landfall 132 (16.1%), indirect fire on a boat in a dodge 91, indirect fire ashore in a dodge 87, a strike in a dodge 78.
The dodge's own meter (the same run):
| Dodges | Hit before the shell landed | By the dodged shell | Killed | |
|---|---|---|---|---|
| Ashore | 7,911 | 2,691 (34.0%) | 1,314 (16.6%) | 582 (7.4%) |
| Afloat | 3,408 | 621 (18.2%) | 258 (7.6%) | 567 (16.6%) |
Three things follow:
- Two situations make 63% of the deaths: the dodge and the run for the beach, in 7.7% of the time. A seat dies once every 26 s it is dodging and once every 40 s on landfall, against once every 135 s of living time overall. The hypothesis is confirmed in its form: two situations, not ten.
- A dodge is a symptom before it is a cause. A tank dodges because a shell is on it; under fire is where tanks die. That a dodge precedes most deaths does not say the dodge kills. Ashore, a sixth of dodges are still hit by the shell they dodged, and a third by something. Afloat, a hit is a kill 91% of the time: a boat sinks. The test is causal: the dodge off (
-dodge) anddodge_hold1, each at 48 seeds against the null run (next step). - Landfall is the second killer, and it is not a dodge. 132 deaths are a boat sunk by direct fire while running for a beach, though
landfall_orderprices each beach by the threats near it. A boat in range of a tank's gun is under fire it cannot answer.
What this changes. Backlog 28's look is done; its part is the dodge, measured causally first, then the landfall (backlog 30). The ladder, rotation and the fleet are a small share of deaths.
Proof: no behaviour changed. The null run reads 0 on every seed (trial 8.38, baseline 8.38). make test: brain 69 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
28. Holding out of the splash, 19 October
Backlog 28, the first causal reading after LR 27. A sixth of ashore dodges are caught by the shell they dodged. One reading: the tank reaches its dodge point early, drops the dodge (Dodge::holding holds only while it drives) and goes back to its errand under the shell. If so, dodge_hold 1, which keeps the tank at its point until the shell lands, should cut the hits by the dodged shell and save tanks.
The run: the defaults dumped (CENSUS_DUMP) with dodge_hold = 1, as the trial team's weights; 48 seeds in three ranges of 16 (CENSUS_SEED0 0, 16, 32), naval.map, 900 s, respawns 3, the fair census, the ledger off. Against the null run (the defaults for all).
| d a seat | Ashore: hit, by the shell, killed | Afloat: hit, by the shell, killed | |
|---|---|---|---|
| Null (LR 27, 16 seeds) | 0 | 34.0%, 16.6%, 7.4% of 7,911 | 18.2%, 7.6%, 16.6% of 3,408 |
dodge_hold 1 (48 seeds) | -1.18 +/- 0.55 | 34.2%, 16.7%, 7.3% of 23,099 | 15.5%, 6.0%, 13.9% of 10,002 |
The meter counts every Commander's dodges, the two baseline teams' as well as the trial team's, so its rates move a third as far as the trial team's own.
Not confirmed, and worse. Holding at the dodge point does not cut the hits by the dodged shell ashore (16.7% against 16.6%). The tank that is caught is caught before it gets there, not after. The trial team loses 1.18 a seat, just outside two errors (1.10): a tank that stands out the whole flight leaves its errand for nothing. Afloat the rates fall a little, but that difference is diluted by the baseline teams and the score says the trade is a loss. dodge_hold stays 0.
What it says about the dodge: the hits come in the drive, so the levers are when it starts (dodge_horizon) and where it goes (dodge_clear), and whether to dodge at all, which the switch off (-dodge, running) answers next.
Proof: no code changed. The weights file differs from the defaults in the one line.
29. The dodge off, 19 October
Backlog 28, the second causal reading. LR 27 found the dodge last held at 43% of deaths; LR 28 that holding it longer costs. Does the dodge save tanks at all, or does it walk them into fire?
The run: CENSUS_CAPS=-dodge for the trial team, the default for the others; 48 seeds as in LR 28.
The dodge pays: -2.45 +/- 0.67 a seat with it off, outside the noise at 3.7 errors (by range: -4.11 +/- 1.10, -1.39 +/- 1.14, -1.84 +/- 1.19). It is the largest single part measured since the capped castle, and it was on all along. The 43% of deaths it precedes are where the fire is, as LR 27 suspected; without it there would be more.
Ashore and afloat apart. With the dodge off, order_with still runs and still meters (commander.rs, the order is dropped by dodges after it), so the meter counts the shells the trial team would have dodged and did not, mixed with the baseline teams' dodges. Taking each team to draw a third of the shells, the undodged rate is three times the run's rate less twice the null's:
| Null (dodged) | -dodge run (mixed) | Undodged, estimated | |
|---|---|---|---|
| Ashore, hit by the shell | 16.6% | 21.1% of 20,783 | about 30% |
| Ashore, killed | 7.4% | 8.6% | about 11% |
| Afloat, hit by the shell | 7.6% | 7.8% of 9,109 | about 8% |
| Afloat, killed | 16.6% | 15.9% | about 14.5% |
The estimate leans on equal shares and should be read as a direction. (LR 30 measured it by team: the shares were not equal, and afloat the dodge cuts the kills by a fifth. The afloat reading below is withdrawn.) Ashore, the dodge nearly halves the dodged shell's hits. Afloat it does nothing: a boat is slow to turn, its dodge point is often water it cannot reach in time, and it is hit at the same rate dodging or not. What it gives up afloat is its crossing.
What this changes. The dodge stays on. The next dodge part is the either/or this table names: dodge afloat or keep sailing. A weight, dodge_afloat (1, today's), the share of a boat's dodges taken (backlog 31). The meter should count a would-be dodge apart from a taken one, per team, so the next reading needs no estimate.
Proof: no code changed.
30. The meter by team, and LR 29 corrected, 19 October
Backlog 31's first part. LR 29 estimated the undodged rates from a meter that mixed the teams, on the assumption that each team draws a third of the shells. The meter now counts by the seat's group and whether the dodge was taken.
The instrument (no change to play):
meter::trial(mask): the census marks the trial team's seats; the meter keeps them apart from the rest.meter::seat(p), called byrun_botsfor each seat, names the seat the counts after it belong to.Dodge::dropped: set by the Commander fromdodgesbefore eachorder_with. A dodge the brain drops (the dodge off) counts as would-be.Meter::dodgesis[group][taken or dropped][ashore or afloat]ofDodges { begun, hit, shell, killed };Meter::add_dodgessums it for the census total. Proof:a_dodge_counts_by_its_seat_and_whether_taken.
The reading: -dodge, seeds 0 to 15 (LR 29's first range). The 16 per-seed differences are LR 29's to the hundredth, so play did not change. In the same matches, the trial team's would-be dodges against the other two teams' taken ones:
| Taken (the rest) | Dropped (the trial team) | |
|---|---|---|
| Ashore, begun | 4,982 | 1,899 |
| Ashore, hit by the shell | 15.5% | 34.0% |
| Ashore, killed | 6.6% | 15.0% |
| Afloat, begun | 2,148 | 1,029 |
| Afloat, hit by the shell | 6.9% | 10.1% |
| Afloat, killed | 14.7% | 18.2% |
Ashore, the dodge more than halves the dodged shell's hits and the kills, as LR 29 estimated. Afloat it is not nothing: it cuts the shell's hits by a third and the kills by a fifth. LR 29's estimate was wrong because its assumption was: the trial team without the dodge drew fewer dodgeable shells (1,899 ashore against about 2,500 for each other team), being dead more often. Taking the shares as equal hid the effect afloat.
What this changes. LR 29's "afloat it changes nothing" is withdrawn. Backlog 31's hypothesis is weakened: a boat's dodge saves a fifth of the boats it is taken for. dodge_afloat can still be worth less than 1 if the crossing time it costs is worth more, but the case is now a trade for the tuner, not a free gain. Backlog 31 stays, its expected gain smaller, below 30 (the landfall).
Proof: play unchanged (the per-seed differences). make test: brain 70 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
31. The run for the beach, 19 October
Backlog 30, from LR 27: 165 of 819 deaths on landfall, 132 of them a boat sunk by direct fire. The hypothesis: landfall_order prices a beach by the threats near it and the run's heading, not the run's passage through a gun's reach, so boats sail past guns.
The part: landfall_shy (weights.rs, 0 by Paul's choice, so play is unchanged; RuleBot::landfall_shy, set by the Commander). The run to each beach is cut into tile steps; each step within a hostile tank's direct range plus 4, or a pillbox's range plus 2, is exposed. The beach's score adds 4 x landfall_shy x the exposed tiles: at 1, a tile in a gun's reach costs as much as four tiles more sailing. Proof: a_shy_boat_runs_for_the_beach_clear_of_the_guns (a near beach whose run passes a gun, and a farther one clear: at 0 the near, at 1 the far).
The reading: landfall_shy = 1 for the trial team, 48 seeds, CENSUS_ACTS on.
| d a seat | Deaths (seeds 0 to 15) | On landfall | Direct fire, sunk, on landfall | |
|---|---|---|---|---|
| Null (LR 27) | 0 | 819 | 165 | 132 |
landfall_shy 1 | +0.65 +/- 0.69 | 821 | 169 | 133 |
Not confirmed. The score is inside the noise and the deaths it was aimed at did not move. (LR 32 measured the explanation below and withdrew it: the boat had seen its killer in the 10 s before in 78% of landfall deaths.) Over all 48 seeds the landfall deaths were 548 of 2,522 (21.7%), as in the null's 20.1%. Choosing a beach clear of the guns the boat sees does not save it. The cause has to be the guns it does not see: sight and direct range are both 10 tiles (the duck's note in weights.rs), so a gun the boat could choose around is seen only once the boat is already in its reach. A price on what is seen cannot help against what is not.
What this changes. landfall_shy stays 0. Backlog 30 closes, not confirmed. Backlog 32: the run for the beach priced by the guns the team has seen (the duck's remembered guns, duck_memory, or the board's sightings), so a boat knows the beach a teammate saw covered.
Proof: play unchanged at 0 by construction (the exposure returns 0 before any work, and a score plus 0.0 is the same float; no null run was repeated). make test: brain 71 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
32. Who saw the gun that sank the boat, 19 October
Backlog 32's read, from LR 31: if the boats on landfall die to guns they never saw, the guns a teammate saw would help.
The instrument (no change to play): under CENSUS_ACTS, once a second, each living trial seat's observation records the hostile tanks it sees. At each trial death whose code held last was landfall and whose killer is named, the killer is classed as seen in the 10 s before by the boat, by a teammate, by both, or by neither.
The reading (16 seeds, null run, in ranges of 6, 5 and 5; 0.00 on every seed):
| Seen in the 10 s before by | Landfall deaths | Share |
|---|---|---|
| both the boat and a teammate | 128 | 78.0% |
| neither | 35 | 21.3% |
| a teammate only | 1 | 0.6% |
| the boat only | 0 | 0 |
| All with a killer | 164 |
(LR 35: sight is shared by the whole team, so the split between the boat and a teammate below differs only by when each was sampled, and says nothing.)
(LR 34: the killer stood a median 8 tiles from the boat at its death, inside its 10-tile range, so "seen" here means seen once the gun could already fire. The conclusion that boats are not blind is withdrawn.)
LR 31's explanation is withdrawn. The boat had seen the gun that sank it in nearly four landfall deaths of five, and a teammate had seen it too: they cross together. A teammate's sighting adds a gun the boat had not seen once in 164. Backlog 32's part, pricing the guns the team has seen, has nothing to work with. The "neither" fifth is an upper bound, since sight is sampled once a second.
What, then? The boat sees the gun, prices the beaches (with or without landfall_shy), and dies anyway. It sinks at one hit and cannot fire back. A beach is chosen once; a change waits 2 s and needs 6 better (landfall_order's change). Once a gun is in sight, the boat's choice is not which beach but whether to land at all, against standing off out of the gun's range until the beach is clear or the gun moves. That is an either/or, so a weight: backlog 33.
Proof: play unchanged (0.00 on every seed). make test: brain 71 passed and 8 ignored; client 31; net 14; proto 3; sim 50.
33. Standing off a covered beach, 19 October
Backlog 33, from LR 32: boats on landfall die to guns they have seen. The hypothesis: the gun covers the beach, and a boat that waits out of its reach until it leaves lives.
The part: landfall_wait (weights.rs; 0 by Paul's choice, so play is unchanged; RuleBot::landfall_wait, set by the Commander). In landfall_order, once the beach is chosen: if a seen hostile tank is within its direct range plus 4 of that beach, the boat moves to 16 tiles from the gun, on the line from the gun through the boat, or holds where it is if already that far. It does so until landfall_wait seconds of the crossing are spent, counted from the first stand-off and not restarted (a gun flickering in and out of sight cannot hold a boat off for ever), then lands. The wait resets ashore, at a respawn, and under a hop. RuleBot::standing_off and a new code, stand_off (status.rs), let the census count it. Proof: a_boat_stands_off_a_covered_beach_for_its_wait (at 0 the boat lands; at 10 it backs off to 16 tiles and lands once the 10 s are spent).
The reading: landfall_wait = 10 for the trial team, 48 seeds, CENSUS_ACTS on.
| d a seat | Deaths (seeds 0 to 15) | On landfall | Stand-off time | |
|---|---|---|---|---|
| Null (LR 27) | 0 | 819 | 165 | |
landfall_wait 10 | -0.51 +/- 0.63 | 798 | 184 | under 0.07% |
Not confirmed. The score is inside the noise, and the landfall deaths did not fall (573 of 2,460 over 48 seeds, 23.3%). The stand-off hardly ever fired: under 0.07% of the trial team's seat-seconds (below the 40 commonest codes in every range), and it was the code held last at about 13 deaths. A gun the boat sees is rarely within reach of the beach it chose. The guns that sink boats on landfall are not at the landing. They are near the boat somewhere along its run.
Three parts on the landfall (LR 31 to 33) have now found nothing to price. Each assumed where the gun is. The next step reads it: at each landfall death, the killer's distance to the chosen beach and to the boat, and whether the killer is ashore (backlog 34).
landfall_wait stays 0.
Proof: play unchanged at 0 by construction (landfall_wait 0 skips the block). make test: brain 72 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
34. Where the gun that sinks the boat stands, 19 October
Backlog 34, from LR 31 to 33: three landfall parts each assumed where the gun is, and none moved the deaths.
The instrument (no change to play): Brain::beach, the beach a boat is running for (RuleBot::landfall; the Commander's inner RuleBot's). Under CENSUS_ACTS, at each trial death on landfall with a killer, the census records the killer's distance to the boat and to that beach, the boat's distance to its beach, and whether the killer was afloat.
The reading (16 seeds, null run, in ranges of 6, 5 and 5; 0.00 on every seed; 164 landfall deaths with a killer, every one with a beach chosen):
| Range 0 to 5 | Range 6 to 10 | Range 11 to 15 | All | |
|---|---|---|---|---|
| Killer to boat, median (quartiles) | 8.1 (7.4, 8.7) | 8.0 (7.5, 9.6) | 8.0 (7.0, 9.4) | about 8 |
| Killer to beach, median (quartiles) | 28.6 (4.1, 33.6) | 28.6 (5.0, 36.8) | 31.8 (6.8, 34.9) | about 29 |
| Boat to beach, median (quartiles) | 24.1 (7.3, 28.4) | 18.3 (6.9, 32.8) | 17.6 (8.1, 30.7) | about 20 |
| Killer within its range + 4 of the beach | 34.0% | 43.3% | 34.1% | 62 of 164, 37.8% |
| Killer nearer the boat than the beach | 60.4% | 53.7% | 68.2% | 98 of 164, 59.8% |
| Killer afloat | 9.4% | 11.9% | 4.5% | 15 of 164, 9.1% |
Three things follow:
- The gun is seen when it can already fire. The killer stood a median 8 tiles from the boat, inside its 10-tile direct range, and sight is also 10 tiles. LR 32's "the boat had seen its killer in the 10 s before" is true, but the sighting came at or inside the gun's range, too late to act on. LR 32's reading stands; its conclusion that the boats are not blind is withdrawn in this sense: they are blind until it is too late.
- Two kinds of death. About two in five killers stood at the beach (within 14 tiles of it, the first quartile 4 to 7 tiles), a beach chosen a median 20 tiles off and so out of sight. That is why the stand-off (LR 33) found nothing to see. About three in five stood along the run, nearer the boat than its beach: guns on the coast the boat sailed past. Under one in ten were afloat.
- The limit is knowledge, not choice. A boat's own sight cannot price either kind in time, and teammates crossing together see the same. What the team does not use is what it knows of the coast: the team board's hostile influence (
board.rs, built underboard, read by nothing since LR 24). A beach and a run priced by that field could see a held coast before the boat is in range.
What this changes. Backlog 34 closes: the gun stands either at the beach or along the coast of the run, out of sight until in range. Backlog 35: price the landfall by the team board's hostile influence, at the beach and along the run (needs board). The parts of LR 31 and 33 (landfall_shy, landfall_wait) stay at 0; either can be fed the board's knowledge instead of the boat's sight.
Proof: play unchanged (0.00 on every seed). make test: brain 72 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
35. Known earlier, and lost, 19 October
Backlog 35's read, from LR 34: a boat sees its killer only once it is in range, so could the team board's hostile influence price the coast before then?
What the code says first. Two facts, read before measuring:
- Sight is shared by the whole team (
World::visible_to: any allied tank or drone within its vision). A seat's observation is the team's. LR 32's split between "the boat" and "a teammate" could only differ by when each was sampled; that split is vacuous and is marked so. - The board keeps no memory (
TeamBoard::rebuildclears its influence and threat each second and fills them from what the team sees now). It cannot know a gun the team does not see at that moment, which is the case LR 34 found. Backlog 35, as written, cannot work.
The question that remains: had the team seen the killer earlier, and lost it?
The instrument (no change to play): under CENSUS_ACTS, each hostile tank in the trial team's sight is logged with its position once a second. At each landfall death with a killer, the killer is classed: seen 10 to 60 s before, then within its direct range plus 4 of where the boat died; seen 10 to 60 s before, elsewhere; or not seen then.
The reading (16 seeds, null run; 0.00 on every seed):
| The killer | Deaths | Share |
|---|---|---|
| seen 10 to 60 s before, within reach of where the boat died | 76 | 46.3% |
| seen 10 to 60 s before, elsewhere | 26 | 15.9% |
| not seen 10 to 60 s before | 62 | 37.8% |
| All | 164 |
Nearly half the landfall deaths were known earlier. The team had seen the gun standing near the place the boat would die, 10 s to a minute before, then lost sight of it (it moved behind the sight line, or the team's eyes moved on), and the boat sailed into it. Nothing in the brain keeps a hostile tank's last position for long. The duck keeps a seen gun for duck_memory (5 s) and only for ducking; the board for 1 s.
What this changes. Backlog 35 closes: the board forgets. Backlog 36: the landfall's guns (its danger, toward, and the landfall_shy and landfall_wait checks) include each hostile tank where it was last seen, for landfall_memory seconds (a weight, Paul's default to ask; 0 is today's). It reaches up to the 46% of landfall deaths whose killer was seen near there earlier.
Proof: play unchanged (0.00 on every seed). make test: brain 72 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
36. Guns remembered for the landfall, 19 October
Backlog 36, from LR 35: in 46% of landfall deaths the team had seen the killer near there 10 to 60 s before, and lost it.
The part: landfall_memory (weights.rs; 0 by Paul's choice, so play is unchanged; RuleBot::landfall_memory, set by the Commander). RuleBot::remember_guns, each tick in think_map, keeps each hostile tank the team sees where it is, and a tank out of sight where it was last seen, until the memory's seconds are up, the tank is seen again, or its spot is in a teammate's sight (within 0.9 of TANK_VISION) with no tank there. RuleBot::known_guns, the seen and the remembered, is what landfall_order prices: the beach's danger, the run's heading (toward), its exposure (landfall_shy) and the stand-off's cover (landfall_wait). At 0 the list is the seen tanks in the same order as before. Proof: a_gun_seen_and_lost_still_prices_the_run (a gun beside the near beach's run, seen and then lost: at memory 0 the boat takes the near beach, at 30 the far one, and the near one again after 31 s).
The reading: the three landfall weights on together for the trial team (landfall_memory 30, landfall_shy 1, landfall_wait 10), 48 seeds, CENSUS_ACTS on.
| Null (LR 27, seeds 0 to 15) | All three (seeds 0 to 15) | All three (48 seeds) | |
|---|---|---|---|
| d a seat | 0 | +1.84 +/- 1.13 | +2.31 +/- 0.58 |
| Deaths | 819 | 747 | 2,298 |
| On landfall (code held last) | 165 | 82 | 281 |
| Killer seen near there 10 to 60 s before | 76 | 16 | 59 |
| Dead share of seat-seconds | 46.5% | 42.2% | |
| Stand-off share of seat-seconds | under 0.07% (LR 33) | 0.17% | 0.16% |
By range: +1.84 +/- 1.13, +3.07 +/- 0.91, +2.00 +/- 0.97.
Confirmed. The trial team scores 2.31 a seat more, four standard errors out, and every range is positive. Landfall deaths halve (165 to 82 on the same seeds), and the deaths the memory was aimed at, a gun seen near there earlier, fall from 76 to 16. The stand-off now acts, because remembered guns give it a cover to see. Of the earlier landfall parts, landfall_shy (LR 31) and landfall_wait (LR 33) read nothing useful on their own; with memory they have something to price.
What is not yet known: which of the three carries the gain. Memory alone also feeds the beach's danger and the run's heading, which were already on. Backlog 37 takes each away in turn. The defaults are Paul's: all three stay 0 until he moves them.
Proof: play unchanged at 0 (known_guns is the seen tanks in order; remember_guns keeps nothing). make test: brain 73 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings.
37. Which landfall weight carries the gain, 19 October
Backlog 37, from LR 36: landfall_memory 30, landfall_shy 1 and landfall_wait 10 together read +2.31 +/- 0.58. Each is taken back to 0 in turn, 48 seeds each, against the null run (the defaults for all), CENSUS_ACTS on.
Memory off (landfall_shy 1, landfall_wait 10, landfall_memory 0):
| d a seat (48 seeds) | Landfall deaths, seeds 0 to 15 | Killer seen near there earlier | |
|---|---|---|---|
| Null (LR 27, 35) | 0 | 165 | 76 |
| All three (LR 36) | +2.31 +/- 0.58 | 82 | 16 |
| Memory off | -1.08 +/- 0.53 | 164 | 72 |
By range: -0.82 +/- 0.86, -0.71 +/- 1.06, -1.70 +/- 0.82.
The memory carries the gain. Without it, the two weights over the guns a boat sees cost about a point, just outside two errors, and the landfall deaths are the null's. That matches LR 31 and 33, each alone, with the same sign. The difference between this run and LR 36, about 3.4 a seat, is what remembering the guns is worth to the other two.
Shy off and wait off (each with the memory on):
| Trial | d a seat (48 seeds) | Landfall deaths, seeds 0 to 15 | All 48 seeds | Killer seen near there earlier (0 to 15) |
|---|---|---|---|---|
| Null | 0 | 165 | 76 | |
| All three (LR 36) | +2.31 +/- 0.58 | 82 | 281 | 16 |
| Memory off | -1.08 +/- 0.53 | 164 | 542 | 72 |
| Shy off (memory, wait) | +1.50 +/- 0.67 | 103 | 341 | 16 |
| Wait off (memory, shy) | +0.69 +/- 0.64 | 113 | 369 | 25 |
By range, shy off: +1.55 +/- 1.04, +1.62 +/- 1.35, +1.33 +/- 1.16; wait off: +0.26 +/- 1.11, +1.23 +/- 1.07, +0.59 +/- 1.20.
All three carry part of it, and the memory is the base. Without the memory the other two cost a point. With it, the stand-off is worth about 1.6 a seat (all three against wait off) and the run's exposure about 0.8 (all three against shy off). Each of those differences is read across two runs against the null, not paired with each other, so they are rougher than the table's errors. Shy off alone is outside two errors (+1.50 +/- 0.67, 2.2 errors); wait off is not (1.1). The landfall deaths order the runs the same way as the score: all three fewest, then shy off, wait off, and memory off with the null.
The best measured setting is all three at once. Its numbers (30 s, 1, 10 s) were picked by hand and are not tuned. Moving any default is Paul's choice.
38. The landfall's three on by default, and the reverse reading, 19 October
Paul, after LR 37: "yes all three on". landfall_memory 30, landfall_shy 1 and landfall_wait 10 are the defaults (weights.rs). A Commander takes them from its Settings; a plain RuleBot (the lone aggro bot, and any RuleBot seat) keeps 0, as it keeps the dodge's constants. Every test passes with the new defaults.
The reverse reading. As proof of the switch, the census was run the other way round: the old defaults (all three 0) as the trial team, against two teams on the new, 48 seeds.
| Trial | Baselines | d a seat (48 seeds) | |
|---|---|---|---|
| LR 36 (forward) | all three on | the old defaults | +2.31 +/- 0.58 |
| Reverse | the old defaults | all three on | -0.08 +/- 0.45 |
By range, reverse: -0.35 +/- 0.82, -0.77 +/- 0.62, +0.86 +/- 0.86.
The two do not mirror. A team with the memory beats teams without it by 2.3 a seat; a team without it among teams with it loses nothing that 48 seeds can see. The gain is real against opponents that play the old way and not shown against opponents that play the new. One reading, untested: the stand-off and the run's price change where boats land and which coasts are contested for every team. A memory team's edge then depends on its opponents landing where the guns are, and a no-memory team among memory teams finds the coasts less contested. What the switch does to play when every team has it is measured by neither census: the census reads a team against others, not a team against itself.
The defaults stay as Paul chose. Backlog 38: what all three do when every team has them. That needs a fixed reference the census does not have: a team of the old defaults kept for comparison, or a score that is not relative (survival time, landfall deaths a seat, matches finished).
Proof: make test: brain 73 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings. 360 functions, 121 undocumented.
39. The landfall's three when every team has them, 19 October
Backlog 38, from LR 38: the relative census reads +2.31 forward and -0.08 in reverse. What do the three do to the game when every team has them?
The method. The census sets only the trial team's weights, but a null run is every team on the defaults. LR 27's null run (seeds 0 to 15) was every team on the old defaults, and play at the old defaults has not changed since (each new weight at 0 adds nothing). A null run now, on the same seeds, is every team on the new. Both with CENSUS_ACTS. These are absolute readings, not paired by seed, so they carry no standard error.
| Seeds 0 to 15, every team | Old defaults (LR 27) | New defaults |
|---|---|---|
| Trial team's deaths | 819 | 848 |
| On landfall (code held last) | 165 | 89 |
| In a dodge (code held last) | 354 | 434 |
| Seat-seconds (the matches' length) | 206,685 | 216,000 (+4.5%) |
| Deaths per 1,000 seat-seconds | 3.96 | 3.93 |
| Dead share | 46.5% | 41.9% |
| Living seat-seconds | 110,615 | 125,471 (+13%) |
| Mean score a seat | 8.38 | 7.95 |
The deaths move; they do not fall. With every team on the new defaults, the boats lost on the run for the beach nearly halve (165 to 89), but the deaths in a dodge rise by about as many (354 to 434), and the death rate is the same. The tanks that used to sink on the way in now land, and die ashore under indirect fire. The matches run 4.5% longer and the team spends 13% more time alive, which is what goal 5 asks for. The mean score a seat falls (8.38 to 7.95). It is a weighted sum that counts kills and holdings, and it is relative to how the other teams play, so without per-seed pairing this change cannot be told from noise.
This explains LR 38's asymmetry. A team with the memory, among teams without it, saves its boats while its opponents' boats still sink: it gains. A team without it, among teams with it, still loses boats, but its opponents' saved tanks die ashore instead, so the relative score evens out.
What this changes. The defaults stay as Paul chose: the game is longer and as deadly, with the deaths moved off the water. The next lever is where the deaths moved to: the dodge ashore, now 51% of the deaths held last. LR 29 showed the dodge pays; its numbers (dodge_horizon, dodge_clear) have never been tuned (backlog 28's close), and the tuner (backlog 23) is the way to tune them.
Proof: no code changed. make test as in LR 38.
40. SPSA on the paired census, 19 October
Backlog 23, from CI-001 VI: the (1 + lambda) tuner selects noise. The recommended replacement is SPSA (Spall 1992), because the fair census already measures a paired difference.
The parts:
CENSUS_BASE_WEIGHTS(fleet.rsbrain_census): a weights file for the two baseline teams, asCENSUS_WEIGHTSis for the trial. The header line prints both sides' changed weights. With it, one census plays theta + c delta against theta - c delta, and every team on some old defaults against the new is one run (LR 39 needed two).scripts/spsa.sh(make spsa): SPSA in log space over the weights named inTUNE, each positive. Each step k draws a sign delta_i for every tuned weight and plays one fair census onSEEDSfresh seeds (SEED0 + (k - 1) SEEDS), the trial team on theta_i exp(+c_k delta_i), the baselines on theta_i exp(-c_k delta_i). The census's difference a seat d is f(+) - f(-); each log theta_i moves by a_k d / (2 c_k delta_i), capped atMAXSTEP(0.5). The gains follow Spall: a_k = A / (k + 1)^0.602, c_k = C / k^0.101 (A 0.6, C 0.4).SETplants values on the start;steps.tsvkeeps every step with its difference and standard error. A result is promoted only after beating the defaults on 48 seeds the search did not use.
The planted test (CI-001 VI.D): a weight with a known large effect, detuned, which a working tuner must move back. w_wounded, named in CI-001, has never been measured, so a failure there would say nothing. landfall_memory has: removing it cost about 3.4 a seat (LR 37). It is set to 5 s (default 30) and tuned alone: 8 steps of 12 seeds. The first four steps, as committed with LR 40 (the whole run in LR 41):
| k | d a seat | Its error | landfall_memory after |
|---|---|---|---|
| 1 | -0.13 | 1.80 | 4.69 |
| 2 | -0.87 | 1.24 | 3.27 |
| 3 | +0.55 | 1.33 | 3.99 |
| 4 | +1.21 | 1.43 | 2.68 |
Every step's difference is inside its error: at 12 seeds a step reads to about 1.4 a seat, and between memory of 2 s and 5 s the true difference is likely far smaller. The walk so far is noise. LR 41 gives the whole run.
Proof: make test: brain 73 passed and 8 ignored. A plumbing run (1 step, 3 seeds of 120 s) wrote both sides' files, and the header shows the trial on 4.48 s and the baselines on 2.01 s.
41. The planted test, first run: a flat slope, 19 October
LR 40's planted test, whole: landfall_memory from 5 s, tuned alone, 8 steps of 12 fresh seeds (1,000 to 1,095), A 0.6, C 0.4.
| k | d a seat | Its error | landfall_memory after |
|---|---|---|---|
| 1 | -0.13 | 1.80 | 4.69 |
| 2 | -0.87 | 1.24 | 3.27 |
| 3 | +0.55 | 1.33 | 3.99 |
| 4 | +1.21 | 1.43 | 2.68 |
| 5 | -0.59 | 0.88 | 3.21 |
| 6 | -0.84 | 1.09 | 4.05 |
| 7 | +0.36 | 1.57 | 4.45 |
| 8 | -0.77 | 1.14 | 3.69 |
Not passed. The weight ended at 3.69 s, below where it started, after a walk in which no step's difference was outside its error. The mean of the eight differences, each signed by its draw, is no different from zero.
Why, most likely: the plant sat on a flat. With C 0.4 the two sides played memories of about 3.4 and 7.5 s around 5 s. The deaths the memory prevents were sightings 10 to 60 s old (LR 35). Below about 10 s, memory barely acts, so the slope there is near zero and each step reads only noise. SPSA was not shown to fail; the test did not give it a slope. A planted test has to sit where the effect is.
What this changes. The test runs again with C 1.0: the two sides then play about 13.6 s and 1.8 s, so every step straddles the range where memory acts. If SPSA climbs from there, the tool works when the slope is visible. If it does not, 12 seeds a step is too few for this effect, and the next try is 24.
Proof: no code changed.
42. The planted test, second run: it climbs, slowly, 19 October
LR 41's planted test again with C 1.0, so each step's two sides straddle the range where memory acts. landfall_memory from 5 s, tuned alone, 8 steps of 12 fresh seeds (the same seeds, 1,000 to 1,095), A 0.6.
| k | Plus side (s) | Minus side (s) | d a seat | Its error | The longer side better by | Memory after |
|---|---|---|---|---|---|---|
| 1 | 1.8 | 13.6 | +1.98 | 1.52 | -1.98 | 3.38 |
| 2 | 8.6 | 1.3 | +2.75 | 1.99 | +2.75 | 5.34 |
| 3 | 13.1 | 2.2 | +1.33 | 1.56 | +1.33 | 6.48 |
| 4 | 2.7 | 15.5 | -0.33 | 1.64 | +0.33 | 6.76 |
| 5 | 2.9 | 15.8 | -1.62 | 0.93 | +1.62 | 8.21 |
| 6 | 18.9 | 3.6 | +1.18 | 1.63 | +1.18 | 9.37 |
| 7 | 4.1 | 21.3 | -3.13 | 0.79 | +3.13 | 12.99 |
| 8 | 29.2 | 5.8 | -0.99 | 1.19 | -0.99 | 11.79 |
Partly passed. The weight climbed from 5 to 11.8 s, toward the known better value. Six steps of eight read the longer memory better, by +0.92 +/- 0.62 a seat on average (1.5 errors), and step 7 alone was outside its error. With the slope in view, SPSA moves the right way. It is slow: after 96 seeds it is less than halfway to 30 s in log terms (ln 11.8 - ln 5 = 0.86 of 1.79). With Spall's decaying gains it would take about as many steps again.
What it says for the dodge. The planted effect was about 3.4 a seat between 0 and 30 s (LR 37), the largest part measured, and SPSA at 12 seeds a step only crawls toward it. The dodge's numbers act on something smaller. Tuning them is a long run: 24 seeds a step (errors about 0.8), A 1.0, and 16 or more steps, about 2.5 hours. The result then has to beat the defaults on 48 seeds the search did not use.
Proof: no code changed.
43. The dodge's numbers by SPSA, 19 October
LR 42's long run: dodge_horizon, dodge_margin and dodge_clear from their defaults (4 s, 0.8, 2.5 tiles), 16 steps of 24 fresh seeds (2,000 on, used by no earlier run), A 1.0, C 0.5. The sixteen steps:
| k | Plus side (horizon, margin, clear) | Minus side | d a seat | Its error | After |
|---|---|---|---|---|---|
| 1 | 2.43, 1.32, 1.52 | 6.59, 0.49, 4.12 | +0.24 | 0.77 | 3.41, 0.94, 2.13 |
| 2 | 5.43, 1.50, 3.39 | 2.14, 0.59, 1.34 | -0.48 | 0.86 | 2.61, 0.72, 1.63 |
| 3 | 1.67, 1.13, 1.04 | 4.08, 0.46, 2.55 | -0.34 | 0.74 | 3.07, 0.61, 1.92 |
| 4 | 4.74, 0.94, 2.97 | 1.99, 0.40, 1.24 | +0.47 | 0.94 | 3.77, 0.75, 2.36 |
| 5 | 5.77, 1.15, 1.54 | 2.47, 0.49, 3.61 | -0.99 | 0.76 | 2.54, 0.50, 3.51 |
| 6 | 1.67, 0.77, 5.32 | 3.85, 0.33, 2.31 | +1.72 | 1.18 | 1.54, 0.83, 5.78 |
| 7 | 2.32, 1.26, 8.72 | 1.02, 0.55, 3.83 | +0.66 | 0.74 | 1.94, 1.05, 7.28 |
| 8 | 1.29, 0.70, 4.86 | 2.91, 1.57, 10.92 | +0.66 | 1.10 | 1.56, 0.84, 5.87 |
| 9 | 1.05, 0.57, 8.76 | 2.33, 1.26, 3.93 | -2.49 | 0.94 | 2.58, 1.39, 3.56 |
| 10 | 1.73, 0.94, 5.29 | 3.83, 2.07, 2.39 | +0.24 | 0.88 | 2.40, 1.30, 3.82 |
| 11 | 3.55, 0.88, 2.58 | 1.62, 1.92, 5.66 | -2.35 | 0.72 | 1.46, 2.14, 6.30 |
| 12 | 0.99, 3.16, 4.27 | 2.15, 1.45, 9.29 | +0.00 | 0.90 | 1.45, 2.14, 6.30 |
| 13 | 0.99, 3.15, 9.26 | 2.14, 1.46, 4.28 | -1.64 | 1.12 | 2.24, 1.39, 4.08 |
| 14 | 1.53, 2.03, 2.78 | 3.29, 0.95, 5.98 | +0.67 | 1.06 | 1.89, 1.65, 3.44 |
| 15 | 2.77, 1.12, 5.03 | 1.29, 2.41, 2.35 | -0.38 | 0.89 | 1.72, 1.81, 3.13 |
| 16 | 1.18, 2.64, 2.15 | 2.51, 1.24, 4.57 | -0.73 | 0.79 | 2.05, 1.52, 3.73 |
Step 9 is the first outside its error (2.7 errors): a horizon of about 1 s, with the narrower margin and the wider clearance, lost by 2.5 a seat to one of about 2.3 s. The early noisy steps had walked the horizon down to where a tank starts too late to get out from under a shell, and SPSA walked it back. Before step 9, no step was outside its error. The gain is too high for this noise: at A 1.0, readings inside their errors move every weight by up to the cap (steps 5 and 6 hit it). dodge_clear is the most consistent mover. Signed toward the side with the larger clearance, the first seven readings average +0.49 +/- 0.28 a seat (1.7 errors); with step 8, which read the smaller clearance better, +0.35 +/- 0.28 (1.3 errors), though each also carries the other two weights' perturbations. The run goes on; if it ends far from the defaults on noise-sized readings, it runs again at A 0.3, and any end point is confirmed against the defaults on 48 unused seeds before it can be a default.
Two steps of sixteen were outside their errors, 9 and 11. In both, the side with the larger margin won (1.26 over 0.57; 1.92 over 0.88). On the horizon they disagree (step 9: about 1 s lost to 2.3 s; step 11: 3.6 s lost to 1.6 s), and step 13 (inside its error) had about 1 s lose again. The end point: dodge_horizon 2.05 s, dodge_margin 1.52, dodge_clear 3.73 tiles, against the defaults of 4, 0.8 and 2.5. Start later, count more near misses, and clear further.
The confirmation: the end point as the trial team's weights, the defaults for the baselines, the 48 standard seeds (0 to 47, which the search, on 2,000 to 2,383, never saw), CENSUS_ACTS on.
| d a seat (48 seeds) | By range | |
|---|---|---|
| End point against the defaults | +1.24 +/- 0.75 | +0.36 +/- 1.59, +1.96 +/- 1.24, +1.39 +/- 1.06 |
| Dodges taken ashore, 48 seeds | Begun | Hit before the shell landed | By the dodged shell | Killed |
|---|---|---|---|---|
| The trial team (end point) | 8,730 | 29.0% | 20.2% | 6.4% |
| The two baseline teams (defaults) | 17,457 | 35.0% | 16.9% | 7.4% |
Promising, not confirmed. +1.24 is 1.65 errors from zero, inside the two-error rule, though all three ranges are positive. The meter agrees in direction: with the end point a dodging tank is caught by its own shell more often (it starts later), but hit by anything less often and killed less often (6.4% against 7.4%). Starting a dodge 4 s out takes a tank off its errand and into other fire; starting at 2 s keeps it where it was until the shell is close.
The defaults stay. The decision rule, set before the next reading: a second fixed batch of 48 seeds (48 to 95); the end point is promoted if, and only if, the 96 seeds pooled read past two errors. No further seeds are added after that.
On the tuner. SPSA moved three noisy weights to a point that reads +1.24 on unseen seeds, from readings of which only two were outside their errors. With A 1.0 the walk was rough, and steps 5, 6 and 11 hit the cap. The next run should use A 0.3 and, for three weights, 32 seeds a step.
The second batch (seeds 48 to 95, the rule above set before it ran):
| d a seat | Errors from zero | |
|---|---|---|
| Seeds 0 to 47 | +1.24 +/- 0.75 | 1.65 |
| Seeds 48 to 95 | +0.53 +/- 0.69 | 0.76 |
| All 96, pooled | +0.88 +/- 0.51 | 1.74 |
By range, second batch: +0.30 +/- 1.30, +1.89 +/- 1.18, -0.62 +/- 1.07.
Not promoted. By the rule set before it ran, the pooled 96 seeds had to pass two errors, and they read 1.74. The defaults stay (4, 0.8, 2.5), and this end point gets no more seeds. If the dodge's numbers are worth something, it is under a point a seat, smaller than the census resolves at 96 seeds. A new search, at A 0.3 and 32 seeds a step, would start a new hypothesis with its own confirmation. Backlog 23 closes: the tool is built, it found a lead, and the lead did not hold.
44. Each switch alone, without running out of tanks, 20 October
Backlog 29, from LR 26: with 3 respawns, 46.7% of seat-time is dead, and a part that pays late (the base cycle, the escort, bridge jobs, the turrets) is cut off before it can. The census as set measures attrition. The hypothesis: with unlimited respawns the late parts read differently from LR 24.
The run: RESPAWNS=255 JOBS=3 make switches. That is every capability off by default, alone, against today's defaults (the landfall's three on, LR 38), 12 seeds of 900 s on naval.map, RESPAWNS_UNLIMITED (255; the roadmap's 254 was a slip). Every match runs the full 900 s. team_rotate, bridge_jobs and posture are tried with board on for every team.
| Switch | Unlimited respawns: d a seat | Its error | Verdict | LR 24 (3 respawns, old defaults) |
|---|---|---|---|---|
smooth | -3.32 | 1.47 | outside: worse | inside |
posture (board) | -1.67 | 1.24 | inside | -0.81 +/- 0.65 (48 seeds) |
priced_crossing | +1.37 | 1.23 | inside | inside |
team_rotate (board) | +1.09 | 1.20 | inside | inside |
bridge_jobs (board) | +1.06 | 0.91 | inside | inside |
forget | +0.60 | 0.33 | inside (1.8) | inside |
cycle | -0.60 | 0.46 | inside | inside |
duck | -0.51 | 1.59 | inside | inside |
escort | -0.34 | 0.34 | inside | never acted |
column | +0.11 | 1.31 | inside | inside |
board | 0.00 | 0.00 | never acts alone | never acted |
island_site | 0.00 | 0.00 | never acts | never acted |
What it shows so far:
- The late parts are no better with time. The base cycle reads -0.60, the escort -0.34, bridge jobs +1.06, all inside the noise. The escort now acts (LR 24: never), and does not pay.
smoothis worse, outside the noise (2.3 errors). It was off already; this is the first clear reading of its cost.- Two parts still never act:
boardalone (nothing reads it unless a part that uses it is on) andisland_site.
The side-by-side: the same run at 3 respawns under today's defaults (RESPAWNS=3 JOBS=3 make switches), and LR 24's figures (3 respawns, the defaults before the landfall's three), all 12 seeds.
| Switch | LR 24 | 3 respawns, today | Unlimited, today |
|---|---|---|---|
bridge_jobs (board) | +0.44 +/- 0.91 | +1.68 +/- 0.70 | +1.06 +/- 0.91 |
duck | +0.39 +/- 1.04 | -2.26 +/- 1.05 | -0.51 +/- 1.59 |
smooth | -1.98 +/- 1.25 | -1.14 +/- 0.93 | -3.32 +/- 1.47 |
team_rotate (board) | -0.83 +/- 0.87 | +0.91 +/- 0.92 | +1.09 +/- 1.20 |
posture (board) | (-0.81 +/- 0.65, 48 seeds) | -0.76 +/- 1.37 | -1.67 +/- 1.24 |
column | -0.06 +/- 0.41 | +0.63 +/- 0.61 | +0.11 +/- 1.31 |
priced_crossing | +0.11 +/- 0.56 | +0.36 +/- 0.67 | +1.37 +/- 1.23 |
cycle | +0.31 +/- 0.43 | +0.02 +/- 0.19 | -0.60 +/- 0.46 |
escort | +0.01 +/- 0.01 | +0.02 +/- 0.05 | -0.34 +/- 0.34 |
forget | -0.36 +/- 0.23 | -0.00 +/- 0.15 | +0.60 +/- 0.33 |
board, island_site | 0 | 0 | 0 |
Bold is outside two errors.
Many tests, few findings. Two passes of twelve are 24 tests at the two-error line, where about one reading in twenty falls outside by chance: about one false finding is expected, and three appeared. Only smooth has the consistency to stand on: it is negative in all four readings on record (-1.92 in LR 20's list, -1.98 in LR 24, and both passes here). bridge_jobs (+1.68) and duck (-2.26) are each outside in one pass and inside in the other. They are leads, to be confirmed on 48 fresh seeds before anything changes (backlog 39).
Backlog 29 answered. The late parts do not read differently given time: the base cycle +0.02 and -0.60, the escort +0.02 and -0.34, all inside the noise. The hypothesis that the census hid them by attrition is not confirmed. With today's landfall defaults the dead share is already lower (LR 39), and unlimited respawns did not uncover them either.
smooth: Paul, on reading the first pass, "smooth sounds like we should remove it", then "maybe not remove it. give it an insignificant coefficient". It becomes a weight (LR 45).
45. A frozen census binary, 20 October
Backlog 41, from Paul: "is there an improved and faster way for you to test ?" Every census ran cargo test, which rebuilds the brain if the source has changed. So no brain code could be edited while a census ran, and every long run was idle time for the work.
The part: census_bin DIR (scripts/common.sh) builds the brain's tests once, finds the test binary in cargo's JSON output, copies it to DIR/census-bin, and prints the path. switches.sh, spsa.sh and tune.sh run the copy ("$BIN" --ignored brain_census --nocapture). A run then plays the code as it was when it began. The copy sits with the run's logs, so the binary behind any result is kept beside it.
The proof: a 1-step SPSA plumbing run (SET="landfall_memory=3", 3 seeds of 120 s) started, and 20 s in, a line that is not Rust was appended to fleet.rs. The run finished. Its reading, d -0.4567 +/- 0.4054 with memory moved to 2.3940, is identical to the first plumbing run of LR 40, which drew the same sign on the same seeds and code. fleet.rs was then restored from a copy; git diff shows the tree clean.
The other ways to test faster (LR 44's companion, CI-020 V.3) stay on the list: metrics of the part's own counts, scenario files (backlog 24), more cores, a cheaper match (finding L2), sequential rules set in advance.
46. smooth as a weight, 20 October
Backlog 40, from Paul on LR 44's table: "smooth sounds like we should remove it.", then "ok. maybe not remove it. give it an insignificant coefficient". smooth was one switch over six behaviours, and it read negative in all four readings on record.
The part: the capability smooth is gone (its bit, 9, retired), and a weight, w_smooth, 0 to 1, default 0.05, takes its place. Read through Settings::smooth (clamped) and Settings::smooth_on (above 0.5).
| Behaviour | Where | How w_smooth acts |
|---|---|---|
| The move's desire vector | Commander::go | the share of the new pull taken blends from 1 (at 0) to alpha (at 1): smooth_alpha; 0.97 at the default |
| The retreat thresholds | Commander::needs_home | blend from the constants (4 shells, 35 armour) to retreat_shells and retreat_armor; equal today, so no change until those are tuned |
| The rung's confirmation | Commander::auto | above 0.5 (a count) |
| The turret's confirmation | Tactician.confirm | above 0.5 (a count) |
| The stance's advance scalar | Tactician.smooth | above 0.5: at any share above 0 it is a different rule (press above the band's middle), not a softer one, so it is yes or no |
| Resupply by the doctrine alone | RuleBot.own_resupply | above 0.5 (yes or no) |
A file or census that names smooth as a capability reads it as w_smooth 1, and -smooth as 0, so old weights files and make switches still work. Proof: smooth_is_a_weight_that_blends_and_thresholds (0.05 near off, the alpha blend in a line, the threshold strictly above 0.5, clamped at 1); a_weights_file_round_trips_and_names_its_mistakes (the old caps = ..., smooth reads as the weight).
The reading: today's play before the change was smooth off, w_smooth 0. As the trial team, against two teams on the new default 0.05, 48 seeds, from a frozen binary (LR 45):
| Trial | Baselines | d a seat (48 seeds) | By range |
|---|---|---|---|
w_smooth 0 (the old play) | 0.05 (the default) | +0.47 +/- 0.60 | -0.96 +/- 1.21, +0.46 +/- 0.89, +1.91 +/- 0.92 |
Insignificant, as asked. At 0.05 only the desire vector acts, a move taking 97% of each new pull rather than all of it, and the census cannot tell it from the old play. The behaviours stay in the brain, reachable by one number, for the tuner or a slider.
Proof: make test: brain 74 passed and 8 ignored; client 31; net 14; proto 3; sim 50. No warnings. 363 functions, 121 undocumented.
47. The two leads on fresh seeds, 20 October
Backlog 39, from LR 44: bridge_jobs (+1.68 +/- 0.70) and duck (-2.26 +/- 1.05) were each outside two errors in one pass of 24 tests. The rule, set before the run: each is confirmed only past two errors, in the same direction, on 48 fresh seeds (96 to 143) alone. Each was run as its trial team against today's defaults (bridge_jobs with board on for every team), from a frozen binary.
| Lead | LR 44 (one pass) | Seeds 96 to 143 | Verdict |
|---|---|---|---|
bridge_jobs helps | +1.68 +/- 0.70 | +0.07 +/- 0.41 | not confirmed |
duck hurts | -2.26 +/- 1.05 | +1.25 +/- 0.52 | not confirmed: past two errors the other way |
Neither lead holds. bridge_jobs reads nothing on fresh seeds: its LR 44 reading was the chance finding the table predicted. duck reads past two errors with the opposite sign. Its readings on record: -0.51 (LR 21), +0.39 (LR 24), -0.51 and -2.26 (LR 44), +1.25 here. A part whose sign changes with the seeds has an effect smaller than the census resolves at 12 seeds, and possibly one that depends on the map's starts. "duck helps" is a new hypothesis, set by this reading, and needs its own fresh seeds before duck could be turned on (backlog 42). Both stay off.
The lesson repeats LR 43's: a reading from a table of tests, or a tuner's best point, regresses on fresh seeds. Of four leads confirmed this way since LR 36, one held (the landfall's memory, consistent in every run), and three did not.
Proof: no code changed.
48. Scenario files, 20 October
Backlog 24, from Paul (18 October): "is there a way to create description and try to attempt these via playtests faster", and his question of 20 October, "is there an improved and faster way for you to test ?". A census answers "does it win?" in 25 minutes. A single behaviour, such as "does the boat steer clear?", should be answerable in seconds, written as text rather than as a Rust test.
The part: scenario.rs, and scenarios/*.toml.
- The form, a part of TOML read in the brain (no new crate):
key = value, numbers, "strings", true and false,[lists],{ inline = tables },[expect], and[[brain]],[[pillbox]],[[depot]],[[shell]],[[event]]. Positions are anchors with offsets:"seat:0","depot:0","pillbox:0". - The setup:
players,seconds,pillboxes = false. A[[brain]]is arulebotor acommanderwith a goal (hunt_pillboxes,defend_depot:0,auto, ...); a seat without one sits still, as in the Rust tests. A[[pillbox]]is placed asone_pillboxplaces it; a[[depot]]is the nearest, given an owner; a[[shell]]lands on an anchor afterlands_inseconds; an[[event]]moves a seat's tank at a second. - The checks (
[expect]):hits_on,hits,moved,shells_spent,pillbox_owned_by(withby),pillbox_damage,pillbox_alive. A run reports every failed check in words, not the first. - The runners:
every_scenario_holdsruns every file underscenarios/incargo test;make scenario S=fileruns one and prints each thinking seat's status code every 5 s, and when the match ends (the world stops then, as in the Rust tests; the trace now says so).
The proof (backlog 24: "three of today's tests rewritten as scenarios pass and fail as they do"). Three tests rewritten: a_bot_drives_out_from_under_a_shell (lib.rs), a_commander_takes_a_neutral_pillbox_without_a_shot and a_commander_defends_its_depot (commander.rs). Each Rust test and its scenario was run unchanged, then under two deliberate breaks of the code, the files restored by a trap on every path:
| Rust test | Scenario | |
|---|---|---|
| Unmutated: the three | pass | pass |
| A, the dodge never acts: shell | FAIL | FAIL |
| B, the Commander never moves: neutral pillbox | FAIL | FAIL |
| B, the Commander never moves: depot | pass | pass |
They agree in every case. (Under B the defender still hits an intruder placed 5 tiles from its depot without moving, in both.) The three run in 0.02 s together: two tanks a world, so a step costs microseconds.
What this changes. A new behaviour can be specified as a scenario before it is built, run in a second, and kept as a test, and the census kept for whether it wins. The Rust tests stay for now; whether to retire the ported three is Paul's call. Next for the form: a [[gun]] or [[tank]] placement for the landfall's situations, and a census-sized mode, a scenario over many seeds with its rate and error.
Proof: make test: brain 76 passed and 9 ignored; client 31; net 14; proto 3; sim 50. No warnings. 18 files, 374 functions, 121 undocumented.
49. The landfall as a scenario, 20 October
LR 48's next step: scenario placements for the landfall, so its situations can be tested in seconds rather than by census.
The form grows (scenario.rs):
teams = [0, 1, 0]: a team for each seat.[[terrain]]: a rectangle of tiles around an anchor,xandyin tiles from it, set to water or grass. A sea and its coasts are carved this way.[[tank]]: a seat's tank put at an anchor and offset at the start, afloat withboat = true."centre:0": the map's centre, as an anchor.[[brain]] weights = { name = value }: a Commander's Settings with those changed; on a RuleBot, which reads no Settings, the landfall's three, its only weights held as fields.[[event]]gainsboat = true(put to sea) anddrones = "recall".[expect]gainsends_near(where a seat finishes) andlands_near(where it first comes ashore).- The outcome keeps every order the world refused a thinking seat, and the trace shows a RuleBot's status text beside its code.
The scenario, a-boat-remembers-the-gun-on-the-near-beach.toml: a RuleBot ashore beside a hostile tank sees it; at 1 s it is put to sea between a near coast (12 tiles east, by the gun, now out of sight) and a far one (22 west); its drones are recalled. With the landfall's defaults it must come ashore within 10 tiles of the far coast.
landfall_memory | Came ashore at | Verdict |
|---|---|---|
| 30 (the default) | (42.8, 64.5), the far coast | holds |
| 0 | (76.2, 56.3), the near coast by the gun | fails: 35.1 tiles from the point |
Three false starts, each a finding. The first two versions held with and without memory, so they tested nothing:
- ~~A Commander on Auto that starts at sea never prices a beach.~~ Withdrawn (LR 50): it does. The trace sampled every 5 s, and the boat had landed before the first sample, so every sample read the rung it took ashore. What was true: a Commander built by
Commander::newran the landfall's weights at 0 while its Settings said the defaults (LR 50). - The first choice binds. With the gun seen at t = 0 through a teammate, the boat chose the far coast at once. The landfall changes beach only after 2 s and for one 6 tiles better, and by then the distances had closed. Memory can only matter when the gun is seen and lost before the boat chooses.
- A drone sees for its tank. A tank moved to sea by hand left its recon drone ashore, still seeing the gun, so even at memory 0 the boat knew it. Recalling the drones made the test honest.
One oddity is kept, not chased: at memory 0 the boat first headed west and later turned east. The landing separates the two cases cleanly.
What this changes. The landfall's memory now has a test that runs in a second and fails when the memory is off, beside its census result (LR 36). The three false starts are why a scenario must be run broken before it is trusted, and the first of them (withdrawn in LR 50) is why a trace must sample faster than the thing it watches.
Proof: make test: brain 76 passed and 9 ignored. No warnings.
50. The circle at the edge of sight, 20 October
Backlog 43, from LR 49: "a Commander on Auto at sea never prices a beach". The scenario runner gained trace_every (seconds between trace lines) and each seat's position in the trace. At one line a second the claim fell at once: the Commander's afloat branch runs before the ladder, and the boat had landed in under 5 s, before the first 5 s sample. LR 49's finding 1 and backlog 43 are withdrawn.
What the fine trace found instead.
Commander::newheld the default Settings but did not apply them. Its inner RuleBot kept the landfall's weights at 0 whilesettingssaid the defaults. Dealt bots were not affected:equip(fleet.rs) and the window's tuning page (tune.rs) callset_settings. Tests, scenarios and any Commander built directly were. Fixed:Commander::newends withset_settings(Settings::default()). The brain's tests all pass. A census re-run of LR 46's first range checks dealt play is unchanged.- Without memory, a boat circles at the edge of sight. A Commander on Auto at sea between a coast 12 tiles east, by a hostile tank it saw and lost, and one 22 tiles west, traced every second at
landfall_memory0:
| s | Boat at | Making for |
|---|---|---|
| 1.9 | (68.5, 63.9) | the near beach (76, 64) |
| 3.0 | (74.2, 64.3) | (76, 64) |
| 4.0 | (73.8, 67.1) | the far beach (42, 64): the gun seen at about 8 tiles |
| 5.0 | (68.1, 66.7) | (42, 64) |
| 6.0 | (68.6, 63.9) | (76, 66): the gun out of sight, forgotten |
| 8.0 | (72.7, 68.5) | (42, 66) |
| ... | ... | the same, every 4 s, drifting south |
It sails toward the near beach, sees the gun, turns away, loses it at 10 tiles, forgets it, and turns back. It is ashore at 40 s, beside the gun. With the default memory the same boat keeps the gun in mind, sails straight for the far coast, and is ashore at 6 s. This is the mechanism behind LR 36's gain and LR 35's finding that the killer had been seen near there earlier: without memory, boats circled where the guns could reach them.
The test: a-commander-boat-does-not-circle.toml, the Commander form of LR 49's scenario. It holds at the defaults, and with landfall_memory = 0 it fails ("came ashore 35.8 tiles from the point").
The check of dealt play (LR 46's seeds 0 to 15, the same weights, the new binary) is LR 51.
Proof: make test: brain 76 passed and 9 ignored. No warnings.
51. Dealt play unchanged by the Settings fix, 20 October
LR 50 changed Commander::new to apply its default Settings. A dealt bot already had them applied (equip), so the census should play exactly as before. The check: LR 46's first range (seeds 0 to 15, w_smooth 0 as the trial against the defaults) again, from a binary built after the fix.
| Run | The 16 per-seed differences |
|---|---|
| LR 46, before the fix | -1.68 -0.58 -0.18 +1.76 -3.31 +1.45 -0.59 -5.96 -0.60 +2.52 -4.61 +4.94 +2.74 -0.95 +6.65 +5.81 |
| After the fix | -1.68 -0.58 -0.18 +1.76 -3.31 +1.45 -0.59 -5.96 -0.60 +2.52 -4.61 +4.94 +2.74 -0.95 +6.65 +5.81 |
Identical to the hundredth: the census, and every measurement in this report, played Commanders with their Settings applied all along. Only Commanders built outside a deal were affected.
52. Scenarios over many seeds, and a dodge into water, 20 October
The mode. scenario::many runs a scenario over many seeds: the world's seed (Scenario::seed, so the map outside what the file carves) and every brain's (its seed xor the run's). It reports how often it held and the binomial error, sqrt(p (1 - p) / n). SCENARIO_SET="name=value" sets a weight on every brain (set_weights), so two settings run on the same seeds. make scenario S=file RUNS=100 SET=...; SEED=n traces one seed. Proof: a_weight_is_set_on_every_brain. Brains in a scenario now think with the map (think_on), as run_bots gives it in a match. That changed no result.
Every scenario over 100 seeds (each takes 0.2 to 2.5 s):
| Scenario | Held |
|---|---|
| a boat remembers the gun on the near beach | 100 |
| a bot drives out from under a shell | 97 (before the fix below) |
| a commander's boat does not circle | 100; with landfall_memory 0, 0 |
| a commander defends its depot | 99 |
| a commander takes a neutral pillbox | 100 |
The memory's curve, the circling scenario at each memory, 100 seeds: 0 s holds 0 times; 2, 4, 6, 8, 10, 15, 20, 30 and 60 s each hold 100 times. In this geometry it is a step, not a slope: any memory longer than the landfall's two-second commitment breaks the circle. That is also why LR 41's planted test found nothing to climb between 3 and 7 s. The census's view (LR 35: killers seen 10 to 60 s before) still argues for a long memory in matches, where guns move and boats cross far.
The dodge into water. On 3 of 100 seeds the shell scenario failed. SEED=8 traced at a quarter second: the dodge's point, across the shell's line on the near side, was water. The world refused the move ("refused: water"), the shell was already marked dodged, and the tank sat under it for 3 s. Dodge::order_on (lib.rs) now takes the map, which both callers hold. It tries the near side, the far side, back along the line, and forward, and takes the first point the tank can reach: passable ground ashore; afloat, water or a beach (a boat that runs ashore lands). Without a map it acts as before (order_with). The shell scenario then held on 1,000 of 1,000 seeds.
Its effect in matches. A change to a reflex every team has cannot be paired against itself in the census, so the same 48 seeds were run as a null before and after, from two frozen binaries (the tree without the fix by git stash, then with it), CENSUS_ACTS on. The first version required water afloat; it made boats worse, so it was corrected to water or a beach.
| Same 48 seeds, every team | Before | Water only afloat | Water or a beach afloat |
|---|---|---|---|
| Deaths (the trial seat's team) | 2,472 | 2,390 | 2,297 (-7.1%) |
| Dead share of seat-time | 42.5% | 40.9% | 40.6% |
| Ashore dodges: hit / by the dodged shell / killed | 32.3% / 15.7% / 6.5% | 24.0% / 10.6% / 4.1% | 25.5% / 10.4% / 5.1% |
| Afloat dodges: hit / by the dodged shell / killed | 16.3% / 7.1% / 14.8% | 17.4% / 7.9% / 16.6% | 15.4% / 6.6% / 14.0% |
Ashore, a third fewer dodges are caught by their own shell. Afloat, the beach restores and slightly betters the old rate. 175 fewer deaths on the same seeds. These are absolute counts from two unpaired runs: no error is given, and the mean score a seat (8.70, 8.81) is relative to how every team plays, so it says nothing here.
What this changes. The census needed an hour to show the memory's gain; a scenario over 100 seeds shows its mechanism in under two seconds, and its first use found a bug that had been in the dodge since it was written. A fix to a reflex every team shares is read by before-and-after nulls on the same seeds, with absolute counts.
Proof: make test: brain 77 passed and 10 ignored; client 31; net 14; proto 3; sim 50. No warnings. 378 functions, 121 undocumented.
53. The dodge planned by the tank's own motion, and a hold that lets go, 20 October
LR 52 left ashore dodges caught by their own shell at 10.4% after the water fix. The dodge picks its point from the shell's line alone: 5 tiles from the impact, across the line, on the side the tank is on. It reads neither the tank's heading nor its speed. Since the scatter is drawn at launch (treadcore-sim impact: *at + scatter), the impact a bot reads is exactly where the shell lands, and the line it came in on says nothing once it is fired.
The instrument. Two scenario families, each one file that draws its situation from the run's seed (scenario.rs): [[tank]] gains speed, heading (degrees, or "any") and a goal drive tiles on, so it starts rolling; [[shell]] gains offset, scatter (up to that many tiles off, by the dice), from_any (the direction by the dice) and lands_in = [lo, hi]. A Commander on Auto, a shell from 30 tiles landing within 2.5 tiles of its tank in 1.5 to 3.5 s, rolling at full speed on any heading or standing (scenarios/families/). The trace now shows where each tank's route ends and where each shell lands.
| 1,000 seeds each | Rolling | Standing |
|---|---|---|
| Today's dodge | 89.6% +/- 1.0% | 96.2% +/- 0.6% |
Two failures, traced at a quarter second.
- F1, the turnaround (rolling, seed 2): the point across the line was behind a tank rolling the other way. The world routes a Move by tiles in eight directions with a waypoint at each turn (
Map::route_by), so the point was two legs off, the first beside the tank; it looped round it and was 0.9 tiles from the impact when the shell landed. - F2, the release (standing, seed 5): the tank reached its point at 1.7 s with 1.65 s of flight left. At
dodge_hold0 the hold ends on arrival; the Auto ladder's fortify errand was routed back across the impact, and the shell landed on it. LR 28's blunt answer, holding for the whole flight (dodge_hold1), cost 1.18 a seat.
The parts.
drive_to(lib.rs): where a tank will be after n ticks sent to a point, by the sim's rules of motion: the route the world will plan (route_by, without the pillbox and mine prices the brain cannot read), then turn rate, easing for a corner and braking before the next, the pivot past 2 rad, momentum, the ground's cost, stopping where the way is shut.drive_to_foresees_the_world: 96 drives (at rest and rolling, four headings, twelve points 5 tiles round) end exactly where the world puts them, worst 0.000 tiles; broken two ways it fails (no corner braking: 1.7 tiles off; a straight line in place of the route: 4.7).dodge_plan(weights.rs, thresholded at 0.5, default 0): the dodge's candidates (today's four, then straight on to where the heading leaves the 5-tile circle, then twelve round the impact) are each driven bydrive_tofor the shell's flight; the first that ends past the splash and the margin is taken, so today's point stands wherever it works; failing all, the one ending farthest off.dodge_release(default 0): after the Commander's think, while a dodged shell is in flight and this tick was not the dodge's own order, a Move thatdrive_tosays has the tank inside the splash and the margin at the landing is struck, and asked for again next think (Commander::think_released,Dodge::release). An errand that keeps clear goes at once, unlikedodge_hold.
Two false starts, each a finding. dodge_plan driving a straight line moved nothing (89.6% to 89.7%): the plan kept today's point because it foresaw a straight drive, and the world drove a route. And the first "straight on" point, the tank's speed times the flight plus 5 tiles, ran one tank 14 tiles out into a neutral pillbox's reach (the family's one failure left with both parts on); straight on now stops at the 5-tile circle.
| 1,000 seeds each | Rolling | Standing |
|---|---|---|
| Today's dodge | 89.6% +/- 1.0% | 96.2% +/- 0.6% |
dodge_plan 1 | 94.5% +/- 0.7% | 97.0% +/- 0.5% |
dodge_release 1 | 95.0% +/- 0.7% | 99.2% +/- 0.3% |
| both | 100.0% (1,000) | 100.0% (1,000) |
Each part mends about half the failures, and together they mend all of them. The traced case of F2 with both on: the tank waits at its point from 1.2 s, the errand is let go at 2.2 s when drive_to finds it still short of the splash at the landing, and the shell lands 3.4 tiles off.
The tests. seed = n in a scenario file pins it to one of many's seeds, so a family's case is kept as a test. a-dodge-does-not-turn-back-under-its-shell.toml (rolling, seed 2, dodge_plan 1) and a-dodge-waits-out-its-shell.toml (standing, seed 5, dodge_release 1) each hold, and fail with their weight at 0.
The census. The trial team on dodge_plan 1 and dodge_release 1 against today's defaults, the fair census from a frozen binary, CENSUS_ACTS on. The first run left out CENSUS_MAP and CENSUS_RESPAWNS and so played the census's own defaults, a generated map without respawns; it is kept as a reading on a second map (backlog 25), and the standard run followed. The meter's dodge counts are the trial team's against the rest of the seats (disjoint groups, meter.rs).
| 48 seeds | naval.map, 900 s, 3 respawns (0 to 47) | generated map, no respawns (0 to 47) |
|---|---|---|
| Difference a seat | +0.92 +/- 0.47 (1.94 errors) | +1.20 +/- 0.60 (2.00 errors) |
| Ashore, caught by its own shell: trial / rest | 5.7% / 10.1% | 6.2% / 10.6% |
| Ashore, killed: trial / rest | 3.8% / 4.9% | 4.2% / 4.8% |
| Afloat, caught by its own shell: trial / rest | 4.2% / 6.9% | (9 dodges in all) |
| Afloat, killed: trial / rest | 11.6% / 14.7% |
Fresh seeds. The rule, set before the run (LR 47): confirmed only past two errors, the same way, on 48 seeds not used before (240 to 287; 144 to 239 are kept for duck, backlog 42), alone.
| naval.map | Seeds | Difference a seat | Ashore, own shell: trial / rest | Afloat, killed: trial / rest |
|---|---|---|---|---|
| First | 0 to 47 | +0.92 +/- 0.47 | 5.7% / 10.1% | 11.6% / 14.7% |
| Fresh | 240 to 287 | +0.70 +/- 0.66 (1.06 errors) | 6.3% / 10.2% | 11.7% / 14.4% |
| Pooled | 96 | +0.81 +/- 0.40 (2.00 errors) |
On the score, not confirmed. The mechanism repeats to the tenth on the fresh seeds: own-shell catches ashore cut by 40%, boats killed in a dodge by a fifth. All three score readings are positive (+0.92, +0.70, +1.20), and pooled they reach two errors, but alone the fresh seeds do not. A seed's spread is 3.9 a seat, so an effect of 0.8 needs about 95 fresh seeds, (2 x 3.9 / 0.8)^2, to pass two errors by itself. Both weights stay 0, today's. Whether to turn them on, as the landfall's three were (LR 38), is Paul's choice; what argues for it is that the dodge is a reflex every team has, measured here by its own counts as well as the score, and that the scenarios show no case it makes worse.
What this changes. The dodge's last two failures in the scenarios have a cause and a mend, each proved in seconds, and the census shows them acting in matches as the scenarios predicted. drive_to gives the brain a way to ask where a tank will be, exactly, which the duck and the landfall choose points without (backlog 46). A scenario family, a situation drawn from the seed, measured a part to a tenth of a percent in three seconds.
Proof: make test: brain 78 passed and 10 ignored; client 31; net 14; proto 3; sim 50. No warnings. 382 functions, 121 undocumented, 88 tests.
54. The planned dodge on by default, and a new rule, 20 October
Paul, on LR 53's question: "yes. keep all the new behavior. keep adding. we need to finish writing the brain. but ofcourse it still must work. even if slightly less. We will continue to adjsut scalars after all features are added. but we need to add them wile finding a sensible starting numbers."
dodge_plan and dodge_release default to 1. The old dodge is one weight away (dodge_plan = 0, dodge_release = 0). With the defaults, both scenario families of LR 53 hold on 1,000 of 1,000 seeds.
The rule changes. Since LR 11 a part landed off and waited for a census to promote it; of the eighteen parts built that way, three were promoted (castle_cap, the landfall's three, now the dodge's two) and the rest wait at the noise floor that 48 seeds cannot see past (LR 24, 47, 53). From here a part lands on, with numbers started where a scenario family or a short sweep puts them; the census guards that the brain still works, at a small cost at most; the tuner follows once the brain is written. The parts already built and off are next (LR 55).
Proof: make test: brain 78 passed and 10 ignored; client 31; net 14; proto 3; sim 50. No warnings.
55. Every part on, and the guard, 20 October
By LR 54's rule the eleven parts built and still off go on: forget, board, team_rotate, priced_crossing, column, cycle, island_site, escort, bridge_jobs, duck, posture. Every bit of Caps is now on by default; each old method is one switch away (caps = baseline, -name).
Two tests compared a part on against Caps::BASELINE as the part off (a_tank_ducks_a_gun_ready_before_its_own, its duel too, and a_teammate_finishes_a_part_laid_bridge); their off cases now name the part off. No other test moved.
The guard. The trial team on the new defaults against the defaults of LR 54 (caps = baseline minus the eleven), the fair census on naval.map, 900 s, 3 respawns, seeds 0 to 47, a frozen binary.
| 48 seeds | Every part on, against LR 54's defaults |
|---|---|
| Difference a seat | -0.85 +/- 0.59 (1.44 errors, inside the noise) |
| The three jobs' means, trial / baseline | 7.33 / 9.26, 7.30 / 9.11, 9.32 / 8.12 |
| Brain time | 37.8 to 38.9 us a seat-tick (26.5 to 29.6 in LR 47) |
| The trial team's rung seat-seconds | fortify 3,780 / 3,630 / 3,694; expand 4,241 / 3,262 / 2,861; turrets 455 / 820 / 444 |
It still works, a little less on the score, inside the noise; by history the posture's lean (LR 24 to 26: -0.81, -1.04) and team_rotate (LR 24: -0.83) are the likely costs, and the tuner's phase will set their numbers (backlog 48). The brain costs about 30% more time; sixteen seats at 4x speed would take about 5% of a core.
56. A dodge against every shell in flight, 20 October
Backlog 45. LR 53's plan drove each candidate against the one shell it dodged, and the release checked only the last shell dodged. Two guns on one tank, or one gun's salvo, are common in the quad.
The families: scenarios/families/a-rolling-tank-under-two-shells.toml and a-standing-tank-under-two-shells.toml, two shells from any sides, each landing within 3 tiles of a Commander's tank in 1.5 to 3.5 s.
The part: dodge_every (weights.rs, thresholded at 0.5, on by the rule of LR 54). Dodge::threats: every hostile indirect shell in flight landing within dodge_horizon, beside the one dodged. drive_path: drive_to's positions after every tick (drive_to is now its last; drive_to_foresees_the_world still reads 0.000). Dodge::clearance: the least, over the landings, of the tank's distance from each impact at its own landing tick, less the splash and the margin. planned takes the first candidate clear of all of them; release strikes an errand foreseen inside any of them, dodged or not.
| 1,000 seeds each | Plan and release on the one shell (LR 53) | On every shell |
|---|---|---|
| Rolling, two shells within 3 tiles | 76.5% +/- 1.3% | 100.0% |
| Standing, two shells within 3 tiles | 79.9% +/- 1.3% | 100.0% |
| Rolling, three within 2.5 tiles | 67.7% +/- 1.5% | 100.0% |
| Rolling, four within 2.5 tiles | 56.6% +/- 1.6% | 99.7% +/- 0.2% |
| One shell, rolling and standing (LR 53) | 100.0% | 100.0% |
(Without LR 53's two parts the two-shell families read 67.0% rolling and 75.9% standing.)
The test: a-dodge-clears-two-shells.toml, seed 2 of the rolling family; it holds, and with dodge_every 0 the tank is hit.
57. The duck foreseen, and a shot that waits, 20 October
Backlog 46. The duck (LR 21, on since LR 55) takes the nearest point to the errand among sixteen round the tank that are out of the gun's reach or line, and never asks whether the tank gets there before the gun is ready.
The family: scenarios/families/a-tank-under-a-ready-gun.toml. A Commander on Auto at rest, any heading, its gun reloading for 2.5 s; a hostile RuleBot 9 tiles off in any direction ([[tank]] around = true), its gun ready in 0.4 to 1.4 s (reload = [lo, hi]). Held: never hit in 3 s, by the direct shot or by the shell it lobs once the tank is past direct range. The trace now shows the ground, speed and heading of each tank, and every hit with its damage and the drones in the air; the RuleBot takes only the weights it holds from SET.
What the traces found.
- The duck jitters. Each think it picks again round the tank's new position; the order is re-sent when the point moves a tile, so the tank pivots toward each new route and, on seed 9, sat a full second at its start.
- A tank has no turret. On seed 4 the duck and then the dodge worked until the tank's own gun came ready at 2.5 s; "the guns stay free", so it lobbed a shell back. Firing turns the hull to the shot (treadcore-sim
FireIndirect:t.heading = (at - pos).heading()): 89 degrees to 212 in one tick, and at 3.9 tiles a second the tank ran on that way, into the splash of the shell it was leaving. - Of the first 21 failures in 150 seeds, 13 were a direct shot at 1.25 to 1.3 s: the gun ready at 0.9 s, the shell 0.3 s in flight. From rest, 0.9 s carries a tank about 1.6 tiles: out of reach in time only by the right way at once, or out of the gun's line.
The parts (both on, LR 54's rule):
duck_plan(weights.rs, thresholded at 0.5): the point is held while it stays safe; otherwise each of the sixteen is driven bydrive_toto the tick the gun is ready, and the nearest to the errand of those out of reach (10.8 tiles, the range and the hit radius) or out of line by then is taken; failing that, the one farthest out of reach then.dodge_fire: with a hostile shell landing within the horizon, each shot is foreseen bydrive_pathtwice, the hull as it is and turned to the shot, toward where the tank is going; a shot that takes a clear drive into a splash is struck (Dodge::hold_fire, fromCommander::think_released, dodging or not).
| Ready-gun family, 1,000 seeds | Held |
|---|---|
w_duck 0 (no duck) | 63.7% +/- 1.5% |
the duck of LR 21 (duck_plan 0, dodge_fire 0) | 76.6% +/- 1.3% |
with dodge_fire | 76.7% +/- 1.3% |
with duck_plan too (the defaults) | 83.4% +/- 1.2% |
duck_plan takes 29% of the duck's failures; dodge_fire saves the tank alone on 2 seeds of 400 here (a tank in this family dodges with its gun ready only late), and seed 4 of the fixed-reload form. Every other family holds as before (100%).
The tests: a-tank-ducks-out-of-reach-in-time.toml (seed 19, duck_plan) and a-dodging-tank-holds-its-fire.toml (seed 261, dodge_fire); each holds, and fails with its weight at 0. The first form of the second, seed 4 at a fixed reload, held with dodge_fire 0 once duck_plan had changed the duck: a test kept only after it fails broken.
The guard (LR 54's rule): the trial team on the new defaults against the same with dodge_every, dodge_fire and duck_plan at 0 (LR 56 and 57 together), the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 47, a frozen binary.
| 48 seeds | Trial (LR 56, 57 on) | The rest (off) |
|---|---|---|
| Difference a seat | +0.21 +/- 0.68 (inside the noise) | |
| Ashore dodges: hit / by their own shell / killed | 16.0% / 1.6% / 4.0% | 20.7% / 6.1% / 4.3% |
| Afloat dodges: hit / by their own shell / killed | 8.9% / 0.5% / 8.0% | 12.4% / 4.0% / 11.4% |
| Brain time | 40.2 to 42.2 us a seat-tick (both sides) |
The score does not move past the noise; the reflexes do what the families say: a dodge caught by its own shell is now rare ashore and afloat, and boats in a dodge die 30% less. Since LR 52 the own-shell share ashore has gone 15.7%, 10.4%, 5.7%, 1.6%.
Proof: make test: brain 78 passed and 10 ignored; client 31; net 14; proto 3; sim 50. No warnings.
58. The front read, and a front remembered, 20 October
Backlog 49. Since LR 24 the board has built its influence, front and territory every second and nothing has read them. The Expand rung took the depot not of the side nearest the base, wherever the two sides met.
The part (on, LR 54's rule): w_front (weights.rs, 0.5, needs board). With the board's front known, Commander::expand costs each open depot (1 - w) its distance to the base plus w its distance to the nearest front sector's centre (TeamBoard::front, our sectors beside theirs), and takes the cheapest. A depot so chosen is kept until it is taken or given up (depot_target): the front changes each second, and a tank whose own arrival tipped the balance found the front gone and turned back for the nearest (the scenario's first trace, 4% of the way into the capture). At 0, or with the board off, the nearest depot to the base as before.
The scenario: an-expanding-tank-takes-the-depot-at-the-front.toml. A Commander with only the Expand and Guard rungs on and a team board (board = true, rungs, new); a teammate 30 tiles east sees a still hostile tank 8 tiles past it (a team shares its sight); two neutral depots placed by [[depot]] at, one 12 tiles south (the nearest), one 18 tiles east toward the front. Expected depot_first (new, with depot_owned_by): the eastern one. It holds; at w_front 0 it fails ("took depot 1 first, at 13.1 s").
First reading: the front is seldom known. The meter (new: Expand's fresh picks with w_front on, by group, with and without a front, and those moved off the nearest depot) over the first 60 matches of the guard census: 151 fresh picks with a front known, 2.5 a trial match, 69 of them moved (1.2 a match). The board reads one member's observation once a second, and a hostile tank counts only while in sight (LR 35's finding for the landfall, again). The census was stopped there: a part that acts once a match cannot be read by it.
The memory: front_memory (30 s, as landfall_memory). Each hostile tank in sight at a rebuild is kept where it is (TeamBoard::seen); one seen dead, or out of sight for front_memory, is dropped; the rest still count where they were, in a second influence (TeamBoard::known) that the front is read from. The influence itself, the threat, the defence points, the posture and the rotation read only what is in sight, as before. Proof: a_front_seen_is_remembered (the front unchanged a rebuild after the hostiles leave sight, gone after 31 s, gone at once at 0).
Expand picks few depots, about four a match, since each is kept until taken.
The guard: the trial team on the defaults against the same with w_front and front_memory 0; the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 47, a frozen binary (build/lr58).
| 48 seeds | Trial (w_front 0.5, front_memory 30) | The rest (both 0) |
|---|---|---|
| Difference a seat | -0.14 +/- 0.42 (inside the noise) | |
| Mean score a seat, by job of 16 seeds | 8.27, 8.57, 8.13 | 8.30, 8.47, 8.60 |
| Expand's fresh picks with a front known, a trial match | 3.2 (459 of 536; 14% with none) | |
| of those, off the nearest depot | 176, 1.2 a match | |
| Ashore dodges: hit / by their own shell / killed | 16.5% / 1.7% / 4.3% | 15.9% / 1.6% / 4.0% |
| Brain time | 67.7 to 69.3 us a seat-tick (both sides; the machine was building and testing beside it, LR 57 read 40 to 42 unloaded) |
The brain still works with the part: the score does not move past the noise, as a part that changes about one decision a match should not. The memory raised the picks with a front known from 2.5 to 3.2 a match and left the ones moved where they were (1.2 and 1.2): when the front is known, it is mostly known from what is in sight. The front read is in; whether it pays waits for the tuning phase (backlog 48), and its larger use, the guard posts (backlog 51).
Proof: make test: brain 81 passed and 11 ignored; client 32; net 14; proto 3; sim 50.
59. The families held to floors, and allies' goals on the screen, 20 October
Backlog 47, the floors. A family (scenarios/families/) now says how many seeds it runs (runs, 200 if unset) and the share it must hold (floor); make families (scenario.rs every_family_holds_its_floor, ignored) runs every family from seed 1 and fails if one is under its floor; SET="name=value" breaks a part on every family at once. A seed takes about 6 ms, so all five families at 1,000 seeds take 31 s on one core.
The floors are set under today's rates, more than two errors below the ready gun's and at 99% for the four dodge families, which hold every seed:
| Family, 1,000 seeds | Today | Floor | dodge_plan and duck_plan 0 | dodge_release 0 |
|---|---|---|---|---|
| a rolling tank under a shell | 100.0% | 99% | 95.0% | |
| a rolling tank under two shells | 100.0% | 99% | 76.4% | |
| a standing tank under a shell | 100.0% | 99% | 99.2% | 97.0% |
| a standing tank under two shells | 100.0% | 99% | 84.2% | |
| a tank under a ready gun | 83.4% +/- 1.2% | 80% | 74.9% |
Each family falls under its floor with the part it proves broken (in bold); the standing tank under one shell is the release's family (LR 53), not the plan's, and falls with dodge_release 0.
Backlog 50, allies' goals (11c). Read first: a window that hosts already lists every allied bot under FLEET on the coordination screen with the bot's own status, its rung and what it is going for ("auto > expand: to the depot at (82,10), 20 tiles"), since 15 October (session.rs step_host, b257eec). What is missing is the same for a window that joins over the network: the bots think on the host, and their goals are not in the Observation, so that half needs the wire (version 16, which goal 4 must not change) and waits on goal 6. The hosting half is now proven: client the_screen_shows_an_allys_rung_and_target runs an allied Commander on Auto for 3 s and finds its line, rung named, on the screen.
60. The crews and the hop tested, and the map documented, 20 October
Backlog 26 (CI-001 A9, A10). crew.rs and hop.rs had no tests of their own; 121 of the brain's functions had no doc comment.
each_crew_gets_its_own_item_and_a_late_one_is_given_up (crew.rs). FleetCommand::think on a castle plan, with two crews answering Ready on the commander's observation: a muster and one Build each, never the same tile, both kept off the commander's own squad (plan.skip); the next tick, both busy, nothing. Late by JOB_FOR, each crew is dropped and its item counted failed; Ready again, the same two items go out; late a second time (FAILS), the plan skips them and the next two items go out instead. Broken (n > FAILS for n >= FAILS) it fails.
a_hop_bridges_a_narrow_gap_and_sails_a_wide_one (hop.rs). A tank sent (Hop::send) across a channel on an all-grass map, 200 tiles of forest behind it: 4 tiles of water are bridged from the near bank, the whole crossing a job of 4 tiles; BRIDGE_MAX + 5 is not bridged. Priced (priced_crossing, LR 6.2), 7 tiles bridge (7.9 against a boat's 8.3) and 8 sail (10.5). The first form, with no forest, sailed at 7: scarcity is the trees of the tank's own island, 12 / (forest + 1), and a bare island makes every bridge dear, which the test now says. Broken (the price's verdict inverted) it fails.
Doc comments for the 21 undocumented functions of crew.rs, hop.rs and board.rs, and the new tests' helpers; take_jobs's own comment had slipped above adopt and is back on it. make brainmap: 392 functions, 99 with no doc comment (121 before), 92 tests.
61. The guard posts at the front, 21 October
Backlog 51, the rest of 49's hypothesis: the Guard rung's post and the rotation's push point, leaned toward the board's front, meet attacks before they reach the base. Before, the post was the base's (behind the castle's gate, else beside the depot) and the push point halfway from the base to the side's objective (the nearest depot not of the side, rotation.rs), wherever the two sides met.
The parts (on, LR 54's rule; each needs board, and is off at 0):
w_front_push(0.75):Rotation::fleet_pushmoves the push point that share of the way toward the front sector nearest it. The Commander hands the rotation the front's sector centres each think (Rotation::front,front_w), before the push point is fitted to ground the tank can reach. Test:the_push_point_leans_to_the_front.w_front_post(0.5):Commander::guard_postmoves the base's post that share of the way toward the nearest front sector, at mostFRONT_POST_REACH(BASE_ALARM - BASE_R, 6 tiles) so the base's ring stays inside the guard's leash; back to the post if the world has refused a drive there. Used bydefend_basewhen the guard is not on rotation.Commander::front_points, the front's sector centres with the board on, now shared with Expand's front read (LR 58).
The family: scenarios/families/a-raid-on-a-guarded-base.toml. A Commander with only the Guard rung and a team board, its base in the map's centre ("centre", an anchor that now takes no number). A brainless hostile tank 40 tiles off, any side, drives at the base (toward, new: a tank headed at a place). A teammate 8 tiles from the raider sees it for half a second and comes home, and the guard's drones are recalled then, so the front on the raid's side is only remembered (front_memory). Held: the guard's first hit on the raider falls more than 10 tiles from the base, the base's ring (met_beyond, new; the outcome now keeps where each hit tank was).
The first form of the family, 30 tiles, the teammate staying and the drones flying, held at every bar with the parts on and at 0 alike: the guard's shells, spotted by the teammate and its recon drone, and a strike drone met the raider 20 to 23 tiles out before the guard had moved. A raid seen far out is met far out whatever the post. With the sight taken away:
| 400 seeds, met beyond 10 tiles | held |
|---|---|
| both 0 (before) | 50.5% +/- 2.5% |
w_front_push 0.25 | 50.5% +/- 2.5% |
w_front_push 0.5, the post at 0 | 69.8% +/- 2.3% |
w_front_push 0.75 (the default) | 85.5% +/- 1.8% |
w_front_push 1 | 89.5% +/- 1.5% |
w_front_post 0.25, 0.75 or 1, the push at 0.5 | 68.2% +/- 2.3% |
w_front_post 1, the push at 0.25 | 50.5% +/- 2.5% |
The push point does it all; the post nothing here. A guard with no raider in its leash is on rotation and goes where the rotation sends it, and once the raider is within 16 tiles it is met wherever the leash is centred. 0.75 was taken over 1: within two errors of it, and a push point on the front itself takes the tank farthest from its base. Floor 0.75; at w_front_push 0 the family reads 50.5%.
The meter (new: a guard's seconds with the push point or the post worked out, and of those, moved toward the front), read before the census was let run (LR 58's lesson): in the first matches the push point leaned in 404 to 537 seconds a match.
The guard: the trial team on the defaults against the same with both weights 0; the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 47, a frozen binary (build/lr61).
| 48 seeds | Trial (w_front_post 0.5, w_front_push 0.75) | The rest (both 0) |
|---|---|---|
| Difference a seat | -0.27 +/- 0.64 (inside the noise) | |
| By job of 16 seeds | +1.79 +/- 1.26, -1.70 +/- 1.05, -0.89 +/- 0.88 | |
| Mean score a seat, by job | 9.31, 7.34, 7.53 | 7.51, 9.05, 8.42 |
| Push point worked out, moved toward the front | 23,950 of 30,216 s (79%) | |
| Guard's post worked out (a raider at the base), moved | 1,039 of 1,130 s (92%) | |
| Ashore dodges: hit | 15.7% (2,086 of 13,316) | 15.8% (3,931 of 24,846) |
| Brain time | 58.7 to 60.7 us a seat-tick, both sides |
The brain still works with the parts: the score does not move past the noise. Unlike the front read, the push point acts most of the time a guard rotates, so the census reads a part that acts and finds no gain or loss a point wide. The push point is worked out about 210 s a trial match (144 of them), the post about 8: a raid on a guarded base is rare on naval.map. Both stay on with their starting numbers for the tuning phase (backlog 48).
Proof: make test: brain 82 passed and 11 ignored; client 32; net 14; proto 3; sim 50. make families: six families, each above its floor.
63. Every part off, one at a time, 21 October
Backlog 48, the starting numbers for the brain with every part on. LR 55 read every part on at -0.85 +/- 0.59 against LR 54's defaults. The hypothesis: one or two parts carry that cost (by history the posture's lean and team_rotate), and their weight halved (w_posture 0.25, swap_gap 0.6) gives it back.
The tool: make parts (scripts/parts.sh), the reverse of make switches. The fair census once for each part, the trial seat on the defaults (every part on), the baseline with that part off: a capability as -name, a weight at the value its doc line names as before it. The difference is what the part is worth; above zero it pays. 31 parts: 17 capabilities and 14 weights. Logs, weights files and the table under build/parts/STAMP.
Each part off: naval.map, 900 s, 3 respawns, 12 seeds, a frozen binary, four at once (JOBS=4 make parts, about 1.5 h).
| Part off | On | Off | Difference | Error |
|---|---|---|---|---|
-dodge | 12.04 | 6.84 | +5.20 | 1.91 (outside) |
landfall_memory=0 | 10.32 | 7.56 | +2.76 | 1.76 |
-posture | 9.77 | 7.96 | +1.82 | 0.94 |
-rotate | 9.71 | 8.11 | +1.60 | 1.27 |
landfall_shy=0 | 9.60 | 8.15 | +1.45 | 1.27 |
-cycle | 9.49 | 8.05 | +1.44 | 0.90 |
dodge_plan=0 | 9.33 | 7.93 | +1.40 | 1.54 |
w_site_beach=0 | 8.92 | 7.54 | +1.38 | 1.45 |
w_front_post=0 | 9.03 | 7.94 | +1.09 | 1.01 |
-island_site | 8.57 | 7.55 | +1.02 | 1.28 |
front_memory=0 | 9.39 | 8.43 | +0.96 | 1.32 |
-team_rotate | 8.94 | 8.12 | +0.83 | 0.88 |
-priced_crossing | 8.60 | 7.76 | +0.83 | 1.09 |
-board | 9.57 | 9.01 | +0.56 | 1.20 |
-bridges | 8.90 | 8.40 | +0.50 | 0.91 |
w_smooth=0 | 8.40 | 8.04 | +0.36 | 0.88 |
duck_plan=0 | 8.67 | 8.49 | +0.18 | 1.26 |
-bridge_jobs | 8.66 | 8.50 | +0.16 | 0.52 |
-escort | 8.21 | 8.21 | 0.00 | 0.00 |
-column | 8.41 | 8.52 | -0.11 | 0.36 |
-forget | 8.11 | 8.31 | -0.19 | 0.49 |
dodge_release=0 | 8.18 | 8.65 | -0.46 | 0.80 |
w_front=0 | 7.80 | 8.56 | -0.76 | 0.82 |
-castle_cap | 8.28 | 9.09 | -0.81 | 0.62 |
-pings | 8.57 | 9.61 | -1.05 | 1.13 |
dodge_every=0 | 7.88 | 9.06 | -1.18 | 1.36 |
w_front_push=0 | 7.78 | 9.09 | -1.30 | 1.23 |
dodge_fire=0 | 7.63 | 9.04 | -1.42 | 1.22 |
-duck | 7.33 | 8.89 | -1.57 | 0.96 |
-kite | 7.58 | 9.34 | -1.76 | 1.04 |
landfall_wait=0 | 6.79 | 8.82 | -2.03 | 1.29 |
One part is outside two errors, and it pays: the dodge. None costs past two errors. The two the hypothesis named read on the paying side, the posture +1.82 and team_rotate +0.83. Of 31 tests at two errors about 1.5 would cross by chance; 21 of 31 read above zero. -escort reads exactly 0: no escort job arose on naval.map in 12 seeds.
The halved weights, the plan's proof, and the worst row halved: the trial seat on each against the defaults; the fair census at 48 seeds (0 to 47), three jobs of 16, frozen binaries (build/lr63a, build/lr63b).
| 48 seeds | Difference a seat | By job of 16 |
|---|---|---|
a: w_posture 0.25, swap_gap 0.6 | +0.61 +/- 0.60 (1.0 errors) | -0.75 +/- 1.00, +1.64 +/- 1.24, +0.92 +/- 0.83 |
b: landfall_wait 5 | -0.09 +/- 0.46 (0.2 errors) | +0.25 +/- 0.93, -0.75 +/- 0.65, +0.22 +/- 0.83 |
Neither is a gain. The defaults stand. The hypothesis is not supported: LR 55's cost is not one or two parts. Read with this table it is most likely the sum of many small effects of both signs, or no cost at all (LR 55 was 1.4 errors). A half point a seat, the size asked of it, takes the 48 seeds per part that one night of make parts cannot give; the table ranks where to look first (kite, landfall_wait, duck below; the dodge's three numbers among them).
Proof: make test: brain 83 passed and 11 ignored; client 32; net 14; proto 3; sim 50. make families: seven families, each above its floor. No code in the brain changed.
64. The bottom of the parts table at 48 seeds, 21 October
Backlog 53. LR 63's table read three parts below zero at 12 seeds, none past two errors: kite -1.76 +/- 1.04, duck -1.57 +/- 0.96 and the dodge's dodge_fire -1.42 +/- 1.22. The hypothesis: one of them costs a half point a seat on naval.map.
The run: PARTS="-kite -duck dodge_fire=0" SEEDS=48 JOBS=3 make parts; the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 47 in one job a part, three at once, a frozen binary, about 50 min (build/parts/20261021-123214).
| Part off | 48 seeds | Seeds above zero | 12 seeds (LR 63) |
|---|---|---|---|
-kite | -0.94 +/- 0.77 (1.2 errors) | 22 of 48 | -1.76 +/- 1.04 |
-duck | +0.12 +/- 0.60 (0.2 errors) | 22 of 48 | -1.57 +/- 0.96 |
dodge_fire=0 | -0.19 +/- 0.58 (0.3 errors) | 26 of 48 | -1.42 +/- 1.22 |
None is past two errors. The duck and the dodge's held shot fall to nothing; the 12-seed reads were the tail of 31 tests, as LR 63 warned. The kite keeps a lean below zero at 1.2 errors. The defaults stand; no part goes to a family.
The kite is the one left to look at, and it cannot be read yet: the meter counts no kiting, so how often it acts, and whether it costs when it acts or only shifts the play, are unknown. LR 58's rule (count before the census) puts the count first: backlog 54.
Proof: no code changed; make test and make families as LR 63.
65. The kite counted, 21 October
Backlog 54. LR 64 read the kite off at -0.94 +/- 0.77 at 48 seeds, the one lean left in the parts table, and the meter counted no kiting. LR 58's rule: count before the census. The hypothesis: the kite acts seldom on naval.map and its lean is noise; if it acts often, the seconds it holds a tank out of the fight are what it costs.
The meter (new): meter::kite(step), once a second for each live Commander seat after its think (think_released), with the kite step of its act (Act::kite(): Kite, WingKite, RallyKite) or none, by the seat's group. The census line: "seconds kiting: N of M alive (massing, closing, stepping in, stepping out, holding)". Test: a_second_alive_counts_its_kite_step. No decision reads it: the 12 per-seed differences below are LR 63's to the hundredth.
The read: the defaults (trial) against the kite off; the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 11 in three jobs of 4, a frozen binary (build/lr65).
| 12 seeds, the trial seat | Seconds | Share |
|---|---|---|
| Alive | 88,122 | |
| Kiting | 4,111 | 4.7% of alive; about 114 a trial match |
| Holding past its reach (reloading, covered, loaded) | 2,549 | 62% of kiting |
| Massing out of its reach | 746 | 18% |
| Stepping out to reload | 433 | 11% |
| Stepping in | 356 | 9% |
| Closing on a tank with no shells | 27 | 1% |
| The rest (kite off) | 0 of 199,437 |
Difference a seat: -1.76 +/- 1.04 (the pooled 12; LR 63's row).
The kite acts, about two minutes of a trial match, and is not the seldom part the hypothesis's first branch guessed. What it does is mostly wait: of every 10 s kiting, 6 are spent holding past the target's reach and 2 massing, and under 1 stepping in to fire. That fits the second branch, a tank held out of the fight, without proving it: the score's lean is still 1.2 errors at 48 seeds, and the meter does not say what the held tank would have done instead.
The kite stays on (the defaults stand). The next question is the hold's numbers in a duel, where a tank's minute held can be scored: backlog 55.
Proof: make test: brain 84 passed and 11 ignored; client 32; net 14; proto 3; sim 50.
66. The kite's hold in a duel, 21 October
Backlog 55. LR 65: the kite acts about 114 s a trial match, 62% of it holding past the target's reach. The hypothesis: the hold waits for a window that seldom opens, and a shorter wait (a smaller kite_spare, a nearer kite_in) scores more in a duel.
By whom (the meter split, new: seconds kiting by an intruder's guard, a wingman, a rally), the trial seat over three seeds of the census (build/lr66): a rally 1,092, a wingman 68, an intruder 27. The kite is the rally's: a tank that called or answered a ping and fights what is there. A duel has to be a rally.
The scenario keys (new): a Commander's pings = true calls what it sees and answers calls, as fleet.rs deals a leader (a scenario's Commander had neither, so it never rallied: in a first duel it went to Fortify and was shelled from out of sight). outscores = { by, on } holds when by hit on more often than it was hit back.
The family: scenarios/families/a-duel-in-the-open.toml. Open grass, no pillboxes, no depots, drones recalled at 0.5 s; a Commander on Auto with pings against a RuleBot (which kites too, as a plain RuleBot does) 16 tiles off in any direction; 60 s. Held: the Commander outscores it. In seed 1 it rallies, steps in while the RuleBot's gun reloads, holds past its reach, and kills it at 28.6 s, 4 hits to 0.
| 400 seeds | Held (outscored) |
|---|---|
The defaults (kite_spare 0.4, kite_in 9, kite_margin 1.5) | 63.0% +/- 2.4% |
kite 0 | 51.5% +/- 2.5% |
kite_spare 0, 0.2, 0.8, 1.2 | 64.0, 63.2, 64.0, 61.0% |
kite_in 7, 8, 10, 11 | 63.0, 64.0, 57.2, 60.5% |
kite_margin 0.5, 1, 2.5, 3.5 | 64.8, 65.0, 63.0, 64.5% |
The kite wins a duel 11.5 points more often than no kite (3.3 errors). Its numbers sit on a flat top: every one within an error or so of the default but kite_in 10, which is worse. The hypothesis is not supported: a shorter wait (kite_spare 0) scores the same. The hold is not waste in a duel; it is how the duel is won. The defaults stand; floor 0.55, above the kite off.
The census's lean (-0.94 +/- 0.77 at 48 seeds, the kite off) is then not the duel's. If it is real it is in the team's fight, the 18% spent massing and the pressing that the fleet's order forces; nothing here says it is real.
Proof: make test: brain 84 passed and 11 ignored; client 32; net 14; proto 3; sim 50. make families: eight families, each above its floor.
67. The bot skill dial, 21 October
Backlog 56. Paul, after watching the autopilot: "the game is hard". The only dial was --rookies N, a yes or no a bot, and it was read only where plain RuleBots are dealt: in a teamed match, which make run is (--teams 3), every bot is a team's Commander and --rookies reached none. The player guide's "start here" did nothing.
The weight (goal 4's rule, every either/or a scalar): skill (1, today's bots). RuleBot::rookie: bool became RuleBot::skill: f32 (RuleBot::rookie is skill 0), each of a rookie's traits by it:
| Trait | At skill s | At 1 | At 0 (a rookie, as before) |
|---|---|---|---|
| A plain RuleBot's think | every quarter second plus (1 - s) of a quarter | 4 Hz | 2 Hz |
| Drones | none before (1 - s) of ROOKIE_LATE (90 s) | at once | 90 s |
| An indirect shell's aim | lead times s; off by up to (1 - s) of ROOKIE_SCATTER (4 tiles) | led | at the tank, 4 tiles |
| A direct shot's aim | lead times s | led | at the tank |
| Dodge, kite, storm; interceptors on station | off under 0.5; one under 0.5 | on; 2 to 4 | off; one |
A Commander takes skill in set_settings (its doctrine's, its tactician's kite under 0.5, its dodge under 0.5) and spoils what it says at one place, after its think (Commander::blunt): every FireIndirect off by up to (1 - s) of 4 tiles, every FireDirect by a tenth of that, and every Launch held before (1 - s) of 90 s. Its doctrine aims in full (RuleBot::aim_skill), so a shot is spoiled once. At 1 nothing is touched: no random draw, no blend computed. Test: skill_spoils_the_aim_and_holds_the_drones.
The dial for a person: --skill on the window, SKILL= on make, dealt to every bot not on the person's team (a teamed match), not of the person's fleet (--fleets), not a wingman; allies play in full.
The proof: the duel family (LR 66) with the Commander's skill swept, 400 seeds each.
| skill | 1 | 0.95 | 0.9 | 0.85 | 0.75 | 0.7 | 0.65 | 0.6 | 0.55 | 0.5 | 0.4, 0.25, 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Duels outscored | 63.0% | 70.8 | 62.0 | 67.8 | 52.0 | 43.2 | 38.0 | 24.5 | 14.2 | 7.8 | 0 |
Flat within the noise from 1 to 0.85, then falling steadily to 0.5; under 0.5, with no dodge and no kite, a Commander never outscores a full RuleBot one on one, as rookies_are_beatable found of rookies against RuleBots. The useful range is 0.5 to 1. Two shapes tried on the way: a direct shot off by a quarter of the indirect scatter read the same at 0.75 (54.5%) as a tenth, so the tenth was kept (a direct shot at short range is the precise one); the doctrine's shot spoiled twice (its own scatter, then the Commander's) read 7.2% at 0.5, once 7.8%, within the noise; once was kept, as the plainer rule.
Nothing changed at 1: every family's count is the same to the seed (duel 252, islet 400, raid 342, the four dodges 1,000 each, the ready gun 834 of 1,000). Whether a person finds 0.6 right is a playtest's: a match at --skill 0.6 was left open for Paul.
Proof: make test: brain 85 passed and 12 ignored; client 32; net 14; proto 3; sim 50. make families: eight, each above its floor.
68. The territory read: the held ground, 21 October
Backlog 57, goal 4's last open item (12e: "Not yet: territory read by nothing"). The board built its influence every second; the posture's ladder read the territory's share (Expand under a quarter of the map), and nothing read where it was. LR 9 gave territory a job: Defend when there is "threat in our territory". LR 24 had cut Defend to hostiles within the base's alarm (16 tiles), because the sector threat counts every hostile on the map.
The read (on, LR 54's rule; needs the board):
- The held ground (
TeamBoard::ground): the influence of the pillboxes and depots alone, ours minus theirs, each atpillbox_worthwith the board's falloff; tanks left out, so a team's tanks do not make the ground around them held, which was LR 24's long tail. A sector is held when it is at least one work's value atGROUND_R(14 tiles, a pillbox's reach and six). Without the threshold every sector is held when the other side has no works. - The incursion: hostile tanks on held ground beyond the base's alarm, their worth (
TeamBoard::incursion) and the nearest the base (incursion_at). - Defend counts it: the base's threat plus
w_defend_ground(1) times the incursion, in the ladder (over 0.5) and the score. - The guard goes there: on the Guard rung with nothing at the base, the guard's post is the side's pillbox or depot nearest the incursion, if that work is an outlying one (past
BASE_R; the base's own are the alarm's), and it does not rotate (Commander::raided, "THE HELD GROUND RAIDED").
At 0 Defend is the base's alone, as LR 24 left it, and the guard as before. Test: a_hostile_on_the_held_ground_is_an_incursion (on held ground 9 tiles from an outlying pillbox: an incursion, where it is; inside the alarm: the base's; far from every work: nobody's).
The family: scenarios/families/a-raid-on-the-held-ground.toml. A Commander on Auto with the board at its base in the centre, a neutral depot 40 tiles west to expand to; an unseen hostile 35 tiles out in any direction, 9 tiles from an outlying pillbox of the side (26 from the base, out of the pillbox's reach). At 25 s, the Commander away west taking the depot, out of its gun's 60 tiles, a teammate comes to the pillbox and sees the raider. Held: the Commander hits the raider. Scenario keys, new: a [[pillbox]]'s toward; a move event's toward and step.
| 400 seeds | Held |
|---|---|
w_defend_ground 0 | 0.0% |
| 0.25 | 95.0% |
| 0.5, 1 (the default) | 100.0% |
Three stagings failed first, each a finding. The raider passing by the base was met at 0 too: the indirect gun reaches 60 tiles and the spotter's sight is the team's. With a spotter set a fixed 2 tiles south of the pillbox, a raider to the north was out of its 10 tiles' sight in 31% of seeds: no read can defend what nobody sees (a pillbox gives no sight). And the Commander was on Guard both ways when nothing else held for it; the neutral depot gives Expand, which the read must turn.
The cost in another family: the raid on a guarded base (LR 61) reads 81.8% (327 of 400), 85.5% at 0. Its spotter is a teammate whose start depot stands by the raider: for the half second the raider is seen, the read sends the guard toward that depot, an outlying work raided, as designed, and the family's measure (met far from the base) loses 15 seeds. Above its floor (0.75); kept.
The meter (new: board seconds with a hostile on the held ground, and those Defend held only for it), the census below: the trial teams 3,125 seconds (about 22 a match), 2,694 of them defended for it alone; the rest, at 0, 6,097 and none.
The guard: the trial team on the defaults against the same with w_defend_ground 0; the fair census, naval.map, 900 s, 3 respawns, seeds 0 to 47, a frozen binary (build/lr68; a first run was stopped and run again when the outlying-work filter went in).
| 48 seeds | Difference a seat |
|---|---|
| Pooled | -0.36 +/- 0.27 (inside the noise, 1.3 errors) |
| By job of 16 | -0.60 +/- 0.66, +0.06 +/- 0.38, -0.54 +/- 0.31 |
| Mean score a seat, by job | trial 7.57, 7.81, 8.23; the rest 7.51, 8.35, 8.83 |
The read acts in every census match and the brain still works with it, at a lean below zero inside the noise. Kept on, by Paul's rule of 20 October; its number is for the tuning that follows.
Proof: make test: brain 86 passed and 12 ignored; client 32; net 14; proto 3; sim 50. make families: nine, each above its floor.
69. The dodge's SPSA point on today's brain, 21 October
12g's tuning, backlog 23's open lead. LR 43's SPSA walk ended at dodge_horizon 2.05, dodge_margin 1.52, dodge_clear 3.73 (the defaults 4.0, 0.8, 2.5) and read +0.88 +/- 0.51 over 96 unseen seeds, 1.74 errors: not promoted. The brain has changed since (the dodge's plan, LR 53; the kite, the duck, the posture, the held ground), so the point was read again on today's brain, on more seeds than before.
First, the families. At the point the five dodge families hold: 99.9 to 100%, and the ready gun 83.5% against 83.4% at the defaults. Nothing the dodge was built for breaks.
The rule, set before the run: promoted only past two errors.
The census: the fair census, trial the point, baseline the defaults; naval.map, 900 s, 3 respawns; fresh seeds 96 to 287 (none read before), four jobs of 48, a frozen binary (build/lr69).
| 192 seeds | Difference a seat |
|---|---|
| Pooled | +0.25 +/- 0.32 (inside the noise, 0.79 errors) |
| By job of 48 (seeds 96, 144, 192, 240 on) | +0.88 +/- 0.60, +0.56 +/- 0.65, -0.43 +/- 0.63, +0.00 +/- 0.69 |
| Mean score a seat, by job | trial 9.02, 8.58, 8.05, 8.38; the rest 8.14, 8.02, 8.48, 8.38 |
The meter (dodges summed over the four jobs; the rest is two teams, so a team's share is half):
| Dodges, a team | Taken | Hit after | By the shell dodged | Killed |
|---|---|---|---|---|
| Ashore, the point | 49,707 | 6,100 | 997 | 1,401 |
| Ashore, the defaults | 26,092 | 5,345 | 316 | 1,398 |
| Afloat, the point | 18,157 | 1,215 | 45 | 1,132 |
| Afloat, the defaults | 10,217 | 1,000 | 36 | 899 |
The point dodges about twice as often (the wider clear, 3.73, counts more shells a threat) and is hit by the shell it dodged three times as often (the shorter horizon, 2.05 s, starts later). Kills after a dodge are level ashore and a quarter higher afloat. Twice the dodging buys nothing the score sees; likely the extra dodges are of shells that would have missed (not measured).
The verdict: not promoted; the defaults stay. One job of four read LR 43's +0.88 again; the other three read nothing. Either LR 43's reading was chance (a 1.7-error reading comes about one time in twenty) or the brain's changes since took the gain away; this census cannot tell which, and either way the point is not worth promoting today. Backlog 23's lead is closed.
Proof: make test and make families unchanged (no default moved).
70. The brain documented, 21 October
Backlog 26's last part (CI-001 A9): of the brain's 403 functions, 100 had no doc comment (LR 60 left 99; the steps since added one).
Trait methods. 29 of the 100 were methods of a trait's impl: the Brain methods of Commander, RuleBot, Defender, JsonBrain and a test's attacker; Default, Display, Drop, Index. Rust's way, and rustdoc's, is that the trait documents them. scripts/brainmap.py now lists such a method as "(the Brain trait's)" and does not count it; the map's first line gives both counts. The scan had credited the test's attacker (an impl Brain inside a test function) to the Drop impl above it; it now reads each impl's trait by its indent.
The rest, 71, documented, each from its code and, where its name promised more, checked against it. What the reading found:
- The trait itself:
Brain::nameandBrain::think, the two methods every brain implements, had no doc;run_bots's ("Run every bot seat for one tick") had slipped ontoretire_takenabove it and is back on its function, which also says now what it does for a dead seat. RuleBot::rookie: a skill-0 bot does more than not dodge (is_rookie): no kite or storm, one interceptor up and late, a slower think, scattered shells. Its doc says all of it.Commander::defend_within: the one engagement every guard, post and defence goes through, now described in its four cases.
make brainmap: 403 functions, 0 with no doc comment, 29 a trait's, 98 tests. Comments only: make test as before (brain 86 passed and 12 ignored; client 32; net 14; proto 3; sim 50).
71. The dodge on every map, 21 October
Backlog 25 (CI-001 A8): every census since LR 11 is naval.map. The census now takes more than one map: CENSUS_MAP a comma-separated list or gen (a map generated from each seed), seed n on the (n mod k)th of k maps, so a seed keeps its map however the seeds are split over jobs. MAP=all is every map under maps/ and gen: eleven. The census prints a difference for each map beside the pooled one; make census (scripts/census.sh) splits the seeds over JOBS from a frozen binary and scripts/pool.sh pools the jobs, overall and by map. make parts, switches, spsa and tune take the same MAP.
The question: the dodge is the largest part on naval.map (LR 29, -2.45 +/- 0.67 off; LR 63, +5.20 +/- 1.91 on against off). Does it hold on the other ten maps?
The rule, set before the run: it holds if the pooled difference is past two errors and no map is past two errors the other way.
The census: the fair census, trial the defaults (the dodge on), baseline -dodge; MAP=all, seeds 1000 to 1175 (16 a map), 900 s, 3 respawns, four jobs of 44, a frozen binary (build/census/20261021-193635).
The reading: all 176 seeds, the census's own pooled.txt (it was read part-way at 123 seeds first, +6.00 +/- 0.48, and committed so before it ended; the final reading replaces it). Four jobs of 44 seeds, each 2 h 45 min.
| 176 seeds | Dodge on less off, a seat | Seeds |
|---|---|---|
| Every map | +5.94 +/- 0.38 (15.6 errors) | 176 |
| island | +10.86 +/- 1.48 | 16 |
| generated | +9.01 +/- 1.19 | 16 |
| industry | +8.41 +/- 0.95 | 16 |
| fidos-airstrip | +7.36 +/- 1.07 | 16 |
| europe | +6.33 +/- 0.84 | 16 |
| big-lizard | +6.19 +/- 1.60 | 16 |
| naval | +4.64 +/- 1.37 | 16 |
| fortressislesx | +4.03 +/- 1.09 | 16 |
| constanine | +3.25 +/- 0.54 | 16 |
| atlantic | +2.99 +/- 0.53 | 16 |
| fortressisles2 | +2.29 +/- 1.15 (inside the noise, 1.99 errors) | 16 |
| By job of 44 (seeds 1000, 1044, 1088, 1132 on) | +6.65 +/- 0.77, +6.58 +/- 0.80, +6.01 +/- 0.78, +4.52 +/- 0.72 |
Ten maps of eleven past two errors, all of them for the dodge; the eleventh a hair inside, and for it; none the other way. A team, every match (528 trial, 1,056 baseline): score 15.83 against 9.88 a seat, kills 22.78 against 14.41, deaths 15.27 against 20.29.
The meter. The baseline's dodges are counted as dropped: the shells it would have dodged, and what came of them. Summed over the per-match meter lines.
| Dodges, ashore | Count | Hit after | By the shell | Killed |
|---|---|---|---|---|
| Trial, taken | 54,088 | 19.5% | 2.4% | 4.8% |
| Baseline, dropped | 74,695 | 49.4% | 23.0% | 12.2% |
| Dodges, afloat | Count | Hit after | By the shell | Killed |
|---|---|---|---|---|
| Trial, taken | 2,827 | 10.0% | 0.5% | 9.0% |
| Baseline, dropped | 4,622 | 13.8% | 4.3% | 12.2% |
Ashore the dodge cuts the dodged shell's hits about ninefold and the deaths after it by three fifths; afloat a quarter (LR 29's finding, on every map).
The matches. How a 900 s match ends, by map, over the 528: by elimination mostly on constanine (46 of 48, median 460 s), fidos-airstrip (43, 649 s) and big-lizard (40, 507 s); 31 of 48 on industry, 26 on island, 18 on naval, 17 on generated; 12 on fortressisles2; 2 on europe, none on atlantic or fortressislesx. MAPS.md's paragraph, written from one seed a map, is corrected to these.
The cost. 528 matches of 900 s in 2 h 45 min on the guest's four cores; each job had about a third of a core (44 min of CPU in its first 2 h 22 min), so the census was about 3.4 CPU-hours, 23 s of CPU a match. Section 72 takes this up.
The verdict: holds, by the rule set before the run. The dodge pays on every map, most where the fighting is ashore and open (island, industry, the generated maps) and least on the island-chain maps (fortressisles2, atlantic), where more of the fighting is afloat and the dodge afloat buys little (backlog 31). Naval.map is mid-table, so the census's one map did not overstate the dodge.
Proof: the census code is the tests' (brain_census, ignored); CENSUS_MAP unset plays gen as before; a frozen binary built from the tree as committed.
72. The method, a machine for it, and competitions, 22 October
Not a census but a look at the method that produced the last 71 sections, written as the census moves to a dedicated machine. The manual is CENSUS.md; this section is the method and what the machine opens.
The method: directed changes, Monte Carlo tests
The brain is not evolved at random. Each change is directed: a reading names a cause and a part is built against it. LR 27 found the dodge was the code held last at 43% of deaths; LR 29 to 53 built and measured the dodge; LR 71 showed it holds on every map. Each change is tested by Monte Carlo: random seeded matches, each seed played with both brains on the same start, the difference averaged over seeds with its standard error. Each test asks one question: is this brain better than that one, and by how much, past the noise?
┌──────────────────────────────────────────────────────────────────┐
│ 1. OBSERVE the meter, CENSUS_ACTS, a family, a playtest, Paul │
│ (where the time goes, what kills, what looks wrong) │
└───────────────────────────────┬──────────────────────────────────┘
▼
2. HYPOTHESIS a cause, a part to answer it; the rule that would
confirm it, written before any run
▼
3. DIRECTED the part built as a weight (either/or is a scalar)
CHANGE and a Caps switch, on by default, old way one switch
away
▼
4. FAMILY a scenario family: does it do what it was built for,
over its seeds, held to a floor? ──── no ──► back to 3
▼ yes
5. CENSUS Monte Carlo, paired:
seed ──► 3 matches (each team in the trial seat)
──► Δ = trial − baseline, a seat
mean Δ ± SE over seeds, overall and by map,
on a frozen binary, JOBS at once
▼
6. VERDICT against the rule from 2:
past +2 SE ───► keep; a default
inside ───────► keep if cheap (features on),
else withdraw
past −2 SE ───► fix (back to 3) or switch off
▼
7. TUNE SPSA on the part's weights over the paired census;
a point promoted only past 2 SE on fresh seeds
▼
8. RECORD LR section, check-in, journal, roadmap, changelog
│
└──► new readings, new questions ──► 1What 71 sections taught about the loop:
- Most parts are small. Of the parts table (LR 63), only the dodge read past two errors at 12 seeds. A half point a seat needs about 200 seeds (LR 24). The dodge, at +5.94, was settled by seed 30.
- The tuner finds noise unless the test is strict. LR 41's planted test failed. LR 69 read LR 43's promising point again on 192 fresh seeds and found nothing. Promotion needs fresh seeds and two errors.
- One map can mislead. Until LR 71 every census was naval.map. The dodge happened to be mid-table there, but on its least favourable map it reads under half its pooled value.
MAP=allis now the default question. - The rule written first keeps a reading honest when it comes in near the line (fortressisles2, 1.99 errors).
The cost, and the machine
LR 71's census was 528 matches of 900 s on eleven maps: 3.4 CPU-hours, 23 s of CPU a match. It took 2 h 45 min in the guest VM because each of its four jobs had a third of a core. The memory it needs is about 5 MB a job. The census wants cores and nothing else.
| Machine | Cores at work | LR 71's census | Backlog 22 (6,600 matches) |
|---|---|---|---|
| The guest VM, as LR 71 ran | ~1.2 | 2 h 45 min (measured) | about 1.5 days |
| The guest VM, its cores fed | 4 | about 50 min | about 10 h |
| A dual-socket server, 48 threads (to be confirmed) | ~20 core-equivalents | about 10 to 15 min | about 2 h |
The last two rows are estimates from the measured CPU-hours. A census run on the new machine will replace them.
The milestone. With a machine of its own, a census stops being a night's wait. A 200-seed reading on every map, the bar for a half point, takes an hour, not a day. The switch census (backlog 22), SPSA on every map, and the round robins below become routine.
Resuming. A census stopped part-way is finished by RESUME=build/census/STAMP make census (scripts/census.sh, its settings in the run's run.env). A job whose log never reached test result: ok is played again whole on the same binary. Checked: a 2-seed census with one job's ending removed, resumed, replayed that job line for line as it first played (timings apart). JOBS now defaults to the machine's cores. CLAUDE.md at the top of the tree carries the working rules, so a session on the new machine resumes from a clone (CENSUS.md section 4 has the prompt).
Competitions: profiles, rock-paper-scissors, rotation
The census compares two weights files, and nothing requires one of them to be the defaults. A profile is a weights file with a character. Every behaviour is already a weight (either/or is a scalar), so a style of play is a point in that space:
- Turtle: Fortify and Turrets first, the capped castle, the guard at home; slow to Expand.
- Raider: Hunt and the push leaned to the front; presses at lower odds; few works.
- Expander: takes ground, roads and depots first; fights for what it holds (the territory read).
- Navy: crosses early and lands where the guns are not (the landfall weights, the island site).
The weights that make each are chosen from weights.rs when the profile is built. None exists yet.
No single method works if the profiles are not ordered: Turtle beats Raider, Raider beats Expander, Expander beats Turtle. The census measures this directly. In a round robin, each ordered pair (A in the trial seat, B the baseline) on every map gives a payoff matrix M[A][B] = Δ ± SE:
vs Turtle vs Raider vs Expander vs Navy
Turtle 0 M_TR M_TE M_TN
Raider M_RT 0 M_RE M_RN
Expander M_ET M_ER 0 M_EN
Navy M_NT M_NR M_NE 0- Transitive if one rating orders it: then there is a best profile, and tuning should climb toward it.
- Rock-paper-scissors if it has a cycle past two errors, A > B > C > A. Then no profile is best, only best against something.
- The matrix also differs by map. A profile's strength there is a property of the map, which is a reason the census keeps by-map readings.
Rotation as a meta-strategy. In a game with a cycle, the best a side can do without knowing its opponent is to mix: play each profile some share of the time, the shares solved from the matrix (its Nash equilibrium, by linear programming or fictitious play). Rotation is that mix made into a brain, in two forms:
- Between matches: draw the profile by the solved shares. Nobody can prepare against it, and the census measures it as one brain.
- Within a match: read what the opponent is (the board already holds its posture, its front and its works) and switch to what beats it. The posture (LR 25) is a first, fixed form of this.
Both are testable the same way as everything else: a rotating brain is one more row in the matrix. The hypothesis to beat is that it ties or beats every fixed profile across the maps. A competition is then a league table over the matrix. The census already plays one; it needs the profiles and the machine.
The machine and resuming are done (backlog 58). Faster reading (backlog 59), the profiles and their round robin (60), and rotation (61) are open in ROADMAP.md.
73. The first night on a real machine: each switch off, the island site, the posture tuned, 22 to 23 October
The machine of LR 72: an i7-13700F (16 cores, 24 threads), 62 GB, Alpine. LR 71's census, played again on it from the same seeds, read the same to every digit (every map +5.94 +/- 0.38) in 977 s, against 2 h 45 min in the VM: about ten times faster. A third of the CPU time is the kernel's (sys) during a census; not yet profiled.
Each switch alone (backlog 22), as written: nothing to read. 200 seeds a switch on every map, SPLIT=2 JOBS=24 (scripts/switches.sh, 5eb9603), 4 h. Eleven of the twelve rows read +0.00 +/- 0.00: every one of those switches is on by default now (LR 44 onward promoted them), so "the default with the switch added" is the default, and both sides played the same games. smooth, a weight since LR 46 and off by default, read -0.92 +/- 0.31: it stays off. The script's list is from before the promotions; the question had become the other one.
Each default off. The same 200 seeds (0 to 199), every map, the trial team with one default taken off (SWITCHES="-forget -board ...", build/switches/20261023-061019), 4 h 6 min. Below zero, the switch earns its place.
| Taken off | Difference a seat | Errors |
|---|---|---|
| dodge | -4.37 +/- 0.28 | 15.6 |
| posture | -0.58 +/- 0.25 | 2.3 |
| priced_crossing | -0.41 +/- 0.19 | 2.2 |
| cycle | -0.30 +/- 0.18 | 1.7 |
| forget, column, escort, bridge_jobs | -0.12 to -0.03, errors 0.05 to 0.11 | under 1.2 |
| duck | +0.31 +/- 0.28 | 1.1 |
| team_rotate | +0.32 +/- 0.21 | 1.5 |
| board (and with it team_rotate, bridge_jobs, posture) | +0.49 +/- 0.26 | 1.9 |
| island_site | +0.43 +/- 0.17 | 2.5 |
Twelve rows at two errors: about one passes by chance, so a row past two errors was read again on fresh seeds before anything was changed. By map, board splits (off: island +3.98, atlantic +1.87, industry +1.56; on: naval -1.29, fidos-airstrip -1.45), and is left for its own study. island_site reads +0.00 on nine maps and all of its harm is on the two of continents: atlantic +3.10 +/- 0.59, europe +2.46 +/- 0.71.
The island site, read. With island_site on, the base is the best of the side's depots by site_score (-1 a small bare islet to 1 a large forested island) less 0.01 a tile from the tank. A tile is a hundredth of a point, so a better site anywhere on the map won. A meter line was added (bases by site: N (M off the nearest depot, T tiles in all)). On atlantic and europe nearly every base went 70 to 275 tiles off the nearest depot, across the sea: the depot a team starts by scores a poor site there, likely the continents cut by rivers into small, bare islands (not measured).
The fix: two weights in base_of (commander.rs).
site_reach(30 tiles): the better site must lie this near the nearest depot. 1000 is the rule before.site_poor(0.5): the base leaves the nearest depot only when that depot's site scores under this. 2 is the rule before. It changed no game in either census below; kept, as the SPSA run below has it in its weights files.
The rule, set before the run: on seeds 200 to 399, none read before, every map, the fix against the rule before (site_poor = 2, site_reach = 1000); kept if past two errors and no map past two errors the other way. Eight jobs of 25 seeds an arm (build/sitepoor/20261023-185003 and -195645), 1 h each.
| Against the rule before | Every map | atlantic | europe |
|---|---|---|---|
site_poor 0.5 alone | +0.03 +/- 0.02 | +0.00 | +0.00 |
site_poor 0 alone | +0.03 +/- 0.02 | +0.00 | +0.00 |
site_reach 30 alone | +0.66 +/- 0.13 | +1.97 +/- 0.48 | +4.95 +/- 0.48 |
| both (the new defaults) | +0.66 +/- 0.13 | +1.97 +/- 0.48 | +4.95 +/- 0.48 |
island_site off | +0.74 +/- 0.16 | +1.97 +/- 0.48 | +4.95 +/- 0.48 |
site_poor alone moved nothing (the start depots score under 0 there). The reach limit takes back the whole of the continents' loss, the same games as island_site off on both maps. No map reads past two errors the other way. Kept: site_reach 30 and site_poor 0.5 are the defaults. The fix against island_site off reads within the noise (+0.66 against +0.74); the switch stays, for a small island's neighbour.
The posture, tuned (running). After the dodge, the posture is the largest part the census read off (-0.58), and it splits by map (industry -1.67, generated -2.37 off; naval +1.81). Its numbers were set by hand in LR 24 and 25 and never tuned.
- Intention: find posture weights that score more a seat across every map, from the new defaults, by SPSA (LR 40).
- The weights:
w_posture,w_pos_defend,w_pos_economy,w_pos_fortify,w_pos_connect,w_pos_expand,w_defend_ground. - The run: 64 steps of 48 fresh seeds (5000 on), every map, 24 jobs, a 0.6, c 0.4 (
build/spsa/20261023-220502), about 8 h, from 22:05 on 23 October. - The rule, set before the run: the point SPSA ends on is promoted only if, on 528 seeds none of the search used (20000 on), every map, it beats the defaults past two errors and no map reads past two errors the other way (ROADMAP goal 4). The census is queued to start when the search ends.
The result and the verdict are a later section.
74. The posture tuned: a ridge, not a peak, 24 October
LR 73's run. SPSA over the seven posture weights, 64 steps of 48 fresh seeds (5000 to 8071), every map, 24 jobs (build/spsa/20261023-220502), 22:05 to 05:36: 7 h 31 min, a step 7 min.
Where it walked:
| Weight | Default | SPSA's point |
|---|---|---|
w_posture | 0.5 | 0.977 |
w_pos_defend | 1.0 | 4.18 |
w_pos_fortify | 1.0 | 2.95 |
w_pos_economy | 1.0 | 1.85 |
w_defend_ground | 1.0 | 0.750 |
w_pos_connect | 1.0 | 0.588 |
w_pos_expand | 1.0 | 0.594 |
The last four steps read -0.08, -0.46, -0.27 and +0.25 a seat (errors 0.15 to 0.34): flat. At w_posture 0.98 a leaned rung climbs past some 200 rungs, so the posture, not the ladder's order, picks the rung nearly always.
The rule, set before the run (LR 73): promoted if, on 528 seeds the search did not use (20000 to 20527), every map, it beats the defaults past two errors and no map reads past two errors the other way.
The census (build/census/20261024-053657, 05:37 to 06:32):
| 528 seeds | The point less the defaults, a seat |
|---|---|
| Every map | -0.00 +/- 0.14 |
| naval | +0.96 +/- 0.53 |
| fidos-airstrip | +0.84 +/- 0.52 |
| atlantic | +0.77 +/- 0.35 (past two errors) |
| europe | +0.46 +/- 0.34 |
| island | +0.20 +/- 0.47 |
| industry | -0.23 +/- 0.43 |
| fortressisles2 | -0.24 +/- 0.52 |
| constanine | -0.40 +/- 0.30 |
| generated | -0.45 +/- 0.51 |
| fortressislesx | -0.55 +/- 0.40 |
| big-lizard | -1.36 +/- 0.67 (past two errors the other way) |
The verdict: not promoted, on both counts; the defaults stay.
The reading. The weights moved far, the posture took over the ladder, and the score a seat did not move: across every map the posture's weights are a ridge, not a peak. What they change is which maps a team does well on (atlantic and naval up, big-lizard down). One set of numbers for every map averages those away. That is LR 72's case for profiles (backlog 60): a posture fitted to a map, or read from it, could keep the gains without the losses. SPSA on one map at a time would find each map's point; a census of each against the defaults on its own map would say whether the maps differ past the noise.
75. The board's parts, and the front read off, 24 October
LR 73 read the team board off as +0.49 +/- 0.26 a seat, inside the noise but split by map. Three runs on the PC, each 400 fresh seeds, every map, 900 s, 3 respawns, a frozen binary.
The board study (build/board/20261024-070936, seeds 30000 to 30399). Each arm against the defaults, a seat:
| Arm | Every map | Maps past two errors |
|---|---|---|
noboard (caps -board) | +0.30 +/- 0.18 | europe +1.97; constanine -0.95, fortressislesx -1.01 |
bare (board on, team_rotate, bridge_jobs, posture off) | -0.21 +/- 0.17 | fortressislesx -1.26, generated -1.04 |
nofront (w_front, w_front_post, w_front_push 0) | +1.11 +/- 0.20 | island +3.14, generated +2.83, europe +2.22, industry +2.01 |
noground (w_defend_ground 0) | +0.15 +/- 0.09 | none |
The board carries both a cost and a gain: its dependents (bare) are worth a little, and its front read costs more than a seat. The front read is the cause of the split.
The front read confirmed (build/front/20261024-105130, seeds 40000 to 40399). Which of the three weights does it:
| Arm | Every map | Maps past two errors |
|---|---|---|
| nofront | +0.88 +/- 0.19 | industry +2.56, europe +2.36, island +1.56, generated +0.93 |
nodepot (w_front 0) | +0.40 +/- 0.14 | europe +2.06, fortressisles2 +1.17, fortressislesx +0.64 |
nopost (w_front_post 0) | +0.30 +/- 0.18 | big-lizard +2.65 |
nopush (w_front_push 0) | +0.63 +/- 0.19 | industry +2.95, island +1.70 |
A fault under both runs. Their binaries came before 0828d4a: the team board folded every map into a 128-tile 16 by 16 grid, so on the seven larger maps the front it read was the wrong place. The study read a broken front read, not the front read.
The front read on the fixed board (build/front2/20261024-140954, seeds 41000 to 41399, a binary from 0828d4a, 14:09 to 16:13):
| Arm | Every map | Maps past two errors |
|---|---|---|
| nofront | +1.13 +/- 0.19 | island +3.21, generated +2.62, europe +1.97, industry +1.80, atlantic +0.95 |
| nodepot | +0.37 +/- 0.15 | fortressisles2 +2.21, europe +1.31, island +1.11 |
| nopush | +0.58 +/- 0.18 | island +1.78, industry +1.77, generated +1.48 |
No map in any arm reads past two errors the other way (nearest: naval -0.59 +/- 0.44 in nofront).
The rule, set before the run: an arm passes past two errors over every map with no map past two errors the other way; of arms that pass, the fewest weights changed (the higher read breaks a tie) unless nofront reads past two errors above it, the error of the difference the two added in quadrature. All three pass; nopush beats nodepot on the tie; nofront less nopush is +0.55 +/- 0.26, past two errors.
The verdict: w_front, w_front_post and w_front_push are 0 by default. The front read is still in the code and still tested: the scenario an expanding tank takes the depot at the front sets w_front to its old 0.5.
The reading. Fixing the grid made the front read no better: off reads +1.13 on the fixed board against +0.88 on the broken one. The two single weights add to about nofront's gain, so neither part is the whole fault; aiming Expand and the rotation at the front draws tanks off cheap nearer depots and toward the enemy's strength. Of note: in the scenario, w_front at 1 takes no depot in 40 s, where 0 and 0.5 each take one. A depot "kept until taken or given up" may hold a tank on a pick it cannot reach in time; worth a look before the front read comes back in any form.
76. Each default off again, on the fixed board, 24 October
LR 73's census of each default off ran before three changes: the base from home (fd381ad), the board and the island labels at the map's own size (0828d4a), and the front read off (5899e8e, LR 75). Its readings of the board and of what rides on it were taken with the front read aimed by a wrong grid on seven of the eleven maps. The same twelve switches, 200 fresh seeds (50000 to 50199), every map, SPLIT=2 JOBS=24 at 5899e8e (build/switches/20261024-190434), 19:04 to 21:16: 2 h 12 min. Below zero, the switch earns its place.
| Taken off | Now | LR 73 |
|---|---|---|
| dodge | -4.50 +/- 0.27 | -4.37 +/- 0.28 |
| posture | -0.88 +/- 0.23 | -0.58 +/- 0.25 |
| board (and team_rotate, bridge_jobs, posture) | -0.67 +/- 0.25 | +0.49 +/- 0.26 |
| priced_crossing | -0.40 +/- 0.18 | -0.41 +/- 0.19 |
| forget | -0.14 +/- 0.07 | -0.12 to -0.03 |
| column | -0.13 +/- 0.10 | -0.12 to -0.03 |
| bridge_jobs | -0.10 +/- 0.09 | -0.12 to -0.03 |
| cycle | -0.09 +/- 0.15 | -0.30 +/- 0.18 |
| escort | +0.03 +/- 0.08 | -0.12 to -0.03 |
| duck | +0.03 +/- 0.25 | +0.31 +/- 0.28 |
| island_site | +0.06 +/- 0.11 | +0.43 +/- 0.17 |
| team_rotate | +0.42 +/- 0.20 | +0.32 +/- 0.21 |
The board turned. Off, it read +0.49 in LR 73; now -0.67, past two errors: with its grid at the map's size and the front read off, the board pays. By map it is worth most on fortressislesx (-2.12 +/- 0.62 off) and island (-1.58 +/- 0.52); naval alone leans the other way (+1.23 +/- 0.85, inside the noise). LR 73's split by map was the front read on a wrong grid.
The posture pays more: -0.88, past two errors on six maps (big-lizard -2.60, atlantic -1.86, constanine -1.75, industry and fortressislesx -1.23), and none the other way.
The island site is fixed: +0.43 (harm) in LR 73, +0.06 now, inside the noise; the base from home (LR 73's fix) holds on fresh seeds.
team_rotate off reads +0.42 +/- 0.20, 2.1 errors; atlantic +2.90 +/- 0.51, no map past two errors the other way (big-lizard -1.14 +/- 0.90 the nearest). Twelve rows at two errors: about one passes by chance. By the rule set before the run (build/switches/NOTES-lr76.txt) it is read again on 400 fresh seeds (70000 to 70399) before it changes; queued behind the profiles' round robin (build/rotate/NOTES.txt).
The verdict, so far: no default changes. The parts that pay are the dodge, the posture, the board and the priced crossing; the rest sit inside the noise.
team_rotate, read again (build/census/20261024-231844, 400 fresh seeds 70000 to 70399, every map, 23:18 to 23:38, the rule in build/rotate/NOTES.txt): off less on +0.55 +/- 0.15 a seat, 3.7 errors; atlantic +2.47 +/- 0.30 and island +0.92 +/- 0.39 past two errors; below zero only naval -0.28 +/- 0.49, fidos-airstrip -0.28 +/- 0.67, fortressisles2 -0.37 +/- 0.59 and constanine -0.01 +/- 0.34, none past two errors. By the rule, team_rotate goes off by default: TEAM_ROTATE defaults to false in weights.rs (the switch and its code stay; caps = baseline, team_rotate turns it on). The brain's tests pass. make families: every family holds but a-raid-on-a-guarded-base, 70.2% +/- 2.3% under its 75% floor, the same 281 of 400 with team_rotate on or off. Bisected, it fell at 5899e8e (the front read off, LR 75): 81.8% at 0828d4a, 70.2% after. That commit kept the front scenario's front read on (w_front 0.5) but not this family's; backlog 62.
77. The play styles' round robin, 24 October
Backlog 60. Paul: "there is no single method that works", play styles "in a rock paper scissors like game". Four profiles as weights files (profiles/, d0c5a7a): turtle (the posture toward Defend and Fortify, a bigger castle, home sooner), raider (toward Expand, closes at even odds, picks off the wounded), expander and navy, with the defaults as a fifth. Are they ordered, or do they beat each other round a cycle on some maps? LR 74 found the posture's weights trade maps against each other, which is what a cycle by map would look like.
The rule, set before the run (build/rr/NOTES.txt): a style is better than another if its row reads past two errors above zero against it. The styles are ordered if those relations have no cycle over every map; a cycle on every map, or on any one, is what backlog 61 builds on. A style past two errors above the defaults over every map, with no map past two errors the other way, is read again on 400 fresh seeds before it becomes a default.
The run: make roundrobin, the fair census for every ordered pair, one team of the row's style against two of the column's: 20 censuses, the same 110 seeds each (60000 to 60109), every map (10 a map), 900 s, 3 respawns, 24 jobs, a frozen binary each (build/roundrobin/20261024-211723), 21:17 to 23:18, 2 h 1 min.
The matrix (the row's style less the column's, a seat; * past two errors):
| defaults | expander | navy | raider | turtle | |
|---|---|---|---|---|---|
| defaults | . | +0.70 +/- 0.30* | -0.30 +/- 0.25 | +0.82 +/- 0.39* | -2.10 +/- 0.33* |
| expander | -1.02 +/- 0.31* | . | -1.22 +/- 0.35* | +0.37 +/- 0.39 | -3.06 +/- 0.36* |
| navy | -0.07 +/- 0.24 | +0.63 +/- 0.38 | . | +1.49 +/- 0.36* | -2.45 +/- 0.35* |
| raider | -1.12 +/- 0.37* | +0.04 +/- 0.38 | -1.19 +/- 0.40* | . | -2.32 +/- 0.40* |
| turtle | +1.34 +/- 0.39* | +2.22 +/- 0.41* | +2.22 +/- 0.43* | +1.83 +/- 0.36* | . |
The two cells of a pair are two censuses (one team of A against two of B, and the other way); wherever either is past two errors, the other reads the same way round (A over B in one, B under A in the other).
No cycle. The relations past two errors over every map: turtle over every other style; the defaults over expander and raider; navy over raider. An order, turtle first, raider and expander last. Map by map (10 seeds a map, so few relations pass: none on big-lizard, eight on generated), no map has a cycle of three, four or five either. The posture trades maps (LR 74), but no style trades them in a ring.
Turtle against the defaults, by map: europe +2.28 +/- 0.99, the generated maps +3.25 +/- 1.28, industry +2.57 +/- 1.14, island +3.66 +/- 0.93, past two errors; naval -0.30 +/- 0.95 and fidos-airstrip -0.27 +/- 1.05 the only ones below zero, inside the noise. By the rule it is read again on 400 fresh seeds before it becomes a default: build/census/20261025-075856, seeds 80000 to 80399, the rule in build/turtle/NOTES.txt: +1.40 +/- 0.18 a seat, 7.8 errors, every map above zero (naval, its lowest in the round robin, +1.58 +/- 0.58; the generated maps +3.56 +/- 0.55, island +2.44 +/- 0.50). By the rule turtle becomes the defaults.
The two changes together. Turtle was read with team_rotate on; team_rotate's own re-read (LR 76) ends with it off. The pair against the old defaults (build/newdefaults, seeds 90000 to 90399, the rule in its NOTES.txt: past two errors, no map the other way, not below turtle alone by more than two errors): +1.75 +/- 0.19, 9 errors, no map below zero; atlantic +5.03 +/- 0.32, island +2.70 +/- 0.54, europe +2.25 +/- 0.48. Both go in.
The new defaults (weights.rs): w_posture 0.5 to 0.95, w_pos_defend and w_pos_fortify 1 to 3, w_pos_expand 1 to 0.4, w_defend_ground 1 to 1.5, castle_inside 2 to 4, castle_road 1 to 2, posts_max 4 to 6, retreat_armor 35 to 50, press_odds 1.5 to 2 (and Tactics::default's), team_rotate off. profiles/turtle.weights is gone, being the defaults; profiles/classic.weights is the old defaults, kept as a style. The other profiles name what they change "from the default", so raider, expander and navy now inherit turtle's values where they name none (w_defend_ground 1.5 in each, team_rotate off). Three tests of a mechanism pinned the old values they were written for (the posture's scores, the ladder's lean, the castle's cap); the tests pass, 87. make families: as before the change, every family holds but a-raid-on-a-guarded-base (backlog 62).
The verdict: the styles are ordered, not a rock paper scissors. Holding what the base has beats going to fight, against every style here: the defaults leaned too far toward Expand, and now lean the other way. Backlog 61 (rotation between styles) has no cycle to use; a mix can only do as well as its best member here, and is shelved until a style beats turtle somewhere.
78. Read sooner: a sequential census, replayed, 24 October
Backlog 59. LR 72 noted the dodge was settled by seed 20 of 176: a census of fixed length spends as many seeds on a clear call as on a close one. Chess engines' testing frameworks stop when the reading passes a bound. The question: does a sequential test reach the fixed census's verdicts with fewer seeds, its false alarm rate held where it is? (LR 77 is kept for the profiles' round robin, still running: its notes named the number before it started.)
The tool: scripts/sequential.py DIR... reads a census's logs as pool.sh does, interleaves its jobs' seeds by round (the order in which parallel jobs finish them), and looks K = 8 times, at each eighth of the seeds, stopping at the first look past its bound. Two bounds, each with c found by simulation (200 000 paths) so that at a true difference of zero the chance of stopping at any look is P(|Z| > 2) = 0.0455, the fixed census's own false alarm rate:
| Test | Bound at look k of 8 | First look | Last |
|---|---|---|---|
| O'Brien-Fleming | 2.117 sqrt(8 / k) | 5.99 | 2.12 |
| Pocock | 2.557 | 2.56 | 2.56 |
The rule, set before the replay: the sequential census holds if it reaches the fixed census's verdict (outside or inside two errors, and the sign) on every census replayed, or differs only where the fixed reading is within 0.1 errors of the bar, with a clear saving of seeds.
The data: LR 63 and LR 71's logs were on the Mac and are gone. In their place, every finished census on the PC, LR 72 to 76: LR 72's reading of the dodge on every map (176 seeds), the census of the SPSA point (528), the board, front and island site families (200 or 400 each), and the switches of LR 73 and LR 76 (200 each). 44 censuses, 11 304 seeds. Left out: the SPSA steps (a census each of a few seeds), the round robin under way, and LR 73's twelve switches turned on that were on already (every seed 0.00: the same brain on both sides). Two pairs repeat a census on the same seeds and binary and read the same, seed for seed (sitepoor off; board noboard and noground); they are counted as they ran.
The reading (z the fixed census's difference over its error; a star past two):
| Census | Seeds | Fixed z | O'Brien-Fleming stops | Pocock stops |
|---|---|---|---|---|
| LR 72, the dodge on every map | 176 | +15.45* | 44, out | 22, out |
LR 76 -dodge | 200 | -16.80* | 25, out | 25, out |
LR 73 -dodge | 200 | -15.37* | 25, out | 25, out |
front2 nofront | 400 | +5.96* | 150, out | 100, out |
board nofront | 400 | +5.55* | 150, out | 50, out |
LR 76 -posture | 200 | -3.90* | 100, out | 75, out |
LR 76 -board | 200 | -2.72* | 125, out | 100, out |
front2 nodepot | 400 | +2.49* | 350, out | 400, in |
LR 73 -posture | 200 | -2.31* | 200, out | 200, in |
LR 76 -priced_crossing | 200 | -2.24* | 175, out | 200, in |
LR 73 -priced_crossing | 200 | -2.16* | 200, out | 200, in |
LR 76 -team_rotate | 200 | +2.08* | 75, out | 50, out |
LR 73 -column | 200 | -1.02 | 100, out | 100, out |
| 21 more inside two errors | 5 328 | -1.90 to +1.87 | every seed, in | every seed, in |
10 more outside (front, front2, sitepoor, smooth, LR 73 -island_site) | 2 800 | 1 500, out | 925, out | |
| Every census | 11 304 | 8 547 (76%) | 7 800 (69%) |
O'Brien-Fleming agrees with the fixed census on 43 of 44 and spends 76% of the seeds. Most of the saving is the large effects: the dodge is settled by seed 25 of 200 at either bound. Pocock spends 69% but loses four of the narrow calls past two errors (its last bound is 2.56, not 2.12) as well as -column.
-column (LR 73): at 100 seeds it read -0.39 +/- 0.13 a seat, z -3.08, past the bound at that look (2.99); by 200 it was back to -0.10, -1.02. A run of bad seeds early, then regression to the mean. Of the 22 censuses inside two errors, about 1.0 would stop wrongly at this design's 4.55%; one did. The test did what it was built to do. The fixed census has the same chance of a false "out"; it pays it at the end, where the sequential one pays it early, and a single stop can't be told from a real one.
The verdict: by the rule set before the replay, not adopted: one census of 44 got a different verdict, and its fixed reading (-1.02) is far from the bar. O'Brien-Fleming would save a quarter of the seeds; the cost is that an early "out" is final, where the fixed census would have had 100 more seeds to recover. A form that keeps the saving and the rule: an early stop marks a reading for a re-read on fresh seeds (as LR 76 did for team_rotate) before any default changes on it. That is Paul's call; backlog 59 stays open with this reading.
79. The sequential census with a re-read, 25 October
LR 78's sequential census saved a quarter of the seeds, but its one false early stop (LR 73's -column) was final. The form LR 78 left open: an early stop is no verdict; the census is read again, sequentially, on fresh seeds, and is outside only if the re-read is outside the same way. A census that reaches its last look is the fixed census, read as before.
The rule, set before the build: it holds if (a) the census's seed differences can be read as it plays, matching its own lists; (b) its false alarm rate, by simulation, is at or below the fixed census's 4.55%; (c) over LR 78's 44 censuses, a re-read drawn from each census's own seeds (with replacement, 200 times) reaches the fixed verdict as often as not on every one, on fewer seeds than the fixed census.
The tool: LOOKS=8 make census. scripts/sequential.py watch follows the jobs' match lines, rebuilds each finished seed's difference, and at each look writes the reading to DIR/sequential.txt; at an early stop the jobs are stopped and a second sequential census plays SEEDS fresh seeds from SEED0 + SEEDS into DIR/reread; sequential.py verdict writes DIR/verdict.txt. Without LOOKS, make census is as it was. sequential.py twostage runs (b) and (c).
(a) Read as it plays: on four finished censuses (976 seeds), the seeds rebuilt from the match lines match each job's "a seed:" list to 0.01, the rounding of the printed scores.
(b) and the power, simulated (200 seeds a stage, 4 000 censuses a row; expected z is the fixed census's, the effect over its error):
| Expected z | Fixed, outside | Two stages, outside | Seeds, two stages |
|---|---|---|---|
| 0 | 4.8% | 2.8% | 102% |
| 1 | 16.6% | 8.8% | 108% |
| 2 | 50.6% | 34.4% | 123% |
| 2.5 | 67.7% | 52.9% | 129% |
| 3 | 84.7% | 74.9% | 130% |
| 4 | 97.5% | 96.0% | 117% |
| 6 | 100% | 100% | 85% |
| 10 | 100% | 100% | 55% |
The false alarm rate falls to 2.8%, below the fixed census's. The cost is power: a true effect at z 2.5 is read outside 53% of the time, not 68%. And the seeds: an effect large enough to stop early but not large enough to stop at once pays for a whole second census.
(c) The 44 censuses: every fixed verdict is reached as often as not (LR 73 -column, LR 78's false stop, now 81%), the closest LR 76 -team_rotate at 52% and -priced_crossing at 68%. Seeds: 11 969 against the fixed census's 11 304, 106%. The dodge is read in 61 to 81 seeds of 176 to 200; a mid-sized effect (front nodepot, z 2.89) in 568 of 400.
The re-read's size (explored after the verdict, not a rule; 3 000 censuses a cell, outside and seeds):
| Re-read | z 0 | z 2 | z 3 | z 4 | z 6 |
|---|---|---|---|---|---|
| a quarter | 2.3%, 100% | 21.8%, 98% | 38.8%, 92% | 53.3%, 82% | 84.3%, 61% |
| a half | 2.7%, 101% | 26.7%, 106% | 53.6%, 107% | 79.8%, 98% | 98.9%, 71% |
| whole | 2.4%, 102% | 34.8%, 122% | 74.0%, 129% | 96.2%, 118% | 100%, 86% |
| fixed census | 4.8%, 100% | 50.4%, 100% | 84.8%, 100% | 97.3%, 100% | 100%, 100% |
No size beats the fixed census on both counts at the effects the census mostly reads (z 2 to 4): fewer seeds buys less power.
The live test (the Mac, naval.map, 120 s, 32 seeds from 900, LOOKS=4 JOBS=8; looks at 8, 16, 24 and 32 seeds). Every capability off (caps =) read +0.54, +1.15, +0.92 and +0.54 errors: no stop, the census to its end, its last reading the same as pool.sh's (+0.11 +/- 0.21). A rookie (skill = 0): z -11.80 at 8 seeds, past the first bound (4.14); the eight jobs stopped, none left; the re-read on seeds 932 to 963, z -5.54 at 8 seeds, past it again; the verdict outside, in 16 seeds of 32. Played again, it read the same to the digit. Defaults against defaults without LOOKS: 0.00 over 8 seeds, make census as it was.
The verdict: by the rule, not adopted as the default: (a) and (b) hold, (c) does not (106% of the seeds). The fixed census stays the census. LOOKS stays as an option for a screen where a large effect is expected (z past 5: a part thought to carry the brain, a broken change), where it reads in a third to a half of the seeds and keeps the false alarm rate below the fixed one. Backlog 59 is done.
80. The guarded-base family after the front read, 25 October
Backlog 62. make families at LR 76's team_rotate change showed a-raid-on-a-guarded-base at 281 of 400, 70.2%, under its 75% floor, and the same at HEAD without the change. Bisected in a worktree: 81.8% at fa8ba05, 3f19029 and 0828d4a; 70.2% at 5899e8e, the front read off (LR 75). The family's own header names its mechanism: the rotation's push point toward the front, 85.5% over 400 seeds against 50.5% with w_front_push at 0 (LR 61). 5899e8e set w_front, w_front_post and w_front_push to 0 and gave the front scenario its old w_front back, so that scenario still tests the front read; this family was missed.
The rule, set before the run (build/b62/NOTES.txt): the family sets w_front_push = 0.75, its old default, as 5899e8e did for the front scenario. Confirmed if make families then holds every family at or above its floor, this one included, on its own 400 seeds; if not, the step stops, and no other weight is tried in it.
The run (the Mac, at 7c18851, the new defaults of LR 77):
| Family | Held | Floor |
|---|---|---|
a-raid-on-a-guarded-base, w_front_push 0 (the default) | 281 of 400, 70.2% | 75% |
a-raid-on-a-guarded-base, w_front_push 0.75 | 326 of 400, 81.5% +/- 1.9% | 75% |
Every other family held as before (a duel 63.0%, a ready gun 83.4%, the rest at or near 100%). The brain's tests, 87 passed.
The verdict: by the rule, confirmed. The family keeps testing the push point it was written for, 81.5% against the 81.8% it read before 5899e8e under the old defaults. The defaults are unchanged: the front read stays off for play, as LR 75's census found. The lesson is CLAUDE.md's newest rule, already in: a default change runs make families, so a weight set to 0 is caught where a scenario leans on it. Backlog 62 is done.
81. Each default off again, on the new defaults, 25 October
Backlog 63. LR 77 made turtle and team_rotate off the defaults; LR 76 read each part against the old ones. A part that paid for an outgoing brain may not pay for a turtle. The same twelve switches, team_rotate now added (it is off), 200 fresh seeds (100000 to 100199), every map, SPLIT=2 JOBS=24 at 7c18851 (build/switches/20261025-131009), 13:10 to 15:05: 1 h 55 min. Below zero, the switch earns its place (for team_rotate, below zero means adding it costs).
The rule, set before the run (build/switches/NOTES-lr81.txt): a default whose removal (or team_rotate's addition) reads past two errors above zero over every map, with no map past two errors the other way, is read again on 400 fresh seeds before it changes. Anything else stays.
| Changed | Now (LR 81) | Old defaults (LR 76) |
|---|---|---|
| dodge off | -4.76 +/- 0.28 | -4.50 +/- 0.27 |
| posture off | -0.51 +/- 0.22 | -0.88 +/- 0.23 |
| priced_crossing off | -0.47 +/- 0.15 | -0.40 +/- 0.18 |
| board off | -0.37 +/- 0.21 | -0.67 +/- 0.25 |
| team_rotate on | -0.24 +/- 0.23 | (off: +0.42 +/- 0.20) |
| escort off | -0.06 +/- 0.06 | +0.03 +/- 0.08 |
| bridge_jobs off | -0.03 +/- 0.08 | -0.10 +/- 0.09 |
| column off | -0.02 +/- 0.11 | -0.13 +/- 0.10 |
| island_site off | -0.01 +/- 0.15 | +0.06 +/- 0.11 |
| forget off | +0.04 +/- 0.06 | -0.14 +/- 0.07 |
| duck off | +0.04 +/- 0.28 | +0.03 +/- 0.25 |
| cycle off | +0.10 +/- 0.13 | -0.09 +/- 0.15 |
By map: the dodge pays everywhere. The posture and the board each pay most on atlantic (-1.90 +/- 0.47 and -1.80 +/- 0.47 off), and no map is past two errors the other way. team_rotate on costs atlantic -4.13 +/- 0.70, as LR 76's re-read found, and pays only on fortressisles2 (+1.18 +/- 0.53). priced_crossing acts on four maps and pays on fidos-airstrip (-3.83 +/- 1.14).
The verdict: by the rule, no default changes: no row is past two errors toward a change. The posture and the board pay less on the turtle than on the old brain (-0.51 against -0.88, -0.37 against -0.67). Turtle's weights already hold the base, so the posture's lean toward holding adds less. Neither difference is past two errors of the pair. team_rotate off is confirmed a third time. Backlog 63 is done.
82. Why atlantic: where the switches move the time, 25 October
Backlog 64. In LR 81, atlantic is the map where the board (-1.80 +/- 0.47 off), the posture (-1.90 +/- 0.47 off) and team_rotate (-4.13 +/- 0.70 on) each count most. Why there? No new run: LR 81's census (build/switches/20261025-131009) prints for every match each team's score parts and the trial team's seat-seconds on each rung. scripts/rungs.py reads them: map by map, the trial team's Expand, Turrets, Hunt and Fortify seconds and its held ground, built and bridges less the other two teams'. -forget (+0.04 +/- 0.06 off) stands for the defaults. A reading only, set before reading: what the trial team does differently on atlantic. No default changes on it.
On atlantic (54 matches a row):
| Row | Expand s | Turrets s | Hunt s | Fortify s | Held | Built | Bridges |
|---|---|---|---|---|---|---|---|
| -forget (the defaults) | 309 | 67 | 1 | 94 | -0.12 | -0.5 | +0.0 |
| -board | 211 | 194 | 29 | 97 | -1.33 | +20.2 | +4.6 |
| -posture | 210 | 194 | 29 | 97 | -1.35 | +20.0 | +4.6 |
| team_rotate on | 114 | 50 | 2 | 81 | -2.22 | -1.5 | -0.4 |
Two mechanisms, not three. The board's row is the posture's to the second: the posture needs the board, so -board takes it too, and on atlantic all of the board's worth is the posture's.
Not that atlantic pays Expand more. Over every row pooled, held ground a 100 Expand seconds is +0.69 +/- 0.05 on atlantic, in the middle (island +2.08, fidos-airstrip +1.86, europe +1.51; industry and generated about 0, where Expand runs 1000 s a match and the ground is already held).
team_rotate takes atlantic's seats off the ladder. The trial team's seconds on the four rungs, the defaults against team_rotate on: atlantic 471 to 247, -48%, Expand alone 309 to 114. Elsewhere -3% to -22% (big-lizard -22%, fidos-airstrip -19%, island -16%, the rest -13% or less). Rotation pulls a seat out to push toward the objective, and on atlantic, an ocean map, that seat is away from the ladder half the match. The held ground falls with it (-2.22). team_rotate is off by default since LR 76, so this is the why of a decision already made.
The posture keeps atlantic's teams from building. Without it the trial team on atlantic builds 20 more things than the others and 4.6 more bridges (elsewhere +2.5 at most, naval), and spends Turrets seconds (67 to 194) where it spent Expand (309 to 210). It loses 1.35 held for 99 Expand seconds: more than the slope's 0.69, so the loss is not only the Expand time. On atlantic the bridges and turrets cost what they took. Elsewhere the posture moves more Expand time (generated 1114 to 595 s) for less held (-0.65), because the ground there is held either way.
What the meter cannot say: when in the match the seconds go, and what a bridge on atlantic costs in builders' time. A meter line for build time by kind would read the second.
The verdict: a reading; no default changes. Atlantic carries the three switches through two mechanisms: rotation takes its seats off the ladder, and the posture stops it spending on bridges and turrets. The first is already off; the second already pays. Backlog 64 is done.
83. The squad's time by task: a meter line, 25 October
Backlog 65. LR 82 could not say what a bridge on atlantic costs in the builders' time: the posture off builds 4.6 more bridges there, and the meter counted what was built, not how long it took. A new count: each second a seat is alive (where the kiting seconds are counted, Commander::think_released), whether its squad is out and on what task (meter::SquadTask): a Build by kind (road; bridge, a road whose site is water or a bridge on the map; wall and concrete; pillbox; mine; structure; boat), each covering the walk to the site and the work; harvest; capture; repair; lift; the walk home; out with no task. By group, the trial's apart, as the other counts. The match line:
seconds the squad out (trial): 491 of 1331 alive (road 0, bridge 16, wall 25, pillbox 56, mine 0, structure 0, boat 5, harvest 41, capture 173, repair 7, lift 11, home 157, idle 0)
Checks. The meter decides nothing. The same census at a6c71f3 and with the count (atlantic, 4 seeds from 500, 300 s, defaults on both sides) prints the same 12 match and 36 team lines, to the digit. a_squad_second_counts_by_its_task tests the group, the bridge and the line; the brain's tests, 88 passed.
A first look, not a reading (those 4 seeds, every seat): the squad is out 37% of the seconds alive. Of that, the walk home 39%, captures 28%, pillboxes 11%, walls 9%, harvest 5%, bridges 1.5%. If it holds over a census, the posture's cost on atlantic is not the bridges' own work but the walking that each job adds. The reading is LR 82's rows on atlantic with this line, its rule written first.
84. Rewards in the census score, 25 October
Backlog 66. Paul: "we want to improve Census with a reward system ... Big points for capturing all neautral pillboxes . Good points for taking enemy pillbox. Good points for doing damage to enemy pillbox. Getting a direct hit with the main tank weapon is a very good amount of points". And pillboxes "taken, defended". The census score (LR 8, ledger::score) judged the win, kills less deaths, ground held over time, works built and road laid. A pillbox counted only by being held.
The terms (ledger::Tally::hear, from the sim's events; a seat's count, a team's score the mean of its seats'):
| Reward | Counted from | Worth |
|---|---|---|
| a direct hit | ShellImpact of a Direct shell (its owner from ShellLaunched) on a hostile tank | 1.5 (about three kill, a kill 1) |
| pillbox damage | PillboxDamaged to a hostile-owned pillbox, in pillboxes' health | 1.5 a whole pillbox |
| a neutral pillbox taken | PillboxCaptured, from none | 3 |
| a hostile pillbox taken | PillboxCaptured, from a hostile | 6 |
| a defence | a kill of a hostile tank within DEFEND_R (16 tiles, twice a pillbox's reach) of a live pillbox of the killer's team | 1.5 |
| every neutral pillbox | the team itself took as many neutral pillboxes as began neutral (counted, since a pillbox lifted and set down is another id) | 15, once |
The switch: on by default; CENSUS_REWARDS=0 scores as before. The census prints brain_census: rewards on (or off) and each team line ends with the rewards' part and its counts:
team 1: score 18.21, ..., held 8.8, rewards 0.70 (direct hits 1, pillbox damage 0.0, pillboxes taken 3 neutral 0 hostile, defends 0, all neutral no)
Checks: the_rewards_count_their_events (a hostile direct hit counted, a teammate's and an indirect shell's not; damage to a neutral pillbox not; each capture; a defence; every neutral pillbox for one team, not the other; without_rewards the old score); the brain's tests, 89 passed. On atlantic (4 seeds from 500, 300 s), with CENSUS_REWARDS=0 every team line matches LR 83's to the digit; on, every term but defences and every neutral fired.
The scale: first set at a third of these (a direct hit 0.5, a pillbox's damage 0.5, neutral 1, hostile 2, a defence 0.5, every neutral 5), the rewards were 0.2 to 0.7 a seat on those seeds against a score of about 17, of which held ground is 2 x 8.7: a nudge. Paul chose "about a third" of the score, so each was tripled, the values in the table. Tripled, on the same four seeds: 10% of the score at 300 s (1.91 of 19.04), and 25% at the census's 900 s (6.80 of 27.24). The rewards are counts and grow with the match, while held ground is averaged over it. The census reads a difference, so a term moves a verdict by how much it differs between teams, not by its size alone.
What the sim does, against Paul's description (read, not changed): pillboxes shoot tanks and drones, not each other; damage does not slow their fire; at zero health a pillbox is dead (silent, capturable), not "highly damaged and still functional"; repair takes material, which comes from forest (harvest), depots or the start; a pillbox of one's own side can be towed by two tanks or lifted, dead or alive; towing through forest does not repair it. Those differences are sim changes, each a step of its own.
85. Personalities, step 1: timid, aggressive, castle, 25 October
Backlog 67. Paul: "make sure these ambitions of personalities are obvious. Such that timid or agressive would be very vissible", and personalities "to want to get tight groupings of pillboxes". Three profiles from existing weights (profiles/): timid (the posture hard toward Defend and Fortify, Expand 0.1; close in at odds of 4, mass 30 s, home at 75 armour or 8 shells), aggressive (past raider: Expand 5; close in at 0.8, never mass, home at 10 armour or 1 shell, duck only near dead, the wounded hunted, one pillbox at home), castle (castle 8 inside and 4 on the road, posts 6 apart, Fortify 5, mending 2).
The rule, set before the run (build/personalities/NOTES.txt): a personality is plain to see if its signature reads past two errors its own way, the trial team less the other two in the same match: timid, deaths and kills both below; aggressive, both above; castle, built above. The score reported, not judged.
The run: each against the defaults, 110 seeds from 110000, every map, 900 s, 24 jobs, the rewards on, at 3e55c83; read by scripts/traits.py (a seed's mean of its three matches, then the mean and its error over the seeds).
| Trial less the others | Timid | Aggressive | Castle |
|---|---|---|---|
| score | +0.06 +/- 0.54 | -2.10 +/- 0.56 | -0.34 +/- 0.35 |
| kills | +0.22 +/- 0.32 | -0.61 +/- 0.38 | -0.06 +/- 0.26 |
| deaths | -0.14 +/- 0.27 | +1.15 +/- 0.31 | -0.06 +/- 0.28 |
| built | +1.13 +/- 0.97 | -12.73 +/- 0.79 | +4.01 +/- 0.60 |
| held | -0.03 +/- 0.14 | -0.70 +/- 0.14 | +0.10 +/- 0.08 |
| rewards | -0.01 +/- 0.22 | -0.03 +/- 0.22 | -0.45 +/- 0.19 |
| pillbox damage | -0.26 +/- 0.26 | +0.85 +/- 0.26 | -0.61 +/- 0.27 |
| neutral pillboxes taken | -0.07 +/- 0.09 | +0.60 +/- 0.11 | -0.18 +/- 0.08 |
| hostile pillboxes taken | +0.06 +/- 0.06 | -0.28 +/- 0.08 | -0.01 +/- 0.06 |
| defends | +0.39 +/- 0.15 | -0.44 +/- 0.14 | -0.07 +/- 0.12 |
| its Expand s a match | 167 | 252 | 158 |
| its Fortify s a match | 133 | 36 | 146 |
The verdict, by the rule: castle is plain to see (built +4.01, 6.7 errors). Aggressive is half: it dies more (+1.15, 3.7 errors) but does not kill more (-0.61, inside); it is plain to see in all else, building 12.7 less, holding less, taking neutral pillboxes and damaging hostile ones more, and it loses (-2.10 a seat, five maps past two errors below). Timid is not: deaths and kills inside the noise, only its defences up (+0.39). The defaults are already turtle (LR 77): the posture at 0.95 toward Defend and Fortify, home at 50 armour. A timid style is a turtle with louder weights, and these weights were near their ends already. By the rule, timid and aggressive are made stronger in a second step, not kept as they are. Castle stays as written.
What a stronger step needs: timid, a lever the defaults do not already pull (a kiting ring farther out, no Hunt rung, the guard inside the castle); aggressive, more kills, not only more deaths (its closing in at odds of 0.8 trades a tank for less than one). Both are behaviour, not values, so they want code: backlog 67's later steps, with the cowboy and the pillbox collector.
86. The cowboy: spending weights, and the defaults spend, 25 October
Backlog 67, step 2. Paul: "coyboy tankers who always spend the expensive ammo with long shots and using all the resources as fast lauching like artillerty". Nothing in the brain weighed spending: the gun shot at its target tank, a stormed depot's guns, and nothing else; a longshot went every 30 s and only at a hostile structure; one strike drone out at a time. Four weights (Tactics, at their old behaviour by default when added): shell_boxes (a ready gun with no tank to shoot at shells the nearest hostile pillbox in the indirect gun's reach), longshot_every (30), longshot_any (with no hostile structure, a longshot at the nearest hostile pillbox, else the target tank), strikes_max (1). The meter's new spent clause counts the shots and launches asked for, by kind and group, and scripts/traits.py reads it. At the old values an atlantic census of four seeds reads the same to the digit as LR 84's.
profiles/cowboy.weights: shell_boxes 1, longshot_every 5, longshot_any 1, strikes_max 3, retreat_shells 1. Rule, set before the run (build/personalities/NOTES.txt in the worktree treadcore-lr86): plain to see if indirect shells and longshots both read above the others past two errors.
The census (110 seeds from 120000, every map, against the defaults): plain to see, and more. Indirect shells +172 a match, longshots +141, strikes +129; +11.54 +/- 0.89 a seat, ten maps past two errors above, none below. Without the rewards (score less rewards, LR 84's old score) +5.20 +/- 0.49.
Read again (the rule for a style past two errors above the defaults, LR 77; 400 fresh seeds from 130000): +11.62 +/- 0.45, every map past two errors above (fortressisles2 the least, +5.23 +/- 1.43; generated +22.82). Kills +6.58, deaths -2.09, held +1.40, pillbox damage +18.6 pillboxes' health, direct shells -23 (the gun busy at longer range).
Taken apart (each alone, 110 seeds from 140000):
| Alone | Score | Kills | Deaths | Held | Rewards | Pillbox damage |
|---|---|---|---|---|---|---|
| shell_boxes 1 | +7.19 +/- 0.81 | -0.26 | +0.13 | +0.95 | +5.27 | +18.51 |
| longshots (every 5, any) | +5.59 +/- 0.55 | +3.01 | -1.19 | +0.71 | +2.62 | +7.02 |
| strikes_max 3 | +1.46 +/- 0.56 | +2.05 | -0.97 | +0.20 | +0.21 | +0.28 |
| retreat_shells 1 | 0.00 exactly | 0 | 0 | 0 | 0 | 0 |
The longshots carry the fighting (+3 kills, -1.2 deaths; +2.97 without the rewards). Shelling pillboxes is mostly the reward LR 84 set for pillbox damage (+5.27 of +7.19); without it +1.92, from ground held. Strikes kill more and die less. retreat_shells does nothing: the retreat thresholds blend from the constants (4 shells, LOW_ARMOR) to the weights by w_smooth, 0.05 by default, so 1 shell rounds back to 4, and retreat_armor moves the real threshold a twentieth of the way. Turtle's "home at 50 armour" (LR 77) and timid's 75 (LR 85) barely acted: part of why timid read as the defaults.
The change, by the rule: the spending weights at the cowboy's values are the defaults (retreat_shells left at 4, since it does nothing). profiles/steady.weights keeps the old spending (the defaults of LR 77 to 85) and classic.weights gains it. make families then found the four dodge families under their 99% floors (91.3% to 96.5%): with shell_boxes, a tank waiting out a shell shot at a neutral pillbox and turned its hull into the splash. A pillbox shot now waits while a hostile shell is coming down within the splash and 3 tiles of the tank: the four back to 100%, the duel family 63% to 78.5%, every family over its floor. the_default_match_plays_as_ teams lost its pings: on its one seed the pinging seat, with the cowboy's drones, happened never to call; with longshots every 30 s it pings 12 times. The test builds its teams on steady's spending, as LR 77 pinned the mechanism tests. The brain's tests, 89 passed.
The fix came after the census, so the defaults as changed were read once more against steady (the old defaults).
The pair (the rule in build/NOTES3.txt of the worktree treadcore-lr86b, set before: the new defaults stand if past two errors above zero with no map past two errors below; trial the defaults as changed, base steady.weights, 400 fresh seeds from 150000): +11.64 +/- 0.43 a seat, every map past two errors above (europe the least, +5.54 +/- 0.43; island +21.91). Kills +6.77, deaths -2.49 (-2.09 before the fix), held +1.35, pillbox damage +17.7. Without the rewards, +5.52 +/- 0.25: four times turtle's +1.40 on the old score alone.
The verdict: by the rule, the cowboy's spending is the defaults: shell_boxes 1, longshot_every 5, longshot_any 1, strikes_max 3. The brain spends what it carries. Two notes for what follows. The rewards (LR 84) pay shelling pillboxes most, and a census on the old score would have adopted it too, at half the size. The retreat weights barely act through w_smooth 0.05; a timid or aggressive personality that goes home sooner or later needs them to act, which is the next step's first change.
87. The methods so far, and the census as a learner, 25 October
Backlog 69. Paul: "lab report about these new methods and looking forward into running census and using this like machine learning." A review, no run: what the last ten sections added to the way the brain is measured, what they found about where gains come from, and a plan for running the census as a learning loop.
The instrument, as it stands
| Method | Section | What it does |
|---|---|---|
| The fair paired census | 24, 71 | A seed's difference: the trial team less the baseline's, a seat, each team in the trial seat once; the mean and its error over seeds. Outside two errors is the bar. |
| The rule before the run | 24 on | What would confirm a change is written to the run's NOTES before it starts. |
| Read again on fresh seeds | 76, 77, 86 | A style past two errors is read again on 400 seeds none read before before a default changes. |
| The sequential census | 78, 79 | LOOKS=8: stops at an O'Brien-Fleming bound, an early stop read again. An option for large effects; it costs seeds on close calls. |
| Families as a guard | 47; 80, 86 | make families at every default change: scenarios over many seeds with floors. It caught a front weight a family needed (LR 80) and a tank shooting itself into a splash on 4% of seeds (LR 86), neither visible to the census. |
| Each part off | 73, 76, 81 | The switch census: every capability removed in turn. |
| Taken apart | 86 | A winning profile's weights each alone: which part carries it. |
| The meter | 5 on; 83, 86 | Per match and group: rungs, postures, dodges, kites, the squad's time by task, shots and launches. |
rungs.py, traits.py | 82, 85 | A census read by map and by behaviour: the trial team's counts less the others', paired by seed. A personality's signature. |
| Rewards in the score | 84 | Direct hits, pillbox damage and captures, defences. CENSUS_REWARDS=0 the old score. |
| Worktrees on the machine | 86 | Uncommitted code measured on the census machine without touching its tree. |
Where the gains came from
| Change | Kind | Reading, a seat |
|---|---|---|
| The posture's seven weights by SPSA (LR 74) | continuous tuning | -0.00 +/- 0.14: a ridge, not a peak |
| The dodge's numbers by SPSA (LR 43, 69) | continuous tuning | +0.88, not read again |
| Turtle (LR 77) | a style, hand-set | +1.40 +/- 0.18 |
| team_rotate off (LR 76) | a switch | +0.55 +/- 0.15 |
| The cowboy (LR 86) | new behaviour, hand-set values | +11.62 +/- 0.45 |
Tuning the weights of behaviour the brain already had found flat ground. The large gains came from behaviour the brain did not have: spending the drones and shells it carried. A learner over today's weights will polish. It will not find the next cowboy unless the behaviour exists to be weighted. Two other findings bear on any learner. A weight can be dead: the retreat weights act through w_smooth 0.05, a twentieth of the way (LR 86), and a search over them reads noise. And a score can be gamed: shelling pillboxes earned +5.27 of its +7.19 from the pillbox-damage reward set an hour earlier (LR 84, 86).
The census as machine learning
The census is a noisy, paired, black-box objective. One evaluation of a weight vector is a census of N seeds: about 7 minutes for 48 to 110 seeds and 20 to 30 minutes for 400 on the machine's 24 threads. The pieces map onto a learning loop like this:
| Learning | The census |
|---|---|
| The objective | The paired score difference, rewards on; the old score beside it as a check on reward hacking |
| A training batch | A step's fresh seeds, never reused |
| The test set | 400 seeds no step saw, read once, with the rule written before |
| Constraints | Every family over its floor; no map past two errors below |
| Regularisation | Few weights a search; a change must read on every map, not one |
| Preregistration | The NOTES rule |
The plan, in the order it pays:
- Make the dead weights act. The retreat thresholds direct (or blended by their own weight, not
w_smooth), so timid and aggressive can differ and a search over survival reads something. Check every weight's slope before tuning it: a one-census screen of each weight at its range's ends, the switch census for weights. - A league, not a single opponent. Every learner so far climbed against today's defaults, which move. Keep fixed opponents (
classic,steady, each adopted default) and score a candidate against the league: the round robin (LR 77) as the evaluation, Elo from the ledger (ledger::ratings, LR 8) as its summary. A candidate that beats today's defaults by losing to an old one is a cycle (backlog 61) and is caught. - Quality-diversity over the personalities.
traits.py's signature (kills, deaths, built, pillboxes taken, spending, rung seconds) is a behaviour descriptor. MAP-Elites keeps the best brain in each cell of a grid over two or three of them: the best timid, the best cowboy, the best castle. That is Paul's list of personalities, found and tuned by the census instead of set by hand, each guaranteed to be plain to see because its cell says so. - Batch search on the threads. SPSA spends a census a step on two points. With 24 threads a step can read 24 candidates on fewer seeds each (CMA-ES or a batched Bayesian optimiser), then read the best few again on more seeds: the two-stage census of LR 79, used for selection, where it saves seeds.
- Context, then policy. Atlantic carries the posture and rotation (LR 82); the cowboy pays +22 on island and +5.5 on europe. Weights by map features (water share, depots, size) are the first step from a weight vector toward a policy. A learned policy (the ladder picked from the board's state by a trained scorer) is the far end. The census and the families stay its judge.
Guards that do not change: fresh seeds every step; a held-out reading with its rule before any default moves; make families at each default change; the old score reported beside the rewarded one; every map read, not only the pool. A learner that wins the pool by losing a map, games a reward, or breaks a family has not won.
Next (backlog 69, step by step): the retreat thresholds made to act and the weight-slope screen; then the league evaluation; then MAP-Elites over the personalities, a night on the machine.
88. The longest run: a night's plan, and what we expect, 25 October
Backlog 69, its first night. Paul: "can you plan about 10 hours a census", then "write about our expectations. the longest run." Three blocks queued on the census machine at 7c97aa9 (build/night/run.sh, started 18:40), each with its rule written before it, and our expectations written here before any of it plays, to be scored against what the night reads. Every census: every map, 900 s, 3 respawns, 24 jobs, a frozen binary, the rewards on.
Block 1, the league row (8 censuses, 110 seeds from 160000, about 65 min): each profile as the trial against the new defaults: classic, steady, timid, aggressive, castle, raider, expander, navy. A reading, no default changes; timid, aggressive and castle read against LR 85's signatures again.
Block 2, the slope screen (32 censuses, 110 seeds from 170000, the same seeds for every row, about 4 h 20 min): sixteen weights each at half and double the default (longshot_every, strikes_max, pillbox_worth, strike_worth, engage_odds, press_odds, mass_for, w_wounded, w_finish, w_focus, w_in_range, w_exposure, w_stick, w_pos_expand, castle_inside, w_mine; the retreat weights left out, LR 86). Rule: an end past two errors above the defaults with no map past two errors below is a candidate for a 400-seed re-read in a later step; a weight inside the noise at both ends is flat here and leaves later searches.
Block 3, SPSA on what the screen finds (about 3 h 15 min): build/night/pick.py takes the five weights with the largest gap between their ends (|double - half| over the root of the squared errors), set before the screen so no one picks by eye. SPSA from the defaults over them, 24 steps of 48 fresh seeds from 200000, then its point against the defaults on 400 held-out seeds from 300000. Rule: promoted only if past two errors above zero with no map past two errors below, and only after make families, in the morning.
What we expect (to be scored in the morning):
| Run | Expected |
|---|---|
| steady | about -11 (LR 86's pair from the other side; -10 to -13) |
| classic | about -13: the spending and turtle's lean both lost |
| timid, castle | -1 to +1; castle's signature passes again, timid's fails again (its retreat weights barely act) |
| aggressive | -1 to -3, dying more |
| raider, expander, navy | -0.5 to -2.5 each, as against turtle in LR 77 |
| any profile above the defaults | none |
| the screen | 10 to 12 of 16 flat at both ends; about 1.5 of 32 rows outside by chance |
| longshot_every | half flat or a little better; double worse by 1 to 3 |
| strikes_max | double flat or a little better; 2 a little worse |
| castle_inside double | a little worse |
| candidates for a re-read | at most two |
| pick.py's five | longshot_every and strikes_max among them |
| SPSA | longshot_every down, strikes_max up, the rest little |
| the held-out point | +0.3 to +1.5, more likely inside the noise; promotion about one chance in three (LR 74 read -0.00 on fresh seeds) |
If the night says otherwise, the night is right.
Block 1, read at 19:33 (written after it played; the expectations above stand as written). Each profile less the new defaults, a seat, 110 seeds from 160000:
| Profile | Reading | Expected | |
|---|---|---|---|
| steady | -9.24 +/- 0.64 | -10 to -13 | missed: a smaller loss |
| classic | -8.73 +/- 0.55 | about -13, below steady | missed: above steady |
| timid | +0.70 +/- 0.52 | -1 to +1, signature fails | held, held |
| aggressive | -1.86 +/- 0.53 | -1 to -3 | held; deaths +0.18 (inside), kills -0.91 |
| castle | +0.32 +/- 0.34 | -1 to +1, signature passes | held (built +1.85), held |
| raider | -1.41 +/- 0.51 | -0.5 to -2.5 | held |
| expander | -1.24 +/- 0.47 | -0.5 to -2.5 | held |
| navy | -0.23 +/- 0.40 | -0.5 to -2.5 | missed narrowly, inside the noise |
Six of nine held. Classic, the old even-handed style with the old spending, lost less than steady, turtle's lean with the old spending: without the cowboy's drones the turtle lean may cost, the reverse of LR 77. A census of turtle's lean on today's brain would say. Blocks 2 and 3 are playing.
Blocks 2 and 3, read on the morning of 26 October (written after they played; the expectations above stand as written). The night ended at 00:42, six hours in, not ten: a census of 110 seeds took about 6.5 minutes, not 8.
The screen, each end less the defaults, 110 seeds from 170000 (the same seeds for every row); the gap is pick.py's:
| Weight | Half | Double | Gap |
|---|---|---|---|
| strikes_max | -1.19 +/- 0.53 | +0.76 +/- 0.39 | 2.96 |
| longshot_every | +0.59 +/- 0.49 | -1.45 +/- 0.51 | 2.88 |
| w_focus | +0.78 +/- 0.43 | -0.98 +/- 0.47 | 2.76 |
| mass_for | +0.33 +/- 0.31 | -0.67 +/- 0.30 | 2.32 |
| castle_inside | -1.17 +/- 0.36 | -0.05 +/- 0.34 | 2.26 |
| w_pos_expand | +0.18 +/- 0.24 | -0.58 +/- 0.28 | 2.06 |
| w_wounded | -0.36 +/- 0.48 | +0.96 +/- 0.49 | 1.92 |
| w_finish | -1.07 +/- 0.47 | -0.34 +/- 0.39 | 1.20 |
| w_stick | +0.34 +/- 0.48 | -0.50 +/- 0.53 | 1.17 |
| w_in_range | -0.25 +/- 0.46 | +0.36 +/- 0.50 | 0.90 |
| engage_odds | +0.56 +/- 0.44 | -0.01 +/- 0.51 | 0.85 |
| strike_worth | +0.16 +/- 0.45 | +0.67 +/- 0.48 | 0.78 |
| press_odds | -0.59 +/- 0.38 | -0.18 +/- 0.40 | 0.74 |
| pillbox_worth | +0.27 +/- 0.22 | +0.28 +/- 0.19 | 0.03 |
| w_mine | +0.00 +/- 0.00 | +0.00 +/- 0.06 | 0.00 |
| w_exposure | +0.00 +/- 0.00 | +0.00 +/- 0.00 | 0.00 |
No candidate: no end reads past two errors above the defaults. Seven rows read past two errors, every one below (strikes_max half, longshot_every double, w_focus double, mass_for double, castle_inside half, w_finish half, w_pos_expand double): the defaults sit at or near the top of each axis alone. Nine weights are flat at both ends. w_exposure and w_mine read exactly zero at both ends, every seed: in a census of every map they do not act at all. Why is not yet looked at (a gate, or a bug); they leave later searches either way.
SPSA (build/spsa/20261025-230519, 24 steps of 48 seeds from 200000) over pick.py's five walked far past the screen's range:
| Weight | Default | SPSA's point |
|---|---|---|
| strikes_max | 3 | 46.68 (rounded, the cap gone in effect) |
| longshot_every | 5 | 11.04 |
| w_focus | 0.5 | 0.444 |
| mass_for | 12 | 52.36 |
| castle_inside | 4 | 18.66 |
Its held-out read, 400 seeds from 300000: +0.597 +/- 0.298, z 2.002, no map past two errors below (fortressislesx -0.94 +/- 0.97 the lowest). Past the rule's bar by two thousandths of an error.
The rule asked next for make families, in the morning with Paul. On the Mac, the point set on every family (SET=): all nine hold. Only the duel moves, 73.5% +/- 2.2% against 78.5% at the defaults, inside the noise and far over its 55% floor (build/night-am/).
A pass by 0.002 errors, from a point no part of the night read alone, is LR 74's case again. One more census, its rule written first in build/reread/NOTES.txt on the machine: the point against the defaults on 400 fresh seeds from 400000, promoted only past two errors above with no map past two below; we expected +0.3 +/- 0.3.
| Map | Held-out (300000) | Read again (400000) |
|---|---|---|
| every map | +0.60 +/- 0.30 | +0.09 +/- 0.27 |
| industry | +0.91 +/- 0.85 | -0.05 +/- 0.88 |
| island | +1.06 +/- 0.82 | -0.94 +/- 0.84 |
| naval | +2.37 +/- 1.28 | -0.09 +/- 1.03 |
| big-lizard | -0.47 +/- 1.48 | -0.66 +/- 1.29 |
| constanine | +0.63 +/- 0.76 | +1.47 +/- 0.54 |
| fidos-airstrip | +1.65 +/- 1.06 | +1.27 +/- 1.09 |
| fortressisles2 | +0.14 +/- 1.03 | -0.29 +/- 0.93 |
| fortressislesx | -0.94 +/- 0.97 | -1.71 +/- 0.91 |
| atlantic | +0.97 +/- 0.84 | +0.86 +/- 0.66 |
| europe | +0.13 +/- 0.56 | +1.00 +/- 0.48 |
| generated | +0.13 +/- 0.96 | +0.10 +/- 0.79 |
Inside the noise (z 0.33): not promoted; no default changes. The two reads pooled, 800 seeds: +0.34 +/- 0.20, z 1.71. Fortressislesx reads below both times, -1.33 +/- 0.66 pooled, just under two errors; constanine and europe above both times. A point that trades maps, if anything, and a small one.
The expectations scored, blocks 2 and 3:
| Expected | Read | |
|---|---|---|
| 10 to 12 of 16 flat | 9 | missed by one |
| about 1.5 rows outside by chance | 7, all below | missed: the slopes are real, and fall away |
| longshot_every half flat or better, double worse by 1 to 3 | +0.59 inside; -1.45 | held |
| strikes_max double flat or better, half worse | +0.76 inside; -1.19 | held |
| castle_inside double a little worse | -0.05; half lost instead | missed |
| at most two candidates | none | held |
| pick.py's five hold longshot_every and strikes_max | both, first and second | held |
| SPSA: longshot_every down, strikes_max up, the rest little | 11.0 up; 46.7 up; mass_for and castle_inside x4 | one of three |
| the held-out point +0.3 to +1.5, more likely inside | +0.60, past by 0.002 | held, at the edge |
| promotion about one chance in three | not promoted, on the second read | held |
Six of ten held; with block 1, 12 of 19. The misses share a cause: we thought the defaults sat on a flat ridge (LR 74), and on these sixteen they sit on a peak, each axis falling away both ways. SPSA, started on that peak, walked out along a direction where the score barely moves, and one census on 400 seeds read a little high. The held-out read was meant to catch that, and on its own it did not: a single read at the bar, the bar is a coin with a two per cent edge. The guard that caught it was the second read.
Proposed, for Paul: an SPSA point is promoted only after two held-out reads (or one of 800 seeds), and SPSA is bounded to the screen's range, half to double, so it does not wander where no census has looked. Next for backlog 69: why w_exposure and w_mine never act; the retreat thresholds made to act; then the screen over the rest of the weights.
89. The second day: why two weights never acted, every weight screened, 26 October
Backlog 69. Paul, on the morning read: "1. yes. yes." (the two arms below, and LR 88's proposal: an SPSA point promoted only after two held-out reads, SPSA bounded to half and double), then "I want you nest ideas to improve in best industry practices. Start again. We got another day. ... we have been adding more variables. Please start more analysis and converge into good values. Start on remote."
Why w_exposure and w_mine read exactly zero
Read on the Mac with a probe in Cycle::improvement, removed after.
w_exposure is tied. Tactician::stand (tactics.rs) prices each of seven points on the arc as guns * w_exposure + (|k| / 3) * w_flank, the cheapest taken. The flank term is at most w_flank, 0.3. Any w_exposure over 0.3 makes one gun dearer than any bearing, so the order is always fewest guns, then bearing: 0.5, 1 and 2 pick the same point every time and play the same match to the bit. The weight acts only under 0.3, where a tank takes one more gun to keep its bearing. The same tie makes w_flank inert anywhere under w_exposure: its half and double (0.15, 0.6) are predicted to read exactly zero.
w_mine almost never decides. An improvement takes the largest of a mend (w_mend times the most hurt pillbox's hurt, offered once a pillbox is a tenth hurt), a mine (w_mine * 0.5, 0.25) and works (w_works * 0.5). The probe priced each improvement step at half, the default and double, over 24 census games (every map, seeds from 500000):
| Improvement steps, 165 | Count |
|---|---|
| the same pick at all three | 159 |
| a mine at double, a mend otherwise | 6 |
| a different pick at half | 0 |
| works offered at all | 0 |
A mine is built when nothing needs mending, at any weight. A mend loses to a mine only when the most hurt pillbox is under w_mine / 2 hurt, between a tenth and a quarter at the default: rare. Works were never on offer (no free pad by the base, or not the material). Neither is a bug; both are weights whose range of action lies outside half to double.
The day (build/day/ on the census machine, started 09:21)
Rule and expectations written first in build/day/NOTES.txt. Every census: every map, 900 s, 3 respawns, 24 jobs, rewards on.
A. Arms where the three can act, 110 seeds from 170000 (the screen's): w_exposure 0.15, w_mine 2 (a mine before any mend), w_flank 1.5.
B. The screen of every remaining scalar weight, in two stages (successive halving, as in algorithm configuration: irace, Hyperband): 68 weights at half and double (a weight bounded by 1 at halfway to 1 instead; w_duck, at 1, half only), 135 censuses of 55 seeds from
- Any weight with an end past 1.5 errors or a gap past 1.5 is
read again on all 110. Left out: the switches (thresholded 0/1), the skill dial, long_game (it changes the score itself), assist, upgrade_vs_mend (no upgrades yet), the front weights (off since LR 75), dodge_hold (0), and the retreat thresholds (blended by w_smooth 0.05; making them act is a change to the brain).
C. Bounded SPSA (scripts/spsa.sh, BOUND=2, new): the eight largest gaps over both screens, none inert, each kept within half and double of its default; 24 steps of 48 seeds from 800000.
D. Two held-out reads of SPSA's point: 400 seeds from 900000 and 400 from 1000000.
Rules. An arm or screen end is a candidate only on a 110-seed read past two errors above the defaults with no map past two errors below; a candidate goes to Paul, and no default changes from a screen alone. SPSA's point is promoted only if both held-out reads pass that bar and make families then holds on the Mac.
Expected. w_flank's two ends exactly zero. The three arms act and read inside the noise, w_exposure 0.15 more likely below. Stage 1: about 7 of 135 ends past two errors by chance alone, more below than above (the defaults a peak, LR 88); 5 to 15 weights inert. At most three candidates after stage 2. SPSA's point within the bounds by construction; the held-out reads +0.0 to +0.5 each; promotion about one chance in five. Cost: about 13 hours.
The weight ledger
A dashboard of the weights over time, as Paul asked: the count of weights at each commit of weights.rs (68 on 18 October, 109 on 25 October), the sixteen default changes, every weight's range tried (as a default, in a census, or in the screen) as a share of today's default, the screen's two ends a weight, and every census on the census machine (70) with its two errors. Its data are files built from git and from build/census on the machine; the day's rows are added as they read. https://claude.ai/artifact/KdMP63zpmJVWgm4ChwgBpf
Next ideas from practice
What LR 87 did not already list, from algorithm configuration, A/B testing at scale, and engine testing (Stockfish's fishtest):
- Correct for many comparisons. A screen of 135 ends at two errors expects seven passes from chance; LR 88's SPSA point passed its held-out read at z 2.002 and then read +0.09. Read a screen with a false discovery rate (Benjamini-Hochberg at 10%) and shrink each end toward zero by its error (empirical Bayes) before ranking. The winner's curse is the reason a searched point needs its second read.
- SPRT for promotion. Fishtest promotes a patch by a sequential probability ratio test between two hypotheses (no gain against a gain worth having), which stops early both ways. The census's O'Brien-Fleming looks (LR 78) stop only for large effects. An SPRT with bounds 0 and +0.3 a seat would end most held-out reads well before 400 seeds.
- Variance reduction. The pairs already share seeds (common random numbers). Two more: stratify by map (each map's difference weighted by a fixed share, so a run's draw of maps adds no noise), and CUPED (regress the paired difference on what is known before the match: the map, the start's distance to the nearest depot, the team's neutral pillboxes in reach). Either can cut the seeds a reading needs; how much is itself a census to run.
- One results ledger. Every census's commit, binary hash,
run.env, trial and pooled result in one table on the machine, as experiment trackers keep (MLflow, Weights & Biases), read by the weight ledger. Today the record is spread overbuild/folders and NOTES files. - A canary census. Each brain commit read against the last tagged defaults on a fixed 110 seeds, nightly: drift seen the day it comes, as performance tests gate a build.
- Screening designs. A definitive screening design reads k weights in about 2k + 1 censuses, every census moving every weight, and separates main effects from curvature (Jones and Nachtsheim 2011). Against one weight at a time it shares each census's noise across all weights, so each estimate needs fewer seeds.
- More machines, one census each. Paul can add CPUs. A census stays on one machine (CENSUS.md section 5); a queue of censuses can be dealt to several, each read against its own defaults, as fishtest deals games to its workers. The screen's 135 rows would split across two machines in half the time.
- A learned brain. LR 90.
90. A learned brain: a study, 26 October
Paul: "If we were to use Cuda, or some sort of Mac or PC based hardware acceleration. Is there a way to define this such that we start training a normal AI neural network given our current reward system. Perhaps we need some simple low effort rewards, like moving to the center of the map. and shooting ammo with hits. It needs rewards in order to find how to aim. Write a lab report about this. Perhaps we can consider training this faster on a GPU." Then: "I can slo spin up some more CPUs." A study; nothing built or run.
Yes, and the pieces are mostly here
| A learning setup needs | treadcore has |
|---|---|
| an environment that steps without a window | treadcore-sim's World, headless and deterministic by seed (the census plays it) |
| an observation | Observation (treadcore-sim): the tank, every tank in sight, shells in flight, depots, pillboxes, mines, structures, boats, nearest forest and pads, the map's size |
| actions | Command: Move { to }, FireDirect { at }, FireIndirect { at }, Launch, Deploy, Stop, Respawn, and the rest |
| rewards | ledger::Tally::hear counts direct hits, pillbox damage and captures, defences, kills and deaths from the sim's events, tick by tick (LR 84) |
| opponents | the scripted brain at any weights, the personalities, the skill dial (LR 67) |
| a judge | the census and make families, unchanged |
What is missing: a bridge from the Rust sim to a trainer, the observation turned into a fixed array, the actions into a fixed set, and a Brain that runs a trained network.
Where the time goes: the CPU, not the GPU
The census machine plays about 625 ticks a second a thread with sixteen seats, scripted brains thinking included (110 seeds, three matches a seed, 18000 ticks a match, in 6.6 minutes on 24 threads). At a decision every 4 ticks (5 a second), 24 threads give about 60,000 seat decisions a second, roughly 200 million an hour if every seat learns.
A network for this game is small: a few hundred inputs, two layers of 256, a few outputs. Its forward pass is microseconds on a CPU. The training update (PPO over a batch of a few hundred thousand decisions) is where a GPU helps: a few times faster than the CPU for a network this size. On a Mac, PyTorch runs on the Apple GPU through Metal (the mps device); on a PC with an NVIDIA card, CUDA. Either is enough. GPU simulators (Isaac Gym, Brax) are fast because the game itself runs on the GPU; that would mean writing the sim again, and is not worth it.
So the speed of training is the speed of the sim: more CPUs is the lever, and it scales nearly linearly. A GPU is a convenience for the update step. Paul's extra CPUs help both the census and training.
The rewards: dense and simple first, the census's score last
A network starting from nothing fires at random and almost never hits, so a hit reward alone teaches nothing for a long time. Paul's instinct is the standard answer: shaped rewards that pay small steps. Proposed, each a scalar (the weights rule), all per seat per decision:
| Reward | Worth | Why |
|---|---|---|
| a direct hit on a hostile tank | +1 | the aim; from Tally::hear as LR 84 counts it |
| a shell fired that hits nothing | -0.05 | ammunition has a price, so it learns when not to fire |
| closer to the objective | potential-based: +0.01 times the drop in distance to the nearest neutral or hostile pillbox (or the map's centre) | Paul's "move to the center"; potential-based (Ng, Harada and Russell 1999), so it cannot pay for circling and does not change what is best in the end |
| armour lost | -0.01 a point | stay alive |
| a kill | +3; a death -3 | |
| the LR 84 rewards | as the census scores them | pillboxes, defences, every neutral |
| the match | +10 a win | the census's own objective |
The shaping terms are annealed to zero over training, so what the network ends up optimising is the census's score. LR 86 showed a reward can be gamed (shelling pillboxes earned most of a +7.19); the census with CENSUS_REWARDS=0 beside it stays the check.
Aim: learn it from the scripted brain first
The scripted brain already aims, leads, dodges and kites. Behaviour cloning (supervised learning of its command from its observation) over a few million decisions from census games gives a network that moves and shoots like today's bot in an hour or two of training. Reinforcement learning (PPO) then starts from that network, not from random. AlphaStar began this way; it saves most of the cold start, and aiming above all.
For aim itself, the action is a target from a short list (the k nearest hostile tanks and pillboxes, or none) and a lead: an offset in the target's own frame, two numbers, which is what a gunner learns. A hit is then a reward the network can find.
The observation and the actions
- Observation, egocentric and fixed: the tank (armour, shells, mines, reload, heading, speed, afloat); the 8 nearest hostile tanks and the 4 nearest allies (offset, velocity, armour, reload); the 8 nearest pillboxes (offset, owner, health); shells landing within 4 s (offset, time to land); a 32 x 32 tile window of terrain around the tank (a small convolution); the direction to the map's centre and to the base. About 300 numbers and the window.
- Actions, at 5 a second: a move (8 headings, or hold), a gun (target from the list, or hold) and its lead, the indirect gun the same way. Building, drones and the squad stay with the scripted brain at first: the network drives and shoots, the commander decides the rest. Later the network can pick the commander's goal.
The plan, in steps (each recorded as usual)
- A bridge: a Rust crate (
treadcore-gym) with PyO3 bindings, a vectorised environment of many worlds a process, each seat's observation as a NumPy array, actions in, rewards out (the Gymnasium and PettingZoo interfaces). Measure decisions a second. - Behaviour cloning from census games of the scripted brain. Check: the cloned brain, as a
Brainin Rust (the network exported to ONNX and run bytract, pure Rust), against the defaults in a census. Expected: below, by little. - PPO, against a pool of opponents: the scripted brain at the skill dial's 0.3, then 0.6, then 1, then the personalities and earlier copies of itself (self-play with a league, so it cannot win by beating only its last self). Sample Factory or CleanRL as the trainer.
- Judged by the census, the same rule as any change: two held-out reads past two errors above, no map past two below, the families held, the old score beside the new.
Costs. Step 1, a day of code. Step 2, an hour of games to collect and an hour to train. Step 3 at 60,000 decisions a second: 100 million decisions in about half an hour on the census machine, a billion in about five hours, if the bridge keeps the sim's pace (a network called from Python each decision may halve it; step 1 measures it). Games of this kind have needed billions of decisions to pass a hand-built bot: a day or more of the machine, less with more CPUs.
What could go wrong. A network that games a reward (the reason the old score stays beside the new). One that beats the defaults on a map or two and loses the rest (every map read). One too strong or strange for a person (the skill dial, LR 67, must still apply). And the scripted brain's strengths may be hard to pass: ninety sections of measured behaviour. The census will say.
Next: Paul to choose whether to start step 1, and on which machine.
91. The census 2.6 times faster, every match the same, 26 October
Paul: "We have CPU . but we can also install some CUDA on Copal and perhaps adjust the brain to simulate much faster. If it won't be faster, then just keep with CPU", then "nice findings and reductions. Continue to refactor. I like these speedups."
Where the time went. macOS sample on a census on the Mac (every map, 300 s): World::visible_to took 58% of the busy time. It is asked for every shell (twice), drone, tank and boat each seat's observation holds, each tick, and each time walked every tank and drone asking allied, itself two searches of the player list.
The rule for a speed-up: the same matches. The check is a census of 6 seeds from 170000, every map, 300 s, 3 respawns (18 matches), whose every team line and meter line (scores, kills, hits, dodges, spending, the squad's seconds by task, postures) must match this morning's binary to the character, the meter's own timing alone apart. Every step below passed it; make families then held with every count as this morning (the duel 314 of 400, the guarded base 326 of 400, the ready gun 836 of 1000); the sim's and brain's tests pass.
| Step | What | Bots' time a seat-tick |
|---|---|---|
| before | 64.5 us | |
| the eyes | World::eyes: a side's tanks and drones with their reach, gathered once an observation; seen_by tests a point against them | 26.9 us |
| the rings | Map::nearest_tiles: forest and water read in rings outward, stopping once no further ring can hold a nearer tile (a tile k rings out is at least k - 0.5 off); the n nearest by (distance, row order), which is the old stable sort | 22.5 us |
| the heap | route_by's open list: f as its bits (never negative, so the same order), the heap kept between searches | 21.9 us |
| sites and radar | pads and banks (few on most maps) kept as a list per map state (Map::sites, keyed as the route cache is); a seat's data centres gathered once | 20.2 us |
The test census: 125.8 s before, 48.7 s after, 2.6 times; the bots' part 3.19 times. Two tries changed nothing measurable and were taken out: regions joined in place as ground opens (702 flood fills a census, not the cost), and the danger of a Move's route marked once (the search's size, not its pricing, is the cost).
New tests: the_ring_read_finds_what_the_whole_square_did (3000 points against the old scan, every terrain, pads, banks, edges, radii to past the map, n from 0 to all, the map edited under the kept lists). Each check was itself checked by breaking the code it guards (the ring bound a tile looser; the site key without the edits): the test failed both times.
Where it goes now: route search, about 40%. 25% is the dodge's forecast (Dodge::hold_fire asks drive_path, which asks the full route to the errand's end to use its first seconds), 15% the sim's route for each Move. Neither can be made cheaper without sometimes another route of the same cost, which changes matches (a recorded replay depends on them): that is a change to play, read by a census (LR 92).
CUDA: the census machine's GPU is an RTX 4060 Ti on Alpine with NVK. CUDA needs NVIDIA's driver and glibc; Alpine has neither. And the census would not use it: its time is branching logic, routes and per-tank rules, not arithmetic over arrays. The GPU is for training a network (LR 90), from a glibc system on that card.
Deployed: the census machine moved onto this code between two of the day's censuses. Every census builds its own binary, and the matches are the same, so the day's results are unchanged; only its hours are fewer.
92. The run: a ledger, a fit, and the census until stopped, 26 October
Backlog 69. Paul: "Is there a way to write the make census write lots that give suggestions that can be picked up and used to adjsut the next generation of runs. We essentially want a method to just start computing at night time which runs concenus to converge the values", then "so night and day need to be consolidated. we just want to have a run. a way for it to choose eventually from all of the variables . doing your methods of analyzing them".
The ledger (scripts/ledger.py, make ledger): every census on a machine, one row: host, stamp, commit (new in run.env from today, a + for uncommitted source), the frozen binary's hash, seeds, first seed, seconds, map, respawns, the trial's and baseline's weights, the difference a seat, its error, z, and each map's. --merge joins machines' ledgers without pooling a census across them. On the census machine at 10:45: 87 censuses since 22 October.
The fit (scripts/suggest.py): each census against today's defaults (since their last change, 18:13 on 25 October) gives d and se for the weights it set. A weight at v is x = log2(v / default), -1 at half and +1 at double. d is fit as the sum over weights of a x + b x^2, weighted by 1 / se^2, with a ridge of one: a response surface, each weight's curve read from the screens and searches alone and in company. Each generation, 12 trials: every weight at its curve's top where that is past two errors (none yet: the defaults sit at the top, LR 88), with it less one weight; trials past 1.5 errors on 55 seeds read again on fresh seeds (successive halving); each weight's half and double not yet read alone (up to half the generation); draws from the fit's uncertainty (Thompson sampling), four weights a draw. Weights from 0 to 1 stop halfway to 1; switches, the skill dial and the score's own blend are left out. Its first fit, at 10:45: 59 censuses, 51 weights; it asked again for coming_r at half (z +1.80), column_past at half (+1.77) and bridge_soft at half (+1.60).
The run (scripts/converge.sh, make converge): generation after generation until stopped or UNTIL, fresh seeds each (SEED0 + g * 10000). Any census past two errors above with no map past two below is read at once twice more, 400 seeds each on seeds no step saw; both must pass, then make families, then Paul. Its rule is written to NOTES before it plays; it changes no default. Tested on the Mac (two seeds of 60 s, four minutes, 36 generations): the ledger, the fit, the reads, the held-out pairs and the stop all ran; its censuses were removed after.
The day folded in. The day's queue (LR 89) stopped after stage 1's half ends; its rows are in the ledger, and the run takes up the rest: the double ends, the reads again (stage 2), and the search (in place of SPSA, whose single direction the fit replaces).
Why a census is quick (Paul: "why would a simulation go so fast"). The matches are simulated, not played in real time: a 900 s match is 18000 ticks computed as fast as the CPU goes, with no window, about 6 s each on the census machine since LR 91. A census of 55 seeds is 165 matches over 24 threads; a job of one seed (55 split 24 ways) ends in 18 s. Every match still runs its 900 game seconds ("over false at 900 s" in each log).
Checked on the census machine: one of the day's rows played again on the binary before LR 91 and the one after, 4 seeds of 300 s, 12 matches: every team line the same. The speed-up is the same matches on x86 as on the Mac.
93. The dodge's forecast along the tank's own route: off, for one map, 26 October
Backlog 71. Paul: "faster route search with consusus", then "start the run and do the route search."
The dodge forecasts a drive every tick a shell is coming (Dodge::hold_fire, release): drive_path asked route_kept for the route to where the tank is going, a fresh A* each time the tank crossed a tile: a quarter of a census (LR 91). dodge_own_route (thresholded at 0.5): at 1, a drive to where the tank's route already ends follows that route, its waypoint and the rest, as the world will drive it, with no search (drive_path_by). It ships at 0, against the habit since 20 October of shipping on: at 1 the defaults would move under the run (LR 92), whose table reads every census against them. At 0 every match is the same as before (the 18-match check). Every team at 1, the test census on the Mac: 47.8 s to 38.9 s, the bots 20.0 to 15.8 us a seat-tick.
The census (build/route/ on the census machine, rule in its NOTES first): 1 against the defaults, 110 seeds from 170000, every map:
| Read | |
|---|---|
| every map | -0.53 +/- 0.48 |
| big-lizard | -6.48 +/- 1.56 |
| atlantic | +1.26 +/- 0.85 |
| generated | +1.43 +/- 1.82 |
| europe | -1.26 +/- 0.89 |
| the other seven | inside one error and a half |
The rule kept it on unless the whole or any map read past two errors below. Big-lizard does, at z -4.2, more than chance among twelve maps gives. It stays off; no default changes. Expected inside the noise, a little above as likely as below: held for the whole, missed on big-lizard. Why big-lizard is not known: the own route is the world's, with pillboxes' reach priced in, so where the world's route and the plain one part (a detour round guns, or a boat's crossing), the forecast now sees a different drive, and the dodge decides differently. To look at before it is tried again: a scenario on big-lizard, and the forecast compared both ways where they part.
The sim's own search for each Move (15% of a census, LR 91) is the same for every team, so the census's pairing cannot read a change to it; a change there would be read as two censuses of the same trial on two binaries, a weaker test, and is not started.
Found and fixed the same day: the own route ashore only, read again on 110 seeds every map, on big-lizard alone and on the generated maps, and on by default (LR 94, part C).
94. How it works: a primer on the faster census, the route forecast and the run, 26 October
Paul: "find why big-lizard costs and fix the route search. also give me a reading link to explain biglizard. also write out your implimentation in the lab report. be detailed, and also give me introductory information and teach the needed lessns."
This section is for reading start to finish. Part A teaches the ideas the day's work rests on. Part B is the implementation, change by change, with what each one does in the code and how it was checked. Part C is big-lizard: why the forecast cost there, the fix, and the reads. Part D is the lessons. The map is drawn and explained at https://claude.ai/artifact/Mias4vtFMwmMM6Fqsd6wsa.
A. The ideas
A1. A tick, and why a match is quick. The world advances in ticks, 20 a second (TICK_HZ). A tick moves every tank and shell, fires every ready gun, and asks every bot what it wants (run_bots). A 900 s match is 18,000 ticks. Nothing waits for a clock: with no window the census computes ticks as fast as the CPU goes, so a match takes a few seconds. "Fast" means fewer CPU instructions a tick, never a shorter match.
A2. The census, and what its numbers mean. A census plays the trial team (the weights under test) against two baseline teams on the same map from the same start, three times a seed, so each team takes the trial seat once (the fair census, LR 24). Its reading is the trial team's score less the others', a seat, averaged over the seeds. The standard error (se) is how much that average would move if the census were run again on other seeds: the spread of the seeds' differences over the square root of their number. z is the reading over its se. "Past two errors" means |z| > 2: chance alone gives that about one time in twenty. Every census since LR 24 has been read this way.
A3. Determinism. A seed fixes every random draw, so a seed plays the same match every time, to the bit, on one machine with one binary. That is what makes a pairing work (both sides meet the same luck), and it is the test for a speed-up: if the new binary's matches are the same, character for character, it computes the same game, only faster. Two machines of different kinds (the Mac's ARM, the census machine's x86) round some float operations differently, so the same seed can play a different match on each (CENSUS.md section 5).
A4. A profile. A sampling profiler (macOS sample) stops the program about a thousand times a second and records which function it is in and who called it. Over 20 s that is a fair picture of where time goes. Two traps: the compiler inlines small functions into their callers, so their time shows under the caller's name; and the linker merges identical machine code, so some time shows under a merged name (<deduplicated_symbol>). The cure for both is the call tree: who called the unnamed thing.
A5. A route: A\* over tiles. A tank told to move is given a route by Map::route_by, an A\* search. A\* keeps an "open list" of tiles to look at, always taking the one with the least f = g + h: g the cost of the cheapest way found to it, h an estimate of the rest (here the straight distance times 0.6, the cheapest terrain's cost, so it never overestimates and the route found is a cheapest one). It looks at a tile's eight neighbours, prices each step (terrain cost; 1.4142 for a diagonal; the world adds danger near hostile guns and on known mines), and stops at the goal. Two routes can cost the same; which one A\* returns depends on the order it looks at tiles. That is why a faster search that looks in another order can change a route, and with it a match.
A6. A cache, and its key. A cache keeps an answer so the same question is not worked out twice. Its key must hold everything the answer depends on, or it returns a stale answer. The map's routes, pads and banks depend on its cells, which change in play (a wall built, a tree felled), so their key holds the map's identity and its count of edits (Edits): any edit makes a new key. A clone of a map gets a fresh identity, so two worlds never share an entry.
A7. The dodge's forecast. When a hostile shell is coming, the dodge asks, for each shot and move the brain wants, where the tank will be when the shell lands (drive_path): it plays the tank's own motion forward, tick by tick, along a route (turning at TANK_TURN, braking for corners, the ground's cost), and checks the path against the splash (clearance). A shot turns the hull (a tank has no turret), so it can carry the tank into the splash (hold_fire, LR 57); a move can drive it there (release, LR 53). Until LR 93 the route it played along came from a fresh search (route_kept) that leaves the guns out.
A8. A response surface. To learn from many censuses at once, each weight's effect is modelled as a curve: the reading d is the sum, over the weights a trial set, of a x + b x², where x = log2(value / default) (x = -1 at half, +1 at double). A weighted least-squares fit (each census counts by 1 / se²) finds a and b for every weight; a small ridge (a prior that a and b are near zero) keeps a weight read once from swinging wildly. The curve's top within half and double is that weight's best value, with an error of its own.
A9. Thompson sampling and successive halving. To choose the next trials the fit draws each weight's curve at random from its uncertainty and takes the drawn curve's top: weights the census knows little about get tried, weights it knows well stay near their best (Thompson 1933). Successive halving reads many trials cheaply (55 seeds) and reads again only those that look good (past 1.5 errors), on fresh seeds (Jamieson and Talwalkar 2016; Hyperband). A read past two errors is then read twice more, on 400 seeds each, before anyone believes it.
B. The implementation
B1. The eyes (World::eyes, World::seen_by; treadcore-sim). The profile: visible_to 58% of busy time. observe(p) builds what player p may see; for each shell (twice: where it is, where it lands), drone, tank and boat it asked visible_to(p, at), which walked every tank and drone asking allied(owner, p), and allied searches the player list twice (team_of). With 16 players, 50 to 100 shells in flight under the cowboy's spending (LR 86), and every seat every tick, that is millions of list searches a second. Now observe first gathers p's eyes once: each living allied tank and each allied drone with its reach in the open and into forest (Eye { pos, open, forest }); then each point is tested against that short list. The arithmetic is the same (TANK_VISION * 0.5, vision * 0.6, the same dist), so every answer is the same. visible_to and tank_visible_to keep their signatures, now built on the eyes.
B2. The rings (Map::nearest_tiles). observe asks each tick for the 8 nearest forest tiles within 20 (forest_near), the 32 nearest pads, and the 12 nearest launch sites within 16 (launch_sites). The old code read the whole square, (2r + 1)² tiles (1,681 at r = 20), kept those within the radius, and sorted them all by distance (a stable sort: equal distances in row order). The new code reads the square in rings outward from the tank's tile. A tile in ring k is at least k - 0.5 from the tank, because the tank is somewhere inside its own tile; so once n tiles are in hand and the n-th nearest is closer than k + 0.5, no tile further out can displace it, and the read stops. The n nearest are kept by (distance, row index), which is the old stable sort's order exactly, so the answer is the same list. In forest the read stops after a few rings instead of 1,681 tiles.
B3. Sites (Map::sites). Pads and banks are few on most maps, so the rings find none to stop on and read the whole square anyway. Each is now also kept as a list of every such tile on the map, built once per map state (the cache of A6), and when the list is shorter than the square the read goes through the list instead. Again the same answer: the same tiles, kept by the same order.
B4. The radar. For each drone not otherwise seen, radar_sees searched every structure for a working allied data centre in reach. Now observe gathers the side's data centres once and tests each drone against them.
B5. The heap. route_by's open list compared f values as floats. f is never negative, and for non-negative floats the order of their bit patterns, read as unsigned integers, is the order of the floats (IEEE 754), so the list now compares integers (Open(u32, usize)): the same comparisons, the same routes, a little cheaper. The heap's memory is kept between searches. 3%.
B6. Two that did not pay. Regions (which tiles can reach which) were relabelled over the whole map whenever a tile changed whether it can be driven: 702 times in the test census. Joining regions in place as ground opens was exact (a test against a fresh labelling) but saved nothing measurable. Marking the danger of a move's route once instead of testing every gun at every tile was exact and saved nothing: the search's size, not its pricing, is its cost. Both were taken out. A profile says where time goes; only a timing says whether a change saves it.
B7. The checks. (1) Identity: a census of 6 seeds from 170000, every map, 300 s (18 matches) compared with this morning's binary line by line, the timer apart. (2) The same on the census machine: one of the day's rows on the old and new binaries, 12 matches. (3) make families: every count the same. (4) Tests that compare the new code with the old scan over 3,000 random points, every terrain, the map edited under the cached lists. (5) Each test broken on purpose (the ring bound one tile looser; the cache key without the edits) to see it fail: a test that cannot fail checks nothing.
B8. The run. scripts/ledger.py reads every census folder into a table (A8's data). scripts/suggest.py fits the surface and writes 12 trials: the best point if a curve's top is past two errors, the best less one weight, reads again of trials past 1.5 errors on fewer than 110 seeds, each weight's half and double not yet read alone, and Thompson draws of four weights each. scripts/converge.sh loops: ledger, suggest, a census of 55 fresh seeds each, and two held-out reads of 400 for any census past two errors with no map past two below. Two faults found in its first hour, both now fixed and both general lessons: ls | head -1 under set -o pipefail dies with SIGPIPE once there are enough folders that head closes the pipe before ls finishes (exit 141; fixed with sed -n 1p, which reads everything); and the run's own setting RESUME leaked into each child census, which also reads RESUME, so every census tried to resume the run's folder and failed at once (fixed by unsetting it; the run now also stops after three failures in a row instead of spinning).
B9. The own route (drive_path_by, dodge_own_route). The dodge's forecast searched a route every tick under fire: 25% of a census. The world has already planned the route the tank is driving (Tank::goal, the waypoint it is heading for, and Tank::route, the rest), and it ends where the tank is going. So when the forecast asks for a drive to exactly that end, it now plays the tank's motion along the tank's own route: no search, and the drive the world will really make, guns and mines priced in. Anywhere else (a dodge to a new point) it searches as before. The weight dodge_own_route (thresholded at 0.5, in the dodge's tune array as its ninth number) switches it; at 0 the code takes the old path exactly, so every match is the same.
C. Big-lizard
C1. The map. 256 by 256 tiles, 94% water, 60 islands. One island of 2,615 tiles holds 13 of the 16 starts, 14 of the 16 depots and 12 of the 16 pillboxes; three players start on islets and must cross by boat. Its matches mostly end in elimination (median 534 s, MAPS.md). A pillbox reaches 8 tiles, and the world prices every tile within 8.5 of a hostile one at six times its cost, so on that island nearly every route bends round a gun.
C2. What the census read. Own route everywhere, every map, 110 seeds on the census machine: -0.53 +/- 0.48, but big-lizard's 10 seeds -6.48 +/- 1.56, every one of them below. Off, by the rule.
C3. Why. A probe computed both forecasts at every ask on big-lizard and counted where they differ and where they turn the dodge's verdict (clear of the splash or not):
| Asks | Apart > 1 tile | Apart > 4 tiles | Verdict turned | |
|---|---|---|---|---|
| ashore | 148,887 | 14% | 1.8% | 0.6% (of 46,493 verdicts) |
| afloat | 5,544 | 32% | 9.7% | 1.7% (of 2,036) |
The forecasts part most at sea. There a boat's own route, planned when the move was given, and a fresh search from where the boat now is differ far more often than ashore (a boat's turns are slow and its routes long, and the landing is priced differently). Near guns ashore the own route is the truer one: the new test the_own_route_foresees_a_drive_round_a_gun puts a hostile pillbox two tiles beside a tank's straight way; after 3 s the own route's forecast is 0.00 tiles from the tank and the search's 7.85.
C4. How big the cost is. Big-lizard alone on the Mac, own route everywhere, 120 seeds: -0.97 +/- 0.66 (z -1.46), inside the noise. A big-lizard seed swings about +/-7 a seat (elimination either way), and ten seeds estimate their own error poorly; with twelve maps read, the worst is drawn from the tail. So the first census's -6.48 was most likely a small real cost (about -1) on top of an unlucky draw; it is not, on 120 seeds, past two errors.
C5. The fix. The own route ashore only (!me.boat); afloat the forecast searches as before. It keeps most of the saving (the test census at 1: 41.4 s against 47.8 s at 0 and 38.9 s with the own route everywhere) and removes the case where the forecasts part most.
C6. The reads (census machine, rule in build/route/NOTES2.txt first; 110 fresh seeds from 400000 each):
| Read | Seeds | Difference a seat |
|---|---|---|
| A. every map | 110 | -0.18 +/- 0.37; generated -1.83 +/- 0.87 (10 seeds), the only map past two below |
| B. big-lizard alone | 110 | +0.58 +/- 0.81 |
| C. the generated maps alone (the rule's read of A's one map) | 110 | +0.51 +/- 0.43 |
| make families at 1 | all nine hold (the duel 75.2% against 78.5% at 0, inside the noise) |
Each passes its rule. dodge_own_route is on by default (1). The expectations held: A and B inside the noise, B nearer zero than the expected -1. The generated maps' -1.83 on 10 seeds read +0.51 on 110: lesson 7 below, in action. The census is faster for it: the test census 47.8 s at 0, 41.4 s at 1 (ashore only), every team.
The run (LR 92) reads its trials against the defaults, and the defaults moved: by CENSUS.md section 4a, a new run begins, its ledger from this change (SINCE). The rows before stay in the table for comparison.
D. Lessons
- Measure, then change, then measure. The profile found
visible_to; the timings showed two plausible changes saved nothing. Both halves are needed. - An exact speed-up is proven by identity. The same matches, to the character, on every machine that runs the census, and families with the same counts. Then no reading anywhere is in doubt.
- A test must be able to fail. Break the code it guards, once, and watch it fail.
- A cache's key is everything the answer depends on. Leave the edits out and the answer goes stale the first time a tree is felled.
- A truer model can still change the game. The own route is the more accurate forecast; the dodge's numbers were tuned against the old one. Accuracy is not the verdict; the census is.
- Few seeds on a wild map mislead. Ten big-lizard seeds read -6.48 +/- 1.56; 120 read -0.97 +/- 0.66. A map that swings needs many seeds before its error means what it says.
- The worst of many is not chance-free. Twelve maps give twelve draws; one past two errors is a reason to read that map again on its own.
- A read that looks good shrinks when read again. column_past at half read +0.39, then -0.35 on fresh seeds (the run's first reread). This is regression to the mean, and it is why the run rereads before it believes.
- Scripts are code.
pipefailand an inherited environment variable each stopped the run once. Test a long-running script the way it will run: with many folders, and resumed.
95. The Tank Generational AI: senses, a small net, generations, in Rust, 26 October
Backlog 70. Paul: "i want to be able to start converging the neural net as well. what kind of general sensors can we give to make the smallest reasonable neural net. Can we use thsee conclusions as inputs. Can we give a sensor like interface to give only reduced knowedge so we can minimze thenumebr of inputs in the transformer. I actually dont know all the AI terminology. I want a tank that has all the actions available to it. I know we need a million generaions , but can we conceptualize this in the lab report about what data is needed. perhaps direction indicators. signal striength. we want a sensor like interface so the tank can expieience its rewards and its decision. Do do this. Can we have a good test plan with milestones to move to the next phase. First the tanks need to be able to be reward to driving towards an enemy. or driving towards bases or pillboxes. Since we have a working Brainz, we also want a parallel system so we can have train the bots. We can baby step the training to with larger goals as we develop milestones of Tank Generational AI Neural Net. Is this a plan for some library. I read earlier I can convert a box to ubuntu. i can use an 1080TI or a 4060TI. I expect to install ubuntu 26.10 I heard its LTS. I also prefer Rust Neural Nets. I really hate python."
A design, nothing built. It replaces LR 90's plan where they differ: all Rust, no Python; a sensor interface instead of raw positions; generations (evolution) first, on the CPUs we have, and gradient training on the GPU second.
A. The words, plainly
| Word | Meaning here |
|---|---|
| neural net | a function from numbers in (the senses) to numbers out (the action's scores), built of layers; each layer multiplies by a table of weights, adds, and bends the result (max(0, x)) |
| weights, parameters | the numbers in those tables: what training changes. A net of 100 inputs, two layers of 64, and 40 outputs has about 13,000 |
| MLP | the plain kind of net above, layer after layer. The right size for this |
| transformer | a net for long sequences (text) or sets of things; it weighs every item against every other. More than a tank needs while its senses are a fixed list |
| policy | the net that chooses actions |
| reward | a number the game gives the tank for what just happened |
| episode | one try: a scenario or a match, from start to end |
| generation | in evolution: a population of nets tried and the best bred into the next |
| evolution strategies (ES) | training by trying many small random changes of the weights and moving toward those that scored better. No gradients; every try independent: made for many CPUs |
| PPO, gradient training | training by working out, from many decisions and their rewards, which way each weight should move. Faster per decision; wants a GPU for the arithmetic |
| behaviour cloning | training a net to copy a teacher (the scripted brain) decision by decision |
| curriculum | easy tasks first, harder ones as each is passed |
| action mask | actions that cannot be taken now (no shells: no shot) switched off, so the net never wastes a try on them |
B. The senses: a sensor interface, reduced knowledge
The idea is Paul's: the tank does not see the world's lists of tanks and positions; it has senses, like a vehicle's instruments, each a direction and a strength. That keeps the inputs few and the same size on every map, and it is what the tank would feel.
The compass. Eight sectors round the tank, relative to its heading (ahead, ahead-right, right, ... ahead-left), so "ahead" means the same thing facing any way. In each sector, each channel reads a signal strength from 0 (nothing) to 1 (touching): the strongest thing of that kind in the sector, as 1 / (1 + d / 8) for something d tiles off (1 at 0 tiles, 0.5 at 8, 0.2 at 32). The channels:
| Channel | What sets it |
|---|---|
| hostile tank | a hostile tank in sight (its armour scales it down when hurt: a weaker signal is an easier kill) |
| allied tank | a teammate |
| hostile pillbox | a live hostile pillbox; strength 1 inside its reach |
| neutral pillbox | one to take |
| our pillbox | one to keep, mend or stand by |
| depot to take | a neutral or hostile depot |
| our depot | where shells and armour come back |
| shell coming | a hostile shell landing in that sector within 4 s, strength by how soon |
| ground | how far the tank can drive that way before water or a wall (a ray), 1 at the tank, 0 at 16 tiles or more |
| cover | forest that way, near |
8 sectors by 10 channels: 80 senses. (A 16-sector compass doubles it, for finer aim later.)
The body. Armour, shells, mines, material (each 0 to 1 of full), the gun's reload (0 ready, 1 just fired), the mortar's, speed, afloat, the squad out, each drone kind ready, respawning: about 14.
The brain's conclusions, as senses. Paul asked whether the scripted brain's conclusions can be inputs. Yes: it already works out things a small net would take long to learn. Each is a few numbers:
| Conclusion | Where it comes from | Senses |
|---|---|---|
| the team's posture | commander's five scores (LR 24, 25) | 5 |
| the odds here | engage_odds reading (tanks near the fight, ours against theirs) | 1 |
| the base threatened | the team board's threat to the base | 1 |
| the way to the errand | the bearing and distance to the brain's chosen goal, as a compass sector | 2 |
About 9. They are hints, and they can be taken away later (sensed as 0 in some episodes, then always) so the net learns to stand without them: "hint dropout".
Memory. A tank that loses sight of an enemy forgets it. The first nets get the last tick's senses beside this tick's (twice the inputs); a small recurrent layer (a GRU, which carries a few numbers from tick to tick) comes later if milestone 6 needs it.
In all: about 100 senses, the same on every map and every size of map. A net of two layers of 64 is about 13,000 weights: small enough to run every tick on a CPU in microseconds and to evolve by ES.
C. The actions: everything the tank can do
The net chooses five times a second, all heads at once, each from a short list (a "factored" action):
| Head | Choices |
|---|---|
| drive | one of the 8 sectors (a Move 6 tiles that way, routed by the world), or stop |
| gun | hold, or fire at the strongest hostile signal in one of the 8 sectors; with a lead of left, none or right |
| mortar | hold, or fire into one of the 8 sectors at that sector's strongest target |
| drone | hold, or launch recon, interceptor, strike or a longshot toward the strongest target |
| squad | hold, capture the nearest pillbox to take, build (wall, pillbox, road, mine), mend, harvest, come home |
| signal | none, or ping a sector for teammates |
Masks switch off what cannot be done now. Early phases use only the first heads; the commander (the scripted brain) does the rest until a milestone hands it over. The net's outputs are scores, one for each choice; it takes the highest (or samples from them while it learns).
D. Rewards it can feel
Each tick the tank's reward is the sum of what it earned. In the order the milestones turn them on:
| Reward | Worth | Milestone |
|---|---|---|
| closer to the target: the drop in distance to the nearest enemy, depot or pillbox | +0.05 a tile, as potential (never pays for circling: Ng, Harada and Russell 1999) | 1 |
| a direct hit on a hostile tank | +1 (LR 84's count) | 3 |
| a shell that hits nothing | -0.05 | 3 |
| armour lost | -0.01 a point | 4 |
| a kill, a death | +3, -3 | 5 |
| a pillbox or depot taken; pillbox damage; a defence | LR 84's worths | 6 |
| the match: a win | +10 | 7 |
The shaping terms (closer, misses) fade to nothing over training, so what the net ends up chasing is the census's own score. The census, with the old score beside it, stays the check against a reward gamed.
E. Milestones: a test plan, each gate before the next
Each phase has a scenario family (TOML in scenarios/, as the dodge's families are) and a floor, run by make families; the net moves on only when the floor holds over 1,000 seeds. The same rule as every change: the test is written before the training.
| Phase | Task | Pass |
|---|---|---|
| 0. Senses | the sensor interface, a NetBrain that drives by a net, a random net plays a census | sensor tests: an enemy placed due right lights "right" at the expected strength; every match the same twice; decisions a second measured |
| 1. Go to | a lone tank on open ground; a target: an enemy, then a depot, then a pillbox | within 3 tiles inside 60 s on 95% of seeds |
| 2. Go round | the same past water, walls and forest, on the prior-art maps | 90% |
| 3. Aim | a still target, then a moving one, then one that shoots back | hits per shot at least the scripted brain's on a-duel-in-the-open |
| 4. Live | the dodge's families (a tank under one or two shells) | their floors (99%) |
| 5. Duel | one against one with the scripted brain at skill 0.3, then 0.6, then 1 | half the duels won at each |
| 6. Supply and take | resupply at depots, capture neutral pillboxes | the raid families' floors |
| 7. Team | four nets against the scripted teams | the census: a team of nets read against the defaults, by the usual rule |
| 8. All of it | every head, the commander's work handed over a head at a time | the census, every map, two held-out reads |
F. Training: generations first, on CPUs, in Rust
Track A, evolution (now, the census machine). Paul's "Tank Generational AI": a population of nets, each a weight vector. Each generation adds small random changes to the current best weights (a population of, say, 96), plays each on the phase's scenario seeds, scores them by reward, and moves the weights toward the changes that scored better (OpenAI's evolution strategies, Salimans and others 2017). Every try is an independent episode, so the 24 threads (and any machine added) are filled; nothing needs a GPU or Python. The run (LR 92) already is a generational loop over weights; ES is the same loop over 13,000 numbers instead of 100.
Cost, estimated from LR 91's speed and to be measured in phase 0: a phase-1 episode is one tank on a small map for 60 s (1,200 ticks), so tens of thousands of episodes an hour on the census machine at the least. A generation of 96 nets on 8 seeds each, about a minute. A phase is hundreds of generations: hours, not the million generations feared. Phases 5 to 8 play full matches (about 6 s each): a generation of 96 on 4 seeds is about 2 minutes; a thousand generations is a day or two.
Track B, gradients (the GPU, later). For phases 5 to 8, if evolution slows: behaviour cloning from the scripted brain first (the senses and the brain's choice recorded from census matches, about 400 MB a million decisions), then PPO. In Rust: Burn (tracel-ai/burn, 0.21), whose CubeCL backends train on CUDA, Vulkan or the CPU, with burn-rl for building blocks. The net trained on the GPU is the same net NetBrain runs; its weights are saved to a file the census loads.
The parallel system. The census and the run go on as now. The trainer is its own program (make train PHASE=1) on the cores the run leaves, or on the GPU machine; it writes each phase's best net to nets/phaseN.bin. A NetBrain reads such a file, and the census can seat a team of them as its trial (CENSUS_NET=nets/phase7.bin), so a net is judged exactly as every weight change is.
G. As a library
Yes: three parts, each a crate or module of this workspace, in Rust.
| Part | What it is |
|---|---|
treadcore-senses | the sensor interface: an Observation (and the brain's conclusions) to the 100 numbers; the action heads to Commands, with masks. Usable by any learner, and testable alone |
NetBrain (in treadcore-brain) | a Brain whose choices come from a net: the MLP's forward pass, a few hundred lines, no framework, so the census needs nothing new |
treadcore-learn | the trainer: evolution strategies over NetBrain weights (std and threads only), and later Burn for gradients, behind a feature so the rest of the workspace never builds it |
H. The machine for track B
The GTX 1080 Ti is a Pascal card: NVIDIA's 580 driver series is the last to support it, and CUDA 12 the last CUDA that targets it. The RTX 4060 Ti (Ada) has neither limit: use it. Ubuntu 26.10 is an interim release with nine months of support; the long-term one is 26.04 LTS (April 2026, supported to 2031), and it is the one to install on the spare disk (LR 92's commands, with its own repository path). Track A needs none of this: it runs on the census machine as it is.
I. The first step
Phase 0, in one sitting: treadcore-senses (the compass, the body, the conclusions, tests for each), a NetBrain with a random net, and a census of a random-net team against the defaults to measure decisions a second. Its rule, before it plays: every match the same twice; the random team reads far below the defaults (it should); the senses' tests pass.
96. Phase 0 of the learned tank: the senses and a NetBrain; and dodge_horizon 2, a candidate, 26 October
Backlog 70 and 69. Paul: "yes. please. start." (phase 0, LR 95).
Phase 0
The senses (senses.rs): sense(obs, map, hints) gives 101 numbers from 0 to 1, or none for a tank that is dead.
- The compass: 8 sectors relative to the tank's heading (sector k turns k eighths of a turn clockwise on the screen: 2 right, 4 behind, 6 left), each with 10 channels (
Channel): hostile tank (weaker when hurt), allied tank, hostile pillbox (1 inside its reach), neutral pillbox, our pillbox, depot to take, our depot, shell coming (landing within 4 s, by how soon and how near), ground (a ray: 1 if the way is shut at once, 0 if open for 16 tiles), cover (forest near). A channel's value is the strongest thing of its kind in the sector,1 / (1 + d / 8). - The body (12): armour, shells, fuel, reload, speed, afloat, the squad out, its material, and each drone kind's share of a full bay.
- The hints (9): the team's five posture scores, the odds, the base's threat, the errand's sector and strength; zero when no brain gives them (phase 0's nets have none).
The net (net.rs): a multilayer perceptron in plain Rust, 101 in, two layers of 64 (max(0, x) between), 51 out: 14,068 weights. Its seed is mixed (SplitMix64) before the world's Rng, which takes 0 as 1 and starts a small seed weakly: before that, seeds 0 and 1 made the same net (found by a probe of eight seeds). Glorot-uniform weights, 0 biases. save/load: a magic, the sizes, the weights, little-endian. flat/with_flat: every weight as one vector, for a trainer.
The heads (Head, HEADS): drive (hold, 8 sectors, stop), gun (hold, 8 sectors), lead (where it is, where it will be, between), mortar (hold, 8 sectors), drone (5), squad (7), signal (9). Each head takes its highest score among the choices allowed now (choose); hold always is. NetBrain decides five times a second: drive gives a Move 6 tiles toward the sector, renewed every 2 s (a Move costs the world a route), or Stop; gun fires at the strongest hostile tank in the sector when the gun is ready and the tank in direct range, aimed by the lead head (the brain's lead); mortar fires at the strongest hostile tank or pillbox in the sector within the mortar's range. Drone, squad and signal are scored and held until their phases (LR 95, E).
The census seats a team of nets in the trial seat with CENSUS_NET (a net's file, or seed:N for a random net); run.env records it as NET, the ledger as the trial net=..., and the run's fit leaves such rows out.
Checks. Tests: a tank due right is felt in the right sector at its strength and nowhere else, at 8 headings (and fails with the compass turned the other way: checked); the body as shares, nothing for a dead tank; the ground ray finds a wall 4 tiles ahead; a net is its seed and its file; a masked choice is never taken; random nets drive (7 of 8 seeds; one prefers to hold), each its own way, a seed the same match twice. The census without CENSUS_NET is the same as before to the character (HEAD against phase 0, 18 matches); a census with a random net is the same twice. On the Mac: 7.8 us a decision (the senses and the forward pass, one tank in a world of sixteen), 128,000 a second a thread; a random team -8.18 +/- 0.78 over 6 seeds of 300 s: it does land direct hits (the gun's mask lets it fire only at a tank in range), and loses.
The census of record (build/net0/, rule first): a random team (seed 1) against the defaults, 110 seeds from 500000, every map. -19.30 +/- 0.36, every map past two errors below (from -14.54 on fortressislesx to -24.64 on atlantic): further below than the Mac's 300 s test, as full matches give the scripted teams longer to pull away. It ran to its end in 4 min 55 s. The rule's third part, a census no slower than the scripted brain's, cannot be read from it: it shared the machine with the run, and its timing with it. The decision's own cost (7.8 us on the Mac, against about 20 us a seat-tick for the scripted brain) says the net's seat is the cheaper. Phase 0 passes on the first two parts and on that measure; phase 1 (go to a target) is next.
dodge_horizon 2: a candidate for Paul
The run's fit and the day's screen read it high three times on 55 seeds (+2.71, +2.62, +2.12). By the rule Paul chose this morning (LR 88 proposal), two held-out reads on today's defaults (dodge_own_route on), 400 seeds each, rule in build/horizon/NOTES.txt first:
| Read | Every map | Lowest map |
|---|---|---|
| from 3500000 | +1.82 +/- 0.27 (z 6.7) | constanine -0.32 +/- 0.60 |
| from 3600000 | +1.66 +/- 0.29 (z 5.7) | fidos-airstrip -0.63 +/- 1.07 |
Both past two errors above, no map past two below. make families at dodge_horizon 2: all nine hold (a standing tank under one or two shells 999 of 1000, floor 990). A candidate: the default (4 s) changes only with Paul. Expected about +1 and one chance in two of passing both: it read higher and passed both.
What it means: the dodge starts out from under a shell 2 s before it lands instead of 4. A tank that starts later spends less of the fight running from shells that would have missed or that it could outrun at the last moment, and more of it shooting. The families say it still gets out from under nearly every shell.
The run found the same weight on its own: its fit's best point at generation 2, from every census since the new defaults, is dodge_horizon 2 alone. Read on 55 seeds at 12:28 (+1.61 +/- 0.76, past two errors), it went to its own two held-out reads, under way as this is written.
97. The learned tank drives and shoots; periscope senses and calls to friends; dodge_horizon 2 the default, 26 October
Backlog 70 and 69. Paul: "write a lab report. and lest go to the next . teach to network tank to drive and shoot, and enemy a depot a pillbox." Then, mid-step: "can you create documentation and show the html about the new design of the sensors. add more sensors if approporate. give tank like sensors. like pariscope tank controls. almost like a submarine ... Also give general request commands to the training to it can cally friends through shared requests queues ... Accept the new defaults." The plan as a whole is written out in AI-BRAINZ-PLAN.md.
A. dodge_horizon 2, the default
The run (LR 92) on the census machine found dodge_horizon 2 as its best in generations 2, 3, 4, 8 and 9 and read it held out, 400 fresh seeds each, ten times:
| Generation | Held-out reads |
|---|---|
| 2 | +2.66 +/- 0.30, +1.71 +/- 0.29 |
| 3 | +1.61 +/- 0.27, +1.44 +/- 0.29 |
| 4 | +1.09 +/- 0.26, +1.98 +/- 0.27 |
| 8 | +2.44 +/- 0.30, +1.84 +/- 0.30 |
| 9 | +2.00 +/- 0.30, +1.73 +/- 0.28 |
Every read past two errors with no map past two below, beside LR 96's two (+1.82, +1.66). Three more trials passed (gen 3 samples 1 and 4, gen 8 sample 0), each dodge_horizon 2 with Thompson draws of other weights riding on it; only the common part is taken. make families holds at 2 (a-duel-in-the-open 76.2%, a-raid-on-a-guarded-base 81.5%, a-tank-under-a-ready-gun 83.6%, the rest 99.9 to 100%). Paul: "Accept the new defaults." The default is 2 (7fc7809); make test passes; the run was stopped and restarts on the new defaults (SINCE).
B. The senses, second edition: instruments, a periscope, calls
Paul's "tank like sensors ... pariscope ... almost like a submarine". Three blocks added to senses.rs, 101 senses to 133:
- A Call channel in each compass sector (11 channels): a friend's call from that way, its strength fading over the call's 10 s.
- The instruments (
Nav, 6): x and y as shares of the map, the heading's sine and cosine (each shifted to 0 to 1), the nearest edge's strength, a hit just taken (1 as it lands, 0 after 2 s, fromlast_hit_tick). A tank's GPS, gyro compass and hull-hit light. - The periscope (
Contact, 18): the compass says which eighth; the periscope gives the exact bearing (sine, right +; cosine, ahead +) and strength of four contacts: the nearest hostile tank (with its closing speed, 0.5 still and 1 closing at top speed; its aspect, 1 pointing at the tank; and whether it is in the direct gun's reach), the nearest thing to take, the nearest depot of ours, and a friend's newest call (with its kind, one of three). A submarine's plot: bearing, range, closing, aspect.
Each is a few comparisons over lists the observation holds already; none is a raw list or a picture, so the count is the same on every map and the cost stays in microseconds.
Tests: an enemy dead right reads sine 1, cosine 0, its strength, closing 1 driving at the tank, aspect 1 pointing at it and 0 turned away; the instruments place the tank; a friend's call north of an east-facing tank lights the left sector's Call channel at its strength and the call contact as a Take.
C. Calls to friends: a shared queue
calls.rs: a team's Calls, one queue a match shared by its learned tanks (as the team board is shared by Commanders). A call is a kind (help: come to me; attack: the nearest enemy; take: the nearest thing to take), a place and a tick; it lives 10 s; the queue keeps 16; a tank calls at most every 2 s. The Signal head is now hold or one of the three kinds (it was hold or a sector), masked to kinds with something to point at. Each call is also said in the world as a Command::Ping, so scripted teammates converge on it as they do on one another's; a teammate's ping heard in the world (Event::Pinged) comes into the queue as an attack call. Brains think in seat order on one thread, so the queue reads the same in every replay. The census gives its net team one queue a match. Test: a call is heard by the others and not its caller, for its life and no longer; the queue keeps the newest. Nothing yet rewards calling (AI-BRAINZ-PLAN.md section 8, upgrade 5).
D. Training: evolution strategies in Rust
learn.rs and make train. A ground: the seed's generated map with a square of grass 49 tiles across round its centre, the tank at the centre facing a way the seed draws, the target 10 to 20 tiles off (12 to 20 to shoot) in a direction the seed draws: an enemy tank standing still, a neutral depot or a neutral pillbox, in turn by the seed; nothing else. The enemy is felt at any range (the scenario's advantage): a tank sees 10 tiles, and phase 1 is going to, not searching. An episode: 60 s at most, five decisions a second.
Evolution strategies (Salimans and others 2017): each generation draws 48 directions in weight space (normal, from the generation's seed), plays the weights 0.05 along and 0.05 back along each on the same 12 grounds (97 nets with the weights themselves, 1,164 episodes), ranks every score from -0.5 to 0.5 (one lucky score cannot pull the weights alone), estimates the gradient as the rank differences times the directions, and takes an Adam step (0.02; decay 0.0005). Threads take episodes from a shared counter. Tests: a ground is open and its target is where it says; a straight drive reaches each kind inside 30 s and a tank that holds never does; an enemy fired at from inside the gun's reach is hit; one generation from a seed moves the weights the same way twice.
Two faults on the way: a world with one seat that the trainer ran past its end (a guard on over now); the trainer's files written under the crate's folder (cargo test runs there; make train gives absolute paths).
E. Phase 1: go to. The rule, then the reads
Rule (LR 95, E; build/train/NOTES.txt on the Mac): within 3 tiles inside 60 s on at least 95% of 999 held-out grounds (seeds 9,000,000 on, a third each kind), each kind alone. On the Mac (M1, 8 threads):
| Net | Enemy | Depot | Pillbox |
|---|---|---|---|
| first senses (101), 200 generations, 110 s | 96.4% | 100% | 94.3% +/- 1.3 |
| first senses, 500 generations | 99.7% | 100% | 95.2% +/- 1.2: passes |
| untrained, new senses (133) | 13.2% | 1.8% | 0.3% |
| new senses, 500 generations, 130 s | 100% | 100% | 100% |
With the periscope the net passed every training ground by generation 67 (the compass alone needed hundreds): an exact bearing is what going to a point wants. nets/phase1.bin.
F. Phase 3: drive and shoot. Three rules, in order, each before its run
- First rule: a hit inside 60 s on 95%. Read on the untrained net first: it passed at 99.8%, its mortar landing 7,728 shells on a tank that never moves. Not a test.
- Amended: the direct gun only (a direct hit counts 1; a direct miss or a lobbed shell -0.05; up to 0.2 for closing to the gun's reach), and hits a shot at least RuleBot's. Read first: RuleBot fires no shot on these grounds (a still tank that never fires is no threat to it), and a direct shell at a still tank never misses (the untrained net: 666 of 666). Hits a shot is phase 3b's measure, a moving target. The untrained net: a direct hit on 44.2%.
- Shoot only, from phase 1, 300 generations (83 s): a direct hit on 999 of 999, 2,995 direct shots all hits, none lobbed, 998 kills. But read on phase 1's grounds it had forgotten the going: enemy 100%, depot 9.3%, pillbox 23.7%.
- Amended again: drive and shoot, all three grounds in turn: the enemy shot (a direct hit), the depot and pillbox gone to. Rule: each kind 95% on the 999. From phase 1, 400 generations (98 s):
| Ground | Held out |
|---|---|
| enemy: a direct hit | 333 of 333 (999 direct hits of 999 shots, 3 a tank, each killed; none lobbed) |
| depot: within 3 tiles | 333 of 333 |
| pillbox: within 3 tiles | 333 of 333 |
nets/phase3.bin: one net that drives to a depot or a pillbox, and drives in on an enemy, holds the mortar and kills it with the gun.
G. The census: a team of drive-and-shoot nets
On the census machine at 7d0af54 (build/net3/NOTES.txt, before it played): a team of nets/phase3.bin in the trial seat against the defaults, 110 seeds from 500,000, every map; then a random net of the same shape on the same seeds. Rule: the trained team above the random by more than two errors. Expected: both far below the defaults (phase 3 knows nothing of supply, capture, cover or teams).
| Team in the trial seat | Every map | Lowest map | Highest map |
|---|---|---|---|
| nets/phase3.bin (drive and shoot) | -23.52 +/- 0.38 | island -28.91 +/- 0.42 | fortressislesx -18.02 +/- 0.76 |
| a random net, the same shape (seed:1) | -20.35 +/- 0.25 | atlantic -23.43 +/- 0.83 | fortressislesx -16.99 +/- 0.57 |
| (LR 96: a random net, the first shape) | -19.30 +/- 0.36 |
The rule fails: the trained team reads 3.17 +/- 0.45 below the random, seven errors. What it learned on open grass, it does in a match: it drives straight at the nearest enemy, depot or pillbox. On the training grounds nothing shoots back and every pillbox is neutral; in a match the nearest pillbox is often hostile and the nearest enemy is a team of five under its own guns. The random team dithers near its start and lives longer. Skills on a clean ground do not carry into a match until the ground teaches what kills (phase 4, live; hostile pillboxes and an enemy that fires back on the grounds). The census did its job: it says the drive-and-shoot net is not yet a better tank, and by how much. Both censuses took two and a half minutes (the net teams go down fast).
Lessons
- Read the rule on the untrained net first. Two of phase 3's rules were met or made meaningless before any training; ten minutes of baselines saved a false pass.
- Train what must be kept. A net trained only to shoot forgot how to go to a depot in 300 generations. A curriculum rehearses.
- The right sense is worth hundreds of generations. The periscope turned a 500-generation pass into a 67-generation one.
98. The record made part of the source; guided training and training maps planned, 26 October
Backlog 70. Paul: "ok. all these artifacts make sure they are reports in the documentation and the process of project createion is part of the MIT open source expieience of developing this. . Describe the process, our interactions. Document our converstaions and deveopment of design using census testing families ... we need to always be testing on and imporving those coefficients. We want to have our gruided training to reward being training along with our design of our functions. We might need to create a series of training maps to assit and place certain situations in a predictable occurance to test predictabily."
The pages, in the repository. Three pages had been published only as shared links; each is now a report of the site (reports/), so it ships with the code under the MIT licence:
2026-10-26-the-weight-ledger.html: rebuilt as a static page from the dashboard's data (the dashboard's framework is not redistributable), with the day's two default changes added (dodge_own_route 1, dodge_horizon 2): 110 weights (68 on 18 October), 18 defaults changed, the 56 screen reads, 98 censuses.2026-10-26-big-lizard.html: the map page of LR 93 and 94.2026-10-26-the-learned-tank.html: already there (LR 97).
The process (PROCESS.md): who does what, how a prompt becomes a commit (with a diagram), where every conversation is kept (the prompts verbatim in the check-ins; their consequences in the decision logs and the journal), how the brain is designed by measurement (weights, families, the census, held-out reads, the run), the fifteen days, what the process caught, the daily rhythm, and the pages.
Guided training and training maps (AI-BRAINZ-PLAN.md section 9): one scenario file in scenarios/training/ a behaviour, read three ways: its [expect] and floor by make families (the scripted brain), its [reward] by make train GROUND=file (the net), its held-out seeds by both. A series of ten training maps, in curriculum order, the first two done (open ground, the gun); maps 4 to 6 (the guarded depot, under a shell, an enemy that fires back) next, as the census of LR 97 asks. Each file is to be written and read on the scripted brain and the untrained net before any training on it.
The run stays on the census machine all day on the new defaults (build/converge/20261026-181011/, generation 1 at 18:34), reading the coefficients; the morning read takes what it found.
99. Training maps from scenario files: the guarded depot learned, the duel not yet, and what a match says, 26 October
Backlog 70. Paul: "ok. and for this dev version. lets continue to develop. impliment ." The plan of LR 98 (AI-BRAINZ-PLAN.md section 9), built: one scenario file a behaviour, read by the scripted brain and by the trainer on the same seeds.
A. One file, three readings
scenario.rs:
- a
[reward]section; kind = "net"brains (the net infile, else the trainer's; a team's nets share one queue of calls);aliveanddeadchecks;run_net;reward, which sums the file's terms for the learner's seat:pass,alive,damage_taken,damage_given,killed,depot_taken,pillbox_taken,pillbox_killed, andcloser(the share of the way to a point closed, a potential).
A term the trainer does not know is refused when the file is read.
learn.rs: a Course is a built phase or a training map. A generation's episodes take the courses in turn, so a mix rehearses what was learned before (LR 97's lesson, built in).
make train GROUND=phase1,phase3,scenarios/training/....tomltrains on a mix.BASELINE=1reads the scripted brain first, with a Commander on Auto in the net's seat.CHECK=netreads a net on every course's held-out seeds.
net.rs: the gun head also takes a live hostile pillbox in direct reach when no tank is in its sector. A net could not kill a guard before.
Tests:
- every training map reads as a course;
- a net plays one the same way twice;
- the scripted reading has no net seat;
- an unknown reward term is refused;
- the gun takes a hostile pillbox in reach, and never a neutral one, a dead one, or one out of reach.
B. Maps 4 to 6, read before training
Rules in build/train/NOTES-maps.txt (the Mac), before any training. Held-out seeds from 9,000,000:
| Map | Scripted brain | Untrained net | Floor |
|---|---|---|---|
| 4. the guarded depot: a hostile pillbox 3 tiles before a neutral depot, the tank 18 off; pass: the depot taken, alive, in 60 s | 100% (300) | 0% | 80% |
| 5. under a shell: rolling at full speed, a shell within 2.5 tiles in 1.5 to 3.5 s; pass: not hit | 99.8% (1,000) | 79.3% | 99% |
| 6. an enemy that fires back: a RuleBot 16 tiles off; pass: it destroyed, alive, in 60 s | 20.3% (300) | 0% | 50% |
Each untrained reading is under its floor, so each map is a test. Map 6's floor is above the scripted brain's own reading, on purpose: the plan's "half the duels won".
C. Training, with rehearsal
| Run | Courses | Generations | Result on held-out seeds |
|---|---|---|---|
| map4 (from nets/phase3.bin) | phase 1, phase 3, map 4 | 400 (7 min) | map 4 93.7% +/- 1.4 (281 of 300): passes; phase 1 99.1 to 100%, phase 3 99.7 to 100%: kept |
| map6 (from map4) | phase 1, 3, maps 4, 5, 6 | 500 (12 min) | map 4 98.7%; map 5 92.1% +/- 0.9 (floor 99); map 6 0% (reward -1.12, untrained -2.08); phases 1 and 3 97.3 to 100% |
| map6b (from map6) | the same, maps 5 and 6 twice each | 800 (22 min) | a plateau: map 5 92.9% +/- 0.8, map 6 0% (reward -1.08), map 4 99.3%; phase 1's enemy ground fell to 89.2%: not kept |
nets/map4.bin passes every course it trained on. Maps 5 and 6 did not pass. Map 5 rose from 79% to 93% and stopped short of the scripted dodge's 99%. On map 6 the trades improved (reward -2.08 to -1.08) but no duel was won. Evolution on a mix of five courses with a flat score moved slowly, then not at all. The lesson for the next maps: one new map at a time beside its rehearsal, as map 4 was, rather than two hard maps at once.
D. The census: a match is about staying alive
On the census machine, in a worktree beside the run (~/code/treadcore-net/build/net4/NOTES.txt before it played), a team of nets/map4.bin on LR 97's seeds: -24.29 +/- 0.45. The rule (above the random net's -20.35 and phase 3's -23.52 by two errors) fails.
Every match's log (g-*.log) says why, the same for every net:
| Team in the trial seat | Deaths a match (of 20) | Kills | Held | Score |
|---|---|---|---|---|
| a random net | 18.3 | 0.8 | 1.8 | 4.7 |
| nets/phase3.bin | 19.0 | 1.0 | 1.9 | 2.8 |
| nets/map4.bin | 18.6 | 0.9 | 1.9 | 2.2 |
| (the scripted teams beside them, one match's log) | 11 to 28 | 21 to 40 | 5.5 to 10.6 | about 27 a seat |
Five tanks with three respawns each can die 20 times. Every net team uses up nearly all of them and kills about once a match. It never builds and never revives. The census reads all three nets as nearly the same tank. The differences between them, a few points of score, are in the parts of a match no net does yet.
So the census cannot tell these nets apart until one survives. The next maps are those that teach survival in a real match:
- low and far from home (resupply, map 7);
- a hurt tank's retreat;
- an episode that is a real match's first minutes, scored on staying alive.
An alternative is proposed beside the curriculum (section E).
E. A proposal: one head inside the scripted brain
Phase 8 of LR 95 hands the Commander's work to a net one head at a time, at the end. The census suggests doing it the other way first. Put a net inside the Commander for one narrow skill: the dodge's choice of where to drive, or the gun's lead. The scripted brain keeps everything that keeps a team alive, and the census reads the net's one skill in real matches now. It would read in the census as any weight change does, because everything else is the defaults. For Paul to choose.
100. Survival maps: low armour means home, the drones were missing, and the first net to climb off the bottom, 26 October
Backlog 70. Paul: "survival maps. implimment". The census of LR 99 said every net team dies nearly every time it can; these maps teach staying alive, one at a time beside the rehearsal.
A. The maps and the format
Scenario [[tank]]s now start hurt and short: armor (a share) and shells. The outcome keeps each tank's armour at the start and the end and when it first went down. New reward terms: alive_time (the share of the run alive) and armor_gained. New check: armor_at_least. Rules were written in build/train/NOTES-maps.txt before each run.
| Map | The situation | Pass | Scripted | Untrained |
|---|---|---|---|---|
| 7. low and far from home | 30% armour, 2 shells, home 18 tiles off, a RuleBot 12 off | alive at 60 s, armour >= 60% | 53.3% | 0% |
| 7a. low, go home (added after 7's first run) | map 7 with no enemy | alive at 30 s, armour >= 90% | 100% | 0% |
| 8. outnumbered (amended before training) | half armour, home 8 off with its pillbox, two RuleBots 14 off | alive at 45 s | 74.7% | 0.7% |
Map 8 as first drawn (home 10, enemies 9) was lived by the scripted brain on 5% of 300. No retreat was possible, so it taught nothing; it was amended before any training.
B. What happened, in order
- Map 7 from the map 4 net: 0 of 300, no progress in 400 generations.
- Map 7a first (low armour means home, no enemy): 99.7%, with every rehearsed course kept. The split worked.
- Map 7 from 7a: still 0. Traced: every death came 2.7 to 4.2 s in, from a 35-damage hit.
- Map 5 stalled at 90 to 93% against the scripted dodge's 99.8%. Probes followed:
- an oracle with the net's drive actions and a straight-line model
dodged 88.5%;
- the same nine actions played forward in a copy of the world
dodged 300 of 300, renewed every decision, every 2 s (as NetBrain does) or near the point.
The actions were enough; the senses were not. The compass's ShellComing folds how soon and how near into one number at 45 degrees. The shell contact (5 senses: the impact's bearing as a sine and cosine, nearness, how soon, inside the splash) took map 5 to 95.9% (959 of 1,000), and a further 400 generations held there (95.5%).
- The 35-damage hit was a strike drone (
STRIKE_DAMAGE), not the mortar (70). A scripted tank keeps interceptors up, the bubble the game is built round. The net's drone head had been held since phase 0, and it had no sense of the air. So two changes:- the drone head is on: an interceptor while fewer than four are
up; a recon orbiting 8 tiles ahead while none is up; a strike at the nearest hostile tank within a strike's reach; a longshot at the nearest hostile structure, pillbox or tank; each masked to a bay with one and something to do;
- the air contact (5 senses: the nearest hostile drone's bearing
and nearness, whether it dives, the tank's own bubble as a share of four).
Both new blocks go last, so an older net is widened with zero weights (Net::widen) and computes exactly what it did (a test).
- Map 7 from the map 5 net, drones on: 44.3% +/- 2.9 (133 of 300, from 0; the scripted brain 53.3%). Map 4 98.3%, map 7a 99.0%, map 5 94.3%, phase 3 passes. Phase 1's enemy ground fell to 64.9%. The failures were read before anything was changed: in 114 of the 117 the net had destroyed the enemy before reaching it (its drones and gun). As written, phase 1 fails there. Counting a destroyed enemy as reached, 99.1%; that reading was chosen after the result and is marked so.
nets/map7.bin.
C. The match reading
make train CHECK=net MATCH=40: seat 9 of a dealt match (a ranger of team 0; nobody follows it), the net or its own scripted brain, first 3 minutes, 40 seeds from 8,000,000, three respawns. It sits between the maps and the census:
| Seat 9 | First down (s of 180) | Deaths in 3 min | Never down |
|---|---|---|---|
| nets/map4.bin | 96 +/- 10 | 1.77 | 11 of 40 |
| nets/map7a.bin (home when low) | 64 +/- 9 | 3.08 | 5 of 40 |
| nets/map5.bin (the shell contact, no drones) | 100 +/- 9 | 1.73 | 9 of 40 |
| nets/map7.bin (drones on) | 129 +/- 9 | 0.82 | 16 of 40 |
| the scripted ranger | 163 +/- 5 | 0.33 | 28 of 40 |
Going home when low made a net worse in a match: what killed it there was not low armour. The bubble halved the deaths.
D. The census
On the census machine at 814d601, in a worktree beside the run (rule in build/net7/NOTES.txt first): a team of nets/map7.bin, LR 97's seeds. -20.33 +/- 0.27. The rule asks for more than two errors above the random net (-20.35 +/- 0.25); it is level: the rule fails. Against LR 99's map 4 net (-24.29 +/- 0.45) it is +3.96 +/- 0.53, the first net to climb off the bottom.
| Team in the trial seat | Deaths a match (of 20) | Kills | Held | Score |
|---|---|---|---|---|
| a random net (LR 97) | 18.3 | 0.8 | 1.8 | 4.7 |
| nets/map4.bin (LR 99) | 18.6 | 0.9 | 1.9 | 2.2 |
| nets/map7.bin | 16.6 | 3.7 | 2.1 | 5.3 |
Four times the kills, two fewer deaths, and the highest score of any net team. Still far under the scripted teams, about 27 a seat.
E. Next
- Map 8 (outnumbered, 15.7% untrained on it) and map 6 (the duel) with the drones on.
- Map 5's last 3% (where the misses are).
- Phase 1's enemy ground rewritten so destroying the enemy counts, with its rule written before the next read.
- Then the census again on these seeds.
Lessons
- Trace a death before training against it. Four runs trained against "dodge the mortar"; the killer was a drone.
- Probe the bound before blaming the learner. The oracle said the actions could dodge everything; that pointed at the senses.
- A step map teaches what a hard map cannot. Map 7a went from 0 to 99.7% where map 7 alone never moved.
101. The morning read of 27 October: thirty generations, no candidate
Paul: "goodmorning. please check". The run of 26 October (build/converge/20261026-181011/, on the defaults with dodge_horizon 2, from 18:10) played 30 generations overnight: 380 censuses, none failed. Seven trials read past two errors on 55 seeds; each was read on 400 fresh seeds twice:
| Trial | Held out |
|---|---|
| gen5/sample-1 | +0.01 +/- 0.25, +0.18 +/- 0.26 |
| gen6/sample-1 | -0.18 +/- 0.19, -0.00 +/- 0.20 |
| gen13/sample-0 | -0.02 +/- 0.28, -0.04 +/- 0.26 |
| gen19/screen-w_called.lo | +0.17 +/- 0.29, -0.25 +/- 0.27 |
| gen24/sample-0 | +0.09 +/- 0.10, -0.02 +/- 0.11 |
| gen25/sample-2 | +0.58 +/- 0.26, +0.35 +/- 0.27 |
| gen26/screen-w_site_small.hi | -0.10 +/- 0.09, -0.01 +/- 0.09 |
No candidate: the run's EXPECTED (LR 92) holds. The defaults sit at or near the top of every weight read so far. Seven lucky 55-seed reads fell back into the noise on 400 seeds; the held-out rule kept them out. gen25/sample-2 (connect_bases 0.375, kite_in 4.5, site_area 800, w_pos_connect 2) passed one read and not the other; its weights are worth reading apart if the fit keeps pointing there. The run goes on.
Release 0.2.0: the draft waits, as it should (tagged 18:34 on 26 October; day 1 opens at 18:34 today). The site serves the new report pages.
Read again at 08:42. The run is at generation 35: 432 censuses in this run, 680 in the machine's ledger (make ledger), none failed. Two more trials were read held out:
| Trial | Held out |
|---|---|
| gen33/sample-6 (bridge_soft 6.9, castle_inside 5.0, commit 0.2, landfall_wait 20) | +0.27 +/- 0.18, -0.05 +/- 0.17 |
| gen34/sample-10 (bridge_reach 80, fetch_gather 40, junction_clear 3.4, w_in_range 1.4) | +0.84 +/- 0.25, +0.37 +/- 0.25 |
There is still no candidate. The fit of generation 34 covers 87 weights, and none has an expected gain at its fitted best of two errors or more. The largest:
- w_stick +0.71 +/- 0.63;
- w_threat +0.60 +/- 0.34;
- press_odds +0.59 +/- 0.40;
- w_near +0.55 +/- 0.47;
- w_smooth +0.51 +/- 0.26.
Each sits at the end of its searched range (double its default). They are where the run's draws go next.
102. Generations watched: clips, a gauntlet of single decisions, the match reading, and the cost, 27 October
Paul: "Can I get a short GIF of every generation of the tank AI battling. Show reaction measurements and progress on census. For each generation (or major milestone), show the capability of the tank AI during its action. Write a lab report in html. Describe out methods of using training based of recently designed brainz, which we are using as a basis to help develop the sandbox for testing and training. Descibe the quality of improvements. Describe the computation time of using the tank with its AI and only its AI. We want to watch, short clips of action. Please create a set gauntlets of test with maps for testing, and single decision with little distractions for testing."
The report as a page: reports/2026-10-27-generations.html (with reports/clips/), https://claude.ai/artifact/Vg35Adn3MmqibS7k69HLW7.
A. The tools
Reactions in the scenario format. The outcome records, for each seat: the first direct shot, the first indirect shot, the first move of more than a tile, and the first launch of each drone kind.
- New checks:
fires_withinandlaunched. - A file's
reaction(fire,lob,move,launch:interceptor) names the reaction it times. [[drone]]puts a hostile drone in the air (a strike at a seat, a longshot, a recon), so a test can send a strike with nothing else on the map.
The gauntlet (scenarios/gauntlet/, make gauntlet): nine single-decision tests, each on open grass with nothing else, 3.5 to 30 s. Each brain plays the same 200 seeds from 9,500,000: the scripted brain (a Commander in the net's seat), the untrained net, and every milestone net in nets/. The output is the share passed and the median reaction, also written to build/train/gauntlet.tsv.
Clips (clip.rs, make clips):
- A recording (
scenario::Recording,run_record;learn::play_onfor built grounds) keeps the map's tiles round the learner once and a frame every 0.1 s. clip::renderdraws a frame in plain Rust: the site's night field, the learner in blue with an armour bar, the enemy in red, shells with their landing marked, drones as diamonds, hits as white rings, and a caption in a 3 by 5 pixel font.- Each frame goes out as a PPM;
ffmpegwith a palette of its own makes the GIF. - Each milestone plays its defining test on the first held-out seed it passes; the untrained net plays its first seed. Ten clips, 852 KB, in 8 s.
The match reading now counts damage dealt and kills by seat 9, besides its deaths.
Timing (TIMING=1): one thread, nets/map7.bin.
B. The gauntlet
Share passed of 200, with the median reaction in seconds:
| Test | Scripted | Untrained | P1 | P3 | Map 4 | Map 7a | Map 5 | Map 7 |
|---|---|---|---|---|---|---|---|---|
| G1 fire at an enemy in reach | 100 (0.05) | 100 | 98 | 100 | 100 | 100 | 100 | 100 (0.65) |
| G2 an enemy behind | 100 | 100 | 100 | 100 | 100 | 100 | 100 | 100 (1.05) |
| G3 a shell on a standing tank | 100 (1.30) | 41.5 | 82 | 100 | 86 | 92.5 | 100 (0.70) | 69.5 |
| G4 a strike drone coming | 95.5 (0.30) | 96 | 75 | 44 | 88.5 | 98.5 | 96.5 | 70 |
| G5 low armour, home in sight | 100 | 0 | 0 | 2 | 32 | 50 | 10.5 | 25.5 |
| G6 a depot to take | 100 | 3.5 | 0 | 20 | 0 | 0 | 0 | 2 |
| G7 a pillbox from outside its ring | 88.5 | 53.5 | 52.5 | 57 | 60.5 | 62 | 53 | 26 |
| G8 out of a pillbox's ring | 0 | 77.5 | 87.5 | 88 | 100 | 89 | 94 | 43 |
| G9 a depot across the water | 100 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
The gauntlet shows four things the training maps did not:
- Shortcuts. The map 7a net went home 99.7% when low on armour and shells (its map) but 50% when only armour is low (G5): it learned "few shells, go home". The map 4 net takes a guarded depot 98% but an unguarded one 0% (G6): it learned the scene, not the depot.
- The scripted brain's blind spot (G8). Inside a hostile pillbox's ring at 40% armour it fights and dies every time; every net does better, and the map 4 net escapes on all 200.
- Terrain is untrained (G9: every net 0%).
- Trade-offs. The map 7 net, the best in matches, is worse than the map 5 net at G3, G7 and G8.
Two tests are easier than meant. G1 and G2 pass for the untrained net because the gun fires into any sector without turning, and the drone's strike counts as a hit; G2 tests no turning.
C. The match reading, every generation
Seat 9, 40 matches from 8,000,000, first 3 minutes, all on today's code (the drone head on for every net; phase 1 and 3 never trained with it):
| Seat 9 | First down (s) | Deaths | Never down | Damage dealt | Kills |
|---|---|---|---|---|---|
| untrained | 155 +/- 7 | 0.42 | 29 | 11 +/- 4 | 0.07 |
| phase 1 | 48 +/- 5 | 3.40 | 0 | 54 +/- 7 | 0.50 |
| phase 3 | 50 +/- 5 | 2.75 | 0 | 49 +/- 11 | 0.35 |
| map 4 | 88 +/- 10 | 1.95 | 11 | 33 +/- 8 | 0.23 |
| map 7a | 67 +/- 9 | 2.05 | 6 | 28 +/- 9 | 0.28 |
| map 5 | 100 +/- 10 | 1.57 | 13 | 29 +/- 8 | 0.12 |
| map 7 | 129 +/- 9 | 0.82 | 16 | 27 +/- 8 | 0.20 |
| the scripted ranger | 163 +/- 5 | 0.33 | 28 | 63 +/- 8 | 0.70 |
Living and fighting pull apart. The untrained net lives because it does nothing. The first nets fight and die three times in three minutes. The survival maps moved the map 7 net toward the scripted ranger's corner on deaths, at half its damage. LR 100's readings of the map 4 and 7a nets (96 s, 64 s) were taken before the drone head was on; these are on today's code.
D. The cost of the AI alone
Apple M1, one thread unless said:
| Measure | Result |
|---|---|
| A decision: 143 senses and the forward pass, 16,431 weights | 10.6 us (94,000 a second) |
| A phase 3 episode (one tank and its target) | 2.1 ms |
| 120 s of a 16-seat match, every seat a net | 0.61 s, 197 times real time; brains 10.9 us a seat-tick, the world 80 us a tick |
| The same, every seat scripted | 0.68 s, 177 times real time; brains 13.3 us a seat-tick, the world 69 us a tick |
| Phase 1, 500 generations of 1,164 episodes, 8 threads | 130 s |
| Map 7 with rehearsal, 400 generations of 2,037 episodes | 705 s |
E. Method: the sandbox is the scripted brain's
Every tool built for the scripted brain became a tool for the learned one:
- the deterministic world and its
Observation; - the scenario files of the families (now with a
[reward]and a net seat), read for both brains on the same seeds; - the rule written first, read on the untrained net and the scripted brain before any training;
- the census as the last judge.
The gauntlet is the families' idea made small: one decision, many seeds, a share and its error.
F. Next
- G5 and G6 become training maps against the shortcuts (low armour with full shells; an unguarded depot).
- Map 8 and the duel with the drones.
- The census again on the same seeds.
103. The school: the learned tank trains until stopped; and more than one census machine, 27 October
Paul: "we will continue to test. are we training both brainz and net ai." Then: "yes. build it and start it on the mac. please. also if I get more CPU. is there a way to have multiple census machines."
A. Both brains training at once
| Brain | Loop | Where |
|---|---|---|
| The scripted brain's 110 weights | make converge (LR 92), since 26 October 18:10 | the census machine, 24 threads |
| The learned tank | make school (here), since 27 October | the Mac, 8 threads |
make school runs the curriculum as make converge runs the census.
- Each round: the next map not yet passed, listed twice beside every passed course; 300 generations from the best net so far. Then every course is read on its held-out seeds.
- Keeping a net: only if the target rose by more than two combined errors and no passed course fell under its floor. A target at its floor joins the passed.
- The gauntlet: a kept net gets it, and its table is saved by round.
- Logs:
build/school/holdslog.txt,rounds.tsv,best.bin, each kept round's net andstate.txt. - Stopping:
touch build/school/STOPends it after the round in hand, and it resumes frombest.binandstate.txt. It also ends when every map is passed, or after rounds with nothing kept (three times the open maps' count).
The rule is in build/school/NOTES.txt, written before it played.
Two new training maps against the gauntlet's shortcuts (LR 102):
- map 9: low armour with full shells, home 15 tiles off (G5 asks at 10);
- map 10: an unguarded depot 14 tiles off, 30 s (G6 asks at 8 in 20 s).
Read first:
| Map | Scripted | Untrained | nets/map7.bin |
|---|---|---|---|
| 9 | 100% | 0% | 11.3% |
| 10 | 100% | 0% | 11.7% |
An amendment, written before the school's first read. Phase 1's enemy ground counts an enemy destroyed as reached. LR 100 found 114 of 117 of the map 7 net's "failures" there were targets it had killed on the way.
Started from nets/map7.bin, with phases 1 and 3 and maps 4 and 7a passed. The queue: 9, 10, 5, 7, 8, 6.
B. More than one census machine
Yes, within the rule that a census is one machine's (CENSUS.md section 5). The census is many independent matches, and the run is many independent censuses, so machines scale it at the level of whole censuses, never of jobs within one:
- More cores in one machine. Nothing to build:
JOBSisnproc. A census's time falls with cores until the machine's memory bandwidth is the limit. - Several machines, one run. Each machine plays whole censuses from one shared queue: the run writes each generation's trials as files, each machine takes the next trial, plays it on its own frozen binary, and returns its census folder, with
HOSTinrun.env. The ledger already joins machines' rows without pooling them (ledger.py --merge, LR 92). The fit can use every row, since each is a complete paired reading on one machine. A held-out read is one census, so it too stays on one machine. Machines of different architectures (an ARM Mac beside the x86 census machine) play different matches; their rows are still each a valid paired reading. A trial that passes on one machine is read held out on that machine. The work is a small worker script and the run reading a queue directory; it is planned (CENSUS.md section 5a), to build when a second machine arrives. - The learned tank on several machines. Evolution's episodes are independent, but a generation waits on all of them, so splitting one generation across a network costs more than it gains. Instead, each machine runs its own school on its own seeds (an island model), and the best net by the gauntlet and the match reading is carried to the others each night. On a GPU machine, gradient training (Burn, AI-BRAINZ-PLAN.md section 8) is the other road.
104. Reading the reports: what is read, what is adjusted; and a fleet of census machines, 27 October
Paul: "write a lab report describing what you read and adjsut when reading these reports. if I was to install copal on more CPUs, Can I get a fleet of these working tandom. How do we orchestrate this."
A. What is read, in order
Two loops are running (LR 103), so a read covers two machines. Each is read the same way: first whether the loop is alive, then what it decided, then whether what it decided is true, and last what it costs.
The school (the Mac, build/school/):
| Step | File | What is asked of it |
|---|---|---|
| 1 | pgrep -fl school, run.log's last line | alive, which round, which map, and whether caffeinate still holds the Mac awake |
| 2 | rounds.tsv | each round's target, its share before and after on held-out seeds, the error, kept or not, passed or not, and the seconds it took |
| 3 | rounds.tsv, a map's rows read together | a map's "before" against its own last "after": an open map can slip while the others train |
| 4 | state.txt | the passed courses; every one held its floor (the rule guards them) |
| 5 | gauntlet-round*.tsv, every kept round side by side | each single decision against round 0's net: what was learned, what was lost |
| 6 | the reaction columns | a share passed with a reaction far later than before is a different skill |
| 7 | the seconds column | the cost: each passed map adds courses to every generation |
| 8 | NOTES.txt | the rule as written before the run, to judge each decision by it and not by hindsight |
The run (the census machine, build/converge/<run>/, over ssh):
| Step | File | What is asked of it |
|---|---|---|
| 1 | pgrep -af census-bin, uptime | alive, its load |
| 2 | progress.txt's tail | the generation, censuses in the ledger, failures |
| 3 | candidates.txt | any trial through both held-out reads |
| 4 | progress.txt, the candidate's lines | its 55-seed reading, both 400-seed reads, and the "best-less" reads that take one weight out at a time |
| 5 | the trial's .weights against defaults.toml | what exactly changed |
| 6 | the fit's largest expected gains | where the run is looking next |
B. What the school's reports said at 11:03
Started 09:30 from nets/map7.bin; round 8 in hand; 7 of 8 rounds kept.
| Round | Map | Before | After +/- error | Floor | Result | Seconds |
|---|---|---|---|---|---|---|
| 0 | 09 low armour, full shells | 11.3% | 100.0% +/- 0.0 | 95% | kept, passed | 460 |
| 1 | 05 under a shell | 93.7% | 95.2% +/- 0.7 | 99% | not kept | 435 |
| 2 | 07 low and far from home | 15.3% | 51.3% +/- 2.9 | 80% | kept | 622 |
| 3 | 08 outnumbered | 56.7% | 78.3% +/- 2.4 | 80% | kept | 739 |
| 4 | 06 an enemy that fires back | 0.0% | 16.0% +/- 2.1 | 50% | kept | 822 |
| 5 | 10 an unguarded depot | 10.7% | 97.7% +/- 0.9 | 90% | kept, passed | 569 |
| 6 | 08 outnumbered | 54.7% | 77.0% +/- 2.4 | 80% | kept | 897 |
| 7 | 06 an enemy that fires back | 2.0% | 29.7% +/- 2.6 | 50% | kept | 909 |
The gauntlet of every kept net, share passed of 200 seeds (%):
| Test | r0 | r2 | r3 | r4 | r5 | r6 | r7 |
|---|---|---|---|---|---|---|---|
| G1 fire in reach | 100.0 | 100.0 | 99.5 | 99.0 | 99.0 | 81.0 | 86.5 |
| G2 an enemy behind | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 |
| G3 a shell on a standing tank | 100.0 | 100.0 | 99.5 | 99.5 | 100.0 | 100.0 | 100.0 |
| G4 a strike drone coming | 95.0 | 72.0 | 34.0 | 92.0 | 90.5 | 92.0 | 97.5 |
| G5 low armour, home in sight | 98.5 | 87.5 | 90.0 | 90.0 | 99.5 | 99.5 | 96.0 |
| G6 a depot to take | 70.0 | 0.0 | 81.0 | 6.5 | 79.0 | 78.0 | 70.0 |
| G7 a pillbox from outside its ring | 43.0 | 49.0 | 12.5 | 23.5 | 27.5 | 25.5 | 27.0 |
| G8 out of a pillbox's ring | 93.0 | 86.0 | 43.0 | 57.0 | 12.5 | 48.5 | 32.5 |
| G9 a depot across the water | 7.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
Findings:
- Two maps learned in one round each: 9 (11% to 100%) and 10 (11% to 98%). The shortcut maps did their job: G5 and G6 recovered with them (G6 0% in round 2, 79% after map 10).
- What is passed stays passed. All six passed courses held their floors through every round, as the rule requires.
- What is open slips. Map 8 reached 78.3% in round 3 and read 54.7% when its turn came again in round 6; map 6 went 16.0% to 2.0%. Nothing guards an open map, so each round on one map spends part of the next round on another winning it back. The net still climbs (map 6 ended round 7 at 29.7%), more slowly than it could.
- The gauntlet is not guarded, and it swings. G4 went 95, 72, 34, 92; G6 70, 0, 81, 6.5, 79. G8 (leave a pillbox's ring) fell from 93% to 32.5% and has not come back: the net has learned to stand in rings, probably because maps 6 and 8 reward staying to fight. G9 (a depot across water) is 0% for every net; no map teaches water.
- Map 5 is stuck at its last few percent. 93.7% to 95.2% is a gain of two errors' width, but not more, against a 99% floor.
- The cost grows: rounds went from 460 to 909 seconds as passed courses joined each generation; about four rounds an hour now.
C. What is adjusted, and what is not
The rule of the run is fixed when it starts (the rule before the run). A read never changes a loop's rule on its own; it proposes, and Paul decides. What a read may do, and what it may not:
| A read may | A read may not |
|---|---|
keep a machine awake (caffeinate -i -w <pid>, done for the school) | change the keep rule mid-run |
| stop a loop that is broken (three failed censuses; a crash) and say so | change a default (a candidate goes to families, then Paul) |
| add logging that changes no decision | pool two machines' jobs into one census |
| write down an amendment for Paul, with its cause | move a floor after seeing a result |
Two amendments proposed for the school, waiting on Paul:
- A high-water mark for each open map. Keep a net only if, besides the present rule, no open map falls more than two combined errors below its best reading so far. Cause: finding 3.
- The gauntlet as a guard. No test falls more than two combined errors below its round 0 share (G1, G2, G3, G5 at their best; G7 and G8 from their round 0 share). Cause: finding 4. G9 stays a test, not a guard, until a map teaches water.
Both cost one read of every open course a round (already made, only not logged) and the gauntlet before the keep instead of after (about 40 seconds). If Paul says yes: the amendment goes into NOTES.txt first, the school is stopped after its round with build/school/STOP, built, and resumed from best.bin; the record notes the round at which the rule changed.
Not adjusted: map 5's floor stays 99% (finding 5): lowering it after seeing 95% is the mistake the rule exists to prevent. A map that teaches water (G9) is a new map, for a later step.
D. Found on this read: the run's first candidate
build/converge/20261026-181011/, generation 36, again-dodge_horizon--w_in_range+:
| Weight | Default | Trial |
|---|---|---|
| dodge_horizon | 2 | 1.08 |
| w_in_range | 0.7 | 1.4 |
| Reading | Seeds | Result |
|---|---|---|
| the trial | 55 | +1.92 +/- 0.71 |
| held out from 4800000 | 400 | +1.20 +/- 0.30 |
| held out from 4900000 | 400 | +1.34 +/- 0.27 |
Both held-out reads past two errors: the first candidate in 39 generations and about 540 censuses. Taking one weight out at a time (generation 36's "best-less" reads, 55 seeds each) left both halves inside the noise (w_in_range alone -0.02 +/- 0.61; dodge_horizon alone +0.42 +/- 0.77), so the gain may need both. dodge_horizon goes back toward 1 from the 2 the run found on 26 October (LR 96, 97): with w_in_range doubled the tank stays in range longer, and a shorter horizon keeps it there until later before it dodges. That is a reading, not yet a finding. Next, by the rule: make families on the trial, then Paul. It is not a default.
E. A fleet of census machines
Yes. Each Copal machine is a worker; saskatchewan is the coordinator. The rule that a census is one machine's holds: a fleet plays more whole censuses at once, never one census over many machines.
How it is orchestrated: a queue of files over ssh, pulled by the workers. Nothing to install beyond Copal, sshd and a key.
saskatchewan (coordinator) worker k (Copal, x86)
build/fleet/<run>/
queue/ gen40-sample-3/ <- run writes loop:
claimed/k/ gen40-sample-3/ <- ssh mv claim: ssh coord mv queue/T claimed/k/T
done/k/ gen40-sample-3/ <- rsync back play: census-bin on T's weights, its seeds
hosts/k heartbeat, binary hash, threads return: rsync result to done/k/T
census-bin the frozen binary, by hash beat: touch hosts/k every minute
stop: hosts/k.STOP- The coordinator is the run (
make converge). Instead of playing a generation's trials itself, it writes each one (weights, seeds, the binary's hash) as a folder inqueue/, waits for them indone/, and reads them into the ledger with the worker's name asHOST. Saskatchewan runs a worker too, so with one machine nothing changes. A generation's size grows with the fleet (one trial for each 24 threads free), so more machines read more weights each hour, not the same twelve sooner. - A worker pulls:
mvwithin one filesystem is atomic, so of two workers claiming the same trial exactly one wins. A fast machine takes more; a slow one takes fewer; a machine joins by starting its worker and leaves byhosts/k.STOPafter the trial in hand. On Copal (Alpine) the worker is an OpenRC service, so it restarts with the machine. - A dead worker: a claim whose heartbeat is older than ten minutes goes back to
queue/, and its partial result is discarded. A census is a whole, so nothing is lost but its time. - Held-out reads go through the same queue as single censuses; each is one machine's complete paired reading (CENSUS.md 5a). The two reads of one trial may fall on two machines, and that is a stronger test, not a weaker one.
- Joining: the identity check. A new machine first plays a reference census (fixed seeds, the defaults) on the coordinator's frozen binary, and its result must match saskatchewan's to the character apart from timing lines, as the frozen binary's check already does (journal 0073). The binary is built once on saskatchewan and copied by hash; workers never build. The build uses no
target-cpu=native(.cargo/config.toml), so one x86 binary runs the same on every x86 CPU; Copal's musl is linked dynamically, so every worker must run the same Copal release (the check catches a mismatch). - What it adds to the code:
scripts/fleet-worker.sh(about 60 lines: claim, play, return, beat), the run's generation step readingdone/instead of its own jobs,make fleet-join HOST=<addr>(copy the binary, run the identity check, start the service), andmake fleet(each worker's state, trials an hour and heartbeats).
What it gains. Saskatchewan plays a 55-seed census in about 100 seconds and a 400-seed read in about 9 minutes, about 28 minutes a generation of twelve. Workers share nothing during a census, so a fleet of n machines of its size plays n times the censuses; the fit and the ledger take seconds. The limit is the coordinator's disk and the network: a census folder is a few hundred kilobytes.
The learned tank on a fleet, in three stages, each only when the one before has plateaued:
- Islands (LR 103): each worker runs
make schoolon its own seed stream. Each night the coordinator collects every island'sbest.bin, reads them all on its own seeds (held out, the gauntlet, the match reading), and sends the best to every island as its next start. Needs a seed offset for the school and a sync script. - Specialists: islands on different open maps at once (one on 8, one on 6, one on 5), each guarded on the passed maps; the best all-round net carried at night. Trains several maps in parallel rather than in turn, and finding 3 above is the reason to try it.
- One evolution over the fleet (Salimans et al., 2017): every worker holds the same net and draws its share of a generation's perturbations from a shared seed; only the scores (a few bytes each) cross the network, and each worker applies the same update. A generation takes about 1.5 seconds on the Mac, and a LAN round trip about a millisecond, so this works over one long-lived connection, not over ssh a generation; every machine waits for the slowest. It needs bit-identical updates, so one architecture only.
Not taken: Slurm, Nomad or Kubernetes: a scheduler for many users and many kinds of job, which knows nothing of the census's rules, for a fleet of a few machines that a folder of files serves; Ray: a Python runtime beside a Rust project.
What Paul does for each machine: install Copal; give it a fixed address on the LAN; enable sshd; add saskatchewan's key. Then make fleet-join HOST=<address> from saskatchewan does the rest. The fleet's code is built when the first such machine is on the network, and the identity check is its first test.
105. A second census machine: columbia, and the identity check, 27 October
Paul: "we got another online at 192.168.8.224. can we get it running census in along with the other already running. gather . whats its ip." Then, after its rename: "yes go ahead with the identity check."
A. The machine
| saskatchewan (the census machine) | columbia (new) | |
|---|---|---|
| Address | 192.168.8.235 | 192.168.8.224 |
| Hardware | Intel Core i7-13700F, 24 threads | MINIX NEO Z300-0dB, Intel Core i3-N300, 8 threads, 32 GB, 467 GB NVMe |
| System | Copal, Alpine 3.24.3 | Copal, Alpine 3.24.3, kernel 6.18.59-lts |
It arrived named saskatchewan too (a Copal card written before that day's hostname fix) and was renamed columbia (Copal's docs/renaming-a-machine-lab-report.md). treadcore was cloned at 221b2ff and built (cargo build --release -p treadcore-brain, 36 s).
B. The identity check (LR 104, E.5)
Rule 1 of CENSUS.md section 5 is that a census is one machine's: two machines may play one seed differently. Before columbia plays censuses whose rows sit beside saskatchewan's, it plays one of saskatchewan's jobs and must match it to the character apart from timing.
Method.
- The reference: census
build/census/20261027-150756of the run of 26 October, jobg-2470000.log: 3 seeds from 2470000, every map (MAP=all, 11), 900 s, respawns 3, the run's trial weights (w_called=0.475898,w_in_range=1.4,pillbox_worth=1.4,w_pos_expand=0.8) against the defaults, played by the run's frozen binary. - The binary: that same
census-bin, copied (sha256cfd62aeb...71fdon both machines), not one built on columbia. - The maps: a worktree of 7d0af54, the run's commit, on columbia, and
census_maps allfrom itsscripts/common.sh. - The command:
census.sh's own job line (CENSUS_MAP,CENSUS_RESPAWNS=3,CENSUS_SEED0=2470000,CENSUS_SEEDS=3,CENSUS_SECS=900,CENSUS_LEDGER=0, the two weights files,--ignored brain_census --nocapture). - The comparison: both logs with the two timing figures masked (
N us a seat-tickin each meter line, andfinished in), thencmp.
Result. Identical: 77 lines each, every score, kill, death, revive, posture count and meter count the same. Unmasked, 22 lines differ, 11 a side, and they are exactly the timing ones: the ten meter lines and finished in (41.17 s on columbia, 51.66 s on saskatchewan, busy with its run).
C. What it means
The same binary on the same Alpine and musl gives the same matches on two different Intel CPUs, so a census played on columbia is a reading saskatchewan would have made. The fleet's first condition (LR 104, E.5) is met. What columbia plays next is LR 106: the candidates' families, or the fleet worker (LR 104, E).
106. Behaviour cloning on the GPU: the rule, before the run, 28 October
Paul: "start the burn training on calber." Then, after the probe: "start the behaviour cloning on calber."
Backlog 70, upgrade 7 (AI-BRAINZ-PLAN.md section 8): behaviour cloning, then PPO, on a GPU, with Burn. The GPU is calber's GTX 1080 Ti (orrery's docs/cuda-node-lab-report.md); Burn 0.22.1 and CubeCL 0.11.3 run on it (orrery's docs/pascal-versions-lab-report.md: a 143-64-64-8 net trained on the card).
A. What is cloned
The scripted brain, seen through the learned tank's own senses. In real matches (16 seats, the census's mix of personalities, respawns 3), five times a second, for every living seat: the 143 senses that seat's NetBrain would see (senses::sense), and what its scripted brain did in that fifth of a second, translated into the net's head choices:
| Head | Label |
|---|---|
| Drive | the sector of the tank's next route waypoint (or goal) from its heading, 1 to 8; 9 (stop) if it is not moving and has no goal |
| Gun | the sector of a FireDirect in the window, else hold |
| Lead | of the gun target's position, its lead point and halfway, the one nearest where it aimed |
| Mortar | the sector of a FireIndirect, else hold |
| Drone | the kind launched: interceptor, recon, strike, longshot, else hold |
| Squad, Signal | not trained (their loss weight 0) |
The drive is labelled from the tank's state, not its commands: a scripted tank follows a route set seconds before and need not say Move again.
B. The net and the training
The learned tank's shape, unchanged (143 -> 64 -> 64 -> 47 scores), so NetBrain runs the result and every existing reading judges it. Each trained head a softmax over its choices with cross-entropy; Adam; trained on calber's GPU with Burn (tools/bc, its own Cargo project so the census machines never build Burn); written in Net::save's form as nets/bc1.bin. Recorded on seeds from 9,000,000; the last tenth of the seeds held out.
C. The rule, written before the run
- It imitates: on the held-out seeds, the drive head chooses the scripted brain's sector more often than a net that always says the commonest label does, and the gun head's fire-or-hold agrees on more than that baseline too. (Without this, nothing was learned.)
- It plays, and plays better than the last net: a team of
bc1in the census on the same machine as a team ofnets/map7.bin, 110 seeds each, paired as usual:bc1abovemap7outside two standard errors. (The census readmap7at -20.33 +/- 0.27 on saskatchewan, level with a random net; censuses on different machines are not compared, so both are read on one.) - Reported, without a pass mark: the gauntlet, and the match reading (first down, deaths, damage dealt) against the scripted ranger.
If 1 holds and 2 fails, cloning alone does not carry into matches, and that is the finding; PPO from the clone is the next step either way.
D. The run, and rule part 1
Recorded on calber (8 threads): seeds 9,000,000 to 9,000,059, 600 s each: 2,490,248 samples in 97 s (1.46 GB, TCBC2); the tank moving in 63.2% of samples, a direct shot 0.26%, mortar 3.56%, a launch 5.12%. The masks were added after a first recording showed a direct shot possible in only 0.17% of samples: trained without them, the gun would learn to hold.
Trained on the GTX 1080 Ti (tools/bc, Burn 0.22.1, CUDA): 12 epochs, batch 4,096, Adam 1e-3, 91 s; training loss 1.441 to 0.986; seeds 9,000,054 to 059 held out (253,411 samples). Written as nets/bc1.bin (65,752 bytes, the shape of every net before it).
Rule part 1: passed. Agreement with the scripted brain on the held-out matches, each head where it had a choice, against always the commonest label:
| Head | bc1 | Always the commonest | Samples |
|---|---|---|---|
| drive | 65.5% +/- 0.1 | 36.5% (stop) | 253,411 |
| gun | 64.2% +/- 1.7 | 37.9% (hold) | 783 |
| mortar | 89.3% +/- 0.2 | 87.7% | 25,813 |
| drone | 96.7% +/- 0.0 | 94.0% | 132,613 |
| lead | 99.8% | 99.8% | 253,411: the scripted brain all but always aims where the target is |
Parity check 1: passed (LEARNING-DESIGN.md VII). AGREE= runs the written net through Rust's own Net::forward on the same held-out samples: 65.5, 64.2, 99.8, 89.3, 96.7, the trainer's figures exactly, on the same counts. The GPU-trained coefficients play in the Rust brain as they were trained.
Rule part 2 (the census, bc1 against map7, one machine): F below.
E. Is the GPU faster? Time, measured
Paul asked for a time-based answer. tools/bc times each part of a run (3567f7f): reading the recording, the first step (where the kernels are compiled), building batches on the CPU, and the device's steps. The same recording, batch 4,096, Adam 1e-3, on calber (i7-9700F, 8 threads; GTX 1080 Ti), 28 October 2026:
| CUDA (1080 Ti) | flex (Burn's CPU backend) | |
|---|---|---|
| Reading the 1.46 GB recording | 1.3 s | 1.3 s |
| First step (kernels compiled, then tuned) | 3.1 s; 23.2 s on a second, colder run | 0.0 s |
| First epoch, steps | 8.1 s; 17.9 s on the colder run | 3.1 s |
| A settled epoch: batches built on the CPU + steps | 0.8 s + 3.5 s | 0.8 s + 3.1 s |
| A settled step | 6.4 ms | 5.6 ms |
| CPU used | 107% (one core feeding the card) | 179% (under two of eight cores) |
| 12 epochs, end to end | 64 s (time-cuda.log) | about 47 s (2 epochs measured: 8 s) |
| Training loss, epochs 1 and 2 | 1.4090, 1.1045 | 1.4058, 1.1020 |
The GPU is not faster for this net. The CPU is, by an eighth a step, and by much more once the GPU's warm-up is counted. The warm-up varied from 3 s to 23 s between two runs of the same program, and the first epoch's steps from 8 s to 18 s: CubeCL compiles each kernel for the card and then tunes it, and how much of that a run repeats depends on what an earlier run left cached. A run of a minute pays it in full.
Why. The net is small: 143 x 64 + 64 x 64 + 64 x 42 = 15,936 multiply-adds a sample forward, about 96,000 floating-point operations with the backward pass, so 0.39 GFLOP a step at 4,096. In 6.4 ms that is 61 GFLOP/s: about half a percent of the card's 11 TFLOP/s. The card is waiting, not computing. An estimate of where its time goes, from the trainer's code (not measured kernel by kernel):
- About two hundred small kernels a step, each launched separately: the three layers forward and back, a masked log-softmax for each of five heads, and Adam's update of six tensors. A launch costs tens of microseconds whatever it computes.
- Fifteen uploads a step: the senses, then a one-hot label and a mask for each of seven heads, each built on the CPU (0.8 s an epoch, one thread).
- A wait every step: the loss is read back to the CPU after each step (
into_scalar), so the CPU cannot queue the next step while the card works on this one.
What would make it efficient, in the order to try them, cheapest first:
| Change | What it removes | Expected | |
|---|---|---|---|
| 1 | Batch 16,384 to 65,536 (the learning rate re-tuned) | four to sixteen times fewer steps; a step costs nearly the same, being launch-bound | the largest single gain on the GPU; the CPU gains little |
| 2 | Read the loss once an epoch (summed on the card) | 546 waits an epoch | the CPU queues ahead |
| 3 | Put the recording on the card once (1.3 GB of senses and the labels as integers, on an 11 GB card), and gather batches there | building batches (0.8 s an epoch) and the uploads | the epoch becomes the card's own time |
| 4 | One masked log-softmax over all heads (padded to 7 x 10), and Burn's kernel fusion | most of the launches | fewer, larger kernels |
| 5 | flex on all eight cores | flex used under two | the CPU's own number, before the GPU is judged against it |
| 6 | Not half precision | Pascal (GP102) runs f16 arithmetic at 1/64 the rate of f32 | f16 for storage only, if memory ever binds |
When the GPU pays. When a step has enough arithmetic to hide the launches: larger batches; a larger net (PPO's value head, or senses of the map as an image); many nets trained at once (an ES population evaluated as one batched forward). For BC on this 16,000-multiply net, the CPU is the right tool today, and the time that matters is recording and playing matches, which are CPU work on every machine (LEARNING-DESIGN.md VIII).
Toward parity check 3 (the same trainer on the CPU and the GPU): the two runs' losses after epochs 1 and 2 are within 0.3% of each other (the first batches differ, being drawn by each device's own random numbers), and flex's net after 2 epochs agrees with the scripted brain on drive 64.1% (CUDA's after 12: 65.3%). Indicative, and not the check: that needs the same seed and epochs on both, and a stated tolerance (LR 107).
The rule for the next timing, written before it: with changes 1 to 3 (batch 32,768, the loss read once an epoch, the recording on the card), a settled CUDA epoch takes under 1.0 s, and less than flex's at the same settings on all eight cores; and the held-out drive agreement stays within one point of 65.5%. If the CUDA epoch stays above flex's, BC stays on the CPU and the GPU waits for PPO.
F. Rule part 2: the census
On columbia (LR 105), at 742037d, one frozen binary for both (census-bin sha256 b58bf40a...), the settings of C: 110 seeds from 500,000, every map, 900 s, respawns 3. The two censuses ran one after the other, 27 October 18:24 and 18:34 (build/census/20261027-182432 and -183427, logs in build/bc/):
| Trial team | Against the defaults, 110 seeds |
|---|---|
nets/bc1.bin | -23.18 +/- 0.49 |
nets/map7.bin | -20.33 +/- 0.27 |
| bc1 less map7, paired by seed | -2.85 +/- 0.44 (6.5 errors below; bc1 ahead on 29 seeds of 110) |
Rule part 2: failed. bc1 plays worse than map7, outside two errors, not above it. map7 read exactly what it read on saskatchewan (-20.33 +/- 0.27), a second confirmation that columbia plays the census machine's matches.
By map (10 seeds each), bc1 is below map7 on nine maps and above it on two, the fortress isles: fortressisles2 -15.27 against -19.58, fortressislesx -14.61 against -17.28. Its worst are island (-29.51 against -21.84), generated (-29.40 against -21.62) and industry (-27.75 against -20.83).
What it means. Cloning taught the net the scripted brain's choices two times in three where the drive had a choice (D), and that is not enough to play: a tank that does the right thing two thirds of the time and something else the rest drives a different route from both the teacher and a net trained for itself. This is the case C named: "if 1 holds and 2 fails, cloning alone does not carry into matches". The finding is that the agreement a held-out sample measures, one decision at a time, does not carry into a match, where errors compound over a route; it is the usual weakness of behaviour cloning (the learner meets states the teacher never led it into).
What follows (LR 107, to be chosen):
- PPO from the clone, as C planned either way: the clone as the starting point, the census's score as the reward.
- Fix the clone's inputs first: 65% on the drive is a ceiling the senses may set; a sector label from a route the net cannot see (the route is planned over the whole map) may not be learnable from 143 senses. A check: agreement on samples where the route's first leg is in sight, against samples where it is not.
- Record where the clone goes (DAgger: the net drives, the scripted brain labels), so it learns from the states its own errors lead to.
Reported without a pass mark (C.3), still to run: the gauntlet and the match reading of bc1.
107. Making the trainer efficient: LR 106 E's list, 28 October
Paul pasted LR 106 E's list back as the work to do: much bigger batches; the loss read once an epoch; the recording loaded onto the card once; the per-head losses combined; the CPU version on all eight cores.
A. What changed in tools/bc
- The recording on the device, once. The senses [N, 143], and each sample's one-hot targets and additive masks laid out for every head, padded to [N, 7 x 10], go to the device at the start (2.49 million samples, 2.8 GB). A batch is gathered there by index (
select); a step uploads nothing; the epoch's shuffled order is uploaded once. - One loss for every head. The net's 47 scores are gathered into a padded [B, 7, 10] (each head's choices in a row of ten, unused slots masked), and one masked log-softmax and one product with the targets replace seven of each. The same loss as before: the sum over heads of each head's cross-entropy, divided by the batch.
- The loss read once an epoch. Each step's loss is added on the device (detached); the one read is at the end of the epoch, so the CPU never waits for a step.
- Batch size is the argument it was; 32,768 is tried with the learning rate re-tuned.
- The CPU on all its cores. Not a setting: Burn's flex backend (0.22.1) splits a matrix product over every core only above 192^3 operations (
PARALLEL_THRESHOLDin itsmatmul.rs) and runs the element-wise work on one thread. At batch 4,096 its three products are above the threshold already; bigger batches give it more parallel work than anything else. Measured, not set.
Timing is now each epoch's wall time, the first apart (it compiles and tunes on CUDA), the epoch ending at its one read of the loss.
B. The rule, written before the runs
From LR 106 E, unchanged: with changes 1 to 3 at batch 32,768, a settled CUDA epoch takes under 1.0 s, and less than flex's at the same settings; and the held-out drive agreement stays within one point of 65.5%. If the CUDA epoch stays above flex's, BC stays on the CPU and the GPU waits for PPO.
And a check that the rewrite changed the speed and not the learning: at LR 106's settings (batch 4,096, Adam 1e-3, 12 epochs) the new trainer's held-out drive agreement is within 0.5 points of LR 106 D's (two runs of the old trainer, differing only in their random start, gave 65.5% and 65.3%), and the other heads within two of their standard errors.
C. Results
On calber (i7-9700F, 8 threads; GTX 1080 Ti), the recording of LR 106 D, 12 epochs, 28 October. Settled epoch: the mean of epochs 2 to 12, with its standard deviation. Logs in calber's ~/bc/ (eq-*.log, b32-*.log).
| Trainer | Device | Batch | LR | Settled epoch | A step | First epoch | Drive | Gun | Mortar | Drone |
|---|---|---|---|---|---|---|---|---|---|---|
| LR 106 | CUDA | 4,096 | 1e-3 | 4.3 s (0.8 + 3.5) | 6.4 ms | 9.1 s | 65.3% | 63.6% | 89.0% | 96.7% |
| LR 106 | flex | 4,096 | 1e-3 | 3.9 s (0.8 + 3.1) | 5.6 ms | 3.9 s | 64.1% (2 epochs) | |||
| LR 107 | CUDA | 4,096 | 1e-3 | 0.815 +/- 0.019 s | 1.49 ms | 14.9 s | 65.5% | 64.2% | 89.1% | 96.6% |
| LR 107 | flex | 4,096 | 1e-3 | 4.507 +/- 0.013 s | 8.24 ms | 4.5 s | 65.4% | 63.3% | 88.8% | 96.7% |
| LR 107 | CUDA | 32,768 | 1e-3 | 0.643 +/- 0.012 s | 9.31 ms | 18.4 s | 64.1% | 59.6% | 88.4% | 95.4% |
| LR 107 | CUDA | 32,768 | 3e-3 | 0.642 +/- 0.006 s | 9.31 ms | 7.4 s | 64.6% | 63.7% | 88.8% | 95.9% |
| LR 107 | CUDA | 32,768 | 1e-2 | 0.643 +/- 0.010 s | 9.32 ms | 7.7 s | 65.1% | 63.3% | 88.8% | 96.6% |
| LR 107 | flex | 32,768 | 1e-2 | 5.035 +/- 0.025 s | 72.98 ms | 5.0 s | 64.9% | 63.3% | 88.6% | 96.5% |
Errors: drive +/- 0.1, gun +/- 1.7 to 1.8, mortar +/- 0.2, drone +/- 0.0 to 0.1 (held-out sampling). flex used 190% of a CPU at 4,096 and 225% at 32,768 (/usr/bin/time), 4.7 GB of memory; the recording on the card took 2.82 GB of its 11.
The equivalence check: passed. At LR 106's settings the new trainer gives drive 65.5% and gun 64.2%, LR 106 D's figures to the decimal, and mortar and drone within one error. The rewrite changed the speed, not the learning.
The rule: passed. At batch 32,768 a settled CUDA epoch is 0.643 s (under 1.0 s), 7.8 times faster than flex's 5.035 s at the same settings, and the drive agreement is 65.1%, within one point of 65.5% (at Adam 1e-2, the best of three rates tried; 3e-3 gave 64.6%, 1e-3 64.1%, all three timed alike).
D. Discussion
The GPU is now the faster place to train, by five to eight times. A settled CUDA epoch went from 4.3 s to 0.815 s at the same batch (5.3 times), and to 0.643 s at 32,768. The three changes that did it are the ones LR 106 E blamed: nothing built on the CPU, nothing uploaded a step, no wait a step. Most of the gain came at batch 4,096; going to 32,768 cut the epoch by only a further fifth: the card is no longer waiting on the CPU, and at 9.3 ms a step for 3.1 GFLOP it does about 340 GFLOP/s, three percent of its peak. Gathering 32,768 rows of 2.8 GB by index each step is the likely bound now; not measured.
The first epoch still costs 7 to 18 s on CUDA (CubeCL compiling and tuning), so a CUDA run of 12 epochs takes 15 to 25 s against flex's 63 s: the GPU wins any run of more than about two epochs.
The CPU is no faster for the same changes, slightly slower (4.5 s an epoch against 3.9): flex gathers by index on one thread, and it never used more than 2.25 of the eight cores. Item 5 of the list has no switch in Burn 0.22.1; using the CPU's cores would mean several trainers at once (one a core), or a backend that threads element-wise work.
What is left, if training grows: the first epoch's compile (a kernel cache kept between runs); the gather (a shuffled copy of the recording on the card each epoch, then contiguous slices); and more epochs at 32,768, now cheap: its loss after 12 epochs (1.011) is above batch 4,096's (0.986), having taken eight times fewer steps.
The defaults stay batch 4,096 and Adam 1e-3, the settings bc1.bin was made with; 32,768 at 1e-2 is the fast setting, named in the usage line.
108. The fleet records, the card trains: twenty times the data, 28 October
Paul: "deploy everywhere, even on spare cpus. run on cuda. consolidate results. plan collection and orchestration of batches. increase batch size to fill up cuda capabilities."
Read as: the fleet's spare CPUs record the scripted brain (the part that is CPU work, LR 106 A), the recordings are gathered on calber, and its 1080 Ti trains on all of them with batches as large as it can use. It also answers one of the three questions LR 106 F left: is 65% on the drive a ceiling set by the senses, or by the amount of data?
A. The fleet, 28 October
| Machine | Threads | Load at the start | Spare | Its part |
|---|---|---|---|---|
| saskatchewan | 24 | 23.8 (make converge) | none | left to the run |
| columbia | 8 | 0.0 | 8 | records |
| calber | 8, and the GTX 1080 Ti | 0.1 | 8 | records, then trains; the shards are gathered here |
| the Mac (M1) | 8 | about 2 | 5 | records, at nice 19; coordinates |
| nasser | not read | not reachable: it refuses this key (Paul to add it) |
B. The rule, written before the runs
The recording: seeds 9,000,060 to 9,001,259 (1,200 matches of 600 s, about 50 million samples, about 29 GB), in jobs of 24 seeds, each machine on its own binary built at one commit (recorded with its hash beside each shard). bc1's held-out seeds, 9,000,054 to 9,000,059, stay held out; the last 120 new seeds are held out too.
- Filling the card (reported, no pass mark): a settled epoch's time, samples a second, the card's busy share (
nvidia-smi dmon) and its memory, at batch 32,768 and each doubling up to the largest that runs. The batch that fills the card is the smallest past which a doubling adds less than 5% to samples a second. - More data:
bc2, trained on every recorded seed outside the held-out ones, agrees with the scripted brain on drive on bc1's held-out seeds above 65.5% by more than two errors (above 65.7%). If it does not, twenty times the data does not move the drive head, and the senses (or the net's size) set the ceiling. - In the census: a team of
bc2against a team ofbc1, on columbia, the settings of LR 106 C (110 seeds from 500,000, every map, 900 s, respawns 3), paired by seed: bc2 above bc1 outside two errors. Reported beside map7's reading.
C. Results: the recording, the card, and bc2's agreement
The recording (scripts/fleet-record.sh, run rec1200, at 65f5683; the pipes, the ssh and the time in LAB-REPORT-FLEET.md): 50 shards, 1,200 seeds, 49,807,225 samples, 29.2 GB, every shard's sha256 equal at source and store; 14 minutes, 19:35:52 to 19:49:51 on 28 October.
| Host | Jobs | Seeds | A job: recording + moving | Seeds a second |
|---|---|---|---|---|
| calber | 24 | 576 | 24 s + 11 s | 1.01 |
| the Mac | 21 | 504 | 26 s + 13 s | 0.94 |
| columbia | 5 | 120 | 152 s + 12 s | 0.16 |
| nasser | 0 | 0 | joined during the run; its binary built in 3.4 min, after the queue was empty |
The same seed recorded on three machines gives three shards, each reproduced exactly by its own binary: deterministic per binary, as the census is. nasser's binary is byte for byte columbia's.
Filling the card (B.1). The trainer's training set kept on the card (2 shards, 1,819,592 samples), 6 epochs at each batch, the rate scaled as the square root of the batch; tools/bc/sweep.sh, calber's ~/bc/sweep*/:
| Batch | Settled epoch | Samples a second | Card busy (dmon sm) | Card memory | Drive after 6 epochs |
|---|---|---|---|---|---|
| 4,096 | 0.812 s | 2,240,670 | 30% | 2,147 MB | 64.4% |
| 8,192 | 0.900 s | 2,022,045 | 74% | 2,141 MB | 64.6% |
| 16,384 | 0.873 s | 2,083,325 | 72% | 2,141 MB | 63.8% |
| 32,768 | 0.576 / 0.580 s | 3,160,676 / 3,139,142 | 33 / 50% | 2,141 MB | 63.5 / 63.8% |
| 65,536 | 0.686 s | 2,650,872 | 54% | 2,143 MB | 63.0% |
| 131,072 | 0.749 s | 2,428,979 | 67% | 2,143 MB | 61.7% |
| 262,144 | 0.810 s | 2,245,291 | 60% | 3,135 MB | 60.3% |
| 524,288 | 0.787 s | 2,311,063 | 65% | 4,127 MB | 52.1% |
| 1,048,576 | 1.102 s | 1,651,750 | 72% | 6,115 MB | 42.1% |
The batch that fills the card is 32,768 (measured twice): past it, a doubling loses samples a second, and so does halving it. Larger batches keep the card busier for less work: its busy share is not a measure of use on this card (8,192 reads 74% busy and is slower than 32,768 at 50%). The jagged curve is CubeCL choosing different kernels for different shapes, by the look of it; not measured kernel by kernel. Bigger batches also learn less in a fixed number of epochs, as expected.
Two faults on the card, found by their failures. A [N, 143] upload is stored with each row padded to 256 floats: two refused allocations were exactly 1,024 bytes times the rows (7,319,076,864 for 7,147,536; 3,664,890,880 for 3,578,995). And one buffer of 3.7 GB was refused with 0.6 GB of 11 in use. The senses are now uploaded flat and reshaped on the card (2.3 GB used where the padded layout asked for 3.7 GB in one piece), and no upload is over 2 million samples; larger sets are streamed.
bc2 (B.2). tools/bc on all of it: 52,297,473 samples from 1,260 seeds (read in 5.3 s, 30.6 GB in memory), 47,057,911 trained on, held out bc1's seeds (9,000,054 to 059) and the last 120 (9,001,140 to 259); batch 32,768, Adam 1e-2, 12 epochs, streamed in uploads of 2 million. 226 s of training: a settled epoch 18.12 +/- 0.12 s, 1,459 steps, 2,597,170 samples a second (83% of the card's peak with the set on it), uploading 0.2 s and waiting on the CPU 0.0 s over all epochs; the CPU 311% busy assembling chunks, 34 GB of memory. Training loss 0.947 (bc1's 0.986).
| Head | bc2 on bc1's held-out seeds | bc1 | bc2 on the 120 new held-out seeds | Always the commonest |
|---|---|---|---|---|
| drive | 66.6% +/- 0.1 | 65.5% | 67.7% +/- 0.0 | 36.5 / 37.4% |
| gun | 62.7% +/- 1.7 | 64.2% | 65.9% +/- 0.4 | 37.9 / 40.3% |
| mortar | 89.9% +/- 0.2 | 89.3% | 89.8% | 87.7% |
| drone | 96.8% | 96.7% | 96.7% | 94.0% |
| lead | 99.8% | 99.8% | 99.8% | 99.8% |
Rule part 2: passed. bc2's drive agreement on bc1's held-out seeds is 66.6%, above 65.7%: twenty times the data moves the drive head by 1.1 points, about eight errors of the difference (0.1 each, 0.14 together). It is a small move for twenty times the data: most of the drive head's ceiling is not the amount of data. The gun on bc1's 783 held-out chances is within its error of bc1's; on 16,224 new chances it reads 65.9%.
nets/bc2.bin (65,752 bytes, sha256 15f7c8fb...), on calber and columbia.
D. In matches: the census, the gauntlet, the match reading
The census (B.3), on columbia, 110 seeds from 500,000, every map, 900 s, respawns 3. bc2's census ran on the binary the fleet's worktree had left in columbia's shared target/ (f1c24a77..., the same source as bc1's 742037d: git diff 742037d 65f5683 -- source/ is empty), not on bc1's (b58bf40a...). So, as the rule said, bc1 was read again on bc2's binary, and the two are paired on it:
| Trial team (columbia, 110 seeds) | Binary | Against the defaults |
|---|---|---|
bc2 | f1c24a77 | -19.76 +/- 0.39 |
bc1, read again | f1c24a77 | -23.18 +/- 0.49 |
bc1 (LR 106 F) | b58bf40a | -23.18 +/- 0.49 |
map7 (LR 106 F) | b58bf40a | -20.33 +/- 0.27 |
| bc2 less bc1, paired by seed, one binary | +3.42 +/- 0.41 (8.4 errors; bc2 ahead on 87 seeds of 110) | |
| bc2 less map7, paired by seed | +0.57 +/- 0.37 (1.5 errors: inside the noise; ahead on 58 of 110) |
Rule part 3: passed. bc2 plays better than bc1, by 3.42 a seat, well outside two errors.
The two binaries play the same matches. bc1 read on both gives the same difference on every one of the 110 seeds: the binaries differ only in the build path written into them (the source is the same). So bc2 can also be paired with map7's census, and reads level with the official net (+0.57 +/- 0.37), not above it. nets/map7.bin stays official (LAB-REPORT-FLEET VII: above, outside two errors, is the bar).
The gauntlet (make gauntlet, GAUNTLET_NETS=bc1,bc2, 200 runs a test, on the Mac; build/bc108/):
| Test | scripted | map7 | bc1 | bc2 |
|---|---|---|---|---|
| G1 fire at an enemy in reach | 100.0% | 100.0% | 100.0% | 29.5% |
| G2 an enemy behind | 100.0% | 100.0% | 100.0% | 100.0% |
| G3 a shell on a standing tank | 100.0% | 69.5% | 37.0% | 5.0% |
| G4 a strike drone coming | 95.5% | 70.0% | 99.0% | 96.5% |
| G5 low armour, home in sight | 100.0% | 25.5% | 0.0% | 0.5% |
| G6 a depot to take | 100.0% | 2.0% | 0.0% | 0.0% |
| G7 a pillbox from outside its ring | 88.5% | 26.0% | 0.0% | 0.0% |
| G8 out of a pillbox's ring | 0.0% | 43.0% | 23.5% | 18.5% |
| G9 a depot across the water | 100.0% | 0.0% | 0.0% | 0.0% |
The match reading (make train CHECK=net MATCH=40: seat 9, the first 3 minutes, 40 seeds from 8,000,000):
| Seat 9 | First down (s of 180) | Deaths | Never down | Damage dealt | Kills |
|---|---|---|---|---|---|
| map7 | 129 +/- 9 | 0.82 | 16 of 40 | 27 +/- 8 | 0.20 |
| bc1 | 176 +/- 4 | 0.03 | 39 of 40 | 4 +/- 2 | 0.05 |
| bc2 | 178 +/- 2 | 0.03 | 39 of 40 | 4 +/- 2 | 0.00 |
| the scripted ranger | 163 +/- 5 | 0.33 | 28 of 40 | 63 +/- 8 | 0.70 |
What the clones learned. The scripted brain's moment-to-moment habits, and not its purposes. They survive: one death in 40 matches each, a tenth of the scripted ranger's, and they meet a strike drone better than it does (G4). They do not go anywhere: every test that needs a journey (home, a depot, a pillbox, across water) is 0%, and in a match they deal a fifteenth of the scripted ranger's damage. The drive label is the sector of a route the scripted brain planned over the whole map; the net sees 143 senses of its surroundings and not the plan, so where the route cannot be read from the senses the safest guess is the commonest one, and a tank that guesses "stop" lives long and does nothing. Twenty times the data sharpened that habit (bc2 holds fire in G1 where bc1 did not) and moved the drive head by a point.
That answers LR 106 F's second question: the senses, not the amount of data, set the clone's ceiling. The next step for imitation is to give the net what the scripted brain's choices depend on (its goal, as a sense: the sector and distance of its route's next waypoint and of its goal), or to stop imitating and train on the census's own score (PPO from the clone), which needs no labels the net cannot see.
109. fleet-train: the process as a loop, and the Jobs view, 28 October
Paul: "create a script to constantly improve like fleet train. a feature of orrery . on this mac." And: "incorporate the implimentation of the rust program to give us the gui to view jobs and pipe commands like a super computer cluster . right now our first usecase is training cuda on treadcore."
A. What was built
| Piece | Where | What it does |
|---|---|---|
| the loop | orrery tools/fleet-train.sh | rounds of a project's stages until stopped; an event a line; STOP; status --json |
| treadcore's stages | scripts/fleet-train-stages.sh | record (the fleet), train (calber's card, continuing the best), judge (a census on columbia) |
| a warm start | tools/bc, START=net.bin | the trainer begins from a saved net: 0 epochs from bc2 writes bc2 back byte for byte, with its agreement to the decimal |
| the lanes as JSON | scripts/fleet-jobs.py | each machine's job, state, age, and the pipe it is running |
| the Jobs view | orrery src/jobs.rs, --jobs-cmd | the museum interface's third face: the loop's rounds and stages, and every machine's lane and pipe, from that document (orrery docs/fleet-train.md) |
Why each round continues the best instead of starting again. Training from scratch on each round's new matches only resamples the same data (bc1 to bc2 showed twenty times the data moves the drive head by a point), and calber's 62 GB holds about 1,500 seeds at once. Continuing the best on each round's new matches lets every round add to what the net has learned without all of it fitting in memory.
B. The rule for every round, written before the first
A round's candidate (the best continued for 4 epochs at batch 32,768, Adam 3e-3, on the round's 1,200 new matches) is read in the census on columbia, 110 seeds from 500,000, every map, 900 s, respawns 3, and paired by seed with the loop's best on the same binary (the best is read again when the binary differs). It becomes the loop's best only if it is above it outside two standard errors. The loop's best is never the official net: nets/map7.bin changes only when Paul says (LAB-REPORT-FLEET VII). Reported each round without a pass mark: the drive agreement on bc1's held-out seeds.
What would show the loop does not improve: three rounds in a row with no promotion. Then continuing on more of the scripted brain's matches has stopped helping, and the next lever is the senses (LR 108 D).
110. One queue for the whole fleet: the scripted brain's weights and the net, in turn, 28 October
Paul: "the orrery should have a treadcore trainer in a way such that It knows to train all the variables and rotate between them. Placing items onto the queue, and keeping these system alive, while allowing a stop adding to the queue button. and a collect results and stop now button." He chose both kinds of variable in one queue, and reward terms as the feedback placeholders. Then: "can we get a sample run of orrery filling up its queue. lets see it work."
A. What was built
The engine is orrery's, tools/fleet-queue.py:
- A lane per machine claims jobs by rename, a job at a time. A job says what it needs (
cpu,gpu, a host), which jobs come first (after), and that its host's build comes first (after_host). - A remote job runs under bash in its own process group, so
stop-nowstops all of it. - Files move three ways:
sendbefore a job,fetchback by tar, anddeliverto the store, hashed as it streams. - The keeper:
- restarts dead lanes;
- requeues what a dead keeper left running;
- tops the queue up from the planner;
- stops adding by itself after five failures in a row (the breaker).
stop-addingdrains, collects and ends.stop-nowstops every job, collects and ends.
The planner is treadcore's, scripts/fleet-plan.py:
- Each host first builds the queue's commit in its own worktree.
- Weight trials:
suggest.pyreads the queue's own ledger. The first generation screens every weight at half and double; later ones follow the fitted curves. Each trial is a census on one machine. A trial past two errors, with no map past two below, is read twice more on held-out seeds. - Net rounds: the best net is continued on calber's GPU (
START=), then judged on columbia by a census of the candidate and of the best on one binary, paired by seed. A round's result is recorded inplan/round-NNN.json.
orrery's Training view shows the queue (fleet-view.py --queue):
- the state, and a bar of done, running and failed;
- the counts for each kind, the next job and the last finished;
- each machine's job;
- for an operator only (
--operator,--queue-ctl): Stop adding / Resume adding, Collect results and stop now, Start the queue.
B. The rules
Unchanged from the runs they come from:
- A weight trial passes as in converge (LR 92): past two errors above the defaults with no map past two errors below, then both held-out reads. It is then a candidate for
make familiesand Paul. - A net candidate replaces the queue's best only above it outside two errors (LR 109 B).
- Nothing becomes a default, and no net becomes official, without Paul.
C. The sample run
build/queue/sample, 28 October, from 21:40:
- Machines: calber, columbia, nasser and the Mac. saskatchewan was left to its run.
- Settings: small censuses (4 seeds of 120 s, seeds from 16,000,000), one net round (24 seeds of rec2400, 1 epoch, judged on 4 seeds).
| builds | the Mac 19 s, columbia 50 s, calber 171 s (with the trainer), nasser |
| net round 1 | trained in 11 s; candidate less bc2 +0.64 +/- 0.40 over 4 seeds: the best stays |
| weight trials in the first 5 minutes after the fixes | 70, none failed: the Mac 38, columbia 16, calber 10, nasser 6; 6.4 s each on average |
| past two errors | 1 (w0082): its two held-out reads queued by the planner |
Two faults, found by the sample run:
- The Mac has no
nproc. Every trial the Mac took failed at once. The Mac's lane, fastest to fail, took all 57 before anything noticed. Nownproc || sysctl -n hw.ncpu. - Nothing stopped the loop. The queue had no breaker, so the keeper kept planning trials that failed. Now it stops adding after five failures in a row.
The 57 failed jobs are kept in failed-before-fix/.
111. The queue at full settings: what four hours played, 28 October
Paul: "continue running census on the fleet." He chose to replace the sample (LR 110 C) with the queue at full settings. Then: "how many computations have been ran. over a million?" and "add lab report about these counts."
A. The run
build/queue/full-1, keeper started 02:17:23 on 28 October:
- Settings: those of orrery's
docs/fleet-train.md.- Commit 8e5df88 on every host.
- Weight trials: 55 seeds of 900 s from 6,000,000.
- Held-out reads: 400 seeds from 7,000,000. saskatchewan's run draws
its trials from 2,000,000 and its held-out seeds from 3,000,000, so the two never share a seed.
- Net rounds: two, each on 1,200 seeds of rec2400 (4 epochs, batch
32,768), judged on 110 seeds.
- Machines: calber, columbia, nasser and the Mac. saskatchewan was left to
make converge(load about 20 of 24). - Before it: the sample queue was stopped with
stop-nowafter 2,175 tiny trials. Its 29 failures were all lost ssh connections (exit 255), none a fault of the code. - The Mac: kept awake with
caffeinate -dimsu, at Paul's word.
B. The counts
scripts/fleet-count.py build/queue/full-1 (new) counts what the queue has fetched back, at 06:26:56, 4 h 10 min after the start:
- A game is one line
seed S on MAP: trial team Tin a census log. A seed plays three games, with the trial in each team's seat. - Seat-ticks, thinks and moves are each job's own meter line.
- A seat-tick is one tank seat advanced one game tick.
- A think is one decision of a brain.
- A move is one route planned.
- Jobs still running are not counted.
| Kind | Census logs | Games | Seat-ticks | Thinks | Moves |
|---|---|---|---|---|---|
| weight trials | 1,164 | 24,585 | 6,757,079,968 | 957,738,999 | 250,016,569 |
| held-out reads | 80 | 12,000 | 3,285,884,736 | 463,260,527 | 120,723,097 |
| net judging | 32 | 1,320 | 364,327,056 | 36,308,633 | 9,769,679 |
| all | 1,276 | 37,905 | 10,407,291,760 | 1,457,308,159 | 380,509,345 |
The net training on calber's GPU:
- Round 1: 52,067,105 samples × 4 epochs = 208,268,420 sample-steps, in 90.6 s.
- Round 2: 51,962,086 samples × 4 epochs = 207,848,344 sample-steps, in 87.8 s.
- Each read 57 million samples (33.5 GB) of 1,380 seeds, and held 5.2 million out.
The machines (seconds busy from events.jsonl; games from the queue's ledger, three to a seed, which lags the logs by a census or two):
| Host | Busy | Jobs done | Games (ledger) |
|---|---|---|---|
| calber | 4.1 h | 61 weight, 2 net (training), 1 build | 13,710 |
| the Mac | 4.1 h | 70 weight, 1 build | 15,360 |
| columbia | 3.8 h | 21 weight, 2 net (judging), 1 build | 5,535 + 1,320 judging |
| nasser | 3.3 h | 7 weight, 1 build | 1,155 |
In a sentence: 37,905 games of 15 minutes, or 9,476 hours of play, in 4 hours 10 minutes.
- That is about 9,100 games, 350 million thinks and 2.5 billion seat-ticks an hour.
- The median census ran at 18.3 µs a seat-tick.
- So: over a million in thinks (1.46 billion), seat-ticks (10.4 billion), moves (381 million) and training sample-steps (416 million). Not yet in games: at this rate a million games is about 4.6 days.
C. What the counts bought
Weight trials. 144 in the ledger at 06:26.
- Past two errors: 6.
- Exactly zero: 19, every seed +0.00 ± 0.00. The log names the changed weight, so it was applied; every seat scored as in the baseline. Among them:
advance_band0.675 and 0.175,beta0.15 and 0.6,bridge_keep90 and 360,bridge_soft3 and 12,c_boat10,commit0.2. On these maps and seeds these weights change no score.
Held-out reads, 400 seeds each:
| Trial | Weights | Read 1 | Read 2 |
|---|---|---|---|
| w0011 | advance_band 0.675, alpha 0.513, answer_cap 2.51 | +0.56 ± 0.24 pass | −0.11 ± 0.24 |
| w0095 | castle_inside 8 | −0.02 ± 0.16 | +0.56 ± 0.21 pass |
| w0113 | base_r 8, beta 0.6, bridge_keep 360, castle_inside 8 | +0.19 ± 0.19 | +0.40 ± 0.19 pass |
| w0116 | column_gap 1 | −0.02 ± 0.17 | −0.20 ± 0.12 |
| w0117 | castle_inside 8 | running | +0.28 ± 0.21 |
| w0128 | castle_inside 8 | +0.80 ± 0.21 pass | running |
- No trial has passed both reads, so none is a candidate under the rule of LR 92.
castle_inside8 alone has ten 55-seed censuses, all at or above zero (+0.01 to +1.18), and three of them reached held-out reads. Of its five reads so far, 2 pass, 3 are inside the noise, none below. saskatchewan's run points the same way: its best hascastle_inside6.78, andbest-less-castle_insideis among its candidates.- Correction: the session first told Paul that three trials passed their re-checks. Each had passed one of two.
Net rounds:
- Round 1: candidate less bc2 −0.77 ± 0.36 over 110 seeds.
- Round 2: +0.18 ± 0.35.
- bc2 stays the queue's best. The clones are at their ceiling (LR 108 D).
D. Decisions
castle_inside8 waits for its two reads still running (Paul's choice). It joins saskatchewan'scandidates.txtwith the verdict those reads give, marked as from the fleet queue at 8e5df88.- The queue goes on at full settings. Nothing is promoted; no default changes; no net becomes official.
112. Families on the fleet: the first make target orrery takes over, 28 October
Paul: "what needs to be done with make families. we want orrery to take over all make training commands. We want to use orrery to orchestrate training as a fleet. We no longer start training from a make command. Remove the training from make, and insert them all into orrery. Make sure all aspects are accomplished, including collecting the data and incorporating the updates into treadcore itself."
He chose:
- all 13 training and measurement targets leave the Makefile;
- a candidate that holds is prepared for treadcore and he approves it;
- families reads every candidate, newest first.
This section is step 1 of 6:
- families on the fleet;
- one ledger of every census;
- every other kind of job;
- starting a run from orrery;
- promotion;
- the Makefile and the documents.
A. What make families was
make families runs every_family_holds_its_floor, which is a test in the brain's test binary:
- It runs the 9 scenario families of
scenarios/families/, 400 or 1,000 seeds each, withSCENARIO_SETapplied. - It fails if any family's share falls under its floor.
- It is the check the census cannot make: a change can score better on average and still break one situation (CI-044, backlog 62).
It runs on one thread. On the Mac it took 55 s for one candidate.
B. The job
scripts/fleet-plan.py has a third kind of job, families, switched on by plan.json's families key:
"families": {"converge": ["build/saskatchewan/converge/20261026-181011"], "queue": true, "batch": 8}The candidates, in order:
- the defaults;
- the queue's own trials that passed both held-out reads (read from the jobs' titles,
held-out read K of wNNNN); - each converge run's
CANDIDATElines, newest generation first.
A job takes a batch of 8 candidates and runs them side by side, one to a core, with the census binary each host already built at the queue's commit. That binary is the brain's test binary, so the families test is in it.
A family under its floor is a verdict, not a failed job. The test's exit status is written as holds or UNDER. Otherwise the queue's breaker would count a weak candidate as a fault.
Reading the results: fleet-plan.py QDIR families (and collect) reads every result into plan/families.json: for each candidate, its set, its verdict, and every family's share, error and floor.
Why the queue's commit is right for saskatchewan's candidates:
- They were found at 7d0af54, and the queue builds 8e5df88.
- The defaults the two commits dump are identical: 111 weights, the same values.
- So a candidate's file of changes means the same on both. A promotion lands on the queue's commit or later.
C. The run
28 October, 09:12 to 09:16, in build/queue/full-1:
- 6 jobs on the Mac and boburner;
- 42 candidates: the defaults, the queue's two trials of
castle_inside 8(w0170 and w0182), and converge's 39 (its 40th line is the queue's own candidate, added to its list at Paul's word); - 38 to 46 s a candidate.
All 42 hold every floor. The family lines carry each candidate's set in brackets, so the weights were applied, and the shares differ:
| Family (floor) | Defaults | Lowest candidate | Highest |
|---|---|---|---|
| a duel in the open (55%) | 76.2% | 75.2% (gen59/best-less-dodge_horizon) | 84.0% |
| a naval start by a bare islet (95%) | 100.0% | 100.0% | 100.0% |
| a raid on a guarded base (75%) | 81.5% | 81.5% | 81.5% |
| a raid on the held ground (95%) | 100.0% | 100.0% | 100.0% |
| a rolling tank under a shell (99%) | 100.0% | 99.8% | 100.0% |
| a rolling tank under two shells (99%) | 100.0% | 99.3% | 100.0% |
| a standing tank under a shell (99%) | 99.9% | 99.9% | 100.0% |
| a standing tank under two shells (99%) | 99.9% | 99.7% | 99.9% |
| a tank under a ready gun (80%) | 83.6% | 82.5% (gen53/best) | 83.6% |
- The tightest family is the ready gun: 2.5 points over its floor at worst.
- gen64/best is
castle_inside6.56,dodge_horizon1.14,kite_in4.5,longshot_every2.59,w_in_range1.4 andw_near2.- Census: +2.97 ± 0.71 over 55 seeds.
- Held out: +2.40 ± 0.28 and +2.32 ± 0.28 over 400 seeds each.
- Families: it lifts the duel from 76.2% to 84.0%.
- This makes it the strongest candidate so far.
D. castle_inside 8 passes
Of its 14 held-out reads, none is below −0.02. Two of its trials passed both reads:
- w0170: +0.43 ± 0.17 and +0.66 ± 0.21;
- w0182: +0.50 ± 0.17 and +0.64 ± 0.21.
Under LR 92's rule it is a candidate. As Paul chose (LR 111 D), it was added to saskatchewan's candidates.txt, marked as from the fleet queue.
E. A fault found on the way: a dead host failed the queue
At 09:07 nasser dropped off the network. Its lane then:
- failed the census it was running;
- failed ten more jobs in 31 s (ssh: "Host is down");
- tripped the breaker, so the whole queue stopped adding.
Removing a host from config.json did not help either: its lane lives until the keeper restarts.
The fix is in orrery's tools/fleet-queue.py:
- An unreachable host's lane waits and takes nothing. It is probed each minute (
ssh … true), with eventshost unreachableandhost back. - A job that ends with ssh's own failure (255) on a host that cannot then be reached goes back on the queue, its log kept. It is not failed, and the breaker does not count it.
- A host removed from
config.jsonends its lane after the job in hand (lane left).
Tested on a throwaway queue of a local host and nasser, still down:
- six jobs all ran on the local host, none failed, and nasser's lane said
unreachable; - removed from the config, its lane left.
The running keeper still has the old code, so it changes on its next start. Until then nasser is parked: its build record is in set-aside/, which stops its lane taking anything.
F. Next
Step 2: one ledger. It joins saskatchewan's 1,133 censuses (copied to build/saskatchewan/), the sample queue and full-1, each row keeping its machine.
113. Orrery holds the orchestration: the newest commit, sync, and shutdown, 28 October
Paul:
"to confirm. that all training of make, make census, make family, and other fleet orchanization is now done, scheduled, and collected by orrery. we want all orchestration coded in orrery, so we don't need to manually collect using manual shell commands, we want a sync updater. we also want the fleet to be able to accept Shutdown Now commands for the fleet . Where we finished, report back, then shut down as they complete."
Then: "yes, go ahead in that order. and make sure at each step. each of the comptuers in the fleet get the updated values for training. i'm sure its already done, but we need to be git pushing and git pulling and using these numbers to always work with the newest. The computer running orrery would be pushing."
The answer to "to confirm" was no. Only weight trials, net rounds and families ran from orrery; the rest were make targets or hand commands. This section is the second step.
A. What moved
From treadcore to orrery:
| treadcore (removed) | orrery |
|---|---|
scripts/fleet-plan.py | tools/plans/treadcore.py, the plan for treadcore |
scripts/fleet-view.py | tools/plans/treadcore-view.py, what the Training view draws |
scripts/fleet-jobs.py | tools/plans/treadcore-jobs.py |
scripts/fleet-count.py | tools/plans/treadcore-count.py |
treadcore keeps the work, which orrery calls:
scripts/census.sh;- the brain's test binary (the census and the families);
scripts/ledger.pyandscripts/suggest.py;tools/bc.
fleet-record.sh and fleet-train-stages.sh, the recording, move with the job kinds (step 3).
B. The newest commit, on every machine
plan.json's "commit": "origin/main" is read again, after a git fetch, at every top-up.
Building. Each machine builds each commit once:
- in
~/fleet/q-NAME/c/SHORT/, as a worktree with the census binary and the defaults; - by a job
prep-HOST-SHORT, which every later job on that machine waits for.
New jobs use the newest commit. A job already planned runs on the build it was planned with.
Pulling. Before building, a machine fast-forwards its own checkout (~/code/treadcore) to origin/main. It does so only when the checkout is clean and on main; otherwise it says so and leaves it as it is. saskatchewan's checkout went from 7d0af54 to ea336a6 ("checkout: main at ea336a6").
Pushing. This Mac, which runs orrery, pushes. Paul asked for a commit and push at the end of each step, so the fleet trains on the newest.
History follows the defaults. The fit reads the queue's own censuses, and the fleet's history ("history": "build/ledger/fleet.tsv") where a row has:
- 55 seeds or more, 900 s, and every map;
- a commit whose
weights.rsis today's (compared by git's blob hash, never a dirty "+").
A promotion changes weights.rs, so later trials are read against the new defaults and are never mixed with the old. grove's first generation read 1,153 history rows: saskatchewan's converge run, and the queues. Its first best combined castle_inside 8 with converge's kite_in, longshot_every, w_in_range and w_near.
C. Sync: gathered without a hand
The keeper runs PLANNER sync every sync_s seconds (1,800), and at the end. A sync does three things:
- Each machine's ledger.
scripts/ledger.pyruns on that machine over ssh, so each row keeps its census binary's hash. The ledgers go tobuild/ledger/HOST.tsv. - Each machine's converge records, without binaries, into
build/fleet-sync/HOST/converge/. - Every queue's ledger.
All of it is joined by ledger.py --merge into build/ledger/fleet.tsv, with a summary in build/ledger/sync.json. The Mac's names (users-iMac, users-iMac.lan, iMac.lan) become one, mac, by the config's aliases.
The first sync took 13.7 s and joined 3,622 censuses:
| Machine | Censuses |
|---|---|
| saskatchewan | 1,139 |
| the Mac | 1,435 |
| columbia | 450 |
| calber | 438 |
| nasser | 151 |
| boburner | 9 |
tools/fleet-queue.py DIR sync, or Sync now in orrery, gathers at once. This replaces the rsync and the ledger script run by hand earlier in the day.
D. Shutdown Now
tools/fleet-queue.py DIR shutdown-now, or Shut the fleet down in orrery, which must be pressed twice.
- Nothing new starts. The flags are STOP_ADDING and SHUTDOWN.
- Each lane finishes the job in hand. Its results are fetched as always.
- It reports:
PLANNER sync HOSTgathers that machine's ledger before it goes. - It powers off, by the host's
poweroffcommand inconfig.json.- The command is run detached, after a 2 s sleep, so ssh's lost
connection is expected.
- A host that then answers is reported
poweroff failed. - A host with no
poweroffisleft running.
- The keeper writes
shutdown.jsonwhen no lane is left, and ends. For each machine it holds the last job, its result and time, whether it reported, and its power state. It also lists the jobs never started.
Tested on a scratch queue of this Mac (no poweroff) and boburner ("poweroff": "true", a command that does not power off), with six 8 s jobs and shutdown asked at 4 s:
here: t00, left running; boburner: t01, poweroff failed; 4 jobs never startedBoth finished the job in hand and reported, and neither took another.
No machine can yet power off without a password. sudo -n and doas -n fail on all five. orrery's tools/fleet-poweroff-setup.sh writes one rule that lets user run only the power-off command:
/etc/doas.d/fleet-poweroff.confon the Alpine machines;/etc/sudoers.d/fleet-poweroffon the Ubuntu ones, checked byvisudo -c.
It is copied to each machine's home folder; Paul runs it once on each.
E. Orrery
--queue DIR. One flag sets both what the Training view reads and what its buttons run:
cargo run --release -- --gui --operator paul --fleet grove --fleet-cmd "$HOME/.local/bin/copal fleet" --queue $HOME/code/treadcore/build/queue/grove- Two buttons: Sync now, and Shut the fleet down (two presses).
- New lines under the bar: the last sync, and the shutdown report.
- The guide,
docs/fleet-train.md, is rewritten for grove. - Checks: 335 tests pass, and the build is clean in the files changed.
F. saskatchewan joins
- Its converge run stopped at the end of generation 64, at 09:31:21, as Paul asked ("stop saskatchewan after this run").
- Generation 65's first census had started 12 s before. Its
census.shkept launching games until it was stopped by process name (pkill -x census-bin).- That census is unfinished: a
RESUMEwould replay it.
- Its data: 1,140 censuses and the converge records, copied to the Mac and now gathered by every sync.
- In grove, it is the census host (it supplies the defaults) and takes CPU work: 24 threads.
G. The queues
- full-1 drained: its 9 queued trials were set aside unstarted, and its running jobs finished.
- grove started at 09:35 on saskatchewan and boburner. Both built ea336a6 in 17 to 19 s, the build cache being shared.
- The other machines join grove as full-1 ends.
- Families on ea336a6: 40 candidates (the defaults and converge's 39), and all 40 hold every floor, as on 8e5df88.
H. Also
- The lost ethernet. Paul unplugged a cable during the day; nasser's outage at 09:07 was that. With the keeper of LR 112's fix, a lost host now waits instead of failing jobs.
- Next, step 3: every other make target as a job kind of the plan:
- a census of A against B;
- parts, switches, roundrobin, SPSA and tune;
- train (evolution strategies), school, gauntlet and clips;
- the recording.
114. Every make target a run of the fleet, 28 October
Step 3 of the move to orrery (LR 112). A run is what a person asks for:
fleet-queue.py Q new KIND key=value ... # or a button, LR 115
It is written to Q/runs/RID.json. orrery's plan for treadcore (tools/plans/treadcore.py) turns it into jobs at the next top-up, and keeps its state and results in plan/runs/RID/.
A. The kinds
| Kind | Was | How it runs on the fleet |
|---|---|---|
| census | make census | one census, trial against base (trial=, base=: a file or the text; @best is the search's newest best), on one machine with every core |
| parts, switches, roundrobin | make parts, make switches, make roundrobin | the censuses the script lists with LIST=1 (new: name, trial, base), a job each, on whichever machine is free; a table when all are back |
| spsa | make spsa | spsa.sh's rule (log space, a step a census of plus against minus, gains a/(k+1)^0.602 and c/k^0.101, each step capped and bounded), a step at a time |
| tune | make tune | tune.sh's (1 + lambda) evolution strategy; a generation's children side by side, the best replacing the parent only by beating it |
| train, gauntlet, clips, school | the same make targets | the learning harness (learn::harness::train in the census binary), one job, on one host (host=); start=/check= nets are sent with it |
| record | scripts/fleet-record.sh | a shard a job (RECORD=), delivered to the store with its sha256 checked at both ends (fleet-queue's deliver) |
| families | make families | every candidate's families read again (LR 112 runs them by themselves) |
| promote | (by hand) | LR 116 |
| (standing) | make converge, make ledger | the queue's weight search (LR 110, 113) and its sync |
The list of what to measure stays in treadcore. LIST=1 in parts.sh, switches.sh and roundrobin.sh prints their censuses and plays none. The rules of spsa and tune are carried over as they are, so their results read as before.
One difference: tune's children now play side by side, and the best of a generation replaces the parent. tune.sh played them one after another, a better child replacing the parent at once.
B. Priority, cancelling
- What a person asks for goes first. A run's jobs carry
"priority": "high", and the keeper files them ahead of the standing trials. - A run is planned at once. The keeper tops up when
runs/changes, not only at its low-water mark. - Runs advance by themselves. A run reads its finished censuses and writes its table whether or not a machine has built the newest commit yet. Only a new job waits for its machine's build.
cancel RIDtakes a run's queued jobs off the queue; jobs already running finish.
C. The test: a run of each kind, tiny
28 October, about 10:00 to 10:20, on grove. Sizes: 1 to 4 seeds of 60 to 120 s, to show the machinery, not to measure.
| Run | Result |
|---|---|
r0001 census, castle_inside = 8, 4 seeds of 120 s, every map | +0.18 ± 0.12 (calber) |
r0002 parts -duck w_front=0, 2 seeds of 60 s, naval | duck off −1.05 ± 0.13 (outside); w_front 0: +0.00 ± 0.00 |
r0003 switches cycle | +0.00 ± 0.00 |
| r0004 spsa, 1 step of 2 seeds | step 1: d +0.01 ± 0.35; θ dodge_horizon 2.0099, dodge_margin 0.7961, dodge_clear 2.5124 |
| r0005 tune, 1 generation of 2, 1 seed | the parent read (0.00, the defaults against themselves); its two children played next |
| r0006 gauntlet, 5 runs | gauntlet.tsv for every milestone net |
| r0007 record, 2 shards of 1 seed of 60 s | shard 9900000: 2,812,816 bytes delivered to calber, sha256 3172c40f… checked at both ends |
| r0008 roundrobin, the defaults and castle | its two censuses queued |
Each run's table, steps or children are in plan/runs/RID/.
Two faults, found by the test and fixed:
- A run's state was saved before the keeper had its jobs. A planner error later in the same top-up would leave the run marked running with no jobs. Run state is now written when the top-up succeeds.
- Runs waited for every machine's build of the newest commit, because of a guard at the top of
run_jobs. Only new jobs wait now.
D. Also
- An accidental run, stopped. While
LIST=1was being tested, an edit toswitches.shfailed to apply, so the test loop ran the realswitches.shon the Mac. It was stopped within two minutes. Its two folders (one switch begun each,forget) were removed, and nothing was pooled from them. - Trial titles end at a whole weight with "+N more" (
short_moved).
116. Promotion, and the Makefile without training, 28 October
Steps 5 and 6. Paul chose: "Prepare, you approve."
A. Promotion
scripts/set-defaults.py FILE(treadcore) writes a weights file's numbers into theweights!table ofweights.rs.--checkprints the changes and writes nothing.- A name the table does not have is an error, and nothing is written.
- A
promoterun (orrery;candidate=NAME, or@strongest) prepares it on this Mac, the one machine with push rights:- a worktree on a new branch
promote/QUEUE-RIDfromorigin/main; - the defaults written;
- the brain's tests run;
- the families run;
- a commit whose message carries the evidence;
- the branch pushed.
- a worktree on a new branch
A test or a family that fails stops it, with nothing committed. The run is then prepared.
- Approve (the button on the Runs page, or
approve RID) pushes the prepared commit toorigin/mainas a fast-forward. If main has moved, the push is refused and the promotion must be asked again. Approval also queues every candidate's families again. The fleet builds the new commit at its next top-up, and the search reads from the new defaults from then on (LR 113 B).
Which candidate. @strongest is the candidate with the best mean of its two held-out reads among those that hold every family. evidence lists all of them:
| Candidate | Held out (400 seeds each) | Families |
|---|---|---|
| 20261026-181011/gen62/best | +2.55 ± 0.28, +2.53 ± 0.29 | hold |
| 20261026-181011/gen64/best | +2.40, +2.32 | hold |
| 20261026-181011/gen58/best | +2.55, +2.00 | hold |
| 20261026-181011/gen53/best | +2.20, +2.20 | hold |
The first promotion, r0009, was asked for at 10:12 and began at 10:19. It prepares gen62/best (census +1.93 ± 0.81) on promote/grove-r0009:
| Weight | Default now | gen62/best |
|---|---|---|
| w_near | 1.0 | 2.0 |
| w_in_range | 0.7 | 1.4 |
| kite_in | 9.0 | 4.5 |
| dodge_horizon | 2.0 | 1.14366 |
| castle_inside | 4.0 | 6.72675 |
| longshot_every | 5.0 | 2.5 |
Its verdict is in section D. It reaches main only with Paul's Approve.
B. The Makefile
Removed: the 13 training and measurement targets (census, converge, ledger, spsa, tune, parts, switches, roundrobin, train, school, gauntlet, clips, families), their help lines, and their .PHONY names.
In their place, one help paragraph points at orrery. make census now says "No rule to make target".
The scripts stay, and their usage lines name them directly: scripts/census.sh plays one census on this machine, and make scenario stays as a development tool.
The documents now point at orrery:
- CENSUS.md sections 2, 4 (and its resume prompt) and 4a;
- PROCESS.md sections 7 to 9;
- CLAUDE.md.
C. Next
- Test orrery in fleet management mode (Paul).
- The user guide (Paul's request of 28 October).
D. The first promotion stopped at a test
r0009 (gen62/best) failed at the brain's tests, and nothing was committed or pushed. That is the guard working. Two findings:
1. The defaults were in two places. default_weights_are_todays_constants holds the weights! table to Tactics::default() (tactics.rs), whose numbers are written out by hand (kite_in as DIRECT_RANGE - 1.0). set-defaults.py now writes a changed weight into Tactics::default() too, where a field has its name (an integer field stays an integer).
2. A designed behaviour changes with every strong candidate. focus_fire_picks_the_wounded_tank_a_teammate_is_on places a tank among two enemies:
- a fresh tank 6 tiles away;
- a wounded tank (armour 40, one shot from dying) 14 tiles away, which a teammate is already shooting.
Today's defaults pick the wounded one, which is focus fire. Tried in a scratch worktree:
- gen62/best (held out +2.54) picks the near one, p1 at 3.93: "6 out, 1 on it, in range, a threat". Its
w_nearis 2 and itsw_in_range1.4. - gen58/best-less-w_near (
w_nearat its default, held out +2.08) picks the near one too:w_in_range1.4 alone tips it. - All 30 candidates with
w_near2 orw_in_range1.4 (every one past +2.10 held out) do the same.
The census says shooting the near tank in range plays better, by about +2.5 a seat; the test says focus fire is the design. Which is Paul's call:
- accept the new behaviour, and change the test to the situation in which focus fire should still win;
- or keep focus fire as it is, and hold those two weights at their defaults (most of the gain goes).
Until he says, nothing is promoted.
117. Focus fire or the near tank: a scalar, 28 October
Paul's answer to LR 116 D: "Make it a scalar." That follows his rule: either/or is a weight that blends the two, defaulting to his preference, plus a Caps switch, and the census finds the value.
A. The weight
focus_blend (weights.rs, default 0.5) chooses between:
- 0: the near tank in range;
- 1: the wounded one a teammate is on.
It is on behind the Caps switch FOCUS_BLEND (bit 21, on; off holds the blend at 0.5).
In the tactician's rate() (tactics.rs), today's score is kept as it was accumulated, and to it is added
(2 · focus_blend − 1) · (team − near)
where:
- team is the wounded term, the one-shot term, and the focus and overkill terms (a teammate on the target);
- near is the distance term and the in-range term.
At 1 the team's terms count double and the near terms nothing; at 0 the reverse. At 0.5 the addition is skipped (lean == 0), so the default scores as before, bit for bit.
focus_fire_picks_the_wounded_tank_a_teammate_is_on now holds the two ends:
- focus_blend 1 picks the wounded tank;
- focus_blend 0 picks the near one;
- both on today's weights and on the search's (
w_near2,w_in_range1.4).
The default's choice is the census's to set.
B. The rule before the run
- At the default the brain plays as before: a census of the defaults on the new code matches the old code's, to the character.
- The fleet's search reads
focus_blendat its half and double (0.25, 1.0) by itself, as it does every weight. A value becomes a candidate only by the usual rule: past two errors, both held-out reads, the families. A promotion is Paul's, by Approve.
C. (1), and a fault found on the way
The first comparison differed: per-seed scores 4.29 against 5.39. Bisecting each part of the change in a scratch worktree, every part played identically (the table line, the field and its mapping, the Caps line, the scoring), and so did a fresh build of the real tree:
old binary twice identical the real tree, a fresh target folder identical to the old binary <- (1) holds
The "new" binary of the first comparison was not the real tree.
- The scratch promotion worktree (gen62/best's defaults written in) and the real tree were built into one
CARGO_TARGET_DIR. - The test binary's file name is the same in both, so the scratch build overwrote it.
- Cargo, holding a separate fingerprint for each worktree, still called the real tree's build fresh.
The same hazard is on the fleet.
- Every census job ran
scripts/census.sh, which rebuilds with cargo, in the commit's worktree, into the one~/fleet/targeta machine keeps. - On a machine that has built two commits there, a job for the older commit could be handed the newer commit's binary.
- No result so far is affected: the brain's source is the same in every commit the fleet has built (8e5df88 to 49f2f3b).
Fixed:
CENSUS_BIN=(census.sh) copies a binary already built for the tree, with no cargo. orrery's census, held-out and judge jobs pass$C/census-bin, the binary the machine built for that commit at its prepare job. The families already used it.- A promotion builds into its own worktree's
target/. - Jobs planned before this commit still rebuild. They play the old or the new binary, and at the defaults those play the same, by (1).
D. Next
The fleet builds the commit at its next top-ups. The families of its defaults run by themselves (defaults@SHORT), and the search adds focus_blend's half and double to its screen. Promotion waits for the census.
118. Auto mode: the cycle closes itself, 28 October
Paul, on reading the first full-size spsa and tune runs: "queue the held-out reads for both. can we have this system auto incorporte the reults. accept them. incorporate them, sync them ... Cadidates. Should self promote, unless we are not in AUTO mode. We should be in auto mode". And, while it was being built: "i want to see some sort of confirmation that seach confirmation candidate shows that the values propigated to all computers in lab/infleet." Before that, on the candidate flags: "I'm going to always accept them. Just make them visible, so I can see progress."
A. The two runs (r0014, r0015)
Both asked 11:02 as tests of the full-size runs, finished 13:38 on commit 92972b7 (the brain's source unchanged since). Neither clears two errors.
r0014, spsa on the dodge (8 steps of 12 seeds, naval.map). Each step is a paired difference f(+) − f(−):
k d se host dodge_horizon dodge_margin dodge_clear 1 +0.18 0.85 boburner 1.83 0.87 2.29 2 -0.16 1.33 nasser 1.71 0.93 2.14 3 +0.36 2.73 calber 1.50 1.07 2.44 4 +0.38 2.00 saskatchewan 1.33 1.21 2.15 5 -0.76 1.14 mac 1.67 0.96 1.71 6 -1.04 1.30 saskatchewan 1.25 0.72 2.29 7 -1.43 2.34 mac 1.00 0.49 1.58 8 -3.84 2.51 mac 1.65 0.82 1.25
From the defaults (2.0, 0.8, 2.5) to 1.65, 0.82, 1.25. dodge_clear stopped at its bound, half its start. The largest step is 1.5 errors. A hint that the dodge runs too far, not a finding.
r0015, tune on fifteen target, odds and stand weights (5 generations of 4 children, 2 seeds each). The best: +2.52 ± 2.47 over the defaults, 1.0 error, picked as the best of noisy reads. It moves the way every strong candidate has (LR 116 D): w_near 1.0 → 1.35, w_in_range 0.7 → 1.16, w_focus 0.5 → 0.38, w_overkill 0.6 → 0.98, w_threat 0.6 → 0.40, press_odds 2.0 → 1.64, c_boat 5.0 → 2.64.
By ROADMAP goal 4 each must beat the defaults on seeds the search never used. Both are now adopted (B) for exactly that.
B. The cycle (orrery tools/plans/treadcore.py)
- Adopt. A finished spsa or tune run's result (
theta.weights,best.weights) becomes a trial of the queue: its census and its two held-out reads queued together, not one after the other (a search already chose it). By hand:treadcore.py Q adopt FILE WHAT. - Current. Every census records its
COMMIT(run.env). A candidate is current when its census and both reads ran on commits whose defaults are today's (same_defaults, number by number). - Auto promotion. The strongest current candidate whose families hold is asked as a promotion run (
by: auto):- one at a time;
- each candidate tried once;
- two failures in a row pause auto mode with a flag.
- Auto approval. A promotion whose tests and full families pass on its branch is pushed to main by the planner (a fast-forward). The commit is read again, so the same pass asks every machine to build it.
- Propagation. Every machine's build now brings back the defaults it compiled (
defaults.toml, before only the census host's). Each approved promotion is checked machine by machine against the values it changed:confirmed at SHORT;building SHORT;differs at SHORT: names;build failed.
Confirmed on all, it is a progress record. A difference or failure is a flag.
- Again. Every candidate's families are read again on the new defaults. The search's history counts only censuses on them.
Auto mode is "auto" in the queue's config.json, on unless false:
fleet-queue.py Q auto on|off;- a button on the Candidates page.
With it off, promotion waits for a person, and so does Approve.
Flags and progress. A candidate, a run finished, a promotion asked, approved or confirmed is progress. Progress needs no answer and is not brought to the agent's hook. A flag asks a person:
- a run failed;
- a family under its floor;
- a machine lost;
- the breaker;
- push refused;
- auto mode paused;
- a machine whose build differs.
The window. The Candidates tab counts candidates. Its page shows:
- auto mode and its switch;
- the last three promotions, each with every machine's state (
ok,..,!!); - every candidate with its two held-out reads.
C. What happens when Orrery closes
The keeper is its own process: its parent is launchd, and it was started apart from the window. Closing Orrery stops none of it:
- the jobs;
- a sync every 30 minutes;
- the plans and auto mode.
The window only reads status and sends its buttons' commands.
D. The first pass
At 14:14 the planner, on this code, asked r0019 to promote grove/w0034:
- census +2.49 ± 0.94;
- held out +2.67 ± 0.27 and +3.17 ± 0.20;
- families hold.
It moves seven defaults: w_near 1.0 → 2.0, w_in_range 0.7 → 1.4, kite_in 9.0 → 4.5, w_pos_fortify 3.0 → 6.0, dodge_horizon 2.0 → 1.08, castle_inside 4 → 8, longshot_every 5.0 → 2.5.
Its tests and families run on a branch on the Mac. Every machine has the newest commit's build queued (9c9abc1). r0014 and r0015 are adopted at the pass after the census host's build of it brings back today's defaults.
E. Next
- The promotion's tests (the focus-fire test, LR 117, holds both ends on the search's weights) and families decide r0019.
- Then the propagation check reads each machine's build.
119. The first auto promotion, the rotation, and a pass that failed, 28 October
A. grove/w0034 is the defaults (r0019)
The promotion asked by auto mode at 14:14 ran on the Mac in 66 s.
- The brain's tests: 109 passed, 0 failed. That includes the focus-fire test, which since LR 117 holds both ends of
focus_blend. It stopped r0009, and w0034 movesw_nearandw_in_rangeto double. - Every family held (
every_family_holds_its_floor):
a-duel-in-the-open 83.5% +/- 1.9% floor 55% a-naval-start-by-a-bare-islet 100.0% floor 95% a-raid-on-a-guarded-base 81.5% +/- 1.9% floor 75% a-raid-on-the-held-ground 100.0% floor 95% a-rolling-tank-under-a-shell 99.8% +/- 0.1% floor 99% a-rolling-tank-under-two-shells 99.3% +/- 0.3% floor 99% a-standing-tank-under-a-shell 100.0% floor 99% a-standing-tank-under-two-shells 99.7% +/- 0.2% floor 99% a-tank-under-a-ready-gun 82.4% +/- 1.2% floor 80%
Auto mode pushed it to main as dc3df01. The new defaults:
| Weight | Was | Now |
|---|---|---|
w_near | 1.0 | 2.0 |
w_in_range | 0.7 | 1.4 |
kite_in | 9 | 4.5 |
w_pos_fortify | 3 | 6 |
dodge_horizon | 2.0 | 1.083 |
castle_inside | 4 | 8 |
longshot_every | 5 | 2.5 |
Its evidence: census +2.49 ± 0.94; held out +2.67 ± 0.27 and +3.17 ± 0.20 (400 seeds each). Every machine has a build of dc3df01 queued, and the propagation check (LR 118 B 5) reads each one's defaults when it is done.
B. A pass that failed, and what it left
The pass that approved r0019 pushed, then failed writing the ticker line. It read set-defaults.py's echo line Tactics::default() kite_in: DIRECT_RANGE - 1.0 -> 4.5 as a number, which it is not.
- Unsaved state. The planner never saved its state, so r0019 still read "running".
- Two rotation runs. The pass had already written two (r0020, r0021). A second pass failed the same way and wrote nothing more: the rotation saw its lanes taken.
- No harm to main: the push is a fast-forward, and pushing the same commit again does nothing.
Fixed in orrery (cf242d0):
changed_vars()reads onlyname old -> newlines. That serves the ticker, and the propagation check too, which would have readTactics::default()as a weight every machine lacked.- New run files are written by
keep(), with the state, so a failed pass leaves no run behind. - The rotation's counters were set by hand to match r0020 and r0021.
The next pass marked r0019 approved.
C. The rotation
Paul: "Is there a do one of each on rotation method that is the default choice? Can we have that." Auto mode now asks one run of each kind in turn, with one run active in each of two lanes:
| Lane | Kinds, in turn |
|---|---|
| brain | spsa, tune, census of @best, parts, switches, roundrobin, record |
| net | gauntlet, train, school |
How it runs:
- Fresh seeds. tune replayed seeds from 0 every run and spsa from
- Both now take
seed0, from 500,000,000 a million a turn. record
takes the next 48 seeds.
- Both now take
- Measurements once per set of defaults: parts, switches, roundrobin and gauntlet.
- Adoption: a search's result is adopted, and so is a passing census of
@best. - Priority: rotation runs share the fleet with the standing search; a run asked by hand goes first.
fleet-queue.py Q rotation on|off turns it on or off, and so does a button on the Runs page. The first two rotation runs are r0020 (spsa) and r0021 (train).
The Runs page, for the cycle:
- each button says what it does now: searches adopted, measurements once per set of defaults, families automatic after a promotion, promote now;
- the runs list marks the rotation's runs (r) and auto mode's (a).
D. The keeper's gap
A keeper plans when its queue runs low or runs/ changes. A prepared promotion therefore waited behind a full queue: r0019 was approved only when a file in runs/ was made and removed by hand.
- Now: every sync (30 min) ends by doing the same, so a finished run or prepared promotion waits at most one sync.
- From the next keeper start: a keeper also plans every 5 minutes.
- Not restarted now: restarting the keeper would requeue its six running jobs.
E. Next
- Each machine's build of dc3df01, and its confirmation (the Candidates page).
- The search goes on from the new defaults. Every candidate before it is now on older defaults, so none promotes itself until it is read again.
- r0014 and r0015 are adopted when the census host's build brings back today's defaults. They are now read against w0034's defaults.
120. The Machines view, and whitewolf joins, 28 October
Paul: "these steps should be made into the process of orrery ... Which the Orrery then applies scripts and allows idempotent running of the config scripts. Make then difficult to press (with confirm with details), we dont wont to run the wrong scripts." His choices:
- root is typed by him, in a Sessions tile;
- commands run as the user (root only in a tile);
- the Machines page first.
A. The view (orrery 4a06d8f, c5d09e9)
Machines (Cmd-3, after Media) takes a machine from a fresh install (Ubuntu 24.04 with OpenSSH and ssh-copy-id) to a fleet node.
- Probe reads it over ssh as the user: system, CPU, memory, disk, addresses, NVIDIA card, driver, CUDA, Rust, treadcore, tmux, copal-tm, the login banner, and the power-off rule. This Mac is read locally.
- Recipes are orrery's four setup scripts (
fleet-node,cuda-node,fleet-login,poweroff). Each shows applied or not, read from what it leaves behind. - Apply… opens a screen with:
- the machine and its system;
- what the recipe does;
- each file and its sha256;
- the command.
Its button lights only when the machine's name is typed. The files are copied and their sums checked on the machine. The command is typed into the machine's Sessions tile, in a window of its own, where Paul types the password. orrery holds no password.
- Join… adds the machine to the queue (caps from what it has) and to the nodes list, after the same kind of confirmation.
tools/machines.py does the work. The first read showed what was still open across the fleet:
- calber and boburner: no power-off rule;
- boburner: no tmux;
- the three Alpine machines: no power-off rule.
B. whitewolf
whitewolf: an i7-6700K (8 threads), 62 GB, a GTX 1080, Ubuntu 24.04.5, two network ports (.124 used).
- Setup.
tools/fleet-node.sh(one run: build tools, Rust, treadcore,cuda-node.sh, topnotch, the power-off rule) left every recipe applied. - The CUDA check: compute 6.1, 20 SMs, 7.9 GB, a million-float add correct.
- Joined: caps cpu and gpu, power-off
sudo -n /usr/bin/systemctl poweroff. - First build of c1e00c0: started 15:10. The census binary was built (
c4c9d0361ea7abe7), then the trainer.
C. Found on the way
cuda-node.shstopped at its first check on a machine whose driver was not yet loaded, before tmux and copal-tm. It now tries the module, and says to reboot if the card is held.joinwould have writtensudo -n poweroff, which whitewolf's rule (/usr/bin/systemctl poweroff) refuses. The probe now records the command exactly as the rule allows it.- The Command-digit shortcuts moved: Training is Cmd-4, Sessions Cmd-5.
D. Next
- Commands by OS: one machine, all, or Alpine / Ubuntu 24.04, as the user, with the same confirmation.
- Then Media: the Ubuntu install stick and its checklist.
121. Training inside the GPU: crucible, a second project for the cards, 28 October
Paul: "we want to create a second project to run in cuda. Perhaps we can size something for the 8GB cards we have. Is there a neural net that we can train and retrain to keep its generations within itself. We need sensor data and a self contained loop ... How much of this simulation can be ran fully in cuda itself. If we can do that, can we write a lab report about general purpose traing and scaling up." The same prompt asks for a redesign of orrery's interface in treadcore's brushed metal. That is the next step: its story needs the second project to exist, with its numbers. The full report is crucible's docs/LAB-REPORT-GPU.md.
A. Since section 120 (orrery)
Steps taken after LR 120, not recorded until now:
| Commit | Change |
|---|---|
| 579375d | Sessions: typing in order and at once; no beep on every key |
| 083e6d1 | Mac window: Quit, Minimize, Close Window |
| 0a977d7 | The menu bar: every view, page, button and document (src/commands.rs) |
| e281681 | A recipe window gets a name of its own |
| ee54a64 | A recipe window is shown until screen says it is gone |
| be8e117 | A click on a tile focuses it |
| 02c22a0 | Orrery.app goes in /Applications |
| 2e32c7b | Propagation: a machine is confirmed by any finished build since the promotion (it read 2 of 7, now 7 of 7) |
| 67b289a | Candidates marked: current, on older defaults (with the commit), or promoted (with the run) |
grove's marks at 67b289a: 1 promoted, 0 current, 36 on older defaults.
B. crucible
~/code/crucible, one CUDA file of 543 lines. Everything is on the card:
- the arena: 64 × 64, six pillars, two tanks, a shell each;
- the sensors: twelve rays, bearing, own state;
- the nets: 32-32-32-3, one per individual;
- the play: one block per individual, one thread per duel, both nets in shared memory;
- selection and mutation: a sort, the elite kept, the top quarter bred with gaussian noise;
- the hall of fame: 4,096 past champions, the learner's opponents.
The host reads back two numbers a generation. --save and --load carry the population and the hall, so training resumes.
C. Results
| Card | Machine | Tank-steps per second | Net arithmetic |
|---|---|---|---|
| GTX 1080 Ti | calber | 581 M | 2.5 TFLOP/s |
| GTX 1080 | whitewolf | 462 M | 2.0 TFLOP/s |
| GTX 1070 Ti | boburner | 419 M | 1.8 TFLOP/s |
All three are at 22 % of FP32 peak in the net alone.
- Registers. Limiting each thread to 128 registers doubled the speed (190 registers: 236 M on the 1080). 80 and 64 registers spill and are slower.
- Width. H = 48 fits; H = 64 does not: two nets need 51.7 KB of shared memory, against 48 KB.
- Memory. A run at population 2,048 uses 313 MB. Population is limited by time, not by 8 GB.
- Power. 55 % (1080) and 66 % (1080 Ti) of the cards' limits: headroom remains (the report's VII.B).
Learning, against a scripted yardstick tank, last five reads of 4,096 duels:
| Run | Yardstick wins | Verdict |
|---|---|---|
| self-play against the hall only, 300 generations | 8.2 % ± 2.7 % | |
| half the games against sparring bots of drawn skill, whitewolf, 400 generations | 21.9 % ± 3.0 % | 3.4 se: confirms the rule written before the run |
| the replicate, calber, seed 8 | 14.3 % ± 2.1 % | 1.8 se: short of the bar |
D. treadcore's simulation in CUDA
Fits:
- the combat core: movement, shells, rays and tile collisions;
- the learned net.
Fits poorly:
- the lists, events and diplomacy (8,437 lines of
treadcore-sim); - the scripted brain's branches.
So the census stays on the CPU fleet. A GPU arena made from treadcore's combat rules could pretrain the net by the billion steps, with the census still the judge.
E. Found on the way
-DH=32on the command line broke Thrust's headers. The macro isHID.- A zsh
$h:cin an rsync target is a modifier. It made three local folders instead of copying. They were removed, and the targets are now written${h}.
F. Next
- orrery's interface in treadcore's brushed metal: every tab and page, the story from a new machine to a promoted result, and projects (treadcore, crucible) in the orchestration.
- crucible as a run kind on
gpuhosts, the three cards as islands passing champions. - Then commands by OS, and Media's Ubuntu stick (from LR 120).
122. orrery in treadcore's brushed metal, 28 October
Paul: "we need to consider the interface from treadcore . the styling. thr rows and columns of brushed metal. Can we redesign all tabs all sub tabs and rethink about the interface progression. tell a story". Later, mid-step: "and incorporate orrery and crucible into its second job. we want to complement and use our fleet to run to improve the canidates and allow fleet processing of the queue in the grove."
This step is the look. The story (the order of the views) and crucible as grove's second job are the next.
A. The look (orrery f82f471)
orrery draws every pixel itself, in software. treadcore's brushed metal is a Bevy texture (treadcore-client/src/metal.rs). The same recipe is now in orrery's canvas (draw.rs), drawn at the size asked rather than sliced:
- a gradient;
- horizontal streaks wavering along each row;
- a rim light on the lit edge;
- a dark outline;
- a breath of blue.
| Finish | Used for |
|---|---|
| Plate | the frame and columns |
| Button | buttons |
| Pressed | the key that is down |
The content sits in black readout wells, rounded and shaded under the steel, as treadcore's HUD does.
| Part | Now |
|---|---|
| Header | one for all five views (nav::header): the name cut into the steel; the views as metal keys, each with a lamp (green for the one shown) and its Command digit; the status in a black strip |
| Training's pages | metal keys; the lamp is green for candidates, red for flags |
| Buttons | metal capsules, pressed in while held |
| Lab's rail and bar | the steel column itself |
| Rows | dark readouts with light text and coloured lamps |
The theme is METAL, and it is the default (features on by default). --light and --dark keep the flat palettes.
Cost. A finish is cached by kind and size, bounded at 12 M pixels. Orrery.app held 389 MB across 40 s, against 368 MB before.
B. Checks
- 344 tests pass, including the strip under 500 pixels in metal.
- Frames of all five views were rendered and looked at (
reports/images/orrery-metal-training.pngand-sessions.png). - Installed in /Applications and relaunched.
C. Next
- The story: the views in the order the work goes, from a new machine to a promoted result. Projects, treadcore and crucible, become a view of their own.
- crucible as grove's second job: a run kind for
gpuhosts in the same queue, the cards as islands, so the GPUs work while the CPUs run treadcore's candidates. - The path to the candidates themselves: a GPU arena from treadcore's combat rules (LR 121 D).
123. orrery's story: five chapters, and a Projects view, 28 October
Paul chose the order "Build, Run, Result" from three (the others: a project switch first, or today's five views plus a sixth). The prompt: "rethink about the interface progression. tell a story, help us orchestrate and add comptuers to configure in the orchestration."
A. The chapters (orrery f0823ec)
The strip holds five keys, Command-1 to 5, in the order the work goes. Each chapter's pages are keys in a steel column on the left, and the chapter's sentence is cut into the foot of the column.
| # | Chapter | Its pages | The story |
|---|---|---|---|
| 1 | Machines | Machines, Media | add a computer: probe it, set it up, join it |
| 2 | Projects | Projects (new) | give it work: treadcore on the CPUs, crucible on the GPUs |
| 3 | Queue | Fleet, Runs | the work runs: each machine's job, and the runs asked for |
| 4 | Results | Candidates, Found, Net, Flags | what it found: candidates promote themselves |
| 5 | Watch | Lab, Sessions | look in: the room, and every terminal |
The window opens on the Queue.
Training is split in two:
- Queue keeps the band and its controls: stop, sync, shut down.
- Results opens with what the fleet found, as of this step: 37 candidates (0 on today's defaults), 717 improvements, auto mode on. Its Candidates key is lit green with the count; Flags is lit red while any are unacknowledged.
The menu bar follows the chapters. A test holds every view to exactly one chapter, and every chapter's sentence to its column.
B. Projects
A card for each project:
- treadcore, from the queue's status: queued, running, done and failed; auto and rotation; candidates.
- crucible, from its own run files (
~/code/crucible/runs/*.tsv, read every 15 s): each run's generations and last yardstick read, and the fastest rate.
Then a row per machine (cores, card), with what each project has on it:
- treadcore: the machine's lane in the queue, lamp lit while it runs.
- crucible: "ready, not queued" on the three GPU machines. That is the truth until crucible is a run kind, the next step.
C. Found on the way
The steel showed a step at half height down each side. treadcore's rim switches from light to shade at half height; on a plate the height of a window that reads as a seam. The rim is now blended down the sides.
D. Checks
- 348 tests pass.
- Frames of every chapter were looked at (
reports/images/orrery-story-projects.png,-results.png). - Installed and relaunched: 398 MB.
E. Next
crucible as grove's second job:
- a run kind for
gpuhosts; - islands passing champions;
- the GPU lanes sharing the cards with treadcore's net runs (train, school), which already use them.
124. Census to commit: the loop drawn, micro versions, Do it now, re-reads, crucible's islands, 28 October
Paul: "Create a learning loop and describe how the coefficients get commited back to treadcore, so the it picks it up in the AI ... show me commit versions, and timestamps ... show me hos the brainz and the net data are stored, show me the matrix for the AI ... essentially creating micro versions of this git submission ... it has a do it now button to sync and push the values". Also: "i want to see a button to add more gpu CUDA to the mix, add crucible"; "describe the results tab. why do we have failed tasks in family. why are some older, when it says promoted ... perhaps add a scrollbar and a filter by type"; and on the Net page, "show each variable with a bar ... expand this for all values. provide more details on the hover."
The lab report, with its diagrams, is reports/census-to-commit/ (shared page https://claude.ai/artifact/Q3UkM5y7rhpw8AyWse6KrD).
A. What was built
| What | Where | Commit |
|---|---|---|
crucible as grove's second job: a NAME-cuda lane per GPU machine, each an island (500 generations a segment, six queued ahead, chained), champions exchanged | treadcore.py crucible_jobs; crucible --export, --import (6878a26) | 7ead828, 0a9ae4b |
The button: Projects, "Add the GPUs to crucible" (two presses); machines.py cuda-lanes add / remove / list | projects.rs, machines.py | 7ead828 |
| Do it now: sync, then promote the strongest current candidate, or re-read the strongest older ones (Results, two presses; Fleet menu, armed) | treadcore.py now, fleet-queue.py now | d959a10 |
| Older candidates re-read on today's defaults when none is current (three a set of defaults; never one already promoted) | reread_older | d959a10, 0a9ae4b |
Micro versions: an approved promotion tagged brain-RID, annotated with what changed and the evidence, pushed (never v*) | micro_tag; tag RID | 3f9d773 |
| A failed promotion names the step that failed, read from its log | promote_failure; why RID | a0f854e |
| Candidates page: a filter (all, promoted, current, older), a scroll bar, an old failure marked superseded | training.rs, ui.rs scroll_list | d959a10 |
| Net page: a bar for every sense (143) and score (47); hover names it, says how it is transformed, marks its column or row on its matrix | training.rs sense_name, score_name | d959a10 |
B. The answers
- Older beside promoted. A census measures a trial against the defaults of its day. When w0034 became the defaults (dc3df01), the other 36 candidates' evidence described a brain the game no longer has. Auto mode promotes only a current one, and until this step nothing made an older one current: the loop stood at "0 current". The re-reads fix that. The first round, at 19:11, re-read w0045 and w0003, and w0034, which is the defaults (a control; promoted ones are now left out).
- Failed tasks in family. No family failed: 85 of 85 reads hold. The queue's one failed job is the promotion r0009 (10:12), which failed
default_weights_are_todays_constants: the table changed andTactics::default()did not. LR 119 fixed the step, and r0019 passed. The page now shows r0009 grey, superseded by r0019.
C. One promotion, timed
| Time | What happened |
|---|---|
| 13:38 | w0034 a candidate |
| 14:14:02 | r0019 asked by auto mode |
| 14:20:54 | dc3df01 on its branch, tests and families passed |
| 14:28:08 | pushed to main |
| 16:05:21 | confirmed on 7 of 7 machines |
| 19:09:33 | brain-r0019 tagged (retroactively; signed tag c066c65) |
git describe --tags --match 'brain-*' origin/main reads brain-r0019-5-g19638a5.
D. Found on the way
- The keeper running since 12:28 plans only when its queue is low or a run changes. Restarting it would have put whitewolf's three-hour school run back on the queue, so the islands queue six segments ahead instead.
- The first crucible segments (50 generations, 21 s) left the cards idle between plans. They are now 500 generations (plan.json
"crucible"). - macOS has no
timeout: one sync silently did not run.
E. Next
- Read the re-reads: does w0045 or w0003 still beat the new defaults?
- A GPU arena from treadcore's combat rules, so the cards can serve treadcore's candidates (LR 121 D), if Paul wants it.
125. crucible's ladder, Paul Accepts or the promotion self-modifies, and the CPU lanes fed again, 28 October
Paul, on seeing the GPUs at 100%: "islands' own past champions, then build the treadcore arena. i love this. keep everything in rotation. improve orrery. improve the process of Census to Commit. The process needs to be scripted without intervention. There are mentions to wait till promoted by paul, reword them to say Paul Accepts when auto promote is on, and ssh credentials allow. Else they self modify, and have a flag for please resovle this by commiting the weights and nets. The cards , when they find good values at the same time, how do we keep consistent generations with keeping data. Also battling known milestones in the bast is a good idea."
Why: calber and whitewolf won 98% and 96% of their yardstick duels and had not moved in 400 generations (boburner was still climbing, 72% to 78%). The cards were busy and the measure had saturated.
A. The ladder (crucible bfe74b2, cbf76fa; orrery 8f14f0f, edc6a45)
- Milestones.
--ladderloads past champions, oldest first. In training they are opponents (--lad, 20% of duels), so an island keeps beating the fleet's whole past, not only its hall's last few thousand. Every--yardgenerations the champion's score against them is written beside the yardstick's;--examwrites the final champion's score against each, 2,048 duels apiece. - The score is wins less losses (-1 to 1). Wins plus half the draws would give a net that never fights 0.5: in the first test a 20-generation net drew 87% and 100% of its duels with two champions.
- Keeping data. Every segment's champion is kept (
crucible/champions/, one line each incrucible/lineage.tsv: job, island, generation, yardstick, the milestone examined against, wins, draws, losses, margin in standard errors, ladder mean, and what became of it). - Consistent generations: the milestone rule, written before the run. A champion becomes milestone M(n+1) only when its exam was against M(n), the newest, it beat M(n) by more than two standard errors (wins less losses), and it does not lose to the ladder on balance. When two islands qualify in one pass, the larger margin becomes M(n+1); the other is kept and meets M(n+1) in its next segment. An exam against an older milestone (a segment queued before the newest was made) makes nothing. So the milestones are one line, each beating the one before, whichever card found it, and nothing a card found is lost.
- Sent at dispatch. The ladder (at most 8: the newest four and four spread over the older) and the other islands' newest champions are slot files the keeper reads when it dispatches a segment, so a segment queued six ahead meets the ladder as it is then.
- Tested on made-up exams: two islands past M1 at once (the larger margin, 16.2 se, became M2; the other, 6.5 se, was kept); a stale exam (kept); a net drawing everything (0 se, kept).
- Projects draws the chain of milestones (those a segment meets outlined) and the lineage, newest first; each island's card shows its ladder score against all milestones and the newest.
B. Paul Accepts, or the promotion self-modifies (orrery 8f14f0f)
A prepared promotion (its tests and families passed on a branch):
- Paul Accepts: auto mode on and this coordinator can push. It is pushed to main and tagged
brain-RID, as before; the record says "Paul Accepts (auto mode)". - Self-modifies: otherwise (no push permission, or auto mode off). It goes onto the local line
orrery/accepted, main with the promotions not on it. The fleet builds the local line: each build is sentplan/accepted.bundle, the line's own commits, and fetches it before its worktree. Aresolveflag stays up: "please resolve this by committing the weights and nets", naming the promotions, the line's commit, and the nets not committed (files undernets/git does not track, and the net rounds' best).fleet-queue.py $Q resolve, or the Runs page's To main, pushes the line. When main holds the line it ends; when main moves on without it, the line is replayed on main; in auto mode, when the credentials come back, it is pushed by itself. - Nothing waits for a person. Before, a promotion without push permission failed at once, and with auto mode off one waited for Approve. A failed accept (main's brain source moved under it) now self-modifies instead of failing.
- Tested in a scratch repository with a bare remote: self-modified (no permission), main moved under it (replayed, its parent main), permission back (pushed, resolved, tagged).
- The words: the Runs page's push line, its Accept and To main buttons, the Candidates page's auto mode line,
fleet-train.mdandCENSUS.mdsay Paul Accepts or self-modifies; none says "waiting for Paul".
The nets in this checkout today: nets/bc1.bin and nets/bc2.bin are not tracked; nets/map7.bin is the official net. The flag will name them until they are committed or ignored.
C. Found on the way: three CPU lanes idle
At 20:40 calber, boburner and the Mac had no treadcore work: weights queue 0. The planner counted crucible's segments, queued six ahead, against the share it gives treadcore, and the keeper running since 12:28 plans only when its queue is low (it never was: 15 crucible segments and 5 builds waiting behind whitewolf's school run) or at its half-hourly sync. Fixed: the GPU lanes' jobs take none of treadcore's share, and the share is three queued jobs a CPU machine whatever the keeper asks (a census is 15 minutes, a sync 30). At 20:42 every lane was running. This was LR 124's fault: the islands came with it.
D. The first exam, and the draw
The keeper planned at 20:30 while A was being written, and made the first finished segment, whitewolf's at generation 7050, milestone M0001 (by order, not by evidence: its line says "yardstick 0.000", written before segments without an exam fell back to their last yardstick read). The first exam, calber's champion at generation 11,550 against M0001: yardstick 99.5% won; against M0001, 2,048 draws of 2,048. The champions learned to wait for an attacker: the scripted yardstick charges in and is shot, two waiters time out. The rule made no milestone, rightly, and the ladder would have stood still.
Fix (either/or is a scalar): --draw, a drawn duel's worth in training, default -1 (a win is +2 and a loss -2, besides damage); 0 is the old behaviour. The rule before the run: within four segments an island, the exams' draws against the newest milestone fall below 50%.
RESULT_DRAW
E. Next
- The treadcore arena on the GPU, the next step Paul asked for.
- whitewolf's school run (r0022) ends; then the keeper can be restarted on its newer code (plans every five minutes).
126. Bot styles, a transform the game reads, and the bridge from crucible, 1 November
Paul: "can you impliment The game reads a transform ... Bridge crucible into treadcore ... We want Bot 0-7 Styles. And naming." styles.rs holds eight named brains of four kinds, each ending in the same Command. patch::slot no longer stops a game for want of a file. A hybrid champion plays as an override with its own gate, on senses built from an Observation: an adapter, where brains.md planned a clone, because a hybrid champion is already an override. No default moved; bot_style is not searched. The design, the census of each style and what is left: orrery's docs/bot-styles-lab-report.md.
127. Physical ranges, the wingman's numbers as weights, eight personas, and the escort gauntlets, 2 November
Paul: "we are on max fix mode ... we start this process by locking physical constraints into a range of values ... I want a wingman trained, such that the wingman following my moves, like an escort mission ... use the topography of local maxima to differentiate different named personas". The week's reason came first: base_r walked 16, 32, 8, 6.15 in a day and coming_r reached 221 tiles on a 128-tile map, numbers with a meaning in the world searched as preferences. RANGES now holds every distance, time and count of the brain to limits the simulator sets (the gun's reach, the map, a reload, the bay, the squad), the shares to 0 to 1, and bio_period as a decree at 60 by Paul's word of 1 November; every default lies inside, so nothing plays differently and the search stays in. The wingman's five constants (ESCORT_DISTANCE, WING_LEASH, WING_GREED, WING_THREAT, WING_AHEAD) became weights with those defaults, so a person's two wingmen can be pulled in or let roam from the U page, a persona can say, and the census can read them. Eight personas, slots 8 to 15 of the roster, each a short weights file over today's defaults named after a culture for the strength it plays (Rome the builder hill of the terrain at w_pos_economy 2.1 and posts_max 12, +2.56 +/- 0.66; Mongolia the Raider with the bold hill's w_duck 0.25; Venice the steady economist's hill; China, Shoshone, Polynesia and Assyria on styles and profiles; Japan a hypothesis), each certified by its own census and none yet read. Three escort gauntlets (G10 to G12) with a station expectation and reward, the share of a run a seat spends within a distance of another: the scripted wingman reads 29 of 30 covering the leader and 30 of 30 coming back after a shell, and holds eight tiles for 57 to 59% of a keep-station run, 0 of 30 at the seven-tenths bar first written, so the bar is a half and 57% is what a trained wingman must beat. rules::physical() gives the constants in real units at 25 m a tile and a tempo of six (a tank 60 km/h, a recon 75, an FPV 97, a full-range shell 36 s), with a test. The whole account, the goals rewritten, the lessons, the register of 380 open entries and the plan's other chapters (the scale system, the transform's shape, the zero-sum economy and the logistics of a base, play with bots and every feature an element, the testing plan, 106 references in IEEE form): reports/2026-11-02-the-plan/, from orrery's docs/book/.
The trainer's depth, proved the same evening. tools/bc took DEPTH beside WIDTH in the afternoon (f0497ad: the hidden layers a list, two of 64 as ever; the net written carries whatever shape it was given), and orrery's shape watch trains 64x3 beside 32, 64 and 128. Its Burn lock wants rustc 1.95 and this Mac had 1.93, so 1.95.0 was installed beside it and the road walked here on Burn's CPU backend (BC_DEVICE=flex), on a recording of four seeds of 120 s (36,743 samples; seed 9,000,003 held out). WIDTH=64 DEPTH=3 wrote a TCNET1 of sizes 143, 64, 64, 64, 47 (82,396 bytes), DEPTH=2 one of 143, 64, 64, 47. Parity check 1 again (AGREE= and CHECK=, Rust's Net::forward on the same held-out samples): the trainer's figures to the decimal on every head, for both, 64×3 drive 71.1, lead 100.0, mortar 76.5, drone 96.6, and 64×2 72.1, 100.0, 77.2, 96.5. The gauntlet played both from nets/ beside the milestones (GAUNTLET_NETS=w64x3,w64x2, 20 runs): 64×3 passes G3 18 of 20 and G7 11 of 20 and loses G2, where 64×2 keeps 20 of 20; nets of 36,000 samples, so the road is proved and the shape is not read. One thing to know for the watch: at three epochs the deeper net had not left the floor (drive 46.6%, the commonest label's share, against 56.2% for two layers); at twelve, the watch's count, the two stand a point apart. The card's run on the fleet's shards and the read against 64 are the first shape-watch job's.
128. Game 6: ore, the smelter and the harvester, 2 November
Paul: "continue. implement the economy." The plan's chapter 13 named four stages and built the first (game 5); this is the second, the large one: a map object, a works kind, the depot's rule, and the brain that runs it.
The rule. A map holds deposits of ore (Deposit { pos, stock }, deposit x y [stock] in a file; Map::seed_deposits gives a map that places none one for every second start, six tenths of the way from that start to the centre, on open ground, DEPOSIT_STOCK 120 each: between two bases rather than at either, and fewer than there are sides). On an ore map (World::ore_game, on unless the scenario says ore = false; treadcore-server --no-ore) DepotStock::make_from(true) passes over shells and the refinery's stop gives fuel alone: a shell exists only because a squad mined ore and a smelter made it. The squad mines a deposit with the same order it fells a tree (PillboyTask::Harvest on the deposit's tile: ORE_TICKS six seconds, ORE_HARVEST four, PILLBOY_ORE_MAX eight aboard), Event::Mined says what is left. A tank within SMELTER_RANGE three of a working smelter of its side unloads all it carries (Event::OreUnloaded); every SMELTER_MAKES_EVERY second a smelter with ore puts one shell into the nearest depot of its side within SMELTER_REACH thirty that has room (Event::Smelted), and the pillboxes draw on the depot as game 5 made them. The smelter and the harvester are StructureKinds that stand at their resource (at_resource): on open ground within SMELTER_SITE three of a deposit or HARVESTER_SITE three of forest, not on a pad, and on their own crew, not the grid (needs_power false); works_site_refused names the refusal. A harvest within HARVESTER_RANGE eight of a working harvester of the side yields HARVESTER_MATERIAL six and the tree returns in HARVESTER_REGROW_TICKS sixty seconds. Four tests in the simulator (the mine, unload and smelt chain against a depot that made nothing; the works' siting and the harvester's yield and regrowth; the map's read, write and seeding; game 5 under ore: false), 64 in all.
The brain. Two weights inside RANGES: ore_low 10 (0 to 20, the depot's stock) and ore_reach 40 tiles (0 to 128). The ore run (Commander::ore_run, Act::OreRun): a tank that is quiet, idle, aboard and not in its base cycle, when the side's nearest depot holds fewer shells than ore_low and a working smelter of the side stands within its reach of the base, drives to the nearest deposit within ore_reach, sends the squad to mine, drives to within the smelter's range and unloads; with no smelter yet, the side's nearest tank to the ore with the material builds one beside it first (Cycle::smelter_site, the open tile by the deposit nearest the base). Who runs: the base's guard (DefendBase) whenever the depot is short, which is the logistics of a base; every other tank once after each fill-up, as the base cycle is once. The cycle's improvement offers the smelter too, at w_works. A tank standing at a depot with nothing to give, its armour whole, leaves after FILL_PATIENCE ten seconds and does not come home for shells for DRY_FOR a minute (needs_home takes the tick): without this a side without ore parked at its depot for the match. The census counts ore mined and shells smelted a seat (ledger::Outcome, the composition line) and a scenario's reward may name ore and smelted.
Read. the_bots_build_a_smelter_mine_and_smelt: nine seats in three teams on the naval map for ten minutes, the fleet's own team brains: 5 smelters built, 40 ore mined, 9 unloads, 36 shells smelted, the longest wait at a dry depot 8 s. A two-seed census of 300 s on the generated island: teams mined and smelted 12 and 20; the team that did neither scored between them, so the economy is not yet the score, which is the floor the trade guard will want (ore, smelted in the composition line, read by orrery from now). An old fault came out under the new rule: the hunter had always lobbed one blind shell at a neutral pillbox its squad was about to take (shell_boxes, LR 86), and the depot's free shell replaced it before a_commander_takes_a_neutral_pillbox_without_a_shot looked; game 6 replaces nothing, so the test failed and the lob is gone: a live target keeps the gun (gun_reserved = p.alive()).
What this is not yet. Workers as stock in transit, the city's four shapes and the quartermaster (stage 3); oil and the refinery's source; trade (stage 4). The deposits are seeded by a rule rather than drawn by a map's author; maps/*.map carry none of their own. The window shows the heap, the smelter, the harvester, the ORE lamps and the smelter's ring, and says what was mined on the wire; it has no page for the side's stocks. GAME_VERSION is 6: every read before this is of the old game.
129. Game 7: oil, the workers, supply and the quartermaster, 2 November
Paul: "continue. stage 3 (workers in transit, the city's shapes, the quartermaster, oil) and stage 4 (trade); map authors' own deposits; a stocks page in the window." Stage 3, the logistics of a base, the same evening.
Oil. Well { pos, stock } as a deposit (well x y [stock], Map::seed_wells for the starts the deposits skipped, WELL_STOCK 120). The squad pumps as it mines (OIL_TICKS, OIL_HARVEST, PILLBOY_OIL_MAX, Event::Pumped); a tank within REFINERY_RANGE of a working refinery of its side unloads (OilUnloaded). A refinery with oil does what it did, the side's burn halved and a fuel stop; one without does neither, and the stop costs an oil a visit. On a map with wells (World::oil_game) a depot's fuel comes from a refinery of the side within REFINERY_REACH 30 with oil, one oil a stop, else DEPOT_FUEL_DRY ten a stop. Fuel is thus not yet zero-sum (a depot still trickles), which is a decision for Paul: the plan's table has oil for fuel and drone batteries, and the trickle is the mercy that keeps a tank from being stuck, as the dry-depot rule was for shells.
The workers. Carry { owner, kind: Shells | Supply, amount, pos, route, from, to } on the world and in the observation (allies', and any the eyes see). A smelter now makes its shells into its own store (Structure.shells) and every WORKER_EVERY ten seconds a worker leaves it with up to WORKER_LOAD five for the nearest depot of the side with room; game 5's instant pillbox supply is a worker too, counting what is already on the way. Workers walk map.route at WORKER_SPEED 1.5 over the terrain's cost, so a road is faster; step_carries delivers (capped at the depot's or pillbox's room), a hostile tank within WORKER_TAKE two takes one (the shells to the tank, WorkerTaken), a splash within SPLASH_RADIUS loses one (WorkerLost). A base with no road to its smelter is a base whose shells come slowly, and a convoy is a thing a raider can take, which stage 4 trades on.
Supply. PROPOSAL-UPGRADES 1.1 as it had it: Player.supply, a live pillbox one every HOLD_EVERY_TICKS, a depot two, a powered structure one every second hold, a kill ten, a capture five. Observation.supply is the side's (supply_of_side): a team spends as one, buyer's own first (spend_supply), the proposal's "a fleet can fund its leader" without the hand-over, because the guard who buys earns the least. One upgrade, Command::Buy { upgrade: DepotStores, at } for STORES_COST 60 beside a depot of the side: it holds STORES_STOCK_MAX thirty and makes twice a second (Depot::cap, DepotStock::make_from(_, cap)). Refusals named in the chair. The window's top strip carries the side's supply, shells, ore, oil and workers; F6 buys.
The quartermaster. city.rs: the next item a base wants, read from what stands, as far as city_plan allows (none | camp | town | works | city; works as built). A camp is the castle plan's and the cycle's. The town: a harvester beside the nearest wood within CITY_R 20. The works: the stores when the side has 60; the road to the smelter, a tile a visit (Cycle::road_to); the industrial zone taken, a structure of nobody's or a wreck within CITY_HAS 40, the power plant first because nothing else of the zone works without it; a refinery built on a pad a plant of ours covers where there is oil and none to take. The city: a data centre and a workshop. The guard is the quartermaster: defend_base runs Commander::city when nothing threatens, before it rotates; the tank drives within the squad's reach, sends the squad, holds, and asks again when the squad is back (city_run, city_ordered, the ore run's patience). The oil run is the ore run with OreRun.oil: when the side's refinery holds less than oil_low 4 and a well is within ore_reach, after the shells. Weights oil_low, city_plan, w_up_stores inside RANGES.
Read. the_bots_build_a_smelter_mine_and_smelt under game 7: 2 smelters, 28 ore mined, 24 smelted, 7 workers left and 6 arrived, 7 of the city's works built or taken, the longest wait at a dry depot 3 s; no oil yet pumped in ten minutes, because the refineries of the zone were taken late and a captured refinery wants the zone's plant first, which this commit teaches the city. A two-seed census of 300 s: supply 258 to 583 a team, ore 8 and 4 smelted for one team, oil 0. Simulator 66 tests (two new: the oil chain and the depot's fuel; the workers taken and lost, supply earned and the stores bought and refused three ways), brain 154 (the city's order). maps/industry, island and naval carry their own deposits and wells, written by the seeding rule (seed_maps.py, the scratchpad), so a map's author can move them.
Not built. Trade (stage 4). Fuel zero-sum. The city's concrete walls, gatehouse and dock (the proposal's other upgrades). A stocks page of its own: the top strip is the page for now.
130. Game 8: trade, the raider, the logistics gauntlets and the economy's senses, 2 November
Paul: "write lab report of the new economy. create a series of gauntlets to train the AI (both). to reward trains moving from oil refinery to factory, to port, to city. continue implementation." Stage 4 and the gauntlets, the last of the evening. The lab report proper is orrery's docs/economy-lab-report.md, on the site as The Zero-Sum Economy; this section is the record.
Trade. step_trade: from TRADE_FIRST twenty seconds and every TRADE_EVERY minute, each owned depot sends one convoy (Carry of CarryKind::Supply) to its nearest partner of the side, by map.route on land or, when there is no land route and both are ports (World::is_port, water within PORT_WATER two tiles), straight over the water at CONVOY_SEA_SPEED. Its load is the route's length times TRADE_RATE_OWN a quarter (a seat's own two depots) or TRADE_RATE_TEAM a half (two seats of a side); trade_arrives pays both ends. One convoy a depot a round, not a pair: the first cut paid a side by the square of its depots (16,951 supply in five minutes on the island). A convoy is a worker: lost to a splash, taken by a hostile tank within WORKER_TAKE, the supply to the taker. The embargo of the plan is not built: allies cannot hurt each other in this game, so nothing can be embargoed until a betrayal is a thing.
The raider and the cover. The scripted brain on Auto or the hunt, not resupplying: a convoy of ours within COVER_REACH 40 with a hostile tank within RAID_REACH 16 of it or six tiles of its road is covered, the tank driven to seven tenths of direct range of the hostile and the doctrine's gun left free; else, quiet, a convoy of another side within 16 is driven onto, meeting it ahead on its road (Act::Raid). Found on the way: the cover must run with the squad out (the tank that fortifies has its squad out for most of a match), and a sitter it cannot see it cannot cover.
The gauntlets. Five files in scenarios/gauntlet/, for both brains as the others are: G13 ore to the smelter (a deposit of forty eight tiles east, a smelter of the side and its empty depot eight tiles west; passed: a shell smelted within the minute); G14 oil to the refinery (a well, a refinery with its plant, dry; passed: oil unloaded); G15 the convoy comes through (two depots of the side twenty-four tiles apart on a road, a tank of another side sitting by it with a fifth of its armour, a third tank of that side far off so the match does not end; passed: a convoy arrived in two and a half minutes); G16 the raider takes the convoy (two depots of another side on a road, the tank six tiles from it; passed: a worker taken); G17 the chain to the city (a deposit, the smelter as the factory, the side's depot on the shore as the port, the side's depot inland up the road as the city; passed: shells delivered to a depot and a convoy arrived, in two minutes). A scenario file now places [[deposit]], [[well]] and [[structure]] (kind, owner, powered = true for a plant beside), road is a terrain kind, and the counts ore, smelted, oil, oil_in, delivered, convoys, taken and lost are expectations ({ seat, at_least | max }) and reward terms (learn::REWARDS, 20), so a training course can pay each leg of the chain.
The senses. Seventeen more (senses::ECON, AT_ECON, 160 in all): the nearest deposit with ore and the nearest well (sine, cosine, nearness over 64 tiles, stock as a share of 120), the side's nearest working smelter and refinery (sine, cosine, nearness), the ore and the oil aboard as shares of eight, the side's supply as a share of 200. An old net of 143 is widened with zeros as before (SENSES_BEFORE_SHELL is now the fixed 133, not "SENSES less the last two blocks", which the third block broke); a transform made for fewer inputs is widened with zero columns where the new senses are (Patch::from_bytes), so slot 1 plays as it did.
Read, ten seeds a cell.
| gauntlet | scripted | untrained | phase1 | phase3 | map4 | map5 | map7 | map7a |
|---|---|---|---|---|---|---|---|---|
| G13 ore to the smelter | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| G14 oil to the refinery | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| G15 the convoy comes through | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 |
| G16 the raider takes the convoy | 10 | 7 | 10 | 9 | 10 | 8 | 9 | 10 |
| G17 the chain to the city | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
The scripted brain passes all five; the nets pass none that needs the squad (no net has a trained squad head, and none has seen the new senses), and pass G15 and G16 because any gun kills a sitter with a fifth of its armour and any tank that wanders near a road meets a convoy. G15 as written is therefore a check that the chain runs when the road is clear, not a decision read; it wants a raider with a brain that cannot capture a depot, which the scenario form does not yet say ([[depot]] held = true is the next line to add). The rest of the gauntlet under game 8 (ten seeds): G1 10, G2 10, G3 9, G4 10, G5 10, G6 10, G7 9, G8 3, G9 10, G10 10, G11 8, G12 10, as before within a seed. the_bots_build_a_smelter_mine_and_smelt under game 8: 3 smelters, 36 ore, 32 smelted, 63 workers left and 62 arrived, 2 stores bought, 7 city works, supply 10 to 376 a seat at ten minutes, the longest wait at a dry depot 5 s. Simulator 66 tests, brain 154; build/train/gauntlet.tsv holds the table for the console's Results → Gauntlet page.
What the fleet will read. Every census from here is game 8. The trade guard's floors (built, road, kd) stand; ore, smelted, oil, supply are on the composition line and read by orrery, not yet floors. The first thing to watch is supply's scale (hundreds a seat in ten minutes with one thing to buy) and whether the quartermaster's city costs the guard its guard.
131. The nets on the logistics gauntlets: the squad head wired, and what the search found, 2 November
Paul: "train the nets on the gauntlets." The first thing the attempt found was in the code, not the training: NetBrain scored the squad head and never acted on it (Head::Squad => false, "held until its phase"), so no net could order a mine, and the zeros of section 130 on G13, G14 and G17 were the mask's, not the nets'.
The squad head. NetBrain::squad(obs, me, map, c) gives the head's seven choices their orders, each offered only when there is one to give within SQUAD_REACH eight tiles (the squad walks at PILLBOY_SPEED, so the tank need not stand on the work, but a long walk is a long time without a squad, so the drive head has to bring the tank near): 1 capture the nearest pillbox or structure that is neutral or dead and not ours; 2 build a smelter where Cycle::smelter_site would put it, beside the nearest deposit that has no working smelter of the side within SMELTER_REACH, with the material; 3 mend the nearest damaged pillbox or structure of the side, with the material; 4 fell the nearest forest tile; 5 mine the nearest deposit, or pump the nearest well, that the squad has room for (the load unloads itself when the tank drives within range of the side's smelter or refinery, as the scripted brain's run relies on); 6 home while the squad is out. None without a squad; 1 to 5 none while it is out, since the world refuses a second order but Return. The recorder (record) now labels the squad head from the Deploy the scripted brain gave (a Harvest on a deposit or a well is 5, on wood 4), without a mask, so tools/bc still leaves the head. Test: the_squad_head_orders_what_is_in_reach.
The courses. G13, G14 and G17 pay the drive as a potential beside the counts: closer = { at = "centre", offset = [-8, 0], worth = 1 }, the share of the way from where the tank began to the works that takes the load, start to end, so the search feels the way west before the first unload pays. The pass conditions did not change; the scripted brain's 10 of 10 stands.
The training. GROUND= the five files, train as built (48 pairs, 12 episodes a generation, sigma 0.05, Adam 0.02, decay 0.0005, 8 threads; 4.3 s a generation on this Mac), 20 held-out seeds a course.
- From
bc2(the clone): flat from generation 6, every perturbation 3.000 exactly, the net's own 3.000, passed 0.17. The clone stands still in an empty field with nothing in sight; its squad mines eight ore where it stands (a quarter each, the 2.0 of the 3.0) and the tank never drives, so the potential is zero everywhere near its weights and the search has no gradient. Stopped at 17. - From
map7(the evolution milestone that drives to things), 300 generations, 22 minutes: the population's mean 1.46 to 2.64, best 1.95 to 2.88. Held out: G15 20 of 20 (reward 6.25); G16 20 of 20 (2.35: it raids the convoys, where the untrained net read 7 of 10 andmap79); G13 0 of 20, reward 0.78; G14 0 of 20, 0.83; G17 0 of 20, 4.73 (the world's four convoys and the drive). The net learned the drive west, the potential, and not the mine: the mine is one choice of one head at the one moment the deposit is within reach, before the drive carries the tank off, and a step of 0.05 on seventeen thousand weights seldom flips a head whose rows were never trained. - From the 300th net, 300 more at sigma 0.1: mean 2.43 to 2.75; G16's reward 3.10 (more taken), G13 0.82, G14 0.91, G17 4.88; the same zeros. A wider step did not find the mine either.
The cause, measured. A probe (squad_head_margin, ignored, NETS= a list) senses G13's start, a deposit eight tiles ahead, and prints what each net's squad head scores. The clone bc2: hold 0.44, mine 4.58; it mines. map7: hold -8.9, mine -237.6; the 300th net from it: hold -80, mine -277; the 600th: hold 12, mine -290. A head that was never trained but rode along through a thousand generations of the search has rows at the search's scale, and a margin of two hundred is not crossed by a step of a twentieth on any weight: the search was moving the rows, in the wrong direction as often as the right, and the drive it could feel.
The reset. Net::zero_head(head): one head's output rows and biases at zero, the rest untouched; its choices tie, choose takes hold, and a step of 0.05 is a choice. RESET=squad in train applies it to the start net. Test: a_zeroed_head_ties_and_leaves_the_rest. From map7 with the squad head at zero, 300 generations, 20 minutes: passed 0.25 at generation 0, 0.92 at 4, 1.00 at 20; the mean 2.34 to a high of 10.02 at 200, 7.68 at the end (the mean moves with the dozen seeds drawn a generation). Held out, every course PASS: G13 19 of 20 (reward 8.93), G14 20 of 20 (10.02), G15 20 of 20 (6.05), G16 17 of 20 (1.50), G17 20 of 20 (17.41). The one G13 miss and the three G16 misses are the seeds' geometry (the heading is "any"); G16 gave back some of what the second net had (3.10 to 1.50) for the mine, which pays more. The net is nets/logistics.bin, 160 inputs, 64, 64, the heads; the gauntlet reads it by name.
Read, ten seeds a cell, the whole gauntlet, with the clone and the new net.
| gauntlet | scripted | untrained | phase1 | phase3 | map4 | map7a | map5 | map7 | bc2 | logistics |
|---|---|---|---|---|---|---|---|---|---|---|
| G1 fire at an enemy in reach | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 2 | 10 |
| G2 an enemy behind | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 |
| G3 a shell on a standing tank | 9 | 9 | 9 | 10 | 10 | 9 | 10 | 8 | 0 | 10 |
| G4 a strike drone coming | 10 | 0 | 9 | 9 | 10 | 10 | 10 | 8 | 10 | 7 |
| G5 low armour, home in sight | 10 | 0 | 0 | 1 | 3 | 4 | 2 | 1 | 0 | 1 |
| G6 a depot to take | 10 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| G7 a pillbox from outside its ring | 9 | 4 | 4 | 4 | 5 | 4 | 4 | 3 | 0 | 3 |
| G8 out of a pillbox's ring | 3 | 2 | 8 | 9 | 10 | 10 | 10 | 6 | 2 | 4 |
| G9 a depot across the water | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| G10 keep station on a moving leader | 10 | 0 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| G11 cover the leader against what appears ahead | 8 | 0 | 5 | 5 | 0 | 1 | 1 | 0 | 0 | 0 |
| G12 come back after a shell | 10 | 0 | 6 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| G13 ore to the smelter | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10 |
| G14 oil to the refinery | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10 |
| G15 the convoy comes through | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 |
| G16 the raider takes the convoy | 10 | 6 | 10 | 9 | 10 | 9 | 10 | 9 | 0 | 8 |
| G17 the chain to the city | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10 |
The logistics net passes the three squad gauntlets ten of ten, where every net before it had none, and keeps its parent's G1 to G3; it gave back some of map7's G4 (8 to 7), G8 (6 to 4) and G16 (9 to 8) to the five courses it was trained on, as a net trained on one course does (LR 102). The clone bc2 beside it, read on the gauntlet for the first time: G1 2 of 10, G3 0, G16 0, a brain that imitates sixteen-seat matches and does not shoot a lone tank in an empty field, which is why the search from it was flat. The console's Results → Gauntlet page reads build/train/gauntlet.tsv.
What is next. A mask byte for the squad head in the recording (a TCBC3) so tools/bc can clone it; a course of the fleet's maps with the squad on, since a net that mines in a field of two may not among sixteen; the census to read what a trained squad head does to a side's ore and smelted lines; RESET=signal the same way when the calls are trained.
132. The squad head cloned, a course on the fleet's maps, and the Quartermaster's census, 3 November
Paul: "a mask byte for the squad head so tools/bc can clone it; a course on the fleet's maps with the squad on, since a net that mines in a field of two may not among sixteen; the census to read what the trained head does to a side's ore and smelted lines." The three next lines of section 131, the same night.
The mask byte. A recording is TCBC3: the header as before, and each sample's masks are four bytes, the fourth the squad head's choices 1 to 6 as bits 0 to 5, from NetBrain::squad at the moment the window opened, as the gun's, the mortar's and the drone's come from their own target functions. tools/bc reads TCBC2 and TCBC3 alike, a file at a time (a TCBC2 sample's fourth byte is zero, so its squad head is never allowed and never trained), takes the senses from the header rather than assuming 143 (the proof of section 127 was of the old width; today's recordings are 160 wide), and trains the squad head where its mask allows, as the others. agree in the harness reads both and prints six heads. Proved as section 127 was: eight matches of 180 s recorded in 6 s (108,207 samples), trained on the Mac's CPU in 7 s, and Rust's Net::forward agrees with the trainer's held-out figures to the decimal on every head, the squad's included (drive 73.9, gun 78.1, lead 99.9, mortar 91.5, drone 95.0, squad 95.1).
What the clone's squad head learned: the floor. Forty matches of 600 s (1,708,039 samples, recorded in 106 s, trained in 311 s on the CPU), held out the last four seeds: drive 78.0% (always-9 55.8), gun 67.5 (41.5), mortar 92.5 (92.3), drone 92.2 (87.9), squad 95.8 against an always-hold floor of 96.6. The recording says why. Of 1.7 million samples the squad head's labels are hold 1,630,160, capture 1,861, build 13,053, mend 17,698, fell 7,106, mine 169, home 37,992; where it had a choice (1,497,295 samples) the offers were capture 182,461, build 13,013, mend 132,517, fell 415,734, mine 35,942, home 1,014,933. The scripted brain in a dealt match of sixteen mines a hundred and sixty-nine times in forty matches: its guard runs ore only when the depot is short, and a dealt match's depots are seldom short of shells. A cross-entropy over that learns hold, and no clone of these matches will mine; the squad head that mines is the one the courses trained (section 131). The clone with the squad head is nets/bc3.bin (styles.tsv); Scholar stays bc2 until Paul says otherwise.
Read, ten seeds a cell, the clones beside the logistics net.
| gauntlet | scripted | map7 | bc2 | bc3 | logistics |
|---|---|---|---|---|---|
| G1 fire at an enemy in reach | 10 | 10 | 2 | 8 | 10 |
| G2 an enemy behind | 10 | 10 | 10 | 8 | 10 |
| G3 a shell on a standing tank | 9 | 8 | 0 | 8 | 10 |
| G4 a strike drone coming | 10 | 8 | 10 | 10 | 7 |
| G5 low armour, home in sight | 10 | 1 | 0 | 0 | 1 |
| G6 a depot to take | 10 | 1 | 0 | 0 | 0 |
| G7 a pillbox from outside its ring | 9 | 3 | 0 | 0 | 3 |
| G8 out of a pillbox's ring | 3 | 6 | 2 | 0 | 4 |
| G9 a depot across the water | 10 | 0 | 0 | 0 | 0 |
| G10 keep station on a moving leader | 10 | 0 | 0 | 0 | 0 |
| G11 cover the leader against what appears ahead | 8 | 0 | 0 | 0 | 0 |
| G12 come back after a shell | 10 | 0 | 0 | 0 | 0 |
| G13 ore to the smelter | 10 | 0 | 0 | 0 | 10 |
| G14 oil to the refinery | 10 | 0 | 0 | 0 | 10 |
| G15 the convoy comes through | 10 | 10 | 10 | 10 | 10 |
| G16 the raider takes the convoy | 10 | 9 | 0 | 0 | 8 |
| G17 the chain to the city | 10 | 0 | 0 | 0 | 10 |
bc3 is a better clone than bc2 on a lone fight (G1 8 for 2, G3 8 for 0: forty matches for the four of bc2's day, and the economy's senses) and mines nowhere, as its labels said it would.
Thirty epochs at half the step on the same forty matches: squad 96.0 against the floor's 96.6, drive 77.7, gun 77.2, the rest as before. More passes over the same labels do not make a hundred and sixty-nine mines a policy.
A course on the fleet's maps. A scenario file may now say map = "maps/naval.map": the fleet's own map in place of one generated from the seed, with its deposits and wells as seed_maps.py wrote them, the seed still dealing the seats. Three courses in scenarios/training/, 11-the-fleets-industry, -island, -naval: nine seats in three sides, the learner at seat 0 as its base's guard, eight scripted Commanders on Auto round it sharing their team boards, four minutes; paid a quarter an ore, a half a shell smelted, the same for oil, and one for being alive at the end; passed when it mined four. An episode of nine seats and four minutes runs in about three tenths of a second, so a generation of the eight courses together (32 pairs, 16 episodes) took 17 s alone and about a minute beside the recorder. From nets/logistics.bin, with the five gauntlets in the same course so they are not forgotten, 28 generations before it was stopped for this record: the five gauntlets held (at generation 20: G13 10 of 10, G14 10, G15 10, G16 7, G17 10), and the fleet's maps 0 of 10 each, the same as the start net: on industry and island the reward is 0.00, the learner dead at the end of every seed; on naval 1.00, alive and idle on its island. A trace on industry says how it dies: it drives from its start into the ring of a neutral pillbox (the map's) and is shelled for 15 three times in the first fourteen seconds, then a hostile tank finds it at 24 s. The scripted brain's first act on these maps is to take that pillbox with its squad. The net has the capture choice now and no reason yet to use it; the course pays only the ore. Paul's premise holds in the reading: a net that mines in a field of two does not among sixteen, and the thing between them is not the mine but the first minute of a dealt match, which is what the phase courses and the gauntlets G5 to G9 are about and the nets read 0 to 6 on.
The census. Style 16, the Quartermaster, is nets/logistics.bin in styles.rs, so a census trial of bot_style = 16 reads it among sixteen seats on the fleet's maps. Asked of the fleet at 03:06 (fleet-queue.py grove new census trial="bot_style = 16" name=quartermaster seeds=400 adopt=no, run r0155); it was running on saskatchewan by 03:15. Its lines, ore, smelted, oil and supply on the composition line beside kills and built, are what section 130 asked for; they will stand on the console's Results → Personas page beside the eight personas when the four hundred seeds are in. Nothing is read from it in this section; it is the fleet's to finish.
What is next. The first minute: a course on the fleet's maps that pays leaving a pillbox's ring and taking it (pillbox_taken, damage_taken) before the ore, from the phase nets; a longer run than 28 generations on the Mac, or the fleet's train kind on a machine with the hours; weighting the rare labels in tools/bc if a clone is ever to mine, or recording matches where the guard is short (ore_low high) so the scripted brain mines on the record.
133. The first minute, the rare labels, the short guard, and the Quartermaster's census read, 3 November
Paul: "a first-minute course on the fleet's maps (leave the ring, take the pillbox) before the ore; rare labels weighted in the clone trainer, or matches recorded with the guard kept short so the scripted brain mines on the record; the census's lines when r0155 finishes." The three next lines of section 132, the same morning.
The census read first, since it was in. Run r0155, bot_style = 16, 400 seeds on the twelve maps, finished on saskatchewan by 06:49. The pooled score of the Quartermaster's team is −30.71 ± 0.68 a seat against the defaults (naval −13, europe −56), the worst of the net slots as every whole net has been (the Scholar read about −20 under the rules of its day). The lines Paul asked for, pooled over the 1,200 matches' team lines, the trial team against the two default teams in the same matches:
| line | the Quartermaster's team | the default teams |
|---|---|---|
| score | 20.53 ± 0.47 | 51.24 ± 0.56 |
| kills | 4.37 ± 0.13 | 17.39 ± 0.16 |
| deaths | 16.39 ± 0.12 | 13.25 ± 0.16 |
| built | 0.49 ± 0.04 | 23.89 ± 0.52 |
| road | 0.00 | 8.75 ± 0.26 |
| ore | 7.78 ± 0.32 | 6.01 ± 0.27 |
| smelted | 0.46 ± 0.07 | 5.08 ± 0.26 |
| oil | 5.05 ± 0.25 | 0.01 ± 0.01 |
| supply | 1,668 ± 58 | 2,371 ± 47 |
| held | 2.54 ± 0.07 | 4.74 ± 0.06 |
| workers lost | 0.24 | 0.73 |
| workers taken | 2.08 | 2.06 |
| pillboxes taken | 1.92 | 2.62 |
The trained squad head does among sixteen what it learned in the field of two: a team of Quartermasters mines more ore than a team of the rules and pumps oil where the rules pump none (the rules' oil run waits on a refinery of the side in reach, which they seldom have in ten minutes; the net pumps when a well is within eight tiles). It smelts a tenth of what the rules smelt, because a team of nets builds nothing (0.49 a match against 23.9): no smelter stands for its ore, and the squad head's build choice is offered only beside a deposit with the material and no smelter of the side within thirty, which among sixteen it reaches seldom. The ore is mined and carried and not delivered; the ore line is up and the smelted line is down. It takes a pillbox and a worker as often as the rules do, and dies more (16.4 for 13.3) and kills a quarter as much, which is the −31. The Results → Personas page carries the slot.
The guard kept short: not the lever. RECORD_TRIAL="ore_low = 20" gives every scripted brain of a recorded match dials over the defaults (fleet::with_settings round fleet::brain; a bad name panics, roles = 2 changes the record, so the dials reach the brains). Forty matches so recorded are byte for byte the forty of section 132: 1,708,039 samples, 169 mines. ore_reach = 128 beside it, the same. The guard of a dealt match is short already (a depot on an ore map starts with no shells and ore_low is 10); what keeps it from the ore is the smelter, which wants four material and a tank of the side with it beside a deposit within reach of the base, and in ten minutes a side builds about one. No dial of the ore run puts more mines on a record of dealt matches.
The courses on the record. RECORD_COURSE=g13,g14,g17 (files, in turn, a seed each) records the scripted brain on a course's learner seat as a match is recorded: a Recorder brain round the Commander the file would deal (course_brain: the file's goal, the defaults), opening a window every DECIDE_EVERY with open_window and closing it with write_sample, the two halves of the match recorder lifted out so both use one. Three hundred runs of the three in 3 s: 119,700 samples, 1,023 mines and 663 builds of a smelter (G17 has none to begin with), mine offered 22,148 times (the squad is out or the tank is driving for most of a run), so the scripted brain mines in one offered window in twenty-two. Six times the mines of forty matches, in a fourteenth of the samples.
The rare labels weighted. RARE=cap in tools/bc: a sample with label l of head h counts the head's commonest label's count over l's, no more than cap, in the loss (label_weights, into valid); RARE_HEADS=squad for some heads only. On the forty matches at RARE=20 over every head: drive 73.1 (78.0 without), gun 76.4, mortar 83.4 (92.5), drone 66.4 (92.2), squad 88.5 (95.8); every frequent-label head agrees less, as it must when a shot counts twenty. On the gauntlet it is the better brain all the same: G1 10 of 10 (bc3 8), G2 10 (8), G3 10 (8), G8 10 (0), G16 4 (0), and G13, G14, G17 still 0: a weight of twenty on a hundred and sixty-nine mines is not a policy either.
The matches and the courses together. tools/bc on the directory of both (1,827,739 samples; held out the matches' last four seeds and the courses' last thirty): on the matches drive 77.9, gun 86.2, mortar 92.5, drone 92.3, squad 96.2 (floor 96.6); on the courses drive 96.7 (floor 61.7), drone 99.7, squad 98.8 (floor 98.8). Rust's forward pass agrees to the decimal on the courses' range. With RARE=20 RARE_HEADS=squad the squad head reads 93.1 on the matches and 94.9 on the courses, the rest within a point. The clone of both is nets/bc4.bin.
| gauntlet | scripted | bc3 | bc4 | bc4 rare squad | logistics |
|---|---|---|---|---|---|
| G1 fire at an enemy in reach | 10 | 8 | 9 | 2 | 10 |
| G2 an enemy behind | 10 | 8 | 0 | 0 | 10 |
| G3 a shell on a standing tank | 9 | 8 | 10 | 8 | 10 |
| G4 a strike drone coming | 10 | 10 | 10 | 10 | 7 |
| G5 low armour, home in sight | 10 | 0 | 1 | 0 | 1 |
| G8 out of a pillbox's ring | 3 | 0 | 4 | 7 | 4 |
| G13 ore to the smelter | 10 | 0 | 0 | 0 | 10 |
| G14 oil to the refinery | 10 | 0 | 1 | 0 | 10 |
| G16 the raider takes the convoy | 10 | 0 | 0 | 0 | 8 |
| G17 the chain to the city | 10 | 0 | 2 | 3 | 10 |
(G6, G7, G9 to G12 and G15 as in the table above: 0, 0, 0, 0, 0, 0 and 10 for every clone.) The first clone that mines at all: two and three of ten on G17, one on G14, where no clone had one, and G2 lost (8 to 0) to the mixing, a course of one seat and no enemy behind it making a seventh of the samples. A clone's squad head wants its share of mines in the data, and the data wants the match's fights beside the course's runs in a proportion this morning did not find; the course trained by reward (section 131) is ten of ten on all three and remains the net that mines.
The first-minute course. scenarios/training/12-the-first-minute-{industry,island,naval}: the nine seats of course 11 for ninety seconds, paid two for a pillbox taken (the new count pillboxes, any pillbox the learner captured, the map's own included, beside pillbox_taken which counts the placed ones), one for being alive, a hundredth back for each point of damage taken; passed when a pillbox is taken. Held out, ten seeds: the logistics net 7 of 10 on industry, 7 on island (reward 0.64: the pillbox is taken and the tank is dead by ninety seconds, 76 points taken on average), 0 on naval (no pillbox within its squad's reach of the start; alive, reward 1.00). Forty generations from it with the five gauntlets in the course (36 s a generation beside the recorder): the population's mean 5.18, 5.19, 4.87, 6.06, 4.92 at 0, 10, 20, 30, 40, the net's pass share 0.62 to 0.81 and back, and the checkpoint at 40 reads what the start read: 7, 7, 0, the gauntlets kept (G14 9, G16 6). The net takes the pillbox already; what it does not do is live, and ninety seconds of nine seats pays "alive" once at the end, a signal the search did not find in forty generations. The course is kept for a longer run with a potential on the armour (armor_gained) and damage_taken weighted more.
What is next. The first minute wants the armour paid as a potential and a run of hundreds of generations on a machine with the hours (the fleet's train kind); a clone that mines wants the courses in proportion (the matches' fights kept, the courses' mines a tenth of the squad labels, not a hundredth); the census of r0155 wants a smelter the Quartermaster's side can deliver to, which is the quartermaster of the rules on its team: a census of a mixed team, two of the rules and one Quartermaster, is the next read.
134. The armour as a potential, the courses in proportion, and a mixed team for the census, 3 November
Paul: "the first minute with the armour paid as a potential and hundreds of generations on the fleet's train kind; the courses in proportion on the clone's record, the matches' fights kept; a census of a mixed team, two of the rules and one Quartermaster, so its ore has a smelter to go to." The three next lines of section 133.
The first minute, to the fleet. The three courses 12-the-first-minute-* pay armor_gained one (the armour at the end less at the start, a share of full: a potential the search feels before "alive", which pays once at the end) and take back two hundredths a point of damage. Asked of the fleet as its train kind: run r0158, from nets/logistics.bin, the five gauntlets and the three courses, 400 generations, 32 pairs, 16 episodes a generation, 20 held out; its net and log come back to out/r0158-train when a machine has had the hours. Nothing is read from it here.
The courses in proportion. A thousand runs of G13, G14 and G17 on the record (398,900 samples, 4 s), beside the forty matches (1,708,039): the courses a fifth of the samples, where the morning's three hundred were a fourteenth; the fights kept by RARE=5 RARE_HEADS=gun,mortar,drone, so a shot, a lob or a launch counts up to five where a hold counts one. Held out, the matches' last four seeds: drive 77.5, gun 75.6, mortar 92.2, drone 90.8, squad 96.2; the courses' last hundred: drive 98.6, squad 98.9 (floor 98.8). The clone is nets/bc5.bin.
| gauntlet | scripted | bc3 | bc4 | bc5 | logistics |
|---|---|---|---|---|---|
| G1 fire at an enemy in reach | 10 | 8 | 9 | 6 | 10 |
| G2 an enemy behind | 10 | 8 | 0 | 1 | 10 |
| G3 a shell on a standing tank | 9 | 8 | 10 | 0 | 10 |
| G4 a strike drone coming | 10 | 10 | 10 | 10 | 7 |
| G7 a pillbox from outside its ring | 9 | 0 | 0 | 4 | 3 |
| G8 out of a pillbox's ring | 3 | 0 | 4 | 7 | 4 |
| G13 ore to the smelter | 10 | 0 | 0 | 2 | 10 |
| G14 oil to the refinery | 10 | 0 | 1 | 3 | 10 |
| G16 the raider takes the convoy | 10 | 0 | 0 | 0 | 8 |
| G17 the chain to the city | 10 | 0 | 2 | 9 | 10 |
(G5 0, 0, 1, 0, 1; G6, G9 to G12 0 for every net; G15 10 for all.) The clone mines now: nine of ten on the chain, three on the oil, two on the ore, where the morning's had two, one and none. It paid for it in the lone fight: G3 0 where bc4 had 10, G2 1, G1 6. Five times the fights in the loss did not hold them against a fifth of the samples with no enemy in them. A clone's heads pull against each other through the shared hidden layers when the data for one head comes from a world the other head's data never sees; the course trained by reward has no such seam, and remains ten of ten on all three with its fights kept. The proportion that keeps both is not found; what is found is that it is a proportion, and that the record of a course of one seat teaches the fight to hold.
The mixed team. bot_mix, a choice (all | one | two | half): how many seats of a census's trial team play its bot_style, the rest the rules; the style goes to the team's last seats, so the leader and the guard, which build the base and the smelter, stay the rules. Asked of the fleet: run r0159, bot_style = 16, bot_mix = one, 400 seeds, named quartermaster-one; its ore and smelted lines against r0155's will say whether a Quartermaster whose side builds the smelter delivers what it mines. Nothing is read from it here.
135. The clone: the courses with an enemy in them, and the squad head trained apart, 3 November
Paul: "For the clone, a record of the courses with an enemy in them, or the squad head trained apart from the fight." Both tried while the fleet runs r0158 (the first minute, 400 generations) and r0159 (one Quartermaster beside two of the rules), which were still running at this writing.
The courses with an enemy in them. scenarios/training/13-g13-with-an-enemy, -g14-, -g17-: the three squad gauntlets with the far sitter replaced by a scripted Commander on Auto eighteen tiles off, for the record and not the gauntlet. A thousand runs: 126,097 samples, 1,052 mines, 366 shots, moving 63.5% (the gauntlets' 37.5%): the fight is on the record beside the mine. The fleet-map courses recorded the same way (three hundred runs of nine seats, 245,866 samples) carry 170 mines and 35 shots, the guard of a dealt map mining about as seldom as in a dealt match. The clone of the forty matches, the fleet courses and the enemy courses together, the fights weighted five (bc6): matches drive 77.4, gun 83.7, squad 96.4; the enemy courses drive 77.2, gun 94.4, squad 99.1 (floor 99.1). On the gauntlet it keeps the fight (G1 6, G2 10, G3 5, G8 7) and mines nowhere (G13, G14, G17 0), where bc5, from the same courses without the enemy, mined 2, 3 and 9 and lost the fight. With an enemy in the course the scripted brain's run is the fight first and the mine after, and the clone learned the first.
The squad head trained apart. TRAIN_HEADS=squad in tools/bc: only the named heads in the loss, from a START clone. From bc3 on the forty matches and the thousand enemy-free runs (bc7): the squad head reads 96.2 and 99.5 (floor 98.8), and the drive head, out of the loss, fell from 78.0 to 34.7 on the matches and to 18.2 on the courses. The hidden layers are shared: training one head moves what every head reads, and the others, out of the loss, drift wherever the squad's gradient takes the layers. On the gauntlet bc7 fires (G1 10, G2 10) and mines nowhere, and G4 fell to 0. A head trained apart wants the hidden layers frozen, which the trainer does not do; the fix is a flag on the layers, not another recording.
The reading of the four clones of the day. bc3 (matches) fights and holds; bc5 (matches and the squad courses) mines and loses the fight; bc6 (with an enemy in the courses) fights and holds; bc7 (the squad apart) fires and drifts. No clone has both, and the net trained by reward on the five courses has both. The clone's road to the mine is a frozen trunk or a separate net for the squad head; neither is built, and neither is needed while the reward-trained net plays. bc6 and bc7 are not shipped; the data files are under build/bc/ on this Mac. The 13-*-with-an-enemy files stay as recording courses.
136. The fleet's two reads: the first minute at 400 generations, and one Quartermaster beside two of the rules, 3 November
Paul: "pool the census team lines, write LR 136." The two runs asked in section 134 came back from the fleet by early afternoon: r0158 (saskatchewan, the first minute, 400 generations, 3 h 57 m) and r0159 (macmini, the mixed-team census, 400 seeds).
The first minute, 400 generations. From nets/logistics.bin on the five logistics gauntlets and the three first-minute courses with armor_gained paid one: the population's mean 4.66 at generation 0, 5.83 at 100, 5.46 at 200, 5.30 at 300, 6.21 at 350 and 5.43 at 399; the best of a generation between 5.4 and 7.9; the net's own pass share between 0.69 and 0.81 the whole run, where it began at 0.81. The checkpoint is nets/first-minute.bin. Held out on twenty seeds:
| course | passed of 20 | reward | at the start (10 seeds, LR 133) |
|---|---|---|---|
| G13 ore to the smelter | 20 | 9.42 | 10 |
| G14 oil to the refinery | 19 | 9.72 | 10 |
| G15 the convoy comes through | 20 | 6.00 | 10 |
| G16 the raider takes the convoy | 15 | 1.15 | 8 |
| G17 the chain to the city | 20 | 22.27 | 10 |
| 12 the first minute, industry | 14 | −1.61 | 7 |
| 12 the first minute, island | 14 | −1.51 | 7 |
| 12 the first minute, naval | 0 | 1.00 | 0 |
Four hundred generations read what forty read and what the start net read: seven of ten on industry and island, none on naval. The reward on the two passed maps is negative: the pillbox is taken (two) and the tank is dead by ninety seconds, the damage taken back at two hundredths a point outweighing the pillbox and the armour, so the potential paid on the armour did not give the search a gradient toward living, and the mean of the population wandered within a point of where it began. Naval has no pillbox within the squad's reach of the start, and the net sits alive for 1.00 every time: there is nothing on that map for ninety seconds to pay for, which is a fault of the course, not the net. The ten-seed gauntlet beside the logistics net: the first minute net keeps the squad (G13, G14, G15, G17 ten of ten; G16 10 where logistics has 8) and the fight (G2 10, G3 10, G4 10 where logistics has 7; G1 9), and loses a point on G5 (0 for 1) and G7 (1 for 3). It is the better net on the gauntlets and no better in the first minute. The search as run, 32 pairs and sigma 0.05 on 17,519 weights, does not find "survive ninety seconds among nine" from a reward that pays the pillbox once and the damage every tick; what the course wants is either a shorter horizon (the pillbox taken and then home) or the damage paid less and the ninety seconds alive paid as a potential too, and either is a change to the course before another four hours of a machine.
One Quartermaster beside two of the rules. Run r0159, bot_style = 16, bot_mix = one, 400 seeds on the twelve maps, finished on macmini. The pooled score of the trial team is −5.05 ± 0.47 a seat against the defaults, where the whole team of Quartermasters read −30.71 (section 133). By map: naval +0.13 and constanine −0.47 inside the noise; island −3.2, fortressislesx −3.4, generated −4.2, fidos-airstrip −4.7, industry −6.0, fortressisles2 −6.9, atlantic −7.8, europe −8.5, big-lizard −10.5, every one outside the noise. The lines, pooled over the 1,200 matches' team lines (the trial team's lines are the whole team's: two of the rules and the one Quartermaster; the seat's own share is the difference between the columns):
| line | one Quartermaster, two of the rules | the default teams | difference | r0155, a team of Quartermasters |
|---|---|---|---|---|
| score | 42.45 ± 0.74 | 47.50 ± 0.55 | −5.05 | 20.53 |
| kills | 13.41 ± 0.21 | 16.60 ± 0.17 | −3.2 | 4.37 |
| deaths | 17.58 ± 0.19 | 15.23 ± 0.14 | +2.3 | 16.39 |
| built | 10.54 ± 0.32 | 13.61 ± 0.29 | −3.1 | 0.49 |
| road | 6.05 ± 0.25 | 8.18 ± 0.24 | −2.1 | 0.00 |
| ore | 3.99 ± 0.25 | 4.26 ± 0.21 | −0.3 | 7.78 |
| smelted | 2.25 ± 0.20 | 3.42 ± 0.19 | −1.2 | 0.46 |
| oil | 0.58 ± 0.06 | 0.01 ± 0.00 | +0.6 | 5.05 |
| supply | 2,011 ± 59 | 2,098 ± 44 | −86 | 1,668 |
| held | 3.99 ± 0.08 | 4.10 ± 0.06 | −0.1 | 2.54 |
| workers lost | 0.77 | 0.91 | −0.1 | 0.24 |
| workers taken | 2.29 | 2.25 | 0.0 | 2.08 |
| pillboxes taken | 2.45 | 2.33 | +0.1 | 1.92 |
The question of section 134 was whether a Quartermaster whose side builds the smelter delivers what it mines: it does not. With the leader and the guard of the rules on its team the smelter stands (built 10.5 a match, where a team of nets built 0.5), the team's ore is what a team of the rules mines (4.0 for 4.3), and the team's smelted line is two thirds of the rules' (2.25 for 3.42). The seat the Quartermaster took was one of the rules' ore runners (every tank of the rules runs ore once after each fill-up), and in its place the net mines about as much and delivers 1.2 shells less a match: the ore it carries does not come to the smelter, which is what r0155 said at a larger scale (ore up, smelted a tenth) and the field of two does not say (G13 ten of ten). The oil line is the one thing the seat adds, 0.58 a match where the rules pump 0.01, and nothing uses it: the refinery of a ten-minute match seldom stands. The rest of the seat's cost is the fight: three kills fewer and two deaths more a match than a rules seat, and three builds and two road tiles fewer, which is the −5. So one Quartermaster costs a team five points a seat where sixteen cost thirty-one, and the saving is the rules' leader and guard, not anything the net does better in company.
Two notes on the read. The rules' own lines moved between r0155 and r0159 (ore 6.0 to 4.3, smelted 5.1 to 3.4, score 51.2 to 47.5 for the default teams), with no change to the rules between the two commits that was meant to move them; the two runs are compared within themselves above, and the drift is a thing to watch across the next censuses of game 8. And the trial team's lines in a bot_mix census are the team's, so a seat's share is read as a difference; a per-seat ore and smelted line in the census log would make the next such read direct.
What is next. The first-minute course wants its reward reshaped (the damage paid less, alive paid as a potential, or a shorter horizon ending at the pillbox) and a pillbox placed within reach on naval, before another train run. The Quartermaster's squad head wants the delivery, not the mine: a course where the smelter is the side's and far, with the ore aboard at the start, so the drive to the works is what the reward pays; and the census log a per-seat ore and smelted. nets/first-minute.bin is shipped as the better gauntlet net; style 16 stays the logistics net until a net reads better among sixteen.
137. The three follow-ups of 136: the first minute reshaped, the delivery course, and the census seat by seat, 3 November
Paul handed back the follow-up list of section 136 as the next job: "reshape the first-minute reward and put a pillbox within reach on naval before another train run; a delivery course for the squad head: the smelter far and the side's own, ore aboard at the start; a per-seat ore and smelted line in the census log, so a mixed-team read is direct." The fourth line, the rules' drift between r0155 and r0159, is a thing to watch, not to build.
The first minute reshaped. The three courses 12-the-first-minute-* pay alive_time one (the share of the ninety seconds the tank lived, a potential the search feels every tick, where alive paid once at the end) and take back half a hundredth a point of damage (two hundredths in section 134, which outweighed the pillbox and the armour and made the reward of a passed run negative, −1.6). Held out, ten seeds, the logistics net: industry 7 of 10 at reward 0.58 (was −1.61), island 7 at 0.58 (was −1.51); the same shares as before, the reward now the right sign, so a run that takes the pillbox and lives longer is paid more than one that does not. The scripted brain on the same courses: industry 0 of 10, island 7 of 10. The scripted Commander on defend_base at seat 0 does not take industry's pillbox in ninety seconds at all; the net does, seven times in ten.
Naval's pillbox. Naval's starts are at sea on landing craft, and the [[pillbox]] placer walked from the start toward the centre asking for a land route from the anchor at every step, which a boat has to nowhere: "no open ground toward the centre" at every distance tried. The placer now skips the route when the anchor is afloat (afloat: a boat stands where the anchor is) and takes the first open ground toward the centre, where the craft lands; the course places one neutral pillbox fourteen tiles south of seat 0's start, on the island with the neutral depot. It stands and nobody takes it: the logistics net 0 of 10 at reward −1.05 (shelled on the way in), the scripted brain 0 of 10 at 1.00 in ninety seconds and at 0.41 in a hundred and eighty (it lands, is hit, and takes nothing). The course is honest now and no brain passes it, so it stays out of the train run below until the landing is a thing a brain does; the naval map's first minute is a landing, not a drive out of a ring.
The delivery course. scenarios/training/14-the-delivery: G13's shape with no deposit, the tank starting with eight ore aboard (ore = 8 on a [[tank]], new) and the side's smelter far to the west with its depot beside it, empty, two minutes; passed when a shell is smelted; the drive paid as the closer potential, the unload by ore_in (new: the ore the seat unloaded, from OreUnloaded, an expectation and a reward term), the shell last. The distance was found by reading three: with the smelter at twenty-four tiles the logistics net delivers 10 of 10 (reward 19.0), at forty 6 of 10 (6.55), at sixty 0 of 10 (0.48); the scripted brain 10 of 10 at every distance (about 20). The course is set at sixty. The net learned the drive to a works eight tiles off (G13) and does not carry it past forty: the senses hold the side's nearest working smelter as a bearing and a nearness, and at sixty tiles either the nearness has gone to its floor or the works is outside what the seat observes, and the net has no run in its training that began that far away. This is the gap r0155 and r0159 read among sixteen: in a dealt match the smelter the leader builds stands where the base is and the net mines where the deposit is, and the way between is the forty tiles the net does not drive. The course is the lever.
The census seat by seat. fleet.rs prints, after the trial team's line and only when a style is on the trial, a line for each of its seats: seat N (<style> | rules): score, kills, deaths, built, ore, ore in, smelted, oil, supply, with styled computed once from the same bot_mix rule the seating uses (the rule itself is hoisted out of the seating closure to serve both). ore in is the ledger's new ore_in; smelted stays the smelter owner's, as the ledger credits it, which is why a seat's delivery is its ore in and not its smelted. The lines begin with seat, so orrery's readers of team (\d) lines (tools/terrain.py, the fortify report) read past them.
Asked of the fleet. Three runs, after this commit was pushed so the machines build it: r0163, train first-minute-2, the five logistics gauntlets and the industry and island first-minute courses from nets/first-minute.bin, 400 generations; r0164, train delivery, the five gauntlets and the delivery course from nets/logistics.bin, 300 generations; and r0165, the census of one Quartermaster beside two of the rules again (quartermaster-one-seats, 400 seeds) for its seat lines, which say directly what r0159 said as a difference. Nothing is read from them here.
138. The Duelist with world senses: crucible's world champion in the game as the Gladiator, and the bridge that reads each new one, 4 November
Paul, to the second of three steps orrery offered that morning ("Champions enter the line: a GPU stage on the Pipeline, the world champion bridged into treadcore, read like a candidate"): "yes do step 2, the duelist with world senses." Since the evening of 3 November crucible's islands train under the game's rules (orrery's WORLD build: the tank's speed, turn and braking, the gun of ten tiles at forty a second for 45 every four seconds, 24 shells, eyes of ten tiles, four neutral pillboxes of range 8; 55 inputs, two hidden layers of 48, 5,236 weights). The bridge of section 126, style 7, reads only a hybrid of the old toy arena: 35 inputs, rays to forty tiles, the toy's yardstick. A world champion fed those 35 would hear a world it did not train in.
What a world champion is given (styles.rs, world_senses, as crucible.cu's sense under WORLD builds them). The thirty-two of the arena as before, but that a tank further than TANK_VISION is seen on no ray (its bearing and armour still are, as crucible gives them), the rays meeting a tank at crucible's radius of one tile; the yardstick at the world's range (the turn is the bearing as a heading error, it stands off at eight, it fires when aimed within the gun's ten); then the twenty of the world: twelve rays to a live pillbox that would fire on this tank (a neutral one or an enemy's; a side's own is nobody's threat), the nearest such pillbox as a sine and a cosine of its bearing, its reach, its reload and health as shares, and 1 when it has the tank (in PILLBOX_RANGE, a clear line on the map), and the tank's own shells and reload as shares. The incoming shell is the nearest hostile direct shell where it is along its flight now, as crucible's is. What the game has and the arena has not is left as the bridge always left it: the rules keep everything but the fight, the gate is the champion's, and among many enemies the duel is with the nearest within forty tiles.
How it drives. As crucible's world driver: the turn output names a heading error (-1 to 1 is a half turn either way) and the throttle has no reverse, so a throttle at or under a twentieth holds the tank (Stop) where the hybrid would back off. A Move goes STRIDE six tiles along the heading at full throttle, as style 7's. The gun stays the rules' (section 126: the champion's fire output means "my hull is on him", which a turret does not need).
Slot 17, the Gladiator. A slot once given keeps its meaning, so style 7 keeps the hybrid it has always played and the world champion is a new style, Gladiator, "the rules, with a champion of crucible's world islands in every fight". The roster is 18; bot_style's choices name it. Champion reads either size by its length and refuses a plain build's (no gate, 3 outputs: it would take every fight). nets/gladiator.bin is the world ladder's newest milestone that morning, M0015, calber's champion at generation 241,300 (nets/styles.tsv).
The rule before the run (written before either census began): the Gladiator is better than the Duelist if, on the same 48 seeds, its read against the defaults is higher than the Duelist's by more than two standard errors of the paired difference; otherwise the world senses have not shown they help.
Read. Two pairs on this Mac (eight jobs, 900 s, respawns 3), the trial team all Gladiator or all Duelist against the defaults, the pairs seed by seed (a seed: lists, the same jobs). The first pair asked for every map and played the generated one only: BSD paste on a Mac drops the map list, the slip orrery's census jobs shim since 1 November and a census by hand here did not. Both of the pair played the same, so the pair stands; the second pair plays every map.
| Read, a seat against the defaults | generated maps, seeds 0 to 47 | every map, seeds 0 to 47 |
|---|---|---|
| Gladiator (style 17, world M0015) | −2.25 ± 0.99 | −2.53 ± 1.21 |
| Duelist (style 7, hybrid) | −13.80 ± 1.35 | −7.52 ± 1.60 |
| Gladiator less Duelist, paired | +11.56 ± 1.75 | +4.99 ± 2.08 |
The rule is met on both pairs: +11.56 ± 1.75 (6.6 errors) on the generated maps and +4.99 ± 2.08 (2.4 errors) on every map. The gain is smaller on the fleet's eleven maps than on the generated one, and two maps lean the other way, inside the noise a map at four seeds (fortressisles2 −8.93 for the Gladiator against +3.43 for the Duelist, europe −7.83 against +3.12); island (+3.16 against −15.95) and big-lizard (+2.59 against −12.26) carry most of it. The Gladiator still reads under the defaults, −2.53 ± 1.21 a seat on every map: the rules without a champion are still the better fighter, by less than the Duelist ever was. The Gladiator ships; style 7 stays as it is.
On the fleet: the bridge (orrery tools/plans/treadcore.py, bridge_jobs; tools/bridge.py). The game carries its champion in its binary, so the fleet reads a champion not yet shipped by GLADIATOR=file, which makes slot 17 play that file (gladiator_file; a file that is not a world champion stops the match with a sentence, since a census on the wrong net would be read as the right one). Each new milestone of the world islands is read so: two censuses on the first fast machine, the candidate and the shipped Gladiator, the same 48 fresh seeds from 80,000,000, every map. Better by more than two errors of the difference raises a flag to ship it; shipping is a commit, so it is a person's press (fleet-queue.py QUEUE bridge ship LABEL, or Ship on orrery's Results, Growth), which tests and commits on a worktree of origin/main and pushes. The bridge waits until this commit is on main.
Not done. The twenty senses match crucible's arithmetic by reading the two side by side and by the tests here (a pillbox ahead that has the tank, one of the side's own that is nobody's threat, an enemy past the eyes); crucible's own sense was not run on a game state, as it runs only on a card. The client's tests key_setup_shows_the_pick_and_the_wait and the_page_moves_a_weight_and_puts_it_back fail on main before this change and after it, untouched here.
139. The score counts building and the base: a thing built half a kill, the guns' life and kills, repairs, wood by the base, and guns ready; the score versioned, 4 November
Every candidate the search had made by the morning of 4 November was held by the trade guard of section 127 (orrery, trades_away): each scored more by building less, w8466 +1.27 a seat held out for 1.48 fewer things built a team, w8669 +2.42 for 3.07 fewer and less road. A thing built was worth a tenth (a kill is 1, a win 10), so the search sold building cheaply and the guard held the line still. Asked whether to promote one by hand, raise the floor, keep holding, or count building, Paul: "Count Building the score. give well defended pillboxes average time duration and longest best turrent survivial and its killrate and for bases, repair rate and usefull rate. in fact, workers mining forrests around bases should receive reward, because now base fills faster." Asked the numbers, he chose each of these.
What the score counts now (ledger.rs, ScoreWeights, all in rewards but the build, which is the score's own term):
| Term | Worth | What it is |
|---|---|---|
| a thing built | 0.5 (was 0.1) | half a kill |
| repairs | 0.25 a repair | a pillbox or structure of the side mended by the seat's squad (PillboxRepaired, StructureRepaired) |
| wood near a base | 0.2 a load | a tree the seat's squad felled with wood gained, within BASE_REACH 16 tiles of a depot of its side (Harvested) |
| pillbox life | 3 at the whole match | the team's: the share of the match each pillbox it held stood alive in its hands, averaged over them |
| the longest turret | 2 at the whole match | the team's: its longest-standing pillbox's share |
| turret kills | 1 a kill | the team's: a hostile tank its pillbox killed (a PillboxFired on that tank in the tick it was destroyed by the pillbox's owner); the owner's seat counts the kill among its own as before |
| turret rate | 2 at a kill a pillbox every ten minutes | the team's turret kills over its pillbox-minutes, never fewer than ten |
| guns ready | 3 at the whole match | the team's: each second, the share of its pillboxes within 16 tiles of a depot of its side that stand alive with shells ("useful rate": its guns ready) |
The team's terms are each of its seats', as castles are. CENSUS_REWARDS=0 is still the score of sections 8 to 83, a thing built at its tenth. The census prints a line a team beginning base of team, which orrery's readers (anchored at team and rewards of team) read past.
Versioned. source/treadcore-brain/SCORE_VERSION (1) beside GAME_VERSION. Paul, asked whether reads under the old score should keep counting: "Start anew". orrery's planner now takes a commit's game as both numbers, so a read of the old score is no evidence about the new, as a read of an old game is not: the waiting candidates are read again before any promotes.
The rule before the run: under the new score, w8466 against the defaults on the same 48 seeds reads under its read of the old score by more than two errors of the paired difference, if the new terms see what it gives away.
Read (this Mac, every map, 48 seeds, 900 s, respawns 3): w8466 +1.95 ± 1.20 a seat under the old score, +1.54 ± 1.20 under the new, paired −0.42 ± 0.15 (2.8 errors). The rule is met: the new score sees the trade, and w8466 still reads above the defaults, so the guard, not the score, is what holds a candidate that builds a team's worth less. Over the 432 teams of that census the new terms run, a team a match: 21.5 built (to 98), 4.7 repairs, 1.45 loads of wood near a base, pillbox life 0.13 and the longest 0.26 of the match, 0.05 turret kills, guns ready 0.16 of the match. Building carries the most points; turret kills are rare enough that their rate weighs little yet.
Not done. A scenario's reward terms do not name the new counts; the trade guard's floors are unchanged (1 built, 0.5 road a team, 0.1 kills less deaths a seat); the weights are Paul's, not the search's, as the rewards of section 84 are.
140. The Duelist's gate a dial, a clone judged on one seat beside the rules, and crucible's partners in the game's styles, 4 November
Two of Paul's answers of the afternoon. Of the Gladiator, still under the defaults by about two and a half a seat (section 138): "The gate as a dial" and "Islands meet the game's styles". Of orrery's clone stage, whose first round read the fresh clone −42.7 a seat as a whole team of nets against −39.9 for the shipped Scholar, inside the noise: "One seat beside the rules".
The gate as a dial. duel_gate (weights.rs, RANGES −1 to 1, the champion's confidence being a tanh): a champion takes the fight only when its confidence is over it, for styles 7 and 17 alike; 0 is the gate as trained, so nothing plays differently by default. The rules' settings carry it, as every dial of a seat, and Duelist::gate reads them there.
The rule before the run: a gate above 0 becomes the Gladiator's default only if it reads better than gate 0 on the same 48 seeds by more than two errors of the paired difference.
Read (this Mac, every map, 48 seeds, 900 s, respawns 3; the score of section 139), the trial team all Gladiator at each gate against the defaults:
| Gate | Read, a seat | Less gate 0, paired |
|---|---|---|
| 0 | −2.58 ± 1.27 | |
| 0.3 | −2.50 ± 1.16 | +0.08 ± 1.37 |
| 0.6 | −0.97 ± 1.52 | +1.61 ± 1.50 |
No gate passes the rule: the default stays 0. At 0.6, a champion kept to the fights it is surest of, the Gladiator reads inside the noise of the defaults; it leans one error better than its own gate, which is the read a larger census must settle.
One seat beside the rules. SCHOLAR=path makes slot 6 play that net instead of the shipped one, as GLADIATOR does for slot 17. orrery's clone stage now judges a clone by bot_style = 6 with bot_mix = one: the clone on the team's last seat, its leader and guard the rules, against the shipped Scholar on the same seat and seeds. A whole team of nets loses forty a seat for want of building and crews, and that gap drowned the difference the stage is there to read.
The game's styles in crucible's arena (crucible cab1adf). A world island's sparring partner now draws a style besides its aim and range: the script as it was, or treadcore's Raider (closes to four and presses while it is not the weaker or the enemy is hurt), Castle (holds its ground and fires on what comes into reach), Expander (shells the nearest live pillbox first) or Timid (turns away unless the odds are well on its side). The yardstick and the hybrid's own answer stay the script, and a build without WORLD trains row for row as before.
The fleet's read (run r0174, macmini, 400 seeds from 82,000,000, every map; the trial team the Gladiator at gate 0.6, the base teams the Gladiator at gate 0, so the census is the difference itself): +0.08 ± 0.40 a seat, inside the noise; naval alone +2.18 ± 0.60, every other map inside its noise. The 48 seeds' lean (+1.61 ± 1.50) was luck. The default stays 0, as the rule written before both reads says.
Not done. The champions trained against the styles, which the bridge will read as they come.
141. The boundary as a test: five gauntlets of which pillbox to shoot, which tank to answer, and when to kite or stand, 4 November
Paul, of the montage's first film, where the champions fought only one pillbox in twelve chapters: "we need to setup a goal where it would be smart to attack some pillboxes while avoiding others. like enemy preference, last attacked enemy we would kite and dance, unless we have backup with fleet allies nearby. we should solve it by creating a coded set of tests that should result into that being the boudray of your question." The gauntlet (section 102) is that form already: one decision a file, little else on the map, read for the scripted brain and every net on the same seeds, a floor a file.
The game's own rules draw the line. A tank's gun reaches ten tiles and a pillbox's ring eight, so a pillbox can always be shot from outside its ring. A live hostile pillbox must be shot dead before a squad can take it ("pillbox is live and theirs: shoot it dead first"), but a live neutral one is taken whole, and fires on whoever enters its ring. So the smart thing with a hostile pillbox is to shoot it from the edge of the gun's reach, and with a neutral one to keep out of its ring and leave it to the squad.
| File | The decision | Passed | Floor |
|---|---|---|---|
| G18 shoot the hostile pillbox, spare the neutral one | a hostile and a neutral pillbox 9 tiles off either way | the hostile one dead in 12 s, the neutral one untouched, the armour whole | 0.8 |
| G19 two pillboxes, one at a time | two hostile pillboxes in a line at 9 and 14 | both dead in 30 s, the armour whole | 0.7 |
| G20 the one that shot you | a passive enemy at 7, one at 9 that opens fire while the tank's gun reloads | its first hit is on the attacker | 0.7 |
| G21 alone: kite the one that shot you | a Raider of the same armour closing from 9 | more hits given than taken in 40 s, and alive | 0.6 |
| G22 with friends near: stand and fight | as G21, two friends beside it holding their ground | the attacker dead in 30 s, more hits given than taken, within 10 tiles of a friend for seven tenths | 0.7 |
G21 and G22 are a pair: alone the smart tank kites, with backup it stands; a brain that does one in both places fails one of them. The scenario form gained three expectations: pillbox_struck (the damage a placed pillbox took), first_hit (of the tanks a seat hit, the first) and pillboxes_standing (how many placed pillboxes stand at the end). Two were found weak and changed before this record: G22's friends first escorted the tank, so staying with them was theirs and not its choice (an untrained net passed 44%), and they now hold their ground; and two hits asked of the tank were most of a kill that friends share (the rules 14%), so it now asks the tank to outscore the attacker.
Read (this Mac, 50 seeds from 9,500,000, the share passed):
| the rules | untrained | map7 | map5 | bc2 | logistics | |
|---|---|---|---|---|---|---|
| G18 | 24% | 0% | 0% | 0% | 0% | 0% |
| G19 | 0% | 0% | 6% | 0% | 0% | 0% |
| G20 | 48% | 0% | 8% | 4% | 0% | 2% |
| G21 | 34% | 2% | 0% | 26% | 0% | 6% |
| G22 | 56% | 30% | 20% | 40% | 16% | 8% |
No brain reaches the boundary, the rules included, and each fails as its file says: the rules drive into the neutral pillbox's ring to take it and trade fire with it (G18: 55 to 65 damage to it, 45% of the tank's armour gone); they take the near pillbox of the pair and walk into the far one's ring (G19); they answer the nearer tank that did nothing (G20); and alone they trade blow for blow and die (G21). The floors are the boundary and are left above every brain: these five are red on the Gauntlet page until a brain earns them.
The floors, today's success (the same evening; Paul: "ajust your floors, keep what we got, maybe normalize some, but it should worke. adjust the floors to be what we have now as sucess, so we can create generations from here forwards"). The five were read again on 200 seeds: the rules 35.0 ± 3.4% on G18, 0% on G19 (map7a the best, 18.5%), 44.0 ± 3.5% on G20, 24.5 ± 3.0% on G21, 53.5 ± 3.5% on G22. Each floor is now the rules' share less two errors of the fleet's hundred seeds, to the twentieth below: G18 0.25, G19 0, G20 0.30, G21 0.15, G22 0.40. The pass conditions stay the boundary; the floors are where play stands today, so a generation that falls below one is a step back, and every one above it is a step on. G19's stays 0 until the rules earn one. The floors of G1 to G17 are kept as they were.
Not done. The styles (the Duelist, the Gladiator) in the gauntlet beside the rules and the nets; the rules taught these decisions; crucible's arena given a hostile and a neutral pillbox, so its champions meet G18's choice.
142. The one that shot at me answered first: w_shooter, the first of the five taught to the rules, 4 November
The first of the boundary's five decisions taught to the rules (section 141; Paul, asked which first: "Teach the rules the five", then crucible, then the styles in the gauntlet). G20: two enemy tanks, a passive one at 7 tiles and one at 9 that opens fire while the tank's gun reloads; the rules answered the nearer one about half the time.
Why the rules missed it. The tactician's target score (Tactician::rate) has no term for who is shooting, and in G20 the tank is not even asking it: hostile tanks near its start put it in defence (defend_within), whose threat is the nearest hostile tank inside the leash and nothing else. A first dial in rate alone moved nothing at any weight (51 of 100 at 0, 1, 2 and 4), which is how the defence path was found.
What changed. Tactician::heed_shots, each think: a hostile direct shell in the air aimed within two tiles of the tank names its owner, remembered for SHOOTER_MEMORY ten seconds (two of a gun's reloads and a half; a shell that misses still says who is shooting). The target score adds w_shooter for that tank ("it shot at me"), and the defence takes it as w_shooter four tiles nearer than it is, so it is answered before a nearer tank that has not fired and not before one far nearer. w_shooter (weights.rs) is a target dial as w_threat is.
The starting number, from a sweep (G20 for the rules, 200 seeds): 0 (as before) 44.0%, 0.5 45.0%, 1 53.5%, 2 72.0%, 3, 4 and 6 73.0%. The default is 2.
The rule before the run: at 2 against 0, the census on the same 48 seeds of every map must not read worse by more than two errors, and the families must hold.
Read. Census, w_shooter 2 against 0: −0.57 ± 0.77 a seat, inside the noise; every family holds its floor. The gauntlet for the rules, 200 seeds, every file before and after: G20 44.0% to 72.0%, every other file the same to the seed (G22 53.5% to 52.5%). G20's floor rises with it to today's success, 0.60, as Paul asked of the floors ("adjust the floors to be what we have now as sucess, so we can create generations from here forwards").
Not done. The four other decisions: G18 (keep out of a neutral pillbox's ring, leave it to the squad), G19 (one pillbox at a time from outside the other's ring), G21 (kite alone), G22 (stand with backup).
143. The gauntlet's floors guard every promotion, and every floor is today's success, 4 November
Paul, of the floors: "adjust the floors to be what we have now as sucess, so we can create generations from here forwards", and asked whether they should guard every promotion as the families do: "Yes, a guard on promotions."
The gauntlet reads a candidate. GAUNTLET_BRAINS=scripted reads the rules alone (twenty-one seconds for every file at a hundred seeds on this Mac, where every brain is ten times that), and GAUNTLET_WEIGHTS=file gives the learner's commander a weights file's dials, so the gauntlet reads the rules a promotion would make. With w_shooter = 0 the rules read G20 at 51%, under its floor of 0.60: a candidate that undid section 142 would be held.
orrery guards the line with it (tools/plans/treadcore.py, guard_jobs, guard_holds): each candidate past its reads and its families is read so at the fleet's commit, and auto promotion takes only one whose rules stand on every file's floor; a candidate under one waits, with a flag naming the files. The promotion job reads the gauntlet again on the batch it commits, beside the brain's tests and the families, and commits nothing under a floor.
Every floor today's success. Read so against the rules of section 142 (200 seeds), two of the first seventeen floors stood above the rules' own play, and the guard would have held every candidate: G3 a shell on a standing tank (the rules 76.5%, floor 0.95) and G8 out of a pillbox's ring (40.5%, floor 0.9). They are now today's success as section 141's are, the rules' share less two errors of a hundred seeds, to the twentieth below: G3 0.65, G8 0.30. The other fifteen stand under what the rules play and are kept. The rules of today hold every floor.
144. Two of the boundary's tests corrected, and every floor today's success, 4 November
Teaching the rules G18 and G19 (Paul: "its risky, we will recover ... 2. yes. perhaps the sensor will indicate a ring, as it would indicate and identify the foe"), a trace of the rules on G18 showed the hostile pillbox firing at the tank in the first tick. A tank with no brain at all was hit there: the test, not the rules, was wrong.
The placer's rounding. A pillbox near = "centre" is placed from the map's centre point (64, 64), and the tank at = "centre" stands on its tile (64.5, 64.5). Nine tiles toward a seat almost due east is 72.999, which the tile snaps to 72.5: the pillbox stood 8.0 tiles from the tank, on its ring's edge, and fired from the start. G18's and G19's hostile pillboxes now stand away = 9.5, nine tiles off; an idle tank is no longer hit in either.
A pillbox taken stands again. G19 asked that no placed pillbox stand at the end; but a pillbox shot dead and taken by the squad is repaired and stands as the taker's, which is the best end of the fight, and the rules did exactly that and failed for it. pillboxes_standing takes owner = 1, counting only the enemy's, and G19 runs 40 s, time for the squad to take one and the tank to step to the next.
The rules meet both. Corrected, the rules pass G18 and G19 on every one of 200 seeds: they shoot a hostile pillbox from outside its ring, spare the neutral one, and take the dead ones with the squad. Section 141's account of their failures there (into the neutral one's ring, into the far one's) was the misplaced pillbox's doing and is withdrawn; its readings of G20 to G22 stand. The nets do not meet them (G18 the best 72%, G19 45%): the ring is the nets' lesson, and Paul's sensor for it.
Every floor today's success ("keep what we got ... adjust the floors to be what we have now as sucess"): read again on 200 seeds, each file's floor is the rules' share less two errors of a hundred seeds, to the twentieth below, wherever that is above the floor it had; none is lowered. G1, G2, G5, G6, G9, G10, G12 to G19 0.95; G11 0.90; G3 0.65, G4 0.90, G7 0.80, G8 0.30, G20 0.60, G21 0.15, G22 0.40 as they were. Today's rules hold every floor, and the promotion guard of section 143 holds every generation to them.
145. The kite redesigned for a tank alone: what G21 is made of, three designs tried, and kite_in_alone, 4 November
Paul, of teaching the rules the boundary's last two decisions (section 141): "its risky, we will recover. i think it indicates faster training." G21 (alone, an enemy of the same armour that shot first: kite it) and G22 (the same with two friends holding their ground beside the tank: stand and fight) read the rules at 24.5% and 52.5% on their 200 seeds. They are a pair by design: a brain that kites in both places fails G22, one that stands in both fails G21. This section is the redesign of how the rules kite when alone, and the reading of the test that made it, failures first.
What G21 is made of. Before changing the rules, the rules' 200 seeds of G21 were read hit by hit. They died in 93, and 37 of those inside six seconds, of the opening direct shot (45) and one or two of the enemy's strike drones (35), before any choice of theirs could matter. Hits by what fired them:
| direct | strike drone | splash | longshot | |
|---|---|---|---|---|
| taken | 291 (200 the opening shot) | 121 | 78 | 21 |
| given | 44 | 191 | 130 | 5 |
The rules' drones out-hit the enemy's, and their mortars its mortars; their direct gun was out-hit two to one after the opening. G21 is lost or won on the direct gun, and the direct gun is what the kite moves.
Why the kite as built did not work alone. The kite (section on tactics.rs, Paul, 17 October) waits on a ring just past the target's reach, never inside the mortar's minimum, so at 12.5 tiles, and steps in only while the target's gun reloads for longer than the trip. It was designed when a mortar reloaded in 4 s, and from that ring it lobbed one each reload. Two things have changed under it since. A mortar ties the gun up 12 s (rules.rs, 30 October), time for the kited tank to step into direct reach and fire three shots unanswered: one seed (9,500,007) shows the mortar from the ring at 2.5 s, the enemy's direct hit at 11.8 s while the gun was still reloading, a second mortar, and death at 16.7 s. And the trip that opens a step in is measured to kite_in from the target, 2.25 tiles (the fleet's number; the module notes still say 9), so from the ring it is (12.5 − 2.25) / 4 + 0.8 s of turning = 3.4 s, and with kite_spare 3.8 s of the target's 4 s reload: THE WINDOW WAS OPEN ONLY ITS FIRST QUARTER OF A SECOND. The kite almost never stepped in; it waited, mortared, and was stepped in on.
A mirror duel cannot be won from behind, and sight is the reason. To see what the tank itself could do, the scenario form gained drones = false (every bay and the sky empty) and G21 was read without drones. The rules held 1 seed in 200 and died in 191; in 157 they took three hits and gave two. The trace is the same each time: the enemy fires first, ducks out of reach while its gun reloads (the duck, section 19: a gun loaded before one's own is stepped away from), both tanks lose sight of each other, and they meet again with both guns loaded and fire on the same tick, a hit for a hit, until the one that started behind is dead. Sight and the direct gun's reach are both 10 tiles, so WITHOUT A RECON DRONE A GUN IS FIRST SEEN INSIDE ITS OWN REACH, and no rule can step away from a shot it learns of as it is fired. Kiting needs the drones' eyes. G21 keeps its drones; the switch stays as the instrument that showed it.
Three designs, the rule written before each: two dropped, one kept.
| Design | The argument | G21 | G22 | Verdict |
|---|---|---|---|---|
duck_alone | alone, and needing fewer hits to die than the enemy, duck a gun loaded no later than one's own, not only one loaded sooner | 24.5% to 24.0%; without drones 0.5% to 1.0% at every radius from 5 to 30 | unchanged | dropped: the gun is seen inside its reach, too late to duck |
kite_mortar off | no mortar at the kited tank from the ring, and the ring just past its reach (11.5) | 24.5% to 19.0% | unchanged | dropped: the mortar's splash hits were a third of what the rules gave, and they count |
kite_in, the one dial | step in shallower, so the trip is shorter and the window opens | 5: 30.5%, 7: 32.5%, 9: 29.0% | 5: 34.5%, 7: 32.5%, 9: 28.5% | the boundary itself: right alone, wrong with friends |
kite_in_alone | the step in's end with no friend inside odds_r (14 tiles) of the target; kite_in stays the step in with one | 2.25: 24.5%, 5: 30.5%, 6: 35.5%, 7: 32.5%, 8: 29.5% | 52.5% at every value | kept, at 6 |
The single kite_in sweep is the clearest reading of the boundary this record has: the same shallow step in that gains a third on G21 costs a third on G22. With friends beside it the tank should close, because the target has one gun and three of them in its face (the square law, tactics.rs); alone it should fire from near the target's reach and be out before that gun is loaded again. The split dial is Paul's sentence as a number: "last attacked enemy we would kite and dance, unless we have backup with fleet allies nearby." odds_r is the radius the odds already count friends in, so "alone" here means what it means to the stance. From the ring, a step in to 6 tiles takes (12.5 − 6) / 4 + 0.8 = 2.4 s, and with the spare the window is open the first 1.2 s of the target's reload instead of a quarter second.
Held out, and paired. Six could be the best of five values on 200 seeds by chance, so it was read again on 400 seeds the gauntlet never plays (from 9,700,000): 2.25 24.5%, 5 25.0%, 6 31.5%, 7 28.2%. Seed by seed, 6 against 2.25:
| Seeds | 2.25 | 6 | Paired difference | Deaths |
|---|---|---|---|---|
| the gauntlet's 200 | 24.5% | 35.5% | +11.0 ± 4.4 points | 46.5% to 39.5% |
| held out, 400 | 24.5% | 31.5% | +7.0 ± 3.2 | 49.5% to 40.5% |
| all 600 | 24.5% | 32.8% | +8.3 ± 2.6 (3.2 errors) | 48.5% to 40.2% |
Over the 600, the rules' direct hits rose from 152 to 214 and their strike drones' from 545 to 663 (a tank alive longer launches and guides more); the longshots they took fell from 65 to 15 and the direct hits from 881 to 839. The gain is where the tally said G21 was lost: the direct gun.
The rule, before the census: 6 becomes the default if the census of kite_in_alone = 6 against 2.25 on 48 seeds of every map is not worse by more than two standard errors, every family holds, and every other gauntlet reads the same.
The census (the trial kite_in_alone = 6, the base 2.25, 48 seeds from 0, every map, 900 s, respawns 3): −1.12 ± 0.92 a seat, 1.2 errors, inside the noise. The rule is met, and 6 is the default. Every map is inside the noise but europe, −11.76 ± 4.78 on its four seeds (2.5 errors); one map in twelve outside two errors is what chance gives nearly half the time, and it is named here to be watched, not explained away.
The families hold, with kite_in_alone = 6 on every brain: a duel in the open 84.2% to 87.2%, a tank under a ready gun 81.3% to 82.1%, the other seven the same to the seed. The gauntlet for the rules with the dial (GAUNTLET_WEIGHTS, 200 seeds): G21 24.5% to 35.5%, G20 72.0% to 71.5% (one seed, above its 0.60 floor), every other file the same to the seed; read again with 6 the default on every brain, G11 95.0% to 94.5% as well (one seed, above its 0.90), and every one of the 22 floors holds. The brain's 165 tests pass, one new (alone_the_step_in_stays_shallow). G21's floor rises from 0.15 to 0.25, today's success: 35.5% less two errors of a hundred seeds, to the twentieth below. G22 stays at 52.5% and its floor at 0.40; it is not yet taught. The enemy in G21 and G22 is a commander, with the Raider's dials in its file and the defaults for the rest, so a default changed reaches it as well: with 6 on every brain, G21 reads 34.5% and G22 50.5% (the lone attacker of G22 steps in shallower too, and is harder to kill). A BOUNDARY TEST'S YARDSTICK MUST NOT MOVE WITH THE RULES IT MEASURES: both files pin the enemy's kite_in_alone at 2.25, its value when they were written, and a dial added after a file is pinned so from now on. G22's friends are never alone, and the dial does not reach them.
What it says about the tests. Section 144 corrected two of the boundary's files whose pillbox stood on the ring's edge; G21 needed no correction but a reading. Its pass condition (more hits given than taken, and alive) asks a tank one hit down to win the exchange from there, against an equal that also ducks, and with the drones in play about a fifth of its seeds are decided before the tank chooses anything. A third of the seeds passed is near what the decision can buy, and the floor is now where the rules stand. The nets do not kite at all (section 141: the best, map5, 26% on 50 seeds); what they lack is the same as what G18 and G19 ask of them: where a gun's ring is, and whose.
A census by hand on a Mac needs the paste shim, and the shim needs bash: the first census of this record ran in zsh, where export -f does nothing, and played the generated maps only (−0.73 ± 0.62 a seat, inside the noise). It was played again under bash.
Not done. G22 taught (the rules stand with friends at 52.5%; with drones off, 34.5%); the ring as a sense for the nets (Paul: "perhaps the sensor will indicate a ring, as it would indicate and identify the foe"), next; whether the waiting ring should stay at the mortar's minimum now that the mortar reloads in 12 s, a question kite_mortar answered no to only on G21.
146. The ring as a sense: built, proved harmless, and not shown to help, 4 November
Paul: "perhaps the sensor will indicate a ring, as it would indicate and identify the foe." The block was built as the senses' last six numbers (so every net and patch is widened with zeros and plays as it did): the nearest live pillbox that fires on the tank, an enemy's or a neutral one, as its bearing (sine, cosine), where the tank stands against its ring (1 on the pillbox, 0.5 on the edge, 0 a ring's breadth outside), whose it is (1 a foe, 0 neutral), its gun (1 loaded) and its health. One test; the brain's 166 tests pass. Harmless, proved: the whole gauntlet for the rules and every shipped net, with the block and without it at LR 145's commit, the same to the seed on all 22 files; only "untrained", a fresh random net drawn at the new size, differs.
The rule, before the run: the ring earns its place if a net imitating the rules with it passes G18 and G19 more often than one imitating them without it, by more than two errors, and is not two errors worse on G7 and G8. Method: the rules recorded on G7, G8, G18 and G19 (RECORD_COURSE, 2,000 seeds from 9,100,000, 163,510 samples, the ring lit in 57% of them); the same file with the six numbers zeroed; treadcore-bc on this Mac's processor (BC_DEVICE=flex, 30 epochs, batches of 512, rate 0.001), five runs of each, the no-ring nets' six input columns zeroed after training (their weights there never moved from the random start, and would read the lit block as noise); each net read on the gauntlet's 200 seeds.
| Passed, of 200 | the ring, five runs | without it, five runs |
|---|---|---|
| G7 | 72, 91, 86, 84, 88: 42.1% | 117, 118, 95, 114, 76: 52.0% |
| G8 | 169, 171, 179, 160, 173: 85.2% | 180, 168, 169, 161, 155: 83.3% |
| G18 (the first run) | 200 | 200 |
| G19 | 2, 0, 71, 164, 170: 40.7% | 0, 57, 172, 171, 45: 44.5% |
The rule is not met, and the block is parked (a stash in this checkout, not on main) for Paul's word. G7 reads the ring 9.9 ± 4.4 points worse (2.2 errors); G8 the same; G19 cannot be read at all, because its result swings from 0 to 172 of 200 with nothing but the training run's random start. Two findings outweigh the sense. THE NETS' FAILURE ON G18 WAS ONE OF EXPERIENCE, NOT OF EYES: a net that has imitated the rules where G18's choice comes up passes it every time without the ring (the best shipped net, 72%, had never seen it). And G19, two rings one at a time, is the one decision here the imitation has not settled: the same data makes a net that never does it and one that nearly always does. The next step for G19 is the training's variance (several starts, the best kept, as the fleet's clone stage could), not another input.
147. The tests play a pinned brain: a promotion moves the game and not the tests, 4 November
Paul: "commit push. then 1 and 2." The first of the two was the finding of the evening's look at the queue (Paul: "back to looking at the flow, the chart, the queue. look into making blockages minized"): from 1 to 4 November 36 promotions ran the brain's tests and 13 failed, every one on a unit test, none on a family. The tests that failed them: the_default_match_plays_as_teams (3), a_hop_bridges_a_narrow_gap_and_sails_a_wide_one (3), the_bots_build_a_smelter_mine_and_smelt (2), kiting_steps_in_only_when_the_gun_cannot_answer (2), the_improvement_goes_by_its_weights, a_slider_steps_and_steps_back, smooth_is_a_weight_that_blends_and_thresholds, with_bio_off_there_is_no_wave_at_all and the_matrix_starts_empty_and_a_row_follows_its_own_wave (1 each). Each read Settings::default(), or a Tactician or Commander built on it, and asserted on what the brain did there. A test of how a part works held still the numbers the fleet is there to move: r0176 and r0177 set c_boat from 5 to 3.45, and the hop test, which asks whether seven tiles of water are bridged at the defaults' prices, found a boat. The first rule (2 November, the plan's chapter 15) asked such a test to name the weights it tests; it let the sixteen written before it stand, and the hop test was one of them.
The design. source/treadcore-brain/pinned.weights: the capabilities and all 162 weights at the defaults of main 43f9f67 (r0178's), written by an ignored test, write_the_pinned_brain, which adds a weight the file does not name at its default and never changes a line already there; to move the pinned brain is a lab report's decision. Settings::pinned() reads it (held to today's ranges, as any brain is), and Tactics::pinned(), Tactician::pinned() and Commander::pinned(seed, name) are built on it. The fleet's match tests deal their bots under with_settings(Settings::pinned(), ..), and the scenario runner takes a base for its commanders (with_base, a thread's setting as with_settings is): the unit tests give it the pinned brain, and the families and the gauntlet, which give it nothing, play the defaults they are there to judge. 51 test functions moved (board 6, city 1, commander 23, cycle 2, facts 2, fleet 3, front 1, hop 1, ledger 1, scenario 2, styles 5, tactics 4). Caps::BASELINE in four commander tests became the pinned brain's capabilities; the line of connect_scores_work_that_can_be_done that asserted connect_idle's default is 1 went (it said, at LR 77, that the dial's arrival moved no default; since then the dial is the fleet's to move). Left on the defaults, on purpose: every ignored test (the censuses, every_family_holds_its_floor, the ladders, the recorders of learn.rs), because they measure the brain as it ships; and the tests of weights.rs, which are about the defaults themselves (today's constants, the ranges, the decrees, the file form).
The lint. a_unit_test_plays_the_pinned_brain replaces a_test_that_uses_the_defaults_names_the_weights_it_tests and its list of sixteen. It reads each test module item by item (a function runs to its own closing brace, and the attributes over it say whether it is a test and whether it is ignored), and a unit test outside weights.rs may not read Settings::default(), Tactics::default(), Tactician::default(), a weight's default_value(), Caps::BASELINE or Commander::new; one that deals bots through the fleet (team_brain, brain, brain_as, commander_as) needs with_settings, and one that plays a scenario file needs with_base. A helper is read when a unit test calls it. Proved on itself: with Settings::default() put back in the hop test and Commander::new in the helper hears, it fails and names both, and the five tests that call hears. A second test, the_pinned_brain_names_every_weight, holds the file to every weight the brain has.
The rule, before the run: the pinned brain holds if every brain test passes at today's defaults; if the moves of r0177 and r0171 (the two latest failures with different tests) pass every brain test; and if a move of every searchable weight by a fifth of its range, away from its default toward the farther end of its range (109 weights, larger than any promotion the fleet has made), fails no unit test outside weights.rs. Method: a worktree of 43f9f67 and one with the change; scripts/set-defaults.py writes each move into weights.rs and Tactics::default() as a promotion does; cargo test -p treadcore-brain --release --lib.
| The defaults moved to | main, 43f9f67 | with the pinned brain |
|---|---|---|
| today's (no move) | 165 pass | 166 pass |
r0177: c_boat 5 to 3.45, beta, advance_band, w_pos_fortify | 1 fails: the hop test | 166 pass |
r0171: sixteen weights, among them kite_turn 0.8 to 1.6 | 165 pass (the kite it failed on was reworked by LR 145) | 166 pass |
| every searchable weight, a fifth of its range | 7 fail: a_far_fetch_gathers_the_fleet, connect_scores_work_that_can_be_done, a_small_island_takes_a_boat, the hop test, every_scenario_holds, alone_the_step_in_stays_shallow, kiting_steps_in_only_when_the_gun_cannot_answer | 166 pass |
| the eight other moves of the 13 failures (r0071, r0072, r0112, r0114 to r0116, r0117, r0125, r0166, r0170; r0176's is r0177's) | 4 fail: r0112 a_fleet_match_plays_out, r0117 the hop test, r0166 and r0170 the smelter test; the other four pass on today's main | 166 pass, all eight |
The rule is met, and beyond it: every one of the 13 failed promotions of 1 to 4 November, ten moves in all, five of which still fail a test on today's main, passes every test with the pinned brain. (The 166 are the 165 less the old lint, plus the new lint and the names test; write_the_pinned_brain is the seventeenth ignored.)
What a unit test no longer guards. The thirteen files at the top of scenarios/ (one seed each: a dodge clears two shells, a dodging tank holds its fire, a tank ducks out of reach in time, and ten more) test the mechanics, and now play the pinned brain. Under the move of every weight two of them failed at the moved defaults: a promotion that left the dodge broken at its new numbers was refused by them before, and is now refused only if a family or a gauntlet file sees it. None of the 13 failures of 1 to 4 November was one of these files. Whether they should also read the candidate's defaults, as a gate with a floor as the gauntlet's files have, is Paul's to say.
148. A test's text reads over the pinned brain, and a promotion writes every field of the tactics: the night's best candidate held back by a parse, 5 November
Paul: "fix the test." The night of 4 November gave the fleet one candidate worth a generation, grove/w10040: ping_style 0 to 2 (as built to warn), census +2.05 ± 1.31 a seat, held out +0.69 ± 0.44, the families held. Its promotion failed twice. At 00:15 (r0182) the machine that took it had no branch of orrery's line, which orrery mended that morning (orrery 4b8e4d2). At 05:42 (r0185) bosporus built it on the line and ran the brain's tests, and one failed: commander::tests::with_pings_on_as_built_a_bot_comes_to_any_ping. LR 147 pinned the brain the unit tests play, and this test still read the defaults: its helper hears built the brain with Settings::parse("caps = baseline, pings"), and parse reads a text over the defaults. With the default moved to warn, the test of pings "as built" played warn, and nobody came to a call for help. LR 147's lint forbade Settings::default(), not a parse over it; its stress moved the 109 searchable numbers, and not the 15 choices or the 18 switches, which RANGES does not hold, so neither saw it.
The rule, before the run. The fix holds if (1) w10040's move passes every brain test; (2) no test fails when every choice the fleet promotes (13 of the 15: bot_style and patch change the brain's whole form and wait for a person) is moved one, two, three and four versions on from its default, when every switch is flipped, and when every true/false weight of the tactics (kite, shell_boxes, longshot_any) is flipped; (3) LR 147's moves (its 13 failed promotions, ten distinct, and its stress) still pass. Method as LR 147's: a worktree of the line the fleet runs (main aebeb1d and r0179's bio 0 to 3, f04f157), and the same with the change; scripts/set-defaults.py writes each move as a promotion does; cargo test -p treadcore-brain --release --lib.
Before the change the stresses failed six tests, in four ways: two of hears's five callers, the fleet's two role tests, the scenario switch test and the tactics' constants.
| The defaults moved to | the line, f04f157 |
|---|---|
w10040: ping_style 0 to 2 | 1 fails: with_pings_on_as_built_a_bot_comes_to_any_ping |
| every promotable choice one version on | 3 fail: at_style_0_with_pings_off_a_bot_answers_no_bot, a_bots_team_takes_its_roles_from_its_settings, three_teams_of_five_and_one_alone |
| two versions on | 2 fail: the pings test as built, the roles test |
| three versions on | 2 fail: the same |
| four versions on | 1 fails: at_style_0_with_pings_off_a_bot_answers_no_bot |
| every switch flipped | 3 fail: at_style_0_with_pings_off_a_bot_answers_no_bot, a_switch_is_set_on_every_commander, default_weights_are_todays_constants |
| every true/false weight of the tactics flipped | 1 fails: default_weights_are_todays_constants |
- A text read over the defaults. Every unit test that wrote the numbers it is about as a text and read it with
Settings::parseplayed the defaults for every number it did not write:hears(five tests of pings), bio'sonandmatrix, the fleet's roles test, the commander's ladder test, the scenario switch test andevery_shipped_file_reads.Settings::pinned_with(text)reads the text over the pinned brain. Underneath isSettings::parse_over(brain, text), andparseisparse_overthe defaults, as it always read. In a caps line read over a brain,baselinestands for that brain's capabilities, notCaps::BASELINE, which a promotion of a switch moves. A new test inweights.rsholds both:a_text_reads_over_its_brain_and_baseline_is_that_brains. - A seat's role asked of the fleet with no brain set.
roleandrole_by_rankreadfleet::settings(), which is the defaults when nowith_settingsis in force.three_teams_of_five_and_one_aloneand the first loop of the roles test asked the defaults'rolesandassist; both now ask underwith_settings(Settings::pinned()). - A scenario's switches read over the code's baseline. A scenario's
weights = { caps = "baseline, -x" }readbaselineasCaps::BASELINEeven underwith_base. It now reads over the brain the scenario starts from: the defaults as dealt, so nothing changes for the families and the gauntlet, which set no base, and the pinned brain in a unit test. This is the one change outside the tests. - A constant a promotion never wrote.
default_weights_are_todays_constantsholdsTactics::default()to whatTactics::from_weightsmakes of the defaults.set-defaults.pywrote its numbers, not its true/false fields (kite,shell_boxes,longshot_any), nor what a switch makes of a field (kiteneeds its switch on;focus_blendis a half with its switch off). A promotion of any of the five would have failed this test, and it is right that it does: the test is about the defaults themselves, and stays on them.set-defaults.pynow writes each such field asfrom_weightsmakes it.
The lint a_unit_test_plays_the_pinned_brain now also forbids Settings::parse( in a unit test or a helper one calls. role, role_by_rank, team_role and equip join the fleet's calls that need with_settings. Proved on itself: before the tests moved, it named the seven of way 1, with hears's five callers; before the fleet tests moved, it named three_teams_of_five_and_one_alone. It reads a test's body whole. The roles test set with_settings for one call and not for another, and passed it; the stress found that one.
After the change (the line with it; 167 tests, the new test among them):
| The defaults moved to | the line with the change |
|---|---|
| none | 167 pass |
w10040: ping_style 0 to 2 | 167 pass |
| every promotable choice one, two, three and four versions on | 167 pass, all four |
| every true/false weight of the tactics flipped | 167 pass |
| every switch flipped | 1 failed, default_weights_are_todays_constants, on its line s.caps.has(Caps::KITE); with the assertions dropped, 167 pass |
every switch flipped but kite and castle_cap, before they were dropped | 167 pass |
w_smooth 1 (the bot smooths) | 1 failed, a_weights_file_round_trips_and_names_its_mistakes; with it mended, 167 pass |
| every searchable weight at its min (108) | 1 failed, a_slider_steps_and_steps_back; with it mended, 167 pass |
| every searchable weight at its max (108) | 1 failed, a_wingman_takes_the_pillbox_by_its_leader; with the wingmen pinned, 167 pass |
| LR 147's stress (109 numbers a fifth of their range) and r0177, r0171 | 167 pass |
| the other eight moves of LR 147's 13 failures (r0071, r0072, r0112, r0114, r0117, r0125, r0166, r0170) | 167 pass, all eight |
The rule is met, but for two lines that were Paul's. w10040's move passes every brain test, every choice and every true/false weight the fleet promotes moves without a test failing, and LR 147's moves still pass. The one failure left was not a test reading the defaults. default_weights_are_todays_constants asserted that two switches are on by default: kite, and castle_cap, "Paul's castle is the default (18 October)". Neither is decreed in RANGES, so the fleet tries each one off in its A/B trials every generation, and a winner would have failed its promotion on this test, hours after its reads. There were two ways out: decree them in RANGES (kite 1 1, castle_cap 1 1), and the fleet stops trying them; or drop the assertions, and the fleet may move them. Paul: "drop assertions." Both lines are gone, and with them the third assertion on the kite's line, that the bot does not smooth (w_smooth, searchable 0 to 1, failed it past one half). A number a person holds is a decree in RANGES, where the fleet can see it; a test's assertion holds it where the fleet cannot.
The smooth move found one more: a_weights_file_round_trips_and_names_its_mistakes set w_smooth to 1 and w_wounded to 1.5, and expected both in the file's list of numbers off their defaults. With w_smooth's default promoted to 1, the list would have been one short. It now sets each a step away from whatever its default is.
The ends of the ranges found two more. The search may carry a number to the end of its range, so every searchable weight was set to its min, and then to its max. At the mins, a_slider_steps_and_steps_back asserted that a weight's step is a tenth of its default, and a default of 0 has none: the step rule gives 0.1 there, and the test now says so. At the maxes, a_wingman_takes_the_pillbox_by_its_leader lost its wingman to a pillbox. Commander::wingman is made by Commander::new, on the defaults, and five tests built one. They now build Commander::pinned_wingman, and the lint forbids Commander::wingman in a unit test as it forbids new; given one back, it names the test. The fleet's own wingmen take their settings from equip after they are made, and were never on the defaults.
The families, with the change on main (every_family_holds_its_floor): all nine hold, from a duel in the open at 87.2% ± 1.7% (floor 55%) to a tank under a ready gun at 82.1% ± 1.2% (floor 80%). They set no base, so the scenario reader's change reads their switches as before.
149. Every default a number: the wingman's five, and the candidate they held back, 5 November
Paul: "fix both. commit push.." The first of the two was the day's strongest candidate, grove/w10839 (ping_defend_reach 90 to 99.27, retreat_armor 50 to 100, w_site_beach 20 to 40, wing_leash 20 to 10; both held-out reads passed, +0.96), whose promotion orrery asked for at 11:09 and which stopped before it began: no such weight in weights.rs: wing_leash (nothing written). The weight is there, and its default was not a number. LR 127 made the wingman's five constants weights (escort_distance, wing_leash, wing_greed, wing_threat, wing_ahead), and their defaults in the weights! table were the constants' names, crate::commander::WING_LEASH and its like. scripts/set-defaults.py writes a promotion by finding the number on a weight's line; it found none and called the weight missing. Orrery's planner reads the table the same way, so it saw no default for the five either, and a promotion that moved only one of them would have read to it as no change of the defaults at all.
The change. The five defaults are numbers in the table, as every other weight's: 4, 20, 16, 10 and 16, the constants' values, so nothing plays differently. The constants in commander.rs are gone; nothing else read them but one unit test, a_wingman_covers_its_leader, which asserted the wingman stays within WING_LEASH and now reads the leash of the pinned brain it plays (LR 147), so a promoted leash does not move the test. A new test, every_default_in_the_table_is_a_number, reads the table's source and fails, naming the line, when a default is not written as a number; given a default written as an expression for a moment, it failed and named it. And set-defaults.py, given a weight whose line is there but whose default is not a number, now says so instead of "no such weight".
Checked. The brain's 168 tests pass. set-defaults.py --check on w10839's file now writes all four of its moves; on a worktree of main with the change and w10839's numbers set as a promotion sets them, the 168 tests pass. Orrery tries the candidate again once main has moved (orrery's planner now counts the writer's failure as one to try again, as it does a failed test), and its families and gauntlet run in the promotion job as for any candidate.
150. The maze and the takeover as tests both machines read: walls in a scenario, and G23 to G28, 6 November
Paul, asking that crucible's card and treadcore's CPUs act within the same rules: "Specifically the goal is to navigate a simple maze, Like a maze of S shaped and 5 Shaped, and @ (ampersat at-sign) shaped mazes on test gauntlet maps of player vs environment, where we add takeover tasks like Pillbox capture, Base capture, Building a base capture ... Code the implementation of more tests, coded in Cuda, for GPU and CPU." And, of what to do with them: "Commit and push both."
The change. A scenario's [[terrain]] rectangle may now be kind = "wall" (scenario.rs, one arm of the match beside water, grass, forest and road): a maze is a few rectangles of wall on a cleared field. Six gauntlet files: G23 the S maze (two staggered bars; the enemy's pillbox at the far end), G24 the 5 maze (a top bar, a middle bar and the bowl's stroke), G25 the @ maze (two square rings, gaps on opposite sides, the pillbox at the centre), each passed when the pillbox becomes seat 0's (pillbox_owned_by); G26 take a pillbox (the enemy's, 12 tiles off in the open), G27 take the base (the enemy's pillbox with its tank holding two tiles off), G28 build a base (a neutral pillbox to take and the ground held while an enemy comes). Crucible's CUDA gauntlet (orrery's report One Rule, Two Machines) reads the same files' words, and plays their twins M1 to M3 and T1 to T3 on the card with walls, trees and a takeover of its own.
Read. The rules alone (GAUNTLET_BRAINS=scripted), 200 runs each: the S 99.5%, the 5 66.5%, the @ 100%, take a pillbox 99.5%, take the base 3.5%, build a base 88.0%. The floors are today's success less two errors at 200 runs, as LR 141 set them: 0.95, 0.55, 0.95 (G25's share has no error at 100%, and a floor of 1.00 would fail a promotion on one unlucky run), 0.95, 0.00 and 0.80. The whole gauntlet with the six runs in 1.8 s at 20 runs on the Mac.
Checked. The brain's 168 tests pass. The six files parse and play for the rules. The gauntlet that guards every promotion (LR 143) now reads them too; a candidate whose dials break a maze is held as any other is.
151. The proving ground: a map of the maze and the takeover, A Flame War Modified's walled city, and G30 to G33, 6 November
Paul: "build a map, with the test setup gauntlet of a the series of tests. A series of spiral mazes, S shaped bases. and roads. in fact, [Birdman's map tiles and A Flame War Modified] look at the map tiles, for ways to stitch togeather componets like blocks to make rivers and roads styled with consistent tiles along with your own map creativity. Also the map A Flame War Modified has a good walled section. Add bases around the island if they are missing, but I think its all good."
The tile sheet. Birdman's map tiles is a prior-art sheet of five-by-five blocks, each a way terrain meets terrain: roads one tile wide, straight, cornered, crossed and teed, leaving every block at the same offset they entered; rivers two tiles wide; a road over a river as wide as the river; forest and rubble edges ragged a tile deep; walls with a road through a gate. Blocks whose edges agree tile without seams; the proving ground is laid by one function per block kind, so its joins agree.
The map (scripts/provingmap.py, maps/proving.map, 128 by 128): an island with a river two tiles wide down its middle, a ring road and a cross road over it on bridges, forest on the river's banks and in blobs, and a place in each quadrant off the ring road -- the S (two staggered bars in a walled square, a road down the S to a base and a pillbox at its end), the @ (three square rings, their gaps north, south, north, the base at the centre), the 5 (a top bar, a middle bar and the bowl's stroke, the base in the bowl), and A Flame War Modified's walled city as it is (its lanes, blocks and pond) beside a compound of wall with concrete corners and no gate, a base and a pillbox inside. Eight bases round the coast with a start beside each; neutral pillboxes at the bridges and at each place's end.
A Flame War Modified joins the prior-art maps (maps/prior-art/flame-war-modified.map, by scripts/priorart2map.py): twelve starts, eleven bases on the island (enough, as Paul thought) and five on a pad of road at sea in the south-east, sixteen pillboxes, 397 wall tiles.
The series, G30 to G33: the tank at a place's door, the enemy's pillbox at its end, the map's own pillboxes and bases cleared so the decision is one; passed when the pillbox is seat 0's. The rules, 200 runs: the S 100%, the @ 100%, the 5 100%, the breach 95.5% -- the rules bring the compound's wall down with the mortar's splash aimed at the pillbox inside. Floors 0.95, 0.95, 0.95, 0.90.
153. The intrusion dial: w_breach, a wall shelled by choice, the press temper moving it, 6 November
Paul: "We need to ammend the brainz coding to consider intrusion given its weights (and maybe temperment of bio)." And of orrery's question which treadcore change to build: "w_breach intrusion dial".
Why. The rules already breach, but only as a side effect: the mortar aimed at a pillbox inside a compound brings down the wall between (G33, 95.5%). A walled base with no gun inside, or a long way round to a base's gate, was never broken into by choice.
What changed.
- A dial,
w_breach(weights.rs,RANGES0 to 4,pinned.weights0). A tank storming a base, with no hostile gun left to shell, finds the first impassable tile on the straight line to the base (breach_wall,lib.rs). If it is a wall, and the way in is shut or longer thanw_breachtimes the straight line, the tank shells that wall from inside the mortar's reach and holds, or moves to a firing positionINDIRECT_MIN_RANGE + 2tiles back along the line. A mortar's splash brings a wall down, so the tank goes through when it falls. Concrete or water first on the line is no breach: a splash only cracks concrete to a wall, and water stays. - THE TEMPER (
commander.rs): where the press row of the temperament matrix is set, it moves the breach's bar as it movespress_odds:breach = w_breach x row(press). A tank in a pressing mood goes through sooner. - 0, the default, never breaches by choice: the brain plays as it did until a census finds a value and promotes it.
Tests, two new (lib.rs): a shut base is breached at the first wall on the line, and there is no breach without a map; a gate on the line is driven through, and a gate round the far side is worth a breach at 1.5 and not at 4. treadcore-brain: 176 passed.
Not measured. A storm begins after long contact with an enemy, so no single scenario shows the dial at work; the census does, and the line's search may now try it.
154. A wall takes the gun: two direct hits open a gap, as crucible's walls do; the breach by the gun in range; game 9, 6 November
Paul: "continue unifiying the motor and the types of direct fire to take down a wall. Treadcore's rules should match crucible. Crucible is made for Cude GPU equivalanet to train and compare with existing gladdiators and ai brinz." Asked which way to unify, he chose: "Treadcore takes direct fire".
Why. The two games felled walls differently. Treadcore's walls fell only to a mortar's splash (a wall tile within 0.7 of the splash radius becomes rubble, concrete becomes wall); crucible has no mortar, and its gauntlet's walled base (B1) was breakable by its gun as a stand-in. A net trained on the card and a brain played in the game should meet the same walls.
The rule (treadcore-sim, WALL_HEALTH 90, game 9):
- A direct shot aimed at a wall tile, the first tile on its line that stops fire, is taken (
wall_shot); a shot at anything behind a wall is still refused, as before. - Its shell takes
DIRECT_DAMAGE(45) from the tile it strikes and from its neighbour along the wall, on the side of the tile it met (hit_wall); at nothing both are rubble. Two hits open a gap two tiles wide, a tank's way through. What each tile has taken is kept (World::wall_hits, serialized with a default) and forgotten when the tile changes. - The mortar's splash fells walls at once, as before. Concrete is not hurt by the gun.
Crucible matches (crucible, the forest branch): every wall takes the shell that stops on it, in training and in every gauntlet file, and at WALL_HP (90) gone a gap of 3 tiles opens where it struck instead of the whole bar falling; what stands on either side is whole again. Crucible's tank is 2 tiles wide where treadcore's is 1.5, hence 3 for treadcore's 2. Crucible's shells stop on any wall they meet, aimed or not; treadcore's gun is refused unless aimed at the wall. That difference is kept: crucible's tank fires along its hull, with no aim point.
The brain. The breach (w_breach, LR 153) uses the gun when the wall is within direct range and in its sight (wall_in_sight), the mortar beyond, and moves to two tiles inside the gun's reach otherwise. The brain aims at a wall only there, and w_breach is 0 by default, so the defaults play as they did on game 8's walls unless a shell is aimed at one.
Tests, three new: two direct hits open a wall two tiles wide and the other side stands; a shot at what is behind a wall is still refused; the wall on the line is in the gun's sight and one behind it is not.
Game 9. GAME_VERSION 8 to 9: the census of game 8 does not count for game 9 (the planner splits them); queued weights jobs of game 8 are moved to stopped/ and the running transform cancelled when it ships.