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>
|
||||
<version>24-ea+5</version>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
|
@ -48,6 +63,11 @@
|
|||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -6,15 +6,14 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class AttackScenario {
|
||||
private static final int PROBABILITY_DENOMINATOR_D6 = 6;
|
||||
|
||||
// Attack attributes
|
||||
private List<Tuple2<Weapon, Integer>> weapons;
|
||||
private Optional<Reroll> hitReroll;
|
||||
private Optional<Reroll> woundReroll;
|
||||
private Optional<Reroll> damageReroll;
|
||||
private boolean unmodifiableHit;
|
||||
private boolean attackerWasStationary;
|
||||
private boolean attackerCharged;
|
||||
|
@ -22,6 +21,16 @@ public class AttackScenario {
|
|||
private boolean hasHitRollBonus;
|
||||
private boolean hasWoundRollBonus;
|
||||
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
|
||||
private DefensiveProfile defensiveProfile;
|
||||
|
@ -32,12 +41,11 @@ public class AttackScenario {
|
|||
private boolean woundRollPenaltyIfStrengthIsHigher;
|
||||
private int damageReduction;
|
||||
|
||||
public AttackScenario() {
|
||||
// Result
|
||||
private AttackScenarioResolution results;
|
||||
|
||||
public AttackScenario() {
|
||||
weapons = new ArrayList<>();
|
||||
hitReroll = Optional.empty();
|
||||
woundReroll = Optional.empty();
|
||||
damageReroll = Optional.empty();
|
||||
unmodifiableHit = false;
|
||||
attackerWasStationary = false;
|
||||
attackerCharged = true;
|
||||
|
@ -45,6 +53,16 @@ public class AttackScenario {
|
|||
hasHitRollBonus = false;
|
||||
hasWoundRollBonus = false;
|
||||
hasLineOfSight = true;
|
||||
hasRerollHitsOnes = false;
|
||||
hasRerollHitsFailures = false;
|
||||
hasRerollHitsAny = false;
|
||||
hasRerollWoundsOnes = false;
|
||||
hasRerollWoundsFailures = false;
|
||||
hasRerollWoundsAny = false;
|
||||
fishCriticalHits = false;
|
||||
fishCriticalWounds = false;
|
||||
criticalHitValue = 6;
|
||||
criticalWoundValue = 6;
|
||||
|
||||
defensiveProfile = null;
|
||||
normalSaveReroll = Optional.empty();
|
||||
|
@ -53,14 +71,16 @@ public class AttackScenario {
|
|||
isInCover = false;
|
||||
woundRollPenaltyIfStrengthIsHigher = false;
|
||||
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));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttackScenario setDefensiveProfile(DefensiveProfile defensiveProfile) {
|
||||
public AttackScenario withDefensiveProfile(DefensiveProfile defensiveProfile) {
|
||||
this.defensiveProfile = defensiveProfile;
|
||||
return this;
|
||||
}
|
||||
|
@ -79,17 +99,66 @@ public class AttackScenario {
|
|||
// Attack probabilities
|
||||
int finalHitRollGoal = calculateHitRollGoal(weapon);
|
||||
int finalWoundRollGoal = calculateWoundRollGoal(weapon.getStrength(), defensiveProfile.getToughness(), weapon);
|
||||
int hitSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(finalHitRollGoal);
|
||||
int hitFailureProbabilityNumerator = 6 - hitSuccessProbabilityNumerator;
|
||||
int passingHits = (weapon.getAttacks()*amountOfWeapons) * (hitSuccessProbabilityNumerator/PROBABILITY_DENOMINATOR_D6);
|
||||
if(hitReroll.isPresent()) {
|
||||
var passingHits = calculatePassingHits(weapon, amountOfWeapons, finalHitRollGoal);
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
var finalHitRollGoal = weapon.getHitValue();
|
||||
if(hasHitRollBonus) {
|
||||
|
|
|
@ -5,17 +5,30 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
|||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
@Getter
|
||||
public class AttackScenarioResolution {
|
||||
private List<Tuple2<String, Double>> wounds;
|
||||
private Map<String, Map<WeaponStat, Object>> weaponStats;
|
||||
|
||||
public AttackScenarioResolution() {
|
||||
this.wounds = new ArrayList<>();
|
||||
this.weaponStats = new HashMap<>();
|
||||
}
|
||||
|
||||
public void addWoundsFromWeapon(Weapon weapon, double inflictedWounds) {
|
||||
this.wounds.add(Tuple.of(weapon.getName(), inflictedWounds));
|
||||
public void setWeaponStat(Weapon weapon, WeaponStat statName, Object statValue) {
|
||||
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.stage.Stage;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class FourtyKCalculatorApp extends Application {
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
|
@ -20,20 +18,8 @@ public class FourtyKCalculatorApp extends Application {
|
|||
|
||||
Button calculateBtn = new Button("Calculate");
|
||||
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();
|
||||
for (DefensiveProfile target : targets) {
|
||||
sb.append(calculate(target, 3, true, true, true, "melta"));
|
||||
sb.append("\n");
|
||||
}
|
||||
output.setText(sb.toString());
|
||||
output.setText("lol");
|
||||
});
|
||||
|
||||
VBox layout = new VBox(10);
|
||||
|
|
|
@ -1,7 +1,44 @@
|
|||
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 {
|
||||
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;
|
||||
@Getter
|
||||
private Optional<Integer> melta;
|
||||
@Getter
|
||||
private Optional<Integer> rapidFire;
|
||||
|
||||
public boolean 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;
|
||||
|
||||
import com.sephire.soa.core.utils.functions.CheckedCallable;
|
||||
import com.sephire.soa.core.utils.functions.CheckedRunnable;
|
||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.CheckedCallable;
|
||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.functions.CheckedRunnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.Getter;
|
||||
import lombok.ToString;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.Getter;
|
||||
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