Keep implementing attack scenario resolution
This commit is contained in:
parent
04cd4a50ff
commit
d93a5c50d1
11 changed files with 219 additions and 39 deletions
20
pom.xml
20
pom.xml
|
@ -29,6 +29,21 @@
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>24-ea+5</version>
|
<version>24-ea+5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JUnit 5 (Jupiter) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.10.5</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.26.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -48,6 +63,11 @@
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
|
@ -6,15 +6,14 @@ 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;
|
||||||
|
|
||||||
// Attack attributes
|
// Attack attributes
|
||||||
private List<Tuple2<Weapon, Integer>> weapons;
|
private List<Tuple2<Weapon, Integer>> weapons;
|
||||||
private Optional<Reroll> hitReroll;
|
|
||||||
private Optional<Reroll> woundReroll;
|
|
||||||
private Optional<Reroll> damageReroll;
|
|
||||||
private boolean unmodifiableHit;
|
private boolean unmodifiableHit;
|
||||||
private boolean attackerWasStationary;
|
private boolean attackerWasStationary;
|
||||||
private boolean attackerCharged;
|
private boolean attackerCharged;
|
||||||
|
@ -22,6 +21,16 @@ public class AttackScenario {
|
||||||
private boolean hasHitRollBonus;
|
private boolean hasHitRollBonus;
|
||||||
private boolean hasWoundRollBonus;
|
private boolean hasWoundRollBonus;
|
||||||
private boolean hasLineOfSight;
|
private boolean hasLineOfSight;
|
||||||
|
private boolean hasRerollHitsOnes;
|
||||||
|
private boolean hasRerollHitsFailures;
|
||||||
|
private boolean hasRerollHitsAny;
|
||||||
|
private boolean hasRerollWoundsOnes;
|
||||||
|
private boolean hasRerollWoundsFailures;
|
||||||
|
private boolean hasRerollWoundsAny;
|
||||||
|
private boolean fishCriticalHits;
|
||||||
|
private boolean fishCriticalWounds;
|
||||||
|
private int criticalHitValue;
|
||||||
|
private int criticalWoundValue;
|
||||||
|
|
||||||
// Defense attributes
|
// Defense attributes
|
||||||
private DefensiveProfile defensiveProfile;
|
private DefensiveProfile defensiveProfile;
|
||||||
|
@ -32,12 +41,11 @@ public class AttackScenario {
|
||||||
private boolean woundRollPenaltyIfStrengthIsHigher;
|
private boolean woundRollPenaltyIfStrengthIsHigher;
|
||||||
private int damageReduction;
|
private int damageReduction;
|
||||||
|
|
||||||
public AttackScenario() {
|
// Result
|
||||||
|
private AttackScenarioResolution results;
|
||||||
|
|
||||||
|
public AttackScenario() {
|
||||||
weapons = new ArrayList<>();
|
weapons = new ArrayList<>();
|
||||||
hitReroll = Optional.empty();
|
|
||||||
woundReroll = Optional.empty();
|
|
||||||
damageReroll = Optional.empty();
|
|
||||||
unmodifiableHit = false;
|
unmodifiableHit = false;
|
||||||
attackerWasStationary = false;
|
attackerWasStationary = false;
|
||||||
attackerCharged = true;
|
attackerCharged = true;
|
||||||
|
@ -45,6 +53,16 @@ public class AttackScenario {
|
||||||
hasHitRollBonus = false;
|
hasHitRollBonus = false;
|
||||||
hasWoundRollBonus = false;
|
hasWoundRollBonus = false;
|
||||||
hasLineOfSight = true;
|
hasLineOfSight = true;
|
||||||
|
hasRerollHitsOnes = false;
|
||||||
|
hasRerollHitsFailures = false;
|
||||||
|
hasRerollHitsAny = false;
|
||||||
|
hasRerollWoundsOnes = false;
|
||||||
|
hasRerollWoundsFailures = false;
|
||||||
|
hasRerollWoundsAny = false;
|
||||||
|
fishCriticalHits = false;
|
||||||
|
fishCriticalWounds = false;
|
||||||
|
criticalHitValue = 6;
|
||||||
|
criticalWoundValue = 6;
|
||||||
|
|
||||||
defensiveProfile = null;
|
defensiveProfile = null;
|
||||||
normalSaveReroll = Optional.empty();
|
normalSaveReroll = Optional.empty();
|
||||||
|
@ -53,14 +71,16 @@ public class AttackScenario {
|
||||||
isInCover = false;
|
isInCover = false;
|
||||||
woundRollPenaltyIfStrengthIsHigher = false;
|
woundRollPenaltyIfStrengthIsHigher = false;
|
||||||
damageReduction = 0;
|
damageReduction = 0;
|
||||||
|
|
||||||
|
results = new AttackScenarioResolution();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttackScenario withWeapon(Weapon weaponType, int numberOfWeaponsInUnit) {
|
public AttackScenario addWeapon(Weapon weaponType, int numberOfWeaponsInUnit) {
|
||||||
weapons.add(Tuple.of(weaponType, numberOfWeaponsInUnit));
|
weapons.add(Tuple.of(weaponType, numberOfWeaponsInUnit));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttackScenario setDefensiveProfile(DefensiveProfile defensiveProfile) {
|
public AttackScenario withDefensiveProfile(DefensiveProfile defensiveProfile) {
|
||||||
this.defensiveProfile = defensiveProfile;
|
this.defensiveProfile = defensiveProfile;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -79,17 +99,66 @@ public class AttackScenario {
|
||||||
// Attack probabilities
|
// Attack probabilities
|
||||||
int finalHitRollGoal = calculateHitRollGoal(weapon);
|
int finalHitRollGoal = calculateHitRollGoal(weapon);
|
||||||
int finalWoundRollGoal = calculateWoundRollGoal(weapon.getStrength(), defensiveProfile.getToughness(), weapon);
|
int finalWoundRollGoal = calculateWoundRollGoal(weapon.getStrength(), defensiveProfile.getToughness(), weapon);
|
||||||
int hitSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(finalHitRollGoal);
|
var passingHits = calculatePassingHits(weapon, amountOfWeapons, finalHitRollGoal);
|
||||||
int hitFailureProbabilityNumerator = 6 - hitSuccessProbabilityNumerator;
|
|
||||||
int passingHits = (weapon.getAttacks()*amountOfWeapons) * (hitSuccessProbabilityNumerator/PROBABILITY_DENOMINATOR_D6);
|
|
||||||
if(hitReroll.isPresent()) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(finalWoundRollGoal);
|
int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(finalWoundRollGoal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double calculatePassingHits(Weapon weapon, int amountOfWeapons, int hitRollGoal) {
|
||||||
|
int hitSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(hitRollGoal);
|
||||||
|
int hitFailureProbabilityNumerator = 6 - hitSuccessProbabilityNumerator;
|
||||||
|
double weaponAttacks = calculateWeaponAttacks(weapon);
|
||||||
|
|
||||||
|
// Base passing hits without rerolls
|
||||||
|
double passingHits = (weaponAttacks*amountOfWeapons) * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
|
||||||
|
|
||||||
|
// Add hits from rerolls
|
||||||
|
var hitRerolls = prepareHitRerolls(hitRollGoal);
|
||||||
|
if(hitRerolls.isPresent()) {
|
||||||
|
var rerolls = hitRerolls.get();
|
||||||
|
passingHits += rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return passingHits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Reroll> prepareHitRerolls(int hitRollGoal) {
|
||||||
|
if(hasRerollHitsOnes) {
|
||||||
|
return Optional.of(Reroll.rerollOnOnes());
|
||||||
|
}
|
||||||
|
if(hasRerollHitsFailures || hasRerollHitsAny) {
|
||||||
|
return Optional.of(Reroll.rerollFailures(hitRollGoal));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateWeaponAttacks(Weapon weapon) {
|
||||||
|
double weaponAttacks = weapon.getAttacks();
|
||||||
|
if(weapon.getRapidFire().isPresent() && attackerInRapidFireRange) {
|
||||||
|
weaponAttacks += weapon.getRapidFire().get();
|
||||||
|
}
|
||||||
|
results.setWeaponStat(weapon, WeaponStat.POTENTIAL_HITS_COUNT, weaponAttacks);
|
||||||
|
|
||||||
|
// Add sustained hits
|
||||||
|
if(weapon.getSustainedHits().isPresent()) {
|
||||||
|
var sustainedHitsValue = weapon.getSustainedHits().get();
|
||||||
|
|
||||||
|
double nonSustainedProbability = (double) (6 - (7 - criticalHitValue)) /PROBABILITY_DENOMINATOR_D6;
|
||||||
|
double sustainedProbability = (double) (7 - criticalHitValue) /PROBABILITY_DENOMINATOR_D6;
|
||||||
|
if(fishCriticalHits && hasRerollHitsAny) {
|
||||||
|
sustainedProbability += nonSustainedProbability*sustainedProbability;
|
||||||
|
}
|
||||||
|
var sustainedHitsAmount = weaponAttacks * sustainedProbability * sustainedHitsValue;
|
||||||
|
results.setWeaponStat(weapon, WeaponStat.SUSTAINED_HITS_COUNT, sustainedHitsAmount);
|
||||||
|
weaponAttacks += sustainedHitsAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weaponAttacks;
|
||||||
|
}
|
||||||
|
|
||||||
private int calculateHitRollGoal(Weapon weapon) {
|
private int calculateHitRollGoal(Weapon weapon) {
|
||||||
var finalHitRollGoal = weapon.getHitValue();
|
var finalHitRollGoal = weapon.getHitValue();
|
||||||
if(hasHitRollBonus) {
|
if(hasHitRollBonus) {
|
||||||
|
|
|
@ -5,17 +5,30 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
||||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class AttackScenarioResolution {
|
public class AttackScenarioResolution {
|
||||||
private List<Tuple2<String, Double>> wounds;
|
private Map<String, Map<WeaponStat, Object>> weaponStats;
|
||||||
|
|
||||||
public AttackScenarioResolution() {
|
public AttackScenarioResolution() {
|
||||||
this.wounds = new ArrayList<>();
|
this.weaponStats = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addWoundsFromWeapon(Weapon weapon, double inflictedWounds) {
|
public void setWeaponStat(Weapon weapon, WeaponStat statName, Object statValue) {
|
||||||
this.wounds.add(Tuple.of(weapon.getName(), inflictedWounds));
|
if(!statValue.getClass().isAssignableFrom(statName.getStatClass())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
format("The stat %s's value does not have the expected class %s, but instead has %s class",
|
||||||
|
statName.name(), statName.getStatClass().getName(), statValue.getClass().getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentStats = weaponStats.putIfAbsent(weapon.getName(), Map.of(statName, statValue));
|
||||||
|
if(currentStats != null) {
|
||||||
|
currentStats.put(statName, statValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ import javafx.scene.control.TextArea;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class FourtyKCalculatorApp extends Application {
|
public class FourtyKCalculatorApp extends Application {
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws Exception {
|
public void start(Stage stage) throws Exception {
|
||||||
|
@ -20,20 +18,8 @@ public class FourtyKCalculatorApp extends Application {
|
||||||
|
|
||||||
Button calculateBtn = new Button("Calculate");
|
Button calculateBtn = new Button("Calculate");
|
||||||
calculateBtn.setOnAction(e -> {
|
calculateBtn.setOnAction(e -> {
|
||||||
List<DefensiveProfile> targets = Arrays.asList(
|
|
||||||
new DefensiveProfile("Rhino", 9, 3, null, 10),
|
|
||||||
new DefensiveProfile("Lehman Russ", 11, 2, null, 13),
|
|
||||||
new DefensiveProfile("Redemptor Dread", 10, 3, 5, 12),
|
|
||||||
new DefensiveProfile("Knight Paladin", 13, 2, 5, 22),
|
|
||||||
new DefensiveProfile("Plagueburst Crawler", 12, 3, 4, 12)
|
|
||||||
);
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
output.setText("lol");
|
||||||
for (DefensiveProfile target : targets) {
|
|
||||||
sb.append(calculate(target, 3, true, true, true, "melta"));
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
output.setText(sb.toString());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
VBox layout = new VBox(10);
|
VBox layout = new VBox(10);
|
||||||
|
|
|
@ -1,7 +1,44 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator;
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
import java.util.Set;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes a D6
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
public class Reroll {
|
public class Reroll {
|
||||||
private Set<Integer> rerollableValues;
|
private final Set<Integer> rerollableValues;
|
||||||
|
|
||||||
|
private Reroll(Set<Integer> rerollableValues) {
|
||||||
|
this.rerollableValues = rerollableValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Reroll rerollOnOnes() {
|
||||||
|
return new Reroll(Set.of(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Reroll rerollFailures(int targetHit) {
|
||||||
|
var rerollableValues = IntStream.range(1, targetHit).boxed()
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
return new Reroll(rerollableValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Reroll rerollOnNot(Set<Integer> allowedValues) {
|
||||||
|
var rerollableValues = IntStream.range(1,7).filter(i-> !allowedValues.contains(i))
|
||||||
|
.boxed().collect(Collectors.toSet());
|
||||||
|
|
||||||
|
return new Reroll(rerollableValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From a D6 returns what proportion of rerollable values this reroll is.
|
||||||
|
*/
|
||||||
|
public double getProbabilityNumerator() {
|
||||||
|
return (double) rerollableValues.size() / 6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ public class Weapon {
|
||||||
private boolean hasIndirectFire;
|
private boolean hasIndirectFire;
|
||||||
@Getter
|
@Getter
|
||||||
private Optional<Integer> melta;
|
private Optional<Integer> melta;
|
||||||
|
@Getter
|
||||||
|
private Optional<Integer> rapidFire;
|
||||||
|
|
||||||
public boolean hasLethalHits() {
|
public boolean hasLethalHits() {
|
||||||
return hasLethalHits;
|
return hasLethalHits;
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum WeaponStat {
|
||||||
|
HITS_COUNT(Double.class),
|
||||||
|
POTENTIAL_HITS_COUNT(Double.class),
|
||||||
|
WOUNDS_COUNT(Double.class),
|
||||||
|
MORTAL_WOUNDS_COUNT(Double.class),
|
||||||
|
TOTAL_DAMAGE(Double.class),
|
||||||
|
LETHAL_HITS_COUNT(Double.class),
|
||||||
|
SUSTAINED_HITS_COUNT(Double.class);
|
||||||
|
|
||||||
|
private final Class<?> statClass;
|
||||||
|
|
||||||
|
WeaponStat(Class<?> statClass) {
|
||||||
|
this.statClass = statClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
||||||
|
|
||||||
import com.sephire.soa.core.utils.functions.CheckedCallable;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.CheckedCallable;
|
||||||
import com.sephire.soa.core.utils.functions.CheckedRunnable;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.CheckedRunnable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
||||||
|
|
||||||
import com.sephire.soa.core.utils.functions.Function3;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.Function3;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
package ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures;
|
||||||
|
|
||||||
import com.sephire.soa.core.utils.functions.Function4;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.Function4;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class RerollTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFailureRerollGenerationSucceeds() {
|
||||||
|
var successHitValue = 4;
|
||||||
|
var failureReroll = Reroll.rerollFailures(successHitValue);
|
||||||
|
var expectedRerollableValues = Set.of(1,2,3);
|
||||||
|
assertThat(failureReroll.getRerollableValues()).isEqualTo(expectedRerollableValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRerollOnes() {
|
||||||
|
var rerollOnes = Reroll.rerollOnOnes();
|
||||||
|
var expectedRerollableValues = Set.of(1);
|
||||||
|
assertThat(rerollOnes.getRerollableValues()).isEqualTo(expectedRerollableValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFishingFives() {
|
||||||
|
var rerollToFishFives = Reroll.rerollOnNot(Set.of(5,6));
|
||||||
|
var expectedRerollableValues = Set.of(1,2,3,4);
|
||||||
|
assertThat(rerollToFishFives.getRerollableValues()).isEqualTo(expectedRerollableValues);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue