bayes for days
sds docs TACTICS.md
20 kB

Tactics, worked through #

docs/FEATURES.md exists because a sentence is not enough to argue with a fitted weight. A tactic needs it for a third reason again. A feature is a number about one candidate; a label is a window over a match; a tactic is a whole weight vector, and the only thing its name tells you is what somebody hoped it would do. Two tactics whose sentences read completely differently can be the same vector with one number changed, and nothing in the vocabulary would show it.

So each figure here draws one tactic as its difference from Engage, feature by feature. That is the honest picture because it is literally how the vectors are built: Engage is Weights::hand_authored() and every other tactic is that set with one family re-priced, so the bars are the tactic and everything not drawn is shared with the default.

Engage itself is drawn as its own weights rather than as an empty chart, because it is the thing all the other figures are differences from.

One more figure sits above them: the hub. Engage is the default, so being the nearest neighbour of most other tactics is a property it has by construction rather than a pattern anybody found - every other vector is Engage with one family moved, and the only way to land far from it is to move a family that dominates the decision. What the hub figure shows is therefore not that there is a hub but which spokes earn their distance, measured over two recorded corpora with no matches played.

Nothing on this page is transcribed. Every bar is read out of Tactic::weights through the same code the bot would score with, and the radii come from sds_core::tactic::MEASURED, which is what sds tactics recorded. crates/sds-core/tests/tacticdoc.rs fails if the files on disk stop matching what this renders.

The hub and its spokes #

Engage is the default: it optimises for winning trades against enemies globally, it is what the bot plays with no tactic layer at all, and Engage::weights() is Weights::hand_authored() rather than merely resembling it. Every other tactic is defined as that vector with one family of features re-priced.

So Engage being the nearest neighbour of most other tactics is the expected property, not a discovery - a spoke can only land far from the hub by moving a family that dominates the decision. The question this figure answers is which spokes earn their distance.

the hub

Each spoke is placed by how often it disagrees with Engage over a recorded corpus, so further out is more independent. The pair of figures under each name is the two corpora, printed rather than averaged: 20260826T021818Z-bench (300 matches, 8214 movement decisions) and advance-arm-engage (24 matches, 1573 decisions). Only the radius carries meaning - the angle is arbitrary.

What this measurement cannot see. Both corpora were recorded by a bot with no tactic layer, so every menu in them was produced by Engage-like play. A tactic that would have steered a force somewhere else entirely never gets asked about the menu it would have created, only about the menu it was handed. Two tactics that diverge here are certainly two things; two that agree here might still diverge over a match. That is the question a run with the layer switched on answers and this one does not.

The vocabulary #

tactic target nearest agreement reads as
advance — or enemy engage 0.50 partly new
bombard enemy — — not measured
break — harass 0.21 a new direction
control location — — not measured
engage — flank 0.66 partly new
entrench — engage 0.38 a new direction
flank — or enemy engage 0.66 partly new
harass — engage 0.45 partly new
regroup — engage 0.44 partly new
reposition location — — not measured
screen own force — — not measured
spot enemy — — not measured
withdraw — break 0.16 a new direction

Agreement is against the tactic each is closest to, on the mirrored corpus; the asymmetric one differs by at most 0.04 everywhere. It is a share of decisions, not a correlation, so chance agreement over a menu 364 long at the median is near zero rather than near a half.

The lance in the figures #

Eight machines, four designs, one of each weight class, and both sides field the same four. None is a legal design and no tonnage adds up: they exist so that four machines in a lance want different ground, which four copies of one chassis cannot show.

call class role weight guns
U1 / E1 Light Scout 25 tons, walk 6 Medium Laser, Medium Laser
U2 / E2 Medium Striker 50 tons, walk 5 AC/10, Medium Laser
U3 / E3 Heavy Missile Boat 70 tons, walk 4 LRM-15, PPC
U4 / E4 Assault Juggernaut 90 tons, walk 3 AC/20, SRM-6

advance #

Close all the way in, to where the fists reach.

advance

a lance under advance

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 5 of the 62 features in the basis: enemy_centroid, enemy_nearest, expected_damage, incoming_damage, range_band_fit. Everything not drawn is priced exactly as Engage prices it.

Agrees most with engage, 0.50 of the time on the mirrored corpus and 0.45 on the asymmetric one - partly new.

bombard #

Drop fire on them from where they cannot answer.

Not measurable against today's basis. Orderable, and not yet measured against the rest of the vocabulary: MEASURED has no pairwise figures for it, so how far it is a new direction rather than a rename of Entrench is unknown. The spotter it depends on is newer than any corpus here.

The bars below are that stand-in, drawn so the gap is legible rather than hidden. They are not this tactic.

bombard

a lance under bombard

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 10 of the 62 features in the basis: cover_quality, elevation_gain, enemy_nearest, incoming_damage, indirect_share, level_terrain, level_tmm, los_in, los_out, percent_options. Everything not drawn is priced exactly as Engage prices it.

break #

Get out of their line entirely, and give up this turn's shot for it.

break

a lance under break

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 7 of the 62 features in the basis: cover_distance, cover_quality, expected_damage, los_in, los_out, p_kill, range_band_fit. Everything not drawn is priced exactly as Engage prices it.

Agrees most with harass, 0.21 of the time on the mirrored corpus and 0.16 on the asymmetric one - a new direction.

control #

Stay near it, and keep them away from it.

Not built. Named here so the shape of the finished vocabulary is visible from the code, with a GAP saying what is missing. There is no Assignment variant for it: nothing can order one.

Not measurable against today's basis. No feature measures distance to an arbitrary hex, which is the same gap Reposition names. Every distance in the basis - enemy_nearest, enemy_centroid, cohesion, edge_distance - is to something the board names for itself, and a scenario objective is by definition not one of those. Control needs that distance twice over: ours to the place, and theirs. See plan/scoring.md and plan/objectives.md.

The bars below are that stand-in, drawn so the gap is legible rather than hidden. They are not this tactic.

control

Moves 9 of the 62 features in the basis: cohesion, cover_quality, elevation_gain, incoming_damage, level_terrain, level_tmm, los_in, los_out, percent_options. Everything not drawn is priced exactly as Engage prices it.

engage #

Fight where our guns work, and take the exchange we are offered.

engage

a lance under engage

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

The baseline itself, so the other figures have something to be differences from. It gives a non-zero weight to 42 of the 62 features in the basis. The rest are not oversights: a firing decision measures none of the movement features and a movement decision none of the firing ones, so a weight for a feature that decision never took would contribute nothing anyway. heat_incurred is the one deliberate zero among the features it does name - see the comment on Weights::hand_authored for why the hand set declines to guess it.

Agrees most with flank, 0.66 of the time on the mirrored corpus and 0.58 on the asymmetric one - partly new.

entrench #

Take ground that is hard to shoot us on, and hold it.

entrench

a lance under entrench

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 8 of the 62 features in the basis: cover_quality, elevation_gain, incoming_damage, level_terrain, level_tmm, los_in, los_out, percent_options. Everything not drawn is priced exactly as Engage prices it.

Agrees most with engage, 0.38 of the time on the mirrored corpus and 0.40 on the asymmetric one - a new direction.

flank #

Take the arc rather than the range: get behind them.

flank

a lance under flank

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 5 of the 62 features in the basis: cohesion, enemy_centroid, enemy_nearest, range_band_fit, rear_arc_gain. Everything not drawn is priced exactly as Engage prices it.

Agrees most with engage, 0.66 of the time on the mirrored corpus and 0.58 on the asymmetric one - partly new.

harass #

Shoot from where we can still leave, and never be where they want us.

harass

a lance under harass

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 6 of the 62 features in the basis: cover_distance, enemy_nearest, expected_damage, incoming_damage, los_in, percent_options. Everything not drawn is priced exactly as Engage prices it.

Agrees most with engage, 0.45 of the time on the mirrored corpus and 0.51 on the asymmetric one - partly new.

regroup #

Get back together before doing anything else.

regroup

a lance under regroup

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 5 of the 62 features in the basis: cohesion, enemy_nearest, expected_damage, friend_support, incoming_damage. Everything not drawn is priced exactly as Engage prices it.

Agrees most with engage, 0.44 of the time on the mirrored corpus and 0.50 on the asymmetric one - partly new.

reposition #

Go there, whatever the ground on the way is worth.

Not built. Named here so the shape of the finished vocabulary is visible from the code, with a GAP saying what is missing. There is no Assignment variant for it: nothing can order one.

Not measurable against today's basis. No feature measures distance to an arbitrary hex. Every distance in the basis - enemy_nearest, enemy_centroid, cohesion, edge_distance - is to something the board names for itself, and an exogenous destination is by definition not one of those. The weights below are Engage with the local scoring flattened, which is the half of this tactic that is expressible.

The bars below are that stand-in, drawn so the gap is legible rather than hidden. They are not this tactic.

reposition

Moves 5 of the 62 features in the basis: expected_damage, incoming_damage, p_kill, percent_options, range_band_fit. Everything not drawn is priced exactly as Engage prices it.

screen #

Stand between one of ours and what is shooting at them.

Not built. Named here so the shape of the finished vocabulary is visible from the code, with a GAP saying what is missing. There is no Assignment variant for it: nothing can order one.

Not measurable against today's basis. No feature measures standing between two things. cover_distance is about terrain and friend_support is about lines to us, not lines through us. The weights below are the nearest the basis can say and are a health check, not a screen - which is the same decay the old Intent::Screen suffered. See plan/scoring.md.

The bars below are that stand-in, drawn so the gap is legible rather than hidden. They are not this tactic.

screen

Moves 4 of the 62 features in the basis: cohesion, friend_support, incoming_damage, los_in. Everything not drawn is priced exactly as Engage prices it.

spot #

See for somebody else, and accept the worse shot.

spot

Moves 12 of the 62 features in the basis: cover_quality, elevation_gain, expected_damage, incoming_damage, level_terrain, level_tmm, los_in, los_out, overlook, p_kill, percent_options, range_band_fit. Everything not drawn is priced exactly as Engage prices it.

withdraw #

Get off the board alive.

withdraw

a lance under withdraw

The same board and the same eight machines under every implemented tactic, so a difference between two figures is a difference between two weightings and nothing else. Each arrow is an argmax over that unit's real reachable set, scored through the builder the bot's own menu calls - not a drawing of where a tactic ought to go. Their lance does not move, and is scored where it stands rather than over everywhere it could reach, so the picture and the arithmetic agree about what ours was deciding against. Two arrows ending in one hex are not a bug either, and are worth looking at: each unit is scored alone, so nothing here stops four machines wanting the same ground. Reconciling that is the force layer's job, and this is what it has to work with.

Moves 9 of the 62 features in the basis: cohesion, edge_distance, enemy_nearest, expected_damage, incoming_damage, level_tmm, los_in, p_kill, range_band_fit. Everything not drawn is priced exactly as Engage prices it.

Agrees most with break, 0.16 of the time on the mirrored corpus and 0.09 on the asymmetric one - a new direction.