Finished attack funnel. Now implementing defence funnel

This commit is contained in:
Loïc Prieto 2025-06-16 12:35:02 +02:00
parent 434b534012
commit 440c9dd9d5
2 changed files with 63 additions and 6 deletions

View file

@ -6,8 +6,6 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class AttackScenario { public class AttackScenario {
private static final int PROBABILITY_DENOMINATOR_D6 = 6; private static final int PROBABILITY_DENOMINATOR_D6 = 6;
@ -96,13 +94,26 @@ public class AttackScenario {
Weapon weapon = weaponData._1; Weapon weapon = weaponData._1;
int amountOfWeapons = weaponData._2; int amountOfWeapons = weaponData._2;
// Attack probabilities // Attack funnel
var passingHits = calculatePassingHits(weapon, amountOfWeapons); var passingHits = calculatePassingHits(weapon, amountOfWeapons);
var passingWounds = calculatePassingWounds(weapon, amountOfWeapons, passingHits); var passingWounds = calculatePassingWounds(weapon, amountOfWeapons, passingHits);
var normalWounds = passingWounds._1;
var mortalWounds = passingWounds._2;
// Defence funnel
executeDefenceFunnel(weapon, normalWounds, mortalWounds);
} }
} }
private void executeDefenceFunnel(Weapon weapon, double normalWounds, double mortalWounds) {
}
/**
* Returns a tuple of normal passing wounds and mortal wounds so that the caller can handle them differently.
*/
private Tuple2<Double, Double> calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) { private Tuple2<Double, Double> calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) {
double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons); double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons);
var normalHits = passingHits - lethalHitsCount; var normalHits = passingHits - lethalHitsCount;
@ -111,8 +122,38 @@ public class AttackScenario {
int woundRollGoal = calculateWoundRollGoal(weapon); int woundRollGoal = calculateWoundRollGoal(weapon);
int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal); int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal);
var passingWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6) var passingNormalWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_WITHOUT_REROLL, passingNormalWounds+lethalHitsCount);
var woundsRerolls = prepareWoundRerolls(woundRollGoal);
if(woundsRerolls.isPresent()) {
var rerolls = woundsRerolls.get();
var failedWounds = normalHits * ((double) (1 - woundSuccessProbabilityNumerator) / PROBABILITY_DENOMINATOR_D6);
var extraWoundsFromRerolls = failedWounds * rerolls.getProbabilityNumerator() / PROBABILITY_DENOMINATOR_D6;
results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_FROM_REROLL, extraWoundsFromRerolls);
passingNormalWounds += extraWoundsFromRerolls;
}
// Mortal wounds
var passingMortalWounds = 0d;
if(weapon.hasDevastatingWounds()) {
var criticalWoundRollGoal = weapon.getBestAntiFor(defensiveProfile)
.orElse(criticalWoundValue);
var criticalProbability = calculateProbabilityNumeratorFrom(criticalWoundRollGoal);
passingMortalWounds += (normalHits*criticalProbability/PROBABILITY_DENOMINATOR_D6);
if(fishCriticalWounds && hasRerollWoundsAny) {
passingMortalWounds += (normalHits * (1-criticalProbability)/PROBABILITY_DENOMINATOR_D6)*criticalProbability/PROBABILITY_DENOMINATOR_D6;
}
results.setWeaponStat(weapon, WeaponStat.MORTAL_WOUNDS_COUNT, passingMortalWounds);
// passing wounds was calculated from the whole, but we must substract the critical wounds from the whole
// to be able to handle them separately, as mortal wounds replace normal wounds.
passingNormalWounds -= passingMortalWounds;
}
passingNormalWounds += lethalHitsCount;
results.setWeaponStat(weapon, WeaponStat.WOUNDS_TOTAL_COUNT, passingNormalWounds+passingMortalWounds);
return Tuple.of(passingNormalWounds, passingMortalWounds);
} }
private double calculateLethalHits(Weapon weapon, int amountOfWeapons) { private double calculateLethalHits(Weapon weapon, int amountOfWeapons) {
@ -143,7 +184,9 @@ public class AttackScenario {
var hitRerolls = prepareHitRerolls(hitRollGoal); var hitRerolls = prepareHitRerolls(hitRollGoal);
if(hitRerolls.isPresent()) { if(hitRerolls.isPresent()) {
var rerolls = hitRerolls.get(); var rerolls = hitRerolls.get();
passingHits += rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); var successfulHitFromRerolls = rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
results.setWeaponStat(weapon, WeaponStat.HITS_COUNT_FROM_REROLL, successfulHitFromRerolls);
passingHits += successfulHitFromRerolls;
} }
results.setWeaponStat(weapon, WeaponStat.HITS_TOTAL_COUNT, passingHits); results.setWeaponStat(weapon, WeaponStat.HITS_TOTAL_COUNT, passingHits);
@ -161,6 +204,17 @@ public class AttackScenario {
return Optional.empty(); return Optional.empty();
} }
private Optional<Reroll> prepareWoundRerolls(int woundRollGoal) {
if(hasRerollWoundsOnes) {
return Optional.of(Reroll.rerollOnOnes());
}
if(hasRerollWoundsFailures || hasRerollWoundsAny) {
return Optional.of(Reroll.rerollFailures(woundRollGoal));
}
return Optional.empty();
}
private double calculateWeaponAttacks(Weapon weapon, int amountOfWeapons) { private double calculateWeaponAttacks(Weapon weapon, int amountOfWeapons) {
double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons); double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons);
results.setWeaponStat(weapon, WeaponStat.POTENTIAL_HITS_COUNT, weaponAttacks); results.setWeaponStat(weapon, WeaponStat.POTENTIAL_HITS_COUNT, weaponAttacks);

View file

@ -5,9 +5,12 @@ import lombok.Getter;
@Getter @Getter
public enum WeaponStat { public enum WeaponStat {
HITS_COUNT_WITHOUT_REROLL(Double.class), HITS_COUNT_WITHOUT_REROLL(Double.class),
HITS_COUNT_FROM_REROLL(Double.class),
HITS_TOTAL_COUNT(Double.class), HITS_TOTAL_COUNT(Double.class),
POTENTIAL_HITS_COUNT(Double.class), POTENTIAL_HITS_COUNT(Double.class),
WOUNDS_COUNT(Double.class), WOUNDS_TOTAL_COUNT(Double.class),
WOUNDS_COUNT_WITHOUT_REROLL(Double.class),
WOUNDS_COUNT_FROM_REROLL(Double.class),
MORTAL_WOUNDS_COUNT(Double.class), MORTAL_WOUNDS_COUNT(Double.class),
TOTAL_DAMAGE(Double.class), TOTAL_DAMAGE(Double.class),
LETHAL_HITS_COUNT(Double.class), LETHAL_HITS_COUNT(Double.class),