--- id: scoring title: One basis, not two status: open dependsOn: [features] exitCriterion: > Every score the bot computes comes from the named feature basis. `Intent`, `Stance`, `Appraisal` and the six-axis `asks` table are deleted, and no weight the bot uses was written by hand. --- # scoring The bot has **two scoring systems** and only one of them is trained. The first is the named feature basis: normalised, documented in [FEATURES.md](../docs/FEATURES.md), fitted from recorded play. It scores candidate stands and volleys. The second is a parallel arithmetic nobody fitted. `unit.rs` scores seven intents from hand-written expressions over `projected_damage`; `surface.rs` maps each intent to a damage weight and six bespoke axes - `take, flank, blind, range, band, heat` - and *those* rank the candidates. The named features are not consulted. This epic deletes the second one. ## What the hand-written arithmetic gets wrong Each of these is measured, and each was found from a different direction: - **Defence is discounted by a hard-coded half.** `Intent::Hold` scores `dealt_now - projected_damage(enemy) * 0.5`, and `asks(Close)` prices the incoming-fire axis at `-0.10` with the comment "the incoming barely priced". The fitted weights independently land at 1.69:1 favouring closing, and the bot takes a quarter of its shots at one hex. - **Intents are denominated inconsistently.** Five are in damage points and read in the tens; `Screen` is `+/-1` and `Withdraw` is `+/-2` to `+/-4`. Each wins 0.2% of decisions - not rarely chosen, arithmetically unable to win. - **Units vote in proportion to their firepower.** `combine` sums raw appraisals, so a unit projecting 40 points outvotes one projecting 1 by forty to one. - **Two quantities answer one question.** `projected_damage` and `expected_damage` both mean "what we would land", computed differently, used by the two systems respectively. ## The work - [x] Express a tactic as a weighting over the **named basis**, not over six bespoke axes. That makes tactics fittable rather than authored, and it is what lets a personality be a weight vector rather than a table somebody typed. `crates/sds-core/src/tactic.rs`: every tactic is `Weights::hand_authored()` with one family re-priced, `Engage` is that set untouched, and `sds tactics` re-scores a recorded corpus under each one. Nothing is wired into the bot yet - the vocabulary and the measurement come first, so the ten can be argued with before anything plays them - [ ] Delete `Intent`, `Stance`, `Appraisal`, `Commitment` and `asks`. The transition model in `plan/tactics.md` replaces what `Commitment` did - [ ] Retire `projected_damage` in favour of `expected_damage`, or say why two are needed - [ ] **`Screen` has no feature to score itself by.** Nothing in the basis measures standing between two things; `cover_distance` and `friend_support` are the nearest and neither is it. This is why the old `Intent::Screen` decayed into a health check. `Screen::GAP` now says so in the type, and `sds tactics` leaves it out of the matrix rather than reporting a stand-in's agreement as its own - [x] **Add one `distance_to_objective` feature.** Three tactics have no honest weighting against this basis, and two of them are the same hole: `Screen` needs to know where it is relative to a friend and their attacker, and `Reposition` needs to know where it is relative to a named hex. Every distance in the basis - `enemy_nearest`, `enemy_centroid`, `cohesion`, `edge_distance` - is to something the board names for *itself*, and both of these are distances to something a **caller** names. So it is one feature rather than two holes: a bounded distance from this hex to a coordinate supplied with the decision, which `Reposition` points at its destination and `Screen` points at the interposing hex between the friend it covers and the enemy nearest that friend. It unblocks `Seize` and `Deny` in `plan/objectives.md` at the same time, since a map objective is exactly a caller-named hex, and it is the smallest thing that closes four gaps. **The parameter travels in the observation.** `Posture` carries an `objective: Option`, so every feature is still a function of the observation alone and a recorded decision holds the hex it was judged against - re-scoring it later cannot reach for a coordinate nobody wrote down, which is the replay property `sds tactics` depends on. The absent case is a **missing column, not a zero one**. `measure` takes a `positional::Objective`, which cannot be built without a hex, so the feature is simply not measured when no order named one. A candidate that was never asked about an objective has not answered badly about one, and the fit sees no row rather than a row of zeroes. **Nothing sets it yet**, so it is inert in this build and a corpus recorded today carries no column for it. It is the scoring half of four gaps; the ordering half belongs to `plan/tactics.md` and `plan/objectives.md` - [ ] **`Delay` stays inexpressible, and that is a separate hole.** Nothing in the basis is denominated in rounds. It is a claim about how long a thing lasts, and every feature is a claim about this turn; no distance feature helps. Three gaps out of thirteen tactics, and the covered ten are all geometry - the same finding as the orphaned clusters below, seen from the tactic side - [ ] Commensurable denominators for the two halves of an exchange. `expected_damage` is a share of our maximum output and `incoming_damage` a share of our remaining health, so their difference is not an exchange ratio - see `plan/matchup.md` ## Orphaned feature clusters Clustering all 51 against the ten tactics leaves four families that no tactic is about, and they are all **resource and method** rather than geometry: | cluster | features | note | |---|---|---| | heat | 5 | a *set point implied by* a tactic, not a tactic. `Break` cools, `Entrench` tolerates | | ammunition | 1 | same shape as heat | | damage kind | `p_kill`, `p_mission_kill`, `p_psr_threshold`, `p_breach` | legging a fast unit and killing a slow one are the same points to opposite purpose | | target choice | the `target_*` family | finish the wounded, kill the dangerous, kill the cheap | The covered clusters are all geometry - range, arc, cover, visibility, cohesion, position. That the bias runs this way probably reflects how the basis grew rather than what matters, and it is the same finding as "offence is priced exactly and defence is not priced at all" seen from the feature side. - [ ] Decide whether damage-kind and target-choice want a **firing solution selector** at the force - one joint choice given final movements - rather than a pre-declared tactic. Fire allocation has complete information after movement, so declaring it in advance can only lose