From ea5eee709e0ece76b9019002e7f164f69bd94292 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Sat, 13 Mar 2021 02:27:09 +0100 Subject: [PATCH] Add function to assign sub checks to dice --- package.json | 1 + spec/rolls/check-evaluation.spec.ts | 241 +++++++++++++++++++++++++++ src/module/rolls/check-evaluation.ts | 57 +++++++ 3 files changed, 299 insertions(+) create mode 100644 spec/rolls/check-evaluation.spec.ts create mode 100644 src/module/rolls/check-evaluation.ts diff --git a/package.json b/package.json index ff899987..a27be464 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "lint": "eslint 'src/**/*.ts' --cache", "lint:fix": "eslint 'src/**/*.ts' --cache --fix", "test": "jest", + "test:watch": "jest --watch", "test:ci": "jest --ci --reporters=default --reporters=jest-junit", "format": "prettier --write 'src/**/*.(ts|json|scss)'", "postinstall": "husky install" diff --git a/spec/rolls/check-evaluation.spec.ts b/spec/rolls/check-evaluation.spec.ts new file mode 100644 index 00000000..5c2796c3 --- /dev/null +++ b/spec/rolls/check-evaluation.spec.ts @@ -0,0 +1,241 @@ +import { assignSubChecksToDice } from "../../src/module/rolls/check-evaluation"; + +Object.defineProperty(globalThis, "game", { value: { i18n: { localize: (key: string) => key } } }); + +describe("assignSubChecksToDice with no dice", () => { + it("should throw an error", () => { + expect(() => assignSubChecksToDice([], 10)).toThrow("DS4.ErrorInvalidNumberOfDice"); + }); +}); + +describe("assignSubChecksToDice with more dice than required by the checkTargetNumber", () => { + it("should throw an error", () => { + expect(() => assignSubChecksToDice([10, 10], 10)).toThrow("DS4.ErrorInvalidNumberOfDice"); + }); +}); + +describe("assignSubChecksToDice with less dice than required by the checkTargetNumber", () => { + it("should throw an error", () => { + expect(() => assignSubChecksToDice([10], 21)).toThrow("DS4.ErrorInvalidNumberOfDice"); + }); +}); + +describe("assignSubChecksToDice with a single die", () => { + it("should assign the checkTargetNumber to the single die.", () => { + expect(assignSubChecksToDice([4], 12)).toEqual([{ result: 4, checkTargetNumber: 12 }]); + }); + + it("should assign the checkTargetNumber to the single die on upper edge case.", () => { + expect(assignSubChecksToDice([4], 4)).toEqual([{ result: 4, checkTargetNumber: 4 }]); + }); + + it("should assign the checkTargetNumber to the single die on lower edge case.", () => { + expect(assignSubChecksToDice([5], 4)).toEqual([{ result: 5, checkTargetNumber: 4 }]); + }); + + it("should assign the checkTargetNumber to the single die on upper edge case '19'", () => { + expect(assignSubChecksToDice([19], 4)).toEqual([{ result: 19, checkTargetNumber: 4 }]); + }); + + it("should assign the checkTargetNumber to the single die on '1'", () => { + expect(assignSubChecksToDice([1], 4)).toEqual([{ result: 1, checkTargetNumber: 4 }]); + }); + + it("should assign the checkTargetNumber to the single die on '20'", () => { + expect(assignSubChecksToDice([20], 4)).toEqual([{ result: 20, checkTargetNumber: 4 }]); + }); +}); + +describe("assignSubChecksToDice with a single die and coup modification", () => { + it("should assign the checkTargetNumber to the single die on 'maximumCoupResult'", () => { + expect(assignSubChecksToDice([2], 4, { maximumCoupResult: 2 })).toEqual([{ result: 2, checkTargetNumber: 4 }]); + }); + + it("should assign the checkTargetNumber to the single die on lower edge case '3'", () => { + expect(assignSubChecksToDice([3], 4, { maximumCoupResult: 2 })).toEqual([{ result: 3, checkTargetNumber: 4 }]); + }); +}); + +describe("assignSubChecksToDice with multiple dice", () => { + it("should assign the checkTargetNumber for the last sub check to the lowest non coup, even if the first is '20'.", () => { + expect(assignSubChecksToDice([20, 6, 15], 48)).toEqual([ + { result: 20, checkTargetNumber: 20 }, + { result: 6, checkTargetNumber: 8 }, + { result: 15, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup if there are only coups.", () => { + expect(assignSubChecksToDice([1, 1, 1], 48)).toEqual([ + { result: 1, checkTargetNumber: 8 }, + { result: 1, checkTargetNumber: 20 }, + { result: 1, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first lowest die, even if it is higher than that value.", () => { + expect(assignSubChecksToDice([15, 15, 15], 48)).toEqual([ + { result: 15, checkTargetNumber: 8 }, + { result: 15, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup if its sum with the lowest non coup is high enough.", () => { + expect(assignSubChecksToDice([15, 15, 1], 48)).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + { result: 1, checkTargetNumber: 8 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup if its sum with the lowest non coup is high enough, even if the last die is '20'.", () => { + expect(assignSubChecksToDice([15, 1, 20], 48)).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 1, checkTargetNumber: 8 }, + { result: 20, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when all dice are successes.", () => { + expect(assignSubChecksToDice([15, 4, 12], 46)).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 4, checkTargetNumber: 6 }, + { result: 12, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when one dice is a failure.", () => { + expect(assignSubChecksToDice([15, 8, 12], 46)).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 6 }, + { result: 12, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 'lowest dice higher than last check target number and coups thrown'-edge case, coup not used for last sub CTN.", () => { + expect(assignSubChecksToDice([15, 1, 8], 46)).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 1, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 6 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups thrown'-edge case, coup not used for last sub CTN.", () => { + expect(assignSubChecksToDice([1, 8], 24)).toEqual([ + { result: 1, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 4 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups thrown'-edge case, coup used for last sub CTN.", () => { + expect(assignSubChecksToDice([1, 19], 38)).toEqual([ + { result: 1, checkTargetNumber: 18 }, + { result: 19, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when there is more than one coup and a coup is used for the last sub CTN", () => { + expect(assignSubChecksToDice([1, 1, 15], 48)).toEqual([ + { result: 1, checkTargetNumber: 8 }, + { result: 1, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + ]); + }); +}); + +describe("assignSubChecksToDice with multiple dice and coup modification", () => { + it("should assign the checkTargetNumber for the last sub check to the lowest non coup, even if the first is '19'.", () => { + expect(assignSubChecksToDice([19, 15, 6], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 19, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + { result: 6, checkTargetNumber: 8 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup if there are only coups ('1' and '2').", () => { + expect(assignSubChecksToDice([2, 1, 2], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 2, checkTargetNumber: 8 }, + { result: 1, checkTargetNumber: 20 }, + { result: 2, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first lowest die, even if it is higher than that value.", () => { + expect(assignSubChecksToDice([15, 15, 15], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 8 }, + { result: 15, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough.", () => { + expect(assignSubChecksToDice([15, 15, 2], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + { result: 2, checkTargetNumber: 8 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough, even if the last die is '20'.", () => { + expect(assignSubChecksToDice([15, 2, 20], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 2, checkTargetNumber: 8 }, + { result: 20, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough, even if the last die is '19'.", () => { + expect(assignSubChecksToDice([15, 2, 19], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 2, checkTargetNumber: 8 }, + { result: 19, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when all dice are successes.", () => { + expect(assignSubChecksToDice([15, 4, 12], 46, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 4, checkTargetNumber: 6 }, + { result: 12, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when one dice is a failure.", () => { + expect(assignSubChecksToDice([15, 8, 12], 46, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 6 }, + { result: 12, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup not used for last sub CTN.", () => { + expect(assignSubChecksToDice([15, 2, 8], 46, { maximumCoupResult: 2 })).toEqual([ + { result: 15, checkTargetNumber: 20 }, + { result: 2, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 6 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup not used for last sub CTN.", () => { + expect(assignSubChecksToDice([2, 8], 24, { maximumCoupResult: 2 })).toEqual([ + { result: 2, checkTargetNumber: 20 }, + { result: 8, checkTargetNumber: 4 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup used for last sub CTN.", () => { + expect(assignSubChecksToDice([2, 19], 38, { maximumCoupResult: 2 })).toEqual([ + { result: 2, checkTargetNumber: 18 }, + { result: 19, checkTargetNumber: 20 }, + ]); + }); + + it("should assign the checkTargetNumber for the last sub check to properly maximize the result when there is more than one coup ('1' and '2') and a coup is used for the last sub CTN", () => { + expect(assignSubChecksToDice([1, 2, 15], 48, { maximumCoupResult: 2 })).toEqual([ + { result: 1, checkTargetNumber: 8 }, + { result: 2, checkTargetNumber: 20 }, + { result: 15, checkTargetNumber: 20 }, + ]); + }); +}); diff --git a/src/module/rolls/check-evaluation.ts b/src/module/rolls/check-evaluation.ts new file mode 100644 index 00000000..830e4ca1 --- /dev/null +++ b/src/module/rolls/check-evaluation.ts @@ -0,0 +1,57 @@ +export function assignSubChecksToDice( + dice: number[], + checkTargetNumber: number, + { + maximumCoupResult = 1, + }: { + maximumCoupResult?: number; + } = {}, +): { result: number; checkTargetNumber: number }[] { + const requiredNumberOfDice = Math.ceil(checkTargetNumber / 20); + if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) { + throw new Error(game.i18n.localize("DS4.ErrorInvalidNumberOfDice")); // TODO: add i18n + } + + const checkTargetNumberForLastSubCheck = checkTargetNumber - 20 * (requiredNumberOfDice - 1); + + const indexOfSmallestNonCoup = findIndexOfSmallestNonCoup(dice, maximumCoupResult); + const indexOfFirstCoup = dice.findIndex((die) => die <= maximumCoupResult); + const indexForLastSubCheck = shouldUseCoupForLastSubCheck( + indexOfSmallestNonCoup, + indexOfFirstCoup, + dice, + checkTargetNumberForLastSubCheck, + ) + ? indexOfFirstCoup + : indexOfSmallestNonCoup; + + return dice.map((die, index) => ({ + result: die, + checkTargetNumber: index === indexForLastSubCheck ? checkTargetNumberForLastSubCheck : 20, + })); +} + +function findIndexOfSmallestNonCoup(dice: number[], maximumCoupResult: number): number { + return dice + .map((die, index) => [die, index]) + .filter((indexedDie) => indexedDie[0] > maximumCoupResult) + .reduce( + (smallestIndexedDie, indexedDie) => + indexedDie[0] < smallestIndexedDie[0] ? indexedDie : smallestIndexedDie, + [Infinity, -1], + )[1]; +} + +function shouldUseCoupForLastSubCheck( + indexOfSmallestNonCoup: number, + indexOfFirstCoup: number, + dice: number[], + checkTargetNumberForLastSubCheck: number, +) { + return ( + indexOfFirstCoup !== -1 && + (indexOfSmallestNonCoup === -1 || + (dice[indexOfSmallestNonCoup] > checkTargetNumberForLastSubCheck && + dice[indexOfSmallestNonCoup] + checkTargetNumberForLastSubCheck > 20)) + ); +}