// SPDX-FileCopyrightText: 2021 Johannes Loher // SPDX-FileCopyrightText: 2021 Oliver Rümpelein // // SPDX-License-Identifier: MIT import { getGame } from "../utils/utils"; export function evaluateCheck( dice: number[], checkTargetNumber: number, { maximumCoupResult = 1, minimumFumbleResult = 20, canFumble = true, }: { maximumCoupResult?: number; minimumFumbleResult?: number; canFumble?: boolean } = {}, ): SubCheckResult[] { const diceWithSubChecks = assignSubChecksToDice(dice, checkTargetNumber, { maximumCoupResult: maximumCoupResult, }); return evaluateDiceWithSubChecks(diceWithSubChecks, { maximumCoupResult: maximumCoupResult, minimumFumbleResult: minimumFumbleResult, canFumble: canFumble, }); } interface DieWithSubCheck { result: number; checkTargetNumber: number; } function assignSubChecksToDice( dice: number[], checkTargetNumber: number, { maximumCoupResult = 1, }: { maximumCoupResult?: number; } = {}, ): DieWithSubCheck[] { const requiredNumberOfDice = getRequiredNumberOfDice(checkTargetNumber); if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) { throw new Error(getGame().i18n.localize("DS4.ErrorInvalidNumberOfDice")); } 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): [number, number] => [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: readonly number[], checkTargetNumberForLastSubCheck: number, ) { if (indexOfFirstCoup !== -1 && indexOfSmallestNonCoup === -1) { return true; } const smallestNonCoup = dice[indexOfSmallestNonCoup]; if ( !Number.isInteger(indexOfFirstCoup) || indexOfFirstCoup < -1 || !Number.isInteger(indexOfSmallestNonCoup) || smallestNonCoup === undefined ) { throw new Error("Received an invalid value for the parameter indexOfSmallestNonCoup or indexOfFirstCoup,"); } return ( indexOfFirstCoup !== -1 && smallestNonCoup > checkTargetNumberForLastSubCheck && smallestNonCoup + checkTargetNumberForLastSubCheck > 20 ); } interface SubCheckResult extends DieWithSubCheck { active?: boolean; discarded?: boolean; success?: boolean; failure?: boolean; count?: number; } function evaluateDiceWithSubChecks( results: DieWithSubCheck[], { maximumCoupResult, minimumFumbleResult, canFumble, }: { maximumCoupResult: number; minimumFumbleResult: number; canFumble: boolean }, ): SubCheckResult[] { return results.map((dieWithSubCheck, index) => { const result: SubCheckResult = { ...dieWithSubCheck, active: dieWithSubCheck.result <= dieWithSubCheck.checkTargetNumber, discarded: dieWithSubCheck.result > dieWithSubCheck.checkTargetNumber, }; if (result.result <= maximumCoupResult) { result.success = true; result.count = result.checkTargetNumber; result.active = true; result.discarded = false; } if (index === 0 && canFumble && result.result >= minimumFumbleResult) result.failure = true; return result; }); } export function getRequiredNumberOfDice(checkTargetNumber: number): number { return Math.max(Math.ceil(checkTargetNumber / 20), 1); }