From fc88ce6c527bb2c58dc3188a07dd1dc6f3fcbd3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Oliver=20R=C3=BCmpelein?= <oli_r@fg4f.de>
Date: Mon, 4 Jan 2021 19:38:26 +0100
Subject: [PATCH] Rename `Test` to `check`, etc., restructure calculation code.

---
 spec/support/ds4rolls/executor.spec.ts |  8 ++---
 src/module/rolls/roll-executor.ts      | 47 ++++++++------------------
 src/module/rolls/roll-utils.ts         | 36 ++++++++++++++++++--
 3 files changed, 52 insertions(+), 39 deletions(-)

diff --git a/spec/support/ds4rolls/executor.spec.ts b/spec/support/ds4rolls/executor.spec.ts
index e141b215..3417e6e6 100644
--- a/spec/support/ds4rolls/executor.spec.ts
+++ b/spec/support/ds4rolls/executor.spec.ts
@@ -93,7 +93,7 @@ describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
         );
     });
 
-    it("Should do a regular success on `20` with a test value of 20", () => {
+    it("Should do a regular success on `20` with a CTN of 20", () => {
         const rollProvider = mockSingleThrow(20);
 
         expect(rollCheckSingleDie(20, { useSlayingDice: true, slayingDiceRepetition: true }, rollProvider)).toEqual(
@@ -209,7 +209,7 @@ describe("DS4 Rolls with multiple dice and no modifiers.", () => {
         );
     });
 
-    it("Should maximize on 'lowest dice higher than last test and crit success thrown'-Edge case, no change required.", () => {
+    it("Should maximize on 'lowest dice higher than last CTN and crit success thrown'-Edge case, no change required.", () => {
         const rollProvider = mockMultipleThrows([15, 1, 8]);
 
         expect(rollCheckMultipleDice(46, {}, rollProvider)).toEqual(
@@ -217,7 +217,7 @@ describe("DS4 Rolls with multiple dice and no modifiers.", () => {
         );
     });
 
-    it("Should maximize on 2-dice 'lowest dice higher than last test and crit success thrown'-Edge case, no change required.", () => {
+    it("Should maximize on 2-dice 'lowest dice higher than last CTN and crit success thrown'-Edge case, no change required.", () => {
         const rollProvider = mockMultipleThrows([1, 8]);
 
         expect(rollCheckMultipleDice(24, {}, rollProvider)).toEqual(
@@ -225,7 +225,7 @@ describe("DS4 Rolls with multiple dice and no modifiers.", () => {
         );
     });
 
-    it("Should maximize on 2-dice 'lowest dice higher than last test and crit success thrown'-Edge case,  change required.", () => {
+    it("Should maximize on 2-dice 'lowest dice higher than last CTN and crit success thrown'-Edge case,  change required.", () => {
         const rollProvider = mockMultipleThrows([1, 19]);
 
         expect(rollCheckMultipleDice(38, {}, rollProvider)).toEqual(
diff --git a/src/module/rolls/roll-executor.ts b/src/module/rolls/roll-executor.ts
index 233950ea..896629a2 100644
--- a/src/module/rolls/roll-executor.ts
+++ b/src/module/rolls/roll-executor.ts
@@ -1,18 +1,19 @@
 import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data";
 import { DS4RollProvider, RollProvider } from "./roll-provider";
-import { isDiceSwapNecessary, isSlayingDiceRepetition } from "./roll-utils";
+import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition } from "./roll-utils";
 
-export function ds4test(testValue: number, rollOptions: Partial<RollOptions> = {}): RollResult {
-    const finalRollValue = testValue;
-    if (finalRollValue <= 20) {
-        return rollCheckSingleDie(finalRollValue, rollOptions);
+export function ds4roll(checkTargetValue: number, rollOptions: Partial<RollOptions> = {}): RollResult {
+    // TODO: Add CTN modifiers from options.
+    const finalTargetValue = checkTargetValue;
+    if (finalTargetValue <= 20) {
+        return rollCheckSingleDie(finalTargetValue, rollOptions);
     } else {
-        return rollCheckMultipleDice(finalRollValue, rollOptions);
+        return rollCheckMultipleDice(finalTargetValue, rollOptions);
     }
 }
 
 export function rollCheckSingleDie(
-    testValue: number,
+    checkTargetValue: number,
     rollOptions: Partial<RollOptions>,
     provider: RollProvider = new DS4RollProvider(),
 ): RollResult {
@@ -21,11 +22,11 @@ export function rollCheckSingleDie(
     const dice = [roll];
 
     if (roll <= usedOptions.maxCritSucc) {
-        return new RollResult(testValue, RollResultStatus.CRITICAL_SUCCESS, dice);
+        return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, dice);
     } else if (roll >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
         return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice);
     } else {
-        if (roll <= testValue) {
+        if (roll <= checkTargetValue) {
             return new RollResult(roll, RollResultStatus.SUCCESS, dice);
         } else {
             return new RollResult(0, RollResultStatus.FAILURE, dice);
@@ -51,13 +52,13 @@ function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): [A
 }
 
 export function rollCheckMultipleDice(
-    testValue: number,
+    targetValue: number,
     rollOptions: Partial<RollOptions>,
     provider: RollProvider = new DS4RollProvider(),
 ): RollResult {
     const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
-    const finalCheck = testValue % 20;
-    const numberOfDice = Math.ceil(testValue / 20);
+    const remainderTargetValue = targetValue % 20;
+    const numberOfDice = Math.ceil(targetValue / 20);
 
     const dice = provider.getNextRolls(numberOfDice);
 
@@ -71,7 +72,7 @@ export function rollCheckMultipleDice(
 
     const [critSuccesses, otherRolls] = separateCriticalHits(dice, usedOptions);
 
-    const swapLastWithCrit: boolean = isDiceSwapNecessary(critSuccesses, otherRolls, finalCheck);
+    const swapLastWithCrit: boolean = isDiceSwapNecessary(critSuccesses, otherRolls, remainderTargetValue);
 
     let sortedRollResults: Array<number>;
 
@@ -83,25 +84,7 @@ export function rollCheckMultipleDice(
         sortedRollResults = critSuccesses.concat(otherRolls);
     }
 
-    const evaluationResult = sortedRollResults
-        .map((value, index) => {
-            if (index == numberOfDice - 1) {
-                if (value <= usedOptions.maxCritSucc) {
-                    return finalCheck;
-                } else if (value <= finalCheck) {
-                    return value;
-                } else {
-                    return 0;
-                }
-            } else {
-                if (value <= usedOptions.maxCritSucc) {
-                    return 20;
-                } else {
-                    return value;
-                }
-            }
-        })
-        .reduce((a, b) => a + b);
+    const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);
 
     if (usedOptions.useSlayingDice && firstResult <= usedOptions.maxCritSucc) {
         return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, sortedRollResults);
diff --git a/src/module/rolls/roll-utils.ts b/src/module/rolls/roll-utils.ts
index 14e95f01..5df493e2 100644
--- a/src/module/rolls/roll-utils.ts
+++ b/src/module/rolls/roll-utils.ts
@@ -3,20 +3,50 @@ import { RollOptions } from "./roll-data";
 export function isDiceSwapNecessary(
     critSuccesses: Array<number>,
     otherRolls: Array<number>,
-    lastTestValue: number,
+    remainingTargetValue: number,
 ): boolean {
     if (critSuccesses.length == 0 || otherRolls.length == 0) {
         return false;
     }
     const amountOfOtherRolls = otherRolls.length;
     const lastDice = otherRolls[amountOfOtherRolls - 1];
-    if (lastDice <= lastTestValue) {
+    if (lastDice <= remainingTargetValue) {
         return false;
     }
 
-    return lastDice + lastTestValue > 20;
+    return lastDice + remainingTargetValue > 20;
 }
 
 export function isSlayingDiceRepetition(opts: RollOptions): boolean {
     return opts.useSlayingDice && opts.slayingDiceRepetition;
 }
+
+export function calculateRollResult(
+    assignedRollResults: Array<number>,
+    remainderTargetValue: number,
+    rollOptions: RollOptions,
+): number {
+    const numberOfDice = assignedRollResults.length;
+
+    const maxResultPerDie: Array<number> = Array(numberOfDice).fill(20);
+    maxResultPerDie[numberOfDice - 1] = remainderTargetValue;
+
+    const rollsAndMaxValues = zip(assignedRollResults, maxResultPerDie);
+
+    return rollsAndMaxValues
+        .map(([v, m]) => {
+            return v <= rollOptions.maxCritSucc ? [m, m] : [v, m];
+        })
+        .filter(([v, m]) => v <= m)
+        .map(([v]) => v)
+        .reduce((a, b) => a + b);
+}
+
+// TODO: Move to generic utils method?
+function zip<T, U>(a1: Array<T>, a2: Array<U>): Array<[T, U]> {
+    if (a1.length <= a2.length) {
+        return a1.map((e1, i) => [e1, a2[i]]);
+    } else {
+        return a2.map((e2, i) => [a1[i], e2]);
+    }
+}