diff --git a/bridge/sds/ClusterGrouping.java b/bridge/sds/ClusterGrouping.java index d987d50..9d8afbd 100644 --- a/bridge/sds/ClusterGrouping.java +++ b/bridge/sds/ClusterGrouping.java @@ -134,6 +134,18 @@ public final class ClusterGrouping { } } + /** The handler itself, for a probe that needs more than one accessor. */ + public static Object handlerFor(WeaponType weapon, AmmoType round, int range) { + if (!(weapon instanceof Weapon)) { + return null; + } + try { + return build((Weapon) weapon, round, range); + } catch (Throwable unavailable) { + return null; + } + } + private static String key(WeaponType weapon, AmmoType round, int range) { return weapon.getInternalName() + " " + (round == null ? "" : round.getInternalName()) + " " + range; diff --git a/crates/sds-core/src/hitloc.rs b/crates/sds-core/src/hitloc.rs index e08ff31..2984b7b 100644 --- a/crates/sds-core/src/hitloc.rs +++ b/crates/sds-core/src/hitloc.rs @@ -81,22 +81,30 @@ pub const TAC_ON_LOCATION_ROLL: f32 = 1.0 / 36.0; /// The share of through-armour criticals that survive their confirmation roll. /// -/// **One, and that is an upper bound rather than a measurement.** MegaMek marks -/// the natural 2 (see [`TAC_ON_LOCATION_ROLL`]) and then resolves it through -/// `TWGameManager.criticalEntity`, and jmm's rule is that a TAC still has to be -/// confirmed - so a through-armour critical is less likely than the raw 1/36. +/// **One, and jmm has confirmed it: the confirmation is the ordinary critical +/// table.** MegaMek marks the natural 2 (see [`TAC_ON_LOCATION_ROLL`]) and then +/// resolves it through `TWGameManager.criticalEntity`, and what that rolls is +/// the same 2d6 as an internal hit - nothing on a 2 to 7, one on 8-9, two on +/// 10-11, three or a limb on 12. /// -/// Whether that confirmation *is* the ordinary critical table, which already -/// gives nothing on a 2 to 7, or a further roll on top of it, is not settled -/// here: it is a rules question and it is written down for jmm rather than -/// guessed at. One means "the critical table is the confirmation", which makes -/// this term the largest it can honestly be. +/// So the confirmation is [`CRITICALS_PER_INTERNAL_HIT`], which this term +/// already multiplies by, and this constant stays at one. It is kept as a named +/// factor rather than folded away because "a TAC is confirmed by the ordinary +/// table" is a rule somebody may want to find, and because an optional rule +/// that changed it would change this and nothing else. /// -/// The term is linear in this constant, so an answer is a one-line change and -/// `plan/features.md` records what each reading would do to the figures. It -/// ships at the bound because the alternative on offer is zero, and zero is -/// what the feature read for every sandblast volley against whole armour - -/// which is 100% wrong rather than wrong by a factor. +/// **Do not read 1.0 as "a TAC is a certain critical".** A through-armour +/// critical arrives at 1/36 x 15/36, about one shot in 86; the 22/36 here is +/// the *expected count* given one, and the two are the distinction +/// [`CRITICALS_PER_INTERNAL_HIT`] already draws. +/// +/// **Where it lands is the centre torso, not the location that was rolled.** +/// That is the default rule; the "floating TAC" optional rule re-rolls the +/// location instead. It does not enter this arithmetic, because this column is +/// a count over the whole target and a count does not care where. It would +/// enter a value-weighted one - every TAC would carry a centre torso's value - +/// which is one more reason that is a different feature. See +/// `plan/features.md`. pub const TAC_CONFIRMED: f32 = 1.0; /// A location index for an abbreviation, or `None` for one no Mek has. diff --git a/plan/features.md b/plan/features.md index 58730fd..5621b1e 100644 --- a/plan/features.md +++ b/plan/features.md @@ -2217,3 +2217,96 @@ was built to be. and it has not changed - [ ] Re-run the ablation at ~190 matches against a corpus that carries the through-armour term, and split the column before weighting it + +## jmm's answers, and what `getBattleForceDamage` turned out to be + +### The TAC rule confirms the arithmetic rather than correcting it + +> "Yes, it's the ordinary crit table. By default this is CT, but many players use +> the 'floating tac' optional rule that re-rolls the TAC's location." + +The confirmation *is* the ordinary critical table, which this term already +multiplies by, so `TAC_CONFIRMED` stays 1.0 and no constant changes. The number +that is "less than 1/36" is `1/36 x 15/36`, about one shot in 86 - that is the +chance of *any* critical, and the 22/36 in the term is the expected *count* +given one. Same distinction `CRITICALS_PER_INTERNAL_HIT` was written to draw. + +The centre torso half is new and does not touch this column: a count over the +whole target does not care where the critical lands. It would matter to a +value-weighted column - every TAC would carry a centre torso's value, and the +default is CT rather than the rolled location - which is one more reason that is +a separate feature. + +### Rare weapons and water, deprioritised + +M-Pod, iATM-3 and the Mech Mortar AP round: *"rare weapons, not important to get +right, right now."* Water ranges and whose-medium-decides: *"out of scope for +now."* The measurements stay above; nothing is built on them. + +### `getBattleForceDamage` is the Alpha Strike conversion + +Read out of the jar rather than recalled. It is declared on +`megamek.common.equipment.WeaponType`, and its neighbours name it: + +``` +public double getBattleForceDamage(int); +public double getBattleForceDamage(int, megamek.common.equipment.Mounted); +public double adjustBattleForceDamageForMinRange(double); +public int getBattleForceClass(); +public int getAlphaStrikeHeat(); +public int getAlphaStrikeHeatDamage(int); +``` + +`megamek.common.alphaStrike.AlphaStrikeElement` is in the same jar. It returns +Alpha Strike damage points, which is why `Observation.averageDamage` multiplies +by ten to get BattleTech damage back. **So the bot's per-weapon damage for every +cluster weapon is being read out of the Alpha Strike conversion**, which is +ruled out of scope for this project. + +### The actual field set, IS against Clan + +`ISLRM20` and `CLLRM20`, identical ranges 7/14/21 and identical rack 20, every +hex from 1 to 21: + +``` +ISLRM20: getBattleForceDamage(hex) 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 +CLLRM20: getBattleForceDamage(hex) 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2 0.0 0.0 0.0 0.0 0.0 +ISLRM20: getDamage(hex) -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 +CLLRM20: getDamage(hex) -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 +ISLRM20: getShortRange()=7 getMediumRange()=14 getLongRange()=21 getRackSize()=20 getMinimumRange()=6 +CLLRM20: getShortRange()=7 getMediumRange()=14 getLongRange()=21 getRackSize()=20 getMinimumRange()=none +``` + +The cutoff is **hex 17**, not a bracket boundary, and it is the Clan weapon +only. `getDamage(hex)` is the cluster sentinel for both, which is why the code +reaches for the Alpha Strike figure at all. + +### Is there a non-BattleForce source? Partly, and the gap is specific + +``` +ISLRM20 bfDamage(hex9)= 12.0 handler perHit=1 handler=LRMHandler +CLLRM20 bfDamage(hex9)= 12.0 handler perHit=1 handler=LRMHandler +ISStreakSRM6 bfDamage(hex9)= 12.0 handler perHit=2 handler=StreakHandler +ISNarcBeacon bfDamage(hex9)= 0.0 handler perHit=0 handler=NarcHandler +ISMediumLaser bfDamage(hex9)= 5.0 handler perHit=5 handler=EnergyWeaponHandler +ISAC20 bfDamage(hex9)= 20.0 handler perHit=20 handler=ACWeaponHandler +``` + +**For direct fire the resolver is exact and BattleForce is unnecessary**: a +Medium Laser and an AC/20 read 5 and 20 from the handler, matching Alpha Strike +exactly. **For a Narc the handler is right where the cluster table is wrong** - +0 against the cluster table's 2. + +**For a rack the resolver gives damage per missile but not how many land.** +`calculateNumHits` needs a live attack and is not reachable from a static probe. +The cluster table fills it for an ordinary rack - `12 missiles x 1 = 12`, which +is exactly what Alpha Strike says - and is wrong for a Streak, which always +lands every missile: `4 x 2 = 8` against the true 12. + +- [ ] **So #270's fix is a bridge, not a cure.** It fills an Alpha Strike zero + from an Alpha Strike figure one bracket nearer. The cure is to stop + reading damage out of the Alpha Strike conversion: the resolving handler + answers direct fire exactly and answers a Narc correctly, and a rack needs + one more number - the hit count - which is reachable from a bridge probe + that builds a real attack, the way `SdsHitLocations` pins dice to read a + table. **jmm should hear this before #270 merges**