Skip to content

M6 PR 1: difficulties, disclosed bot bonus, personalities, retreat and influence maps (lines 1-4) - #56

Merged
tonianev merged 25 commits into
mainfrom
claude/confident-hopper-ho6dz1
Oct 30, 2026
Merged

tonianev merged 25 commits into
mainfrom
claude/confident-hopper-ho6dz1

Conversation

@tonianev

Copy link
Copy Markdown
Owner

What and why

M6's first PR covers the plan's slices 1 and 2, the AI half of the fun gate. This is a draft. The fun-gate CI job is expected to fail on the line-2 and flip steps until the retune now running on this branch lands. Per M6 owner decision 13, this PR merges only when every fun-gate step is green.

Slice 1 (SIM 17 → 18, rules 25 → 26)

  • Difficulty data: data/ai/difficulty/{easy,standard,hard}.ron with validators.
  • Disclosed bot bonus: a new sim step, 6d'. It pays a fixed bundle every 30, 25 or 20 s (Easy, Standard, Hard) from 3:00 and emits a BonusIncome event. It is rebuilt on restore and is never rubber-banded (M6 owner decisions 2 and 14).
  • Varied games: SimView::ai_noise is a pure, tick-free hash that never touches the Pcg32. Each seed is now a different game; before, all 20 seeds of the Standard mirror played identically.
  • Stats: MatchStats, engine-free and unhashed. It feeds the play-bots CSV and the new sim-cli match-log.
  • play-bots: accepts --a/--b easy|standard|hard and personality lists, played as distinct ordered pairs (--mirrors to opt in).
  • CI and scripts: CI line 3 runs seed 7 twice on ubuntu and macOS and diffs the results. Scripts: fun_gate.sh, check_ordering.sh, check_difficulty_flip.sh, check_attack_before.sh, bot_matrix.sh.

Slice 2 (rules 27 → 28)

  • Bot behaviour:
    • influence maps computed per plan in crates/ai, never stored;
    • retreat-when-losing as a grouped Move;
    • counter-weighted composition and raids;
    • Hard's deadline raid before minute 12;
    • seat_last: waves take the second Town before the Seat.
  • Personalities: rush, boom and tower as data.
  • Fairness: seed-parity planning phase and mirror-aware site ties.
  • Easy handicap: Easy trains 15% fewer Yeomen, disclosed (M6 owner decision 2's fallback).
  • CI: a new fun-gate job.

Review: four lenses raised 32 findings; 30 were confirmed and fixed and 2 refuted.

Status against the ROADMAP (nothing ticked yet)

Line Status at 2f31f5a
1. Personality matrix Passes: decisive 20/20, median tick 29648.5, 16/20 past 24000, no Draws; it also holds on seeds 21..40 and 41..60
2. Difficulty ordering Fails: Standard beats Easy 9/20 (5 Draws), Hard beats Standard 8/20
3. Seed-7 determinism Passes locally; CI on this PR shows both operating systems
4. Hard before minute 12, and the interval flip The attack clause passes in 3/3 games; the flip fails by one game
5, 6 Slice 3 and the owner's logged matches

Why lines 2 and 4 fail: when two intact bots meet, defence beats attack. In Easy vs Standard, 15 of 17 games reach the 45-minute cap with no Town taken. Mirrored sites then make each one an exact tie or a 3-tile tiebreak, so the bonus difference never shows.

Fix in progress (M6 owner decision 12): give difficulty the behaviour the ROADMAP names:

  • Easy defensive;
  • Standard and Hard keep up siege pressure until a Town falls;
  • mirrored sites no longer make cap games exact copies.

The bonus stays under the ceiling, read as averaged over a run (decision 14).

Determinism and fixtures

SIM 18 and rules 28, with all 11 fixtures re-recorded. Only m5_first_match.hash moved, and the hash-dump explains each move: the first bonus payment at tick 4100 at rules 26, then slice 2's phase change.

Tier

Code PR

  • just ci passes locally. machete and deny are not installed here; CI runs them.
  • Tests:
    • rules 146, ai 94 lib + 6 (15 ignored), sim-cli 38 + 16, game 258;
    • ignored m5b_ 15, m6_ 5;
    • every new behaviour test was mutation-checked.
  • No f32/f64/HashMap/HashSet/Instant in sim, rules or ai, and no engine dependency outside crates/game.

Data PR

  • data-check: OK rules_version=28 rules_hash=0xed93afd3bba71fb9 ... personalities=4 build_orders=4 difficulties=3
  • Every rules_version bump has its one-line reason and a re-record.

Licensing and disclosure

  • Contribution licence agreement: for the owner to tick.
  • AI assistance: written by Claude Code agents with adversarial review, per AI_CONTRIBUTIONS.md. Owner to confirm the review.

How to verify

just ci && just verify
cargo run -p sim-cli --release -- play-bots --seeds 1..20 --a standard --b standard --personality-a rush,boom,tower --personality-b rush,boom,tower --max-ticks 72000 --jobs 5
scripts/fun_gate.sh   # see .github/workflows/ci.yml, job fun-gate

Generated by Claude Code

claude added 16 commits October 28, 2026 23:55
…hStats

Bump SIM_VERSION to 18 and rules_version to 26: difficulty levels in
data/ai/difficulty/ and step 6d' pays each bot seat its level's
disclosed bonus bundle; every fixture re-recorded, only
m5_first_match.hash moves.

- rules: Difficulty, FirstRaid and WaveAgeGate loaded from
  ai/difficulty/<id>.ron with validators and negative tests, resolve()
  for a level's wave knobs, and a build-order rule that an armed
  trainer stands before the first Advance; data-check prints
  difficulties=3.
- sim: Sim.bonus derived from the header and the rules (rebuilt in
  rebuild_derived), economy::bonus_step as step 6d' with
  SimEvent::BonusIncome, the tick-free SimView::ai_noise,
  RateInfo::at_cap, sim::stats::MatchStats and verify_observed.
- ai: every difficulty id is a Standard bot playing the resolved knobs
  (bot_ids, is_standard_bot), bot_stats, resolve_personality for
  random, and the 26 M6 CSV columns fed by MatchStats.
- sim-cli: play-bots takes difficulty ids, walks distinct ordered
  personality pairs (--mirrors adds mirrors) and resolves random;
  match-log; bench --bots --bot/--personality.
- game: --difficulty and --personality for a plain launch and
  --skip-menu; SimHandle feeds MatchStats; the amber rule is
  RateInfo::at_cap.

m5_first_match moves from 0x3d2552033d51b4a2 at tick 26012 to
0x7207c1e8dc3ce351 at tick 22832: its per-tick hash-dump equals the
pre-bump one through tick 4099 and first differs at tick 4100 in
economy, the first Standard bonus payment.
- scripts/fun_gate.sh (line 1 by the owner's readings),
  check_ordering.sh (line 2), check_attack_before.sh and
  check_difficulty_flip.sh (line 4) and bot_matrix.sh (the tuning
  matrix); just fun-gate, just bot-matrix and just match-log.
- CI: ROADMAP M6 line 3 in the macOS check and ubuntu headless jobs
  (play-bots --seeds 7..7 twice, diff, final hash to
  hash-<os>-seed7.txt, with a Hard-against-Easy pair beside it) and
  the hash-parity comparison of the two files.
- Docs: ai.md's difficulty table as built, the play-bots pairing, the
  M6 CSV columns, match-log and the scripts; DETERMINISM step 6d',
  ai_noise and rule 5's wording, MatchStats, the re-recorded
  m5_first_match; DATA_FORMAT, data/ai/README, CONTRIBUTING ("A
  difficulty"), ARCHITECTURE and STREAMING.
- play_bots test: assert! instead of assert_eq! with a literal bool.
Two pure modules in crates/ai/src/standard that no rule calls yet, so
no bot decision and no fixture changes:

- influence.rs: per-plan grids of hit points and damage per second
  (own units, enemy units, enemy shooting buildings) built from the
  view, Lanchester strength over a block of cells, a per-plan
  (owner, kind) stats cache and an integer square root.
- compose.rs: enemy shares, the counter advantage and factor from the
  units.ron counter table, and the counter-weighted mix with counter
  kinds that join only while they counter what the enemy shows.

Unit tests cover Arms upgrades, civilians, Wains and the dead, the
shooter layer, Bowmen turning the mix to Skirmishers, the Outrider
joining against Mangonels, and weight 0 keeping the mix.
…ings

m5b_defend_outranks_saving_for_an_age (ignored, CI --ignored m5b_) set
up its premise at tick 6700 on the M5b timeline: the Masonry Advance
saving up with the Muster Hall idle. The Standard level's bonus from
tick 4100 fills the stock earlier and keeps the Muster Hall busy, so
the premise no longer held. The test now runs on a copy of the rules
whose standard level pays no bonus (income_interval_ds 0), asserts that
no bonus is paid, and keeps every assertion.

BUILD_TIMES: the sim and ai test times, the seed-7 runs, M5b line 1,
the m5_first_match verify and bench --bots in M6 slice 1.
The Standard bot gains the M6 behaviours, all integer, tick-free and
restore-exact (rules_version 26 -> 27, SIM_VERSION stays 18 in PR 1):

- influence.rs is wired in: per-plan grids built lazily from the view
  (Ctx::influence), with enemy units read through facts::visible_enemies,
  the one seam M7's fog filters.
- retreat.rs: a wave whose strength falls below retreat_ratio_permille of
  the enemy's around it, after losing retreat_loss_permille of its units,
  goes home in one grouped Move, except within the commit window, on a
  weak target, in the all-in or on Hold; a cooldown delays the next launch.
- army.rs: the mix is weighted by compose's counter factors and per-kind
  noise; emergency production toward the level's raid size runs before
  the military latch; Wains are trained only while the Wain rule wants one.
- waves.rs: raids (RaidUntilSiege) at the most worked enemy building
  outside tower reach, the level's deadline raid (Hard by 10:00), the
  Wain rule, seat_last, a siege target scored by distance and influence,
  a tech gate, and bounded noise on the first wave, sizes (upward only)
  and intervals.
- defend.rs: defend_economy, tower guards attack Wains first, and the
  defended building that build_order's reactive towers answer.
- sites.rs: mirror-aware ties from doubled centres and the partner tile,
  and noise among the top Expansion and BorderEdge candidates.
- The plan phase alternates with the seed; BotMemory appends the raid,
  retreat and wipe counters and per-wave kind, size and contact.

rules: the personality knobs and their validators (negative test per
rule), WaveMode and WainRule, Difficulty::worker_target_permille and
resolve_order. data/ai: the rush, boom and tower rows and build orders
in the random pool, the new knobs in standard.

Tests: unit tests per behaviour, tests/m6.rs (ignored, CI --ignored m6_:
Hard attacks before 14400 for every personality from both seats, rush
before 8400, every level and personality takes Passive's Seat, memory
under 1 KiB, restore during a raid and after a retreat). M5b tests that
pinned the old timeline use quiet knobs or the new seed-1 cadence.
sim-cli bench --bots reports influence_plans.

Every fixture is re-recorded for its header; only m5_first_match.hash
moves (0x7207c1e8dc3ce351 at 22832 to 0xa97c1bd612bca5f5 at 34098, now
P1's Seat capture).
…icap

rules_version 27 -> 28. Reason: with one Muster Hall the bonus floated
as stockpile (a 4x bundle won 6/12 and 10/12 against 1x), so
Standard's build order takes a second Muster Hall after arms_1 and a
third after Masonry (the same 4x bundle then won 12/12 from both
seats); and, because line 2 does not pass under the bonus ceiling, Easy
gets owner decision 2's disclosed handicap, worker_target_permille 850.

Every fixture is re-recorded for its header; only m5_first_match.hash
moves (0xa97c1bd612bca5f5 at 34098 to 0xf78f9cf501cfe707 at 41889, P0's
Seat capture). The rules tests follow the build order's new step
indices (38 steps; the armed-trainer case makes both early Muster Halls
optional), and the restore test plays rush against tower at seed 2,
where the rush seat raids and both seats retreat (against Standard the
rush seat now wins before any retreat).
CI: a fun-gate job on ubuntu, in parallel with headless, runs ROADMAP
M6 lines 1, 2 and 4 with the literal commands (fun_gate.sh,
check_ordering.sh, check_attack_before.sh from both seats and every
Random style, check_difficulty_flip.sh) and the ignored m6_ tests; each
step runs even when an earlier one failed. The seed-7 pair beside line
3 now plays Hard rush against Easy tower. At rules_version 28 the job
fails on line 2 and on the flip's 9/20 bar (docs/design/ai.md).

bench --bots: the first-wave noise held both first waves past the
bench's minute, so the bench zeroes noise_first_wave_ds and its waves
go out on M5b's timeline; the influence gate is 20 plans once a siege
wave went out (32 of 240 in the default bench; the maps are built only
on plans that read them), not 200.

Docs: ai.md gets the personality table, the influence maps' location,
retreat, targets and counters as built, the difficulty table as tuned
with the line-2 and flip readings and what the tuning measured, the
fun gate in CI with line 1 over seeds 1..60 and the B1-B6 and F1-F6
proxies, and the answered open questions; DETERMINISM.md the slice's
fixture moves and m5_first_match at 41889; STREAMING.md seed 1;
BUILD_TIMES.md the slice's timings.
enemy_military_near (the Build steps' border backoff) and the ring hold's
defender list read every unit directly, so M7's fog would not filter them.
Both now go through facts::visible_enemies, which passes the same units in
the same order today: no decision changes and every fixture verifies with
its committed hash. A test pins the remaining units() reads per module, and
fog.md names Ctx::occupied as a placement-legality read.
The weak-target case built an Assault wave a few ticks into the arena, so
the commit window returned first and the retreat_commit_town_hp_permille
check was never reached: with that check disabled every retreat test still
passed. The case now uses a staging wave, asserts that assess() turns the
rule off, and keeps the same staging wave against the sound Seat as its
paired control.
rules::ai::resolve_order applied worker_target_permille with no test: making
it return the order unchanged left every rules and ai test green. A rules
test now pins the shipped Standard order's Workers counts at Easy's 850
(7, 9, 11, 14, 17, 20, 23 to 6, 8, 10, 12, 15, 17, 20, splits and other
steps unchanged, identity at 1000, never below 1), and the ai knobs test
checks that each seat's StandardBot and describe play the resolved order.
waves::end counts a wave as wiped when it ends with fewer than
recall_below_units armed units, but no test asserted the counter: with the
increment disabled every non-ignored ai test passed. wave_ends_without_annexers
now checks it for all four of its cases (two armed counts; three Mangonels
ending by the annexer rule, three Skirmishers ending by age and a wave that
goes on do not). ai.md said 'no armed unit left'; it now states the code's
rule.
choose_target skips the enemy Seat only while another enemy Town has no
annexation timer, but no test set one up: dropping the condition left every
ai test and fixture green. A new test puts P0's own annexation on P1's only
other Town (0 HP, Skirmishers on its ring) and checks that the Seat, scoring
better, becomes the target again.
match_log_stats_equal_the_harness wrote a replay with no Tick records and
stopped before first contact, so verify_observed's Tick branch (the one a
human-against-bot replay uses) was never observed by a test: reverting it to
an unobserved step passed everything. A new test records 4000 ticks of a
human against Hard with Tick records (some on ticks whose next step is a
20-tick sample), past first contact and the human's first losses, and checks
that match-log re-derives exactly the MatchStats fed after every step.
The influence_plans gate applied only when a siege wave went out, so the
plan's Hard-rush bench (raids only) exited 0 with influence_plans=0 and its
ai_ms covered none of the influence, retreat or target code. The gate now
applies once any wave went out, in a bots_exercised function with a unit
test. The 1200-tick Hard-rush bench fails it (its raids are still inside
their 20 s commit window when it ends), so its timing is taken at
--ticks 1800: influence_plans=49, ai_ms mean 0.0287/0.0259/0.0230, exit 0.
ai.md and BUILD_TIMES record both runs.
…n the flip

CI's line-2 step stopped at the first play-bots exit 1 (any draw) under
bash -eo pipefail, so it printed nothing and never ran Standard against
Hard; line 1 and line 4a had the same shape. Each step now records every
failure and exits at its end, and line 2's runs pass --allow-undecided
(same CSV; the line counts wins, a panic still fails).

check_difficulty_flip.sh passed the flipped run on Hard <= 9/20 alone, so
draws counted for Easy and a run with Hard still ahead of Easy could pass.
It now also requires Easy >= 11/20 in both seat orders (plan 2a #3) and
prints Easy's wins, the draws and the untouched cap games; its header no
longer calls the 300/300 run an equal-income control (Easy keeps its 850
Yeomen handicap). check_ordering.sh prints a second line: cap games, draws,
untouched cap games and how many of those ended within 3 tiles.
- ai.md: influence maps live in crates/ai, built per plan (target table
  and the first design paragraph); the plan phase alternates with the
  seed's parity; difficulty ids in the AI seat; Hard keeps Standard's wave
  sizes; first_attack_tick can be a raid's launch since M6 (also
  waves.rs's module doc); the flip table gets seat orders, Easy's wins,
  draws, the bundle and the commit, and the bar names Easy's majority;
  the bonus ceiling's formula, its Standard-mirror readings (pooled 177,
  line 1 184; single losing seats up to 228 and 267) and the open pooled-
  or-per-match question; untouched cap games as the mechanism behind
  line 2's and the flip's draws and 3-tile tiebreaks (site noise between
  mirrored bots), replacing the seat-bias reading; the bundle sweep table;
  owner decision 1's fun-gate readings; no scratch path.
- ARCHITECTURE: ai_noise, bench --bot/--personality, play-bots ids,
  pairing and random, a match-log row, six CI jobs with fun-gate and the
  seed-7 steps; IMPLEMENTER_NOTES lists fun-gate among the jobs.
- DATA_FORMAT, CONTRIBUTING and data/rules/README: random is required,
  the walkthroughs are at rules_version 28 with personalities=4
  build_orders=4, and CONTRIBUTING states the share formula and the
  Standard-mirror run it is defined on.
- BUILD_TIMES: the seed-7 CI step is about 25 s of games at rules 28.
- ROADMAP M6 gains a Notes entry recording the eleven owner decisions of
  2026-10-28 (no acceptance line changed), and bare citations now say
  'M6 owner decision N' (easy.ron's comment only; rules_hash unchanged).

Copy link
Copy Markdown
Owner Author

fun-gate (ubuntu-24.04) failed on 2f31f5a in exactly the two steps the PR description lists as open. This PR caused both failures, and neither is a flake:

  • Step 6, M6 acceptance 2: Standard beat Easy 9/20 and Hard beat Standard 8/20. The bar is 15/20.
  • Step 8, M6 acceptance 4, second clause: at income_interval_ds 300, Easy has no majority in either seat order.

Step 5 (line 1), step 7 (Hard attacks before minute 12) and step 9 (the ignored M6 bot tests) passed.

Cause: when two intact bots meet, defence beats attack. In Easy vs Standard, 15 of 17 games reach the 54000 cap with no Town taken. Mirrored sites then reduce them to exact ties or 3-tile tiebreaks, so the bonus difference never shows.

Fix: a retune is running on this branch now (M6 owner decision 12):

  • Easy plays defensively.
  • Standard and Hard keep up siege pressure until a Town falls.
  • Mirrored sites no longer make cap games exact copies.

It will be pushed here once every fun-gate step passes locally. This PR stays a draft until then, and merges only on a fully green fun-gate (decision 13).


Generated by Claude Code

…oise, all off

Owner decision 12 gives the difficulty levels real behaviour. This adds the
knobs, neutral in the shipped data, so behaviour is unchanged:

- rules: Difficulty.pressure (reinforce_min_units, commit_ds,
  retreat_ratio_permille, retreat_cooldown_permille, home_guard_units,
  lure_ds, cohesion_tiles, launch_ratio_permille) with validators;
  resolve() scales the personality's retreat ratio and cooldown;
  Personality.noise_border_top_k picks BorderEdge sites among the first k
  candidates by each seat's own noise.
- ai: reinforcement groups from the rally join an assaulting siege wave
  (and the all-in goes in such groups), a committed siege neither retreats
  nor times out while its target loses hit points, the lure holds an
  assault at the Wain point while defenders stand near the target, the
  vanguard stops for the rear quarter, a launch can wait until the wave
  outweighs the enemy army at home, and a home guard stays; the siege
  wave's memory gains its target's last hit points. Unit tests for each.
- data: rules_version 29; every new field neutral; fixtures re-recorded
  for their header; only m5_first_match.hash moves (memory bytes),
  0xf78f9cf501cfe707 -> 0x71ed5e727f1430f2, still P0 at tick 41889.
Owner decision 12: Standard and Hard keep up siege pressure and Easy stays
defensive. Standard and Hard reinforce assaults with rally groups, commit to
a siege that is taking a Town down, never retreat, lure defenders out and
keep their waves together; Standard launches only when it outweighs the army
at home (0.8), Hard assaults a staged wave only at 1.3 to 1 and waves 0.8
times as far apart. The all-ins go Hard 30:00, Easy 33:00, Standard 36:00.
Easy and the tower personality research Statecraft II, so a cap game in
which no Town fell is no longer a mirror image; Easy drops the 850 Yeomen
fallback, which measured as a stronger Easy.

Code: the cohesion rule counts only units past the stage point and never
stops a unit under fire, a rally group always leaves one armed unit home,
Pressure gains assault_ratio_permille and Difficulty gains extra_research.
The measured-and-unused standoff knob and the personality
noise_border_top_k are removed.

rules_version 30; every fixture re-recorded for its header, only
m5_first_match's .hash moves.
The bench's mirrored world never lets a wave outweigh the enemy army at
home, so it sets each level's nonzero launch and assault gates to 1 per
mille (still checked and timed) and gives a level that never retreats the
personality's retreat back, so the influence and retreat paths it times
still run.

docs/design/ai.md gains the retuned difficulty table, the behaviour, sweep
and stalemate analysis, the rules-30 flip and fun-gate readings, and
answers the open questions owner decisions 12 and 14 settled; the data
READMEs, DETERMINISM, STREAMING, CONTRIBUTING, DATA_FORMAT and BUILD_TIMES
follow rules_version 30.
…nforcement

Pin the research that keeps mirrored cap games apart: Easy's resolved
order differs from Standard's and Hard's, tower researches Statecraft
II, and Standard and Hard still resolve to the same order.
The review of the M6 retune found that pressure.retreat_ratio_permille 0
had switched retreat-when-losing off for Standard (and its raids). A
committed siege still never retreats. Hard keeps 0: restoring it at 500
or 1000 cost line 2's second clause and the flip's 200 clause.
rules_version 31; only m5_first_match.hash moves (38818, P0).
… owner

Rewrite the flip's explanation from the measured capture ticks and drop
the general all-in-first rule; correct the sweep figures; record the
seeds 41..60 flip miss, the Balanced-only ordering, the balance band,
the F1 and F2 proxies at rules_version 31 and the same-research mirror
limit; list Easy's earlier all-in and Hard's missing retreat as
deviations pending the owner. Fix the docs that still said the standard
level resolves to the personality itself, the SIM_VERSION 18 line, the
ARCHITECTURE rows and the BUILD_TIMES line-3 row.
tonianev pushed a commit that referenced this pull request Oct 30, 2026
Each line keeps its text and gains the evidence observed locally at
rules_version 31 and in Actions run 41008686740 at de5b0cd (fun-gate,
macOS check, headless and hash parity). Lines 5 and 6 are slice 3's and
the owner's.
Each line keeps its text and gains the evidence observed locally at
rules_version 31 and in Actions run 41008686740 at de5b0cd (fun-gate,
macOS check, headless and hash parity). Lines 5 and 6 are slice 3's and
the owner's.
@tonianev
tonianev force-pushed the claude/confident-hopper-ho6dz1 branch from 237c3ee to 9808600 Compare October 30, 2026 20:51
@tonianev
tonianev marked this pull request as ready for review October 30, 2026 20:51
@tonianev
tonianev merged commit 651e26b into main Oct 30, 2026
6 checks passed
@tonianev
tonianev deleted the claude/confident-hopper-ho6dz1 branch October 30, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants