diff --git a/baselines/hand-authored.json b/baselines/hand-authored.json new file mode 100644 index 0000000..f629622 --- /dev/null +++ b/baselines/hand-authored.json @@ -0,0 +1,48 @@ +{ + "name": "hand-authored", + "bot": "/work/target/release/sds-bot --weights /work/weights/hand-authored.json", + "scenario": "suite:suite:bfe96866022f", + "games": 24, + "decided": 19, + "wins": 4, + "outcomes": { + "victory": 19, + "round-limit": 3, + "stalled": 2 + }, + "bv_left": 0.21960347553646897, + "bv_left_opponent": 0.5672029341088792, + "bv_destroyed_percent": 70, + "max_rounds": 40, + "opponent": "princess", + "epoch": 3, + "commit": "5639825", + "recorded": "2026-08-20T01:00:52Z", + "notes": "hand-authored weights vs Princess, epoch 3, with imitation corpus", + "seeds": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24 + ] +} diff --git a/crates/sds-core/src/features/mod.rs b/crates/sds-core/src/features/mod.rs index fd690eb..19e96bf 100644 --- a/crates/sds-core/src/features/mod.rs +++ b/crates/sds-core/src/features/mod.rs @@ -309,14 +309,19 @@ impl Weights { // the enemy's guns costs about what the same share of damage dealt // is worth, and the bot has been paying it without a term for it. .with::(-3.0) - // Being seen at all is the precondition for being shot. Small next - // to exposure, which prices *whose* guns are doing the seeing. - .with::(-1.0) + // Being seen at all is the precondition for being shot. Equal and + // opposite to `los_out` on purpose: MegaMek's trace is only + // asymmetric when the two units differ in height or elevation, so + // on flat ground the pair cancels and `exposure` prices the danger + // on its own. Where a ridge does make them differ, this pair is the + // only term that says so. + .with::(-1.5) // Terrain that spoils their aim is worth a good share of the damage // it prevents, and unlike range it costs us nothing to stand in. .with::(2.0) // We cannot shoot what we cannot see, and the projected volley a - // move is scored on assumes a line that may not exist. + // move is scored on assumes a line that may not exist. See + // `los_in` for why the two are the same size. .with::(1.5) // Standing where our own guns work is the largest single thing a // move can buy, so it outranks every cost on this list. diff --git a/docs/HIERARCHY.md b/docs/HIERARCHY.md index a1ac741..da11fe8 100644 --- a/docs/HIERARCHY.md +++ b/docs/HIERARCHY.md @@ -108,10 +108,11 @@ self-report, so a unit that is wrong about itself is never contradicted. `sds_core::features` is the replacement. A feature is a named number computed from the observation, and it declares how it is normalised — `bounded`, `rank` or `local` — in the type system, so an unnormalised feature does not compile and -a `local` one cannot be given a learned weight. Firing, target, latch and -force-level features exist and are tested; the positional group waits on line of -sight. Nothing in the bot reads them yet, and `serves` is still what decides a -match. +a `local` one cannot be given a learned weight. Firing, target, latch, +force-level and positional features exist and are tested. The positional group +runs off `sds_core::los` through one survey per movement candidate, which is +what lets a hex be scored on what could shoot it and not only on what it could +shoot. ## What is not done yet diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 86409a2..f4ef806 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -45,9 +45,12 @@ host: still several times the bot's whole budget. ## What will dominate once the thinking lands -**Line of sight**, and it turned out cheaper than feared. `cover_quality`, -`arc_exposure`, `crossfire`, `break_los` and a terrain-aware threat map all need -it, and it is O(hexes along the line) per pair: ~1280 queries a round at 8v8. +**Line of sight**, and it turned out cheaper than feared. `exposure`, +`cover_quality`, `los_in`, `los_out`, `rear_arc_gain` and a terrain-aware threat +map all need it, and it is O(hexes along the line) per pair: ~1280 queries a +round at 8v8. The nine positional features that shipped ask through one +`Posture::survey` per candidate, so the count is two traces per enemy per +candidate however many features read the result. This file predicted 5-20 us each and 6-25 ms/round. Measured, one query is **0.7-1.5 us** and a whole round is **0.3-0.7 ms** — better than a fortieth of the guess even taking the high numbers, and about a tenth of what the host diff --git a/weights/hand-authored.json b/weights/hand-authored.json index f58c74a..bf154a1 100644 --- a/weights/hand-authored.json +++ b/weights/hand-authored.json @@ -13,7 +13,7 @@ "has_been_fired_upon": 0.5, "heat_incurred": -2.0, "honour_broken": 1.0, - "los_in": -1.0, + "los_in": -1.5, "los_out": 1.5, "overkill": -3.0, "p_kill": 8.0,