parent
9d02647cdd
commit
84c6744080
@ -0,0 +1,6 @@ |
||||
<component name="InspectionProjectProfileManager"> |
||||
<profile version="1.0"> |
||||
<option name="myName" value="Project Default" /> |
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> |
||||
</profile> |
||||
</component> |
@ -0,0 +1,11 @@ |
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="All Tests" type="JavaScriptTestRunnerJest" nameIsGenerated="true"> |
||||
<node-interpreter value="project" /> |
||||
<node-options value="" /> |
||||
<jest-package value="$PROJECT_DIR$/node_modules/react-scripts" /> |
||||
<working-dir value="$PROJECT_DIR$" /> |
||||
<envs /> |
||||
<scope-kind value="ALL" /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="VcsDirectoryMappings"> |
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,174 @@ |
||||
import {AssertionConfig, AssertionMode, Assertions} from "./Assertions"; |
||||
|
||||
describe("Assertion", () => { |
||||
describe("check", () => { |
||||
const defaults = { |
||||
value: Symbol("test value"), |
||||
config: AssertionConfig.Skip, |
||||
conditionImpl: () => true, |
||||
messageImpl: () => "Mockery", |
||||
} as const |
||||
|
||||
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Skip, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T} |
||||
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Throw, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error} |
||||
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: ReturnType<typeof AssertionConfig.Log>, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T} |
||||
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error} |
||||
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error} { |
||||
const assertion = new Assertions() |
||||
assertion.config = config |
||||
let log: jest.Mock<void, any[]>|undefined = undefined |
||||
if (config.mode === AssertionMode.LOG) { |
||||
log = jest.fn(config.logger) |
||||
assertion.config = AssertionConfig.Log(log) |
||||
} |
||||
const condition = jest.fn(conditionImpl) |
||||
const message = jest.fn(messageImpl) |
||||
|
||||
let error: Error|undefined = undefined |
||||
let retval: T|undefined = undefined |
||||
try { |
||||
retval = assertion.check(value, condition, message) |
||||
} catch (e) { |
||||
if (e instanceof Error && config.mode === AssertionMode.THROW) { |
||||
error = e |
||||
} else { |
||||
throw e |
||||
} |
||||
} |
||||
|
||||
return { |
||||
value, log, condition, message, error, retval |
||||
} |
||||
} |
||||
|
||||
it(`returns the input and does not throw in SKIP`, () => { |
||||
const {value, retval} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Skip |
||||
}) |
||||
|
||||
expect(retval).toBe(value) |
||||
}); |
||||
it(`does not call the condition or message in SKIP`, () => { |
||||
const {condition, message} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Skip |
||||
}) |
||||
|
||||
expect(condition).not.toHaveBeenCalled() |
||||
expect(message).not.toHaveBeenCalled() |
||||
}); |
||||
|
||||
it(`calls the log function with an error, returns the value and does not throw when the condition fails in LOG`, () => { |
||||
const {value, log, retval} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Log(() => {}), |
||||
conditionImpl: () => false, |
||||
messageImpl: () => "Mockery" |
||||
}) |
||||
|
||||
expect(log).toHaveBeenCalledTimes(1) |
||||
expect(log).toHaveBeenCalledWith(expect.objectContaining({message: "Mockery"})) |
||||
expect(log).toHaveBeenCalledWith(expect.any(Error)) |
||||
expect(retval).toBe(value) |
||||
}); |
||||
it(`throws an error when the condition fails in THROW`, () => { |
||||
const {error} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Throw, |
||||
conditionImpl: () => false, |
||||
messageImpl: () => "Mockery" |
||||
}) |
||||
|
||||
expect(error).toEqual(expect.objectContaining({message: "Mockery"})) |
||||
expect(error).toBeInstanceOf(Error) |
||||
}); |
||||
it(`does not log and returns the input when the condition succeeds in LOG`, () => { |
||||
const {value, retval, log} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Log(() => {}), |
||||
conditionImpl: () => true |
||||
}) |
||||
|
||||
expect(log).not.toHaveBeenCalled() |
||||
expect(retval).toBe(value) |
||||
}); |
||||
it(`does not throw and returns the input when the condition succeeds in THROW`, () => { |
||||
const {value, retval, error} = run({ |
||||
...defaults, |
||||
config: AssertionConfig.Throw, |
||||
conditionImpl: () => true |
||||
}) |
||||
|
||||
expect(error).toBeUndefined() |
||||
expect(retval).toBe(value) |
||||
}); |
||||
[AssertionConfig.Log(() => {}), AssertionConfig.Throw].forEach((config) => { |
||||
it(`calls the condition, but not the message, once with the value in ${config.mode} when the condition succeeds`, () => { |
||||
const {value, condition, message} = run({ |
||||
...defaults, |
||||
config, |
||||
conditionImpl: () => true |
||||
}) |
||||
|
||||
expect(condition).toHaveBeenCalledWith(value) |
||||
expect(condition).toHaveBeenCalledTimes(1) |
||||
expect(message).not.toHaveBeenCalled() |
||||
}) |
||||
it(`calls the condition and message once each with the value in ${config.mode} when the condition fails`, () => { |
||||
const {value, condition, message} = run({ |
||||
...defaults, |
||||
config, |
||||
conditionImpl: () => false |
||||
}) |
||||
|
||||
expect(condition).toHaveBeenCalledWith(value) |
||||
expect(condition).toHaveBeenCalledTimes(1) |
||||
expect(message).toHaveBeenCalledWith(value) |
||||
expect(message).toHaveBeenCalledTimes(1) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
function testCheck<T>(run: (input: T) => void, valid: T[], invalid: T[]) { |
||||
valid.forEach((input) => { |
||||
test(`passes ${input}`, () => { |
||||
expect(() => run(input)).not.toThrow() |
||||
}) |
||||
}) |
||||
|
||||
invalid.forEach((input) => { |
||||
test(`fails ${input}`, () => { |
||||
expect(() => run(input)).toThrow() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
describe("checkInteger", () => { |
||||
function run(input: number) { |
||||
const assertion = new Assertions() |
||||
assertion.config = AssertionConfig.Throw |
||||
|
||||
assertion.checkInteger(input, () => "Assertion failed") |
||||
} |
||||
|
||||
const valid: number[] = [0, 1, -1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] |
||||
const invalid: number[] = [Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON] |
||||
|
||||
testCheck(run, valid, invalid) |
||||
}) |
||||
|
||||
describe("checkPositiveIntegerOrZero", () => { |
||||
function run(input: number) { |
||||
const assertion = new Assertions() |
||||
assertion.config = AssertionConfig.Throw |
||||
|
||||
assertion.checkPositiveIntegerOrZero(input, () => "Assertion failed") |
||||
} |
||||
|
||||
const valid: number[] = [0, 1, Number.MAX_SAFE_INTEGER] |
||||
const invalid: number[] = [-1, Number.MIN_SAFE_INTEGER, Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON] |
||||
|
||||
testCheck(run, valid, invalid) |
||||
}) |
||||
}) |
@ -0,0 +1,58 @@ |
||||
export enum AssertionMode { |
||||
SKIP = "SKIP", |
||||
LOG = "LOG", |
||||
THROW = "THROW", |
||||
} |
||||
|
||||
/** Configurations for the different assertion modes. */ |
||||
export namespace AssertionConfig { |
||||
/** The types of the assertion configurations. */ |
||||
export type Type = typeof Skip|typeof Throw|ReturnType<typeof Log> |
||||
/** An assertion configuration suitable for production, that skips assertions without evaluating them. */ |
||||
export const Skip = {mode: AssertionMode.SKIP} as const |
||||
/** An assertion configuration suitable for testing, that throws an exception when an assertion fails. */ |
||||
export const Throw = {mode: AssertionMode.THROW} as const |
||||
/** An assertion configuration suitable for debugging, that logs an exception when an assertion fails but allows the program to continue. */ |
||||
export function Log(logger: (message?: any, ...params: any[]) => void) { |
||||
return {mode: AssertionMode.LOG, logger} as const |
||||
} |
||||
} |
||||
|
||||
/** Assertion class for test-only checks on preconditions and postconditions. */ |
||||
export class Assertions { |
||||
config: AssertionConfig.Type = AssertionConfig.Skip |
||||
|
||||
/** General assert method. Checks that the condition is true. */ |
||||
check<T>(value: T, condition: (value: T) => boolean, message: (value: T) => string): T { |
||||
if (this.config.mode === AssertionMode.SKIP) { |
||||
return value |
||||
} |
||||
const result = condition(value) |
||||
if (result) { |
||||
return value |
||||
} |
||||
const err = Error(message(value)) |
||||
if (this.config.mode === AssertionMode.THROW) { |
||||
throw err |
||||
} else { |
||||
// All we can do is stand by and watch in horror...
|
||||
this.config.logger(err) |
||||
return value |
||||
} |
||||
} |
||||
|
||||
/** Checks that the value is an integer. */ |
||||
checkInteger(value: number, message: (value: number) => string): number { |
||||
return this.check(value, Number.isSafeInteger, message) |
||||
} |
||||
|
||||
/** Checks that the value is an integer and either positive or zero. */ |
||||
checkPositiveIntegerOrZero(value: number, message: (value: number) => string): number { |
||||
return this.check(value, (value) => { |
||||
return Number.isSafeInteger(value) && value >= 0 |
||||
}, message) |
||||
} |
||||
} |
||||
|
||||
/** Global assertion object for use by other modules. */ |
||||
export const assertion = new Assertions(); |
@ -0,0 +1,558 @@ |
||||
import { |
||||
checkValidConfidenceLimit, |
||||
checkValidCurrentHitPoints, |
||||
checkValidHitPoints, |
||||
checkValidHitPointsWithoutConfidence, |
||||
checkValidMaxHitPoints, |
||||
confidenceLimit, |
||||
damageConfidence, |
||||
damageHealth, |
||||
HitPoints, |
||||
HitPointsWithoutConfidence, |
||||
hitPointsWithoutConfidenceToString, |
||||
hitPointsToString, isValidConfidenceLimit, |
||||
isValidCurrentHitPoints, |
||||
isValidHitPoints, |
||||
isValidHitPointsWithoutConfidence, |
||||
isValidMaxHitPoints, |
||||
recoverConfidence, |
||||
recoverHealth, setCurrentConfidence, |
||||
setCurrentHealth, |
||||
setMaxConfidence, |
||||
setMaxHealth, checkConsistentConfidenceDelta |
||||
} from "./HitPoints"; |
||||
import {assertion, AssertionConfig} from "./Assertions"; |
||||
|
||||
const InvalidHitPoints = {currentHealth: 999, maxHealth: 99, maxConfidence: 100, currentConfidence: 20}; |
||||
let originalConfig: AssertionConfig.Type |
||||
|
||||
beforeEach(() => { |
||||
originalConfig = assertion.config |
||||
assertion.config = AssertionConfig.Throw |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
assertion.config = originalConfig |
||||
}); |
||||
|
||||
describe("hitPointsWithoutConfidenceToString", () => { |
||||
const pairs: [HitPointsWithoutConfidence, string][] = [ |
||||
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200}, "{Health: 99/100, Confidence: -/-/200}"], |
||||
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200}, "{Health: -25/0, Confidence: -/-/-200}"], |
||||
] |
||||
pairs.forEach(([hp, expected]) => { |
||||
test(`for ${hp} = "${expected}"`, () => { |
||||
expect(hitPointsWithoutConfidenceToString(hp)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}); |
||||
|
||||
describe("hitPointsToString", () => { |
||||
const pairs: [HitPoints, string][] = [ |
||||
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200, currentConfidence: 10}, "{Health: 99/100, Confidence: 10/198/200}"], |
||||
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200, currentConfidence: -15}, "{Health: -25/0, Confidence: -15/<err>/-200}"], |
||||
] |
||||
pairs.forEach(([hp, expected]) => { |
||||
test(`for ${hp} = "${expected}"`, () => { |
||||
expect(hitPointsToString(hp)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}); |
||||
|
||||
describe("isValidMaxHitPoints", () => { |
||||
const validValues = [1, 2, 99999] |
||||
const invalidValues = [0, -1, 1.5, NaN, Infinity, -Infinity, 100000, 999999] |
||||
validValues.forEach((value) => { |
||||
test(`returns true for ${value}`, () => { |
||||
expect(isValidMaxHitPoints(value)).toBe(true) |
||||
}) |
||||
}) |
||||
invalidValues.forEach((value) => { |
||||
test(`returns false for ${value}`, () => { |
||||
expect(isValidMaxHitPoints(value)).toBe(false) |
||||
}) |
||||
}) |
||||
}); |
||||
|
||||
describe("isValidCurrentHitPoints", () => { |
||||
const validValues: [number, number, number][] = [[1, 1, 0], [-1, 1, -1], [0, 1, 0], [0, 1, -1], [50, 99999, 0]] |
||||
const invalidValues: [number, number, number][] = [[2, 1, 0], [-1, 1, 0], [0.5, 1, 0], [-2, 1, -1]] |
||||
validValues.forEach(([current, max, min]) => { |
||||
test(`returns true for ${current} between ${min} and ${max}`, () => { |
||||
expect(isValidCurrentHitPoints(current, max, min)).toBe(true) |
||||
}) |
||||
}) |
||||
invalidValues.forEach(([current, max, min]) => { |
||||
test(`returns false for ${current} between ${min} and ${max}`, () => { |
||||
expect(isValidCurrentHitPoints(current, max, min)).toBe(false) |
||||
}) |
||||
}) |
||||
}); |
||||
|
||||
describe("isValidHitPointsWithoutConfidence", () => { |
||||
const validValues: HitPointsWithoutConfidence[] = [{ |
||||
currentHealth: 100, |
||||
maxHealth: 100, |
||||
maxConfidence: 100, |
||||
}, { |
||||
currentHealth: 50, |
||||
maxHealth: 100, |
||||
maxConfidence: 25 |
||||
}, { |
||||
currentHealth: 0, |
||||
maxHealth: 1, |
||||
maxConfidence: 1, |
||||
}] |
||||
const invalidValues: HitPointsWithoutConfidence[] = [{ |
||||
currentHealth: 0, |
||||
maxHealth: 0, |
||||
maxConfidence: 99, |
||||
}, { |
||||
currentHealth: 0, |
||||
maxHealth: 20, |
||||
maxConfidence: 0, |
||||
}, { |
||||
currentHealth: -300, |
||||
maxHealth: 100, |
||||
maxConfidence: 99, |
||||
}] |
||||
validValues.forEach((value) => { |
||||
test(`returns true for ${hitPointsWithoutConfidenceToString(value)}`, () => { |
||||
expect(isValidHitPointsWithoutConfidence(value)).toBe(true) |
||||
}) |
||||
}) |
||||
invalidValues.forEach((value) => { |
||||
test(`returns false for ${hitPointsWithoutConfidenceToString(value)}`, () => { |
||||
expect(isValidHitPointsWithoutConfidence(value)).toBe(false) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("isValidHitPoints", () => { |
||||
const validValues: HitPoints[] = [{ |
||||
currentHealth: 100, |
||||
maxHealth: 100, |
||||
currentConfidence: 100, |
||||
maxConfidence: 100, |
||||
}, { |
||||
currentHealth: 50, |
||||
maxHealth: 100, |
||||
currentConfidence: 30, |
||||
maxConfidence: 60, |
||||
}, { |
||||
currentHealth: 0, |
||||
maxHealth: 100, |
||||
currentConfidence: 0, |
||||
maxConfidence: 30, |
||||
}] |
||||
const invalidValues: HitPoints[] = [{ |
||||
currentHealth: 11, |
||||
maxHealth: 10, |
||||
currentConfidence: 99, |
||||
maxConfidence: 99, |
||||
}, { |
||||
currentHealth: 10, |
||||
maxHealth: 20, |
||||
currentConfidence: 20, |
||||
maxConfidence: 20, |
||||
}, { |
||||
currentHealth: 20, |
||||
maxHealth: 20, |
||||
currentConfidence: 25, |
||||
maxConfidence: 20, |
||||
}] |
||||
validValues.forEach((value) => { |
||||
test(`returns true for ${hitPointsToString(value)}`, () => { |
||||
expect(isValidHitPoints(value)).toBe(true) |
||||
}) |
||||
}) |
||||
invalidValues.forEach((value) => { |
||||
test(`returns false for ${hitPointsToString(value)}`, () => { |
||||
expect(isValidHitPoints(value)).toBe(false) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("isValidConfidenceLimit", () => { |
||||
const validValues: [number, HitPointsWithoutConfidence][] = [ |
||||
[200, { |
||||
currentHealth: 100, |
||||
maxHealth: 100, |
||||
maxConfidence: 200, |
||||
}], [0, { |
||||
currentHealth: 50, |
||||
maxHealth: 100, |
||||
maxConfidence: 60 |
||||
}], [10, { |
||||
currentHealth: 50, |
||||
maxHealth: 100, |
||||
maxConfidence: 60 |
||||
}], [0, { |
||||
currentHealth: 0, |
||||
maxHealth: 100, |
||||
maxConfidence: 100, |
||||
}]] |
||||
const invalidValues: [number, HitPointsWithoutConfidence][] = [ |
||||
[195, { |
||||
currentHealth: 100, |
||||
maxHealth: 100, |
||||
maxConfidence: 200, |
||||
}], [25, { |
||||
currentHealth: 0, |
||||
maxHealth: 100, |
||||
maxConfidence: 100, |
||||
}]] |
||||
validValues.forEach(([value, hp]) => { |
||||
test(`returns true for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => { |
||||
expect(isValidConfidenceLimit(value, hp)).toBe(true) |
||||
}) |
||||
}) |
||||
invalidValues.forEach(([value, hp]) => { |
||||
test(`returns false for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => { |
||||
expect(isValidConfidenceLimit(value, hp)).toBe(false) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("checkValidMaxHitPoints", () => { |
||||
test("returns on valid max HP", () => { |
||||
expect(checkValidMaxHitPoints(30)).toEqual(30) |
||||
}) |
||||
test("throws on invalid max HP", () => { |
||||
expect(() => checkValidMaxHitPoints(-1)).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe("checkValidCurrentHitPoints", () => { |
||||
test("returns on valid current HP", () => { |
||||
expect(checkValidCurrentHitPoints(30, 30, 0)).toEqual(30) |
||||
}) |
||||
test("throws on invalid current HP", () => { |
||||
expect(() => checkValidCurrentHitPoints(-10, 30, 0)).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe("checkValidHitPointsWithoutConfidence", () => { |
||||
test("returns on valid hit points base", () => { |
||||
expect(checkValidHitPointsWithoutConfidence({currentHealth: 30, maxHealth: 40, maxConfidence: 99})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99}) |
||||
}) |
||||
test("throws on invalid hit points base", () => { |
||||
expect(() => checkValidHitPointsWithoutConfidence({currentHealth: -20, maxHealth: 10, maxConfidence: 30})).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe("checkValidHitPoints", () => { |
||||
test("returns on valid hit points", () => { |
||||
expect(checkValidHitPoints({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30}) |
||||
}) |
||||
test("throws on invalid hit points", () => { |
||||
expect(() => checkValidHitPoints({currentHealth: -20, maxHealth: 10, maxConfidence: 30, currentConfidence: 50})).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe("checkValidConfidenceLimit", () => { |
||||
test("returns on valid confidence limit", () => { |
||||
expect(checkValidConfidenceLimit(30, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toEqual(30) |
||||
}) |
||||
test("throws on invalid confidence limit", () => { |
||||
expect(() => checkValidConfidenceLimit(40, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe("checkConsistentConfidenceDelta", () => { |
||||
const consistentValues: [HitPoints, number, number][] = [ |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, -10, 0], |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 0, 0], |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 10, 15], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 10, 4], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 9, 4], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 25, 10], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 24, 10], |
||||
] |
||||
consistentValues.forEach(([hp, confidence, health]) => { |
||||
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health} |
||||
test(`returns for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => { |
||||
expect(checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toEqual(confidence) |
||||
}) |
||||
}) |
||||
const inconsistentValues: [HitPoints, number, number][] = [ |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, 0, 0], |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 20, 0], |
||||
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 5, 15], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 10, maxConfidence: 25}, 8, 4], |
||||
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 23, 10], |
||||
] |
||||
inconsistentValues.forEach(([hp, confidence, health]) => { |
||||
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health} |
||||
test(`throws for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => { |
||||
expect(() => checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toThrow() |
||||
}) |
||||
}) |
||||
test("throws an error starting with the evaluation of the action function", () => { |
||||
expect(() => checkConsistentConfidenceDelta( |
||||
30, "frozzing the snagler", |
||||
{maxHealth: 10, currentHealth: 0, maxConfidence: 30, currentConfidence: 0}, |
||||
9 |
||||
)).toThrow(/^frozzing the snagler /) |
||||
}) |
||||
}) |
||||
|
||||
describe("confidenceLimit", () => { |
||||
test("throws on invalid input", () => { |
||||
expect(() => confidenceLimit({ currentHealth: -99, maxHealth: 30, maxConfidence: 20})).toThrow() |
||||
}) |
||||
|
||||
const values: [HitPointsWithoutConfidence, number][] = [ |
||||
[{maxConfidence: 99, maxHealth: 30, currentHealth: 30}, 99], |
||||
// Even 1 missing health results in a loss of at least 1 confidence.
|
||||
[{maxConfidence: 10, maxHealth: 99999, currentHealth: 99998}, 9], |
||||
// Confidence is 0 when each point of confidence represents multiple points of health and there aren't enough to make 1 point of confidence
|
||||
[{maxConfidence: 10, maxHealth: 50, currentHealth: 4}, 0], |
||||
// Confidence is 0 when health is 0
|
||||
[{maxConfidence: 99999, maxHealth: 99, currentHealth: 0}, 0], |
||||
// or when it's below 0
|
||||
[{maxConfidence: 99999, maxHealth: 99, currentHealth: -50}, 0], |
||||
] |
||||
|
||||
values.forEach(([hp, expected]) => { |
||||
test(`returns ${expected} for ${hitPointsWithoutConfidenceToString(hp)}`, () => { |
||||
expect(confidenceLimit(hp)).toBe(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("damageHealth", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1], |
||||
] |
||||
invalidValues.forEach(([hp, damage]) => { |
||||
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => damageHealth(hp, damage, {damageConfidence: false})).toThrow() |
||||
}) |
||||
}) |
||||
const values: [[HitPoints, number, {damageConfidence: boolean}?], Partial<HitPoints>][] = [ |
||||
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}], |
||||
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: -10}], |
||||
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}], |
||||
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}], |
||||
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -10}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -5}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: true}], {currentHealth: 5, currentConfidence: 8}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: false}], {currentHealth: 5, currentConfidence: 10}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: true}], {currentHealth: -5, currentConfidence: 0}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: false}], {currentHealth: -5, currentConfidence: 0}], |
||||
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: true}], {currentHealth: 93, currentConfidence: 16}], |
||||
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: false}], {currentHealth: 93, currentConfidence: 18}], |
||||
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: true}], {currentHealth: 91, currentConfidence: 17}], |
||||
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: false}], {currentHealth: 91, currentConfidence: 18}], |
||||
] |
||||
values.forEach(([[hp, damage, options = {damageConfidence: true}], expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`for ${damage} on ${hitPointsToString(hp)} with damageConfidence ${options.damageConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(damageHealth(hp, damage, options)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("recoverHealth", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1], |
||||
] |
||||
invalidValues.forEach(([hp, healing]) => { |
||||
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => recoverHealth(hp, healing, {recoverConfidence: false})).toThrow() |
||||
}) |
||||
}) |
||||
const values: [[HitPoints, number, {recoverConfidence: boolean}?], Partial<HitPoints>][] = [ |
||||
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}], |
||||
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: 0}], |
||||
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: 0}], |
||||
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {}], |
||||
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: true}], {currentHealth: 9, currentConfidence: 12}], |
||||
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: false}], {currentHealth: 9, currentConfidence: 8}], |
||||
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: true}], {currentHealth: 10, currentConfidence: 14}], |
||||
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: false}], {currentHealth: 10, currentConfidence: 8}], |
||||
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: true}], {currentHealth: 5, currentConfidence: 10}], |
||||
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: false}], {currentHealth: 5, currentConfidence: 0}], |
||||
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: true}], {currentHealth: 3, currentConfidence: 7}], |
||||
[[{currentHealth: 3, maxHealth: 10, maxConfidence: 25, currentConfidence: 7}, 3, {recoverConfidence: true}], {currentHealth: 6, currentConfidence: 14}], |
||||
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: false}], {currentHealth: 3, currentConfidence: 0}], |
||||
] |
||||
values.forEach(([[hp, damage, options = {recoverConfidence: true}], expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`for ${damage} on ${hitPointsToString(hp)} with recoverConfidence ${options.recoverConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(recoverHealth(hp, damage, options)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
describe("setMaxHealth", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0], |
||||
] |
||||
invalidValues.forEach(([hp, newMax]) => { |
||||
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => setMaxHealth(hp, newMax)).toThrow() |
||||
}) |
||||
}) |
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}], |
||||
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxHealth: 5, currentHealth: 5}], |
||||
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 20, {maxHealth: 20}], |
||||
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 19, {maxHealth: 19}], |
||||
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 21, {maxHealth: 21, currentConfidence: 3}], |
||||
] |
||||
values.forEach(([hp, newMax, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(setMaxHealth(hp, newMax)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("setCurrentHealth", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -11], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11], |
||||
] |
||||
invalidValues.forEach(([hp, newValue]) => { |
||||
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => setCurrentHealth(hp, newValue)).toThrow() |
||||
}) |
||||
}) |
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {currentHealth: 0}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentHealth: 5}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentHealth: 10}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -5, {currentHealth: -5}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -10, {currentHealth: -10}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentHealth: 0, currentConfidence: 0}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentHealth: 8}], |
||||
] |
||||
values.forEach(([hp, newValue, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(setCurrentHealth(hp, newValue)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("damageConfidence", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1], |
||||
] |
||||
invalidValues.forEach(([hp, damage]) => { |
||||
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => damageConfidence(hp, damage)).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 0}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 0}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 1}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 9, {currentConfidence: 1}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 11, {currentConfidence: 0}], |
||||
] |
||||
values.forEach(([hp, damage, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`for ${damage} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(damageConfidence(hp, damage)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("recoverConfidence", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1], |
||||
] |
||||
invalidValues.forEach(([hp, healing]) => { |
||||
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => recoverConfidence(hp, healing)).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 10}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 10}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 9}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 9, {currentConfidence: 9}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 11, {currentConfidence: 10}], |
||||
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 6, {currentConfidence: 10}], |
||||
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {currentConfidence: 10}], |
||||
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 4, {currentConfidence: 9}], |
||||
] |
||||
values.forEach(([hp, healing, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`for ${healing} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(recoverConfidence(hp, healing)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("setMaxConfidence", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0], |
||||
] |
||||
invalidValues.forEach(([hp, newMax]) => { |
||||
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => setMaxConfidence(hp, newMax)).toThrow() |
||||
}) |
||||
}) |
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {maxConfidence: 6}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxConfidence: 5}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {maxConfidence: 4, currentConfidence: 4}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 30, {maxConfidence: 30}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {maxConfidence: 5, currentConfidence: 2}], |
||||
] |
||||
values.forEach(([hp, newMax, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(setMaxConfidence(hp, newMax)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe("setCurrentConfidence", () => { |
||||
const invalidValues: [HitPoints, number][] = [ |
||||
[InvalidHitPoints, 10], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1], |
||||
[{currentHealth: 5, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11], |
||||
] |
||||
invalidValues.forEach(([hp, newValue]) => { |
||||
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => { |
||||
expect(() => setCurrentConfidence(hp, newValue)).toThrow() |
||||
}) |
||||
}) |
||||
const values: [HitPoints, number, Partial<HitPoints>][] = [ |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentConfidence: 5}], |
||||
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentConfidence: 10}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentConfidence: 0}], |
||||
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentConfidence: 8}], |
||||
] |
||||
values.forEach(([hp, newValue, expectedChanges]) => { |
||||
const expected = {...hp, ...expectedChanges} |
||||
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => { |
||||
expect(setCurrentConfidence(hp, newValue)).toStrictEqual(expected) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,314 @@ |
||||
import {assertion} from "./Assertions"; |
||||
|
||||
/** |
||||
* HitPointsWithoutConfidence contains the hit points data without the current confidence as is needed for calculating |
||||
* the confidence limit itself. It's impossible to check whether a hit points object is valid without checking its |
||||
* confidence limit, and when calculating said confidence limit, this would lead to endless recursion. |
||||
*/ |
||||
export type HitPointsWithoutConfidence = Omit<HitPoints, "currentConfidence"> |
||||
|
||||
/** Formats the HitPointsWithoutConfidence object into a human-readable string for logging. Should never throw. */ |
||||
export function hitPointsWithoutConfidenceToString(hp: HitPointsWithoutConfidence): string { |
||||
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: -/-/${hp.maxConfidence}}` |
||||
} |
||||
|
||||
/** |
||||
* The full data for a character's hit points - i.e., their counters that limit their survival, and which are primarily |
||||
* impacted by the opponent's attacks. |
||||
*/ |
||||
export interface HitPoints { |
||||
/** The maximum Health the character can have, which they recover to when resting. Valid values are integers between 1 and 99999. */ |
||||
readonly maxHealth: number |
||||
/** The current Health the character has, after any damage taken since the last rest. Valid values are integers between -maxHealth and maxHealth. */ |
||||
readonly currentHealth: number |
||||
/** |
||||
* The maximum Confidence the character can have when maxHealth = currentHealth. |
||||
* Below this, the confidence limit is scaled down proportionately. |
||||
* Valid values are integers between 1 and 99999. |
||||
*/ |
||||
readonly maxConfidence: number |
||||
/** The current Confidence the character has, after any damage taken since the last victorious battle. Between 0 and confidenceLimit(this). */ |
||||
readonly currentConfidence: number |
||||
} |
||||
|
||||
/** Formats the HitPoints object into a human-readable string for logging. Should never throw. */ |
||||
export function hitPointsToString(hp: HitPoints): string { |
||||
let confidenceLimitString: string |
||||
try { |
||||
confidenceLimitString = confidenceLimit(hp).toString() |
||||
} catch (e) { |
||||
confidenceLimitString = "<err>" |
||||
} |
||||
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: ${hp.currentConfidence}/${confidenceLimitString}/${hp.maxConfidence}}` |
||||
} |
||||
|
||||
/** Returns true if the value given is valid for maxHealth or maxConfidence. See their documentation for what that means. */ |
||||
export function isValidMaxHitPoints(maxHitPoints: number): boolean { |
||||
return maxHitPoints > 0 && maxHitPoints <= 99999 && Number.isSafeInteger(maxHitPoints) |
||||
} |
||||
|
||||
/** Returns true if the value given is valid for currentHealth or currentConfidence. See their documentation for what that means. */ |
||||
export function isValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number) { |
||||
return currentHitPoints <= maxHitPoints && currentHitPoints >= minHitPoints && Number.isSafeInteger(currentHitPoints) |
||||
} |
||||
|
||||
/** Returns true if the given HitPointsWithoutConfidence object lives up to all its requirements. See the HitPoints documentation for what that means. */ |
||||
export function isValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): boolean { |
||||
return isValidMaxHitPoints(hp.maxHealth) |
||||
&& isValidMaxHitPoints(hp.maxConfidence) |
||||
&& isValidCurrentHitPoints(hp.currentHealth, hp.maxHealth, -hp.maxHealth) |
||||
} |
||||
|
||||
/** Returns true if the given HitPoints object lives up to all its requirements. See the HitPoints documentation for what that means. */ |
||||
export function isValidHitPoints(hp: HitPoints): boolean { |
||||
return isValidHitPointsWithoutConfidence(hp) |
||||
&& isValidCurrentHitPoints(hp.currentConfidence, confidenceLimit(hp), 0) |
||||
} |
||||
|
||||
/** Returns true if the given confidenceLimit value is at least plausible. See the confidenceLimit documentation for what that means.*/ |
||||
export function isValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): boolean { |
||||
return (hp.currentHealth > 0 ? limit >= 0 : limit === 0) |
||||
&& (hp.currentHealth === hp.maxHealth ? limit === hp.maxConfidence : limit < hp.maxConfidence) |
||||
&& Number.isSafeInteger(limit) |
||||
} |
||||
|
||||
/** Asserts that the given value is valid for maxHealth or maxConfidence. */ |
||||
export function checkValidMaxHitPoints(maxHitPoints: number): number { |
||||
return assertion.check(maxHitPoints, isValidMaxHitPoints, (maxHitPoints) => `Invalid max hit points: ${maxHitPoints}`) |
||||
} |
||||
|
||||
/** Asserts that the given value is valid for currentHealth or currentConfidence. */ |
||||
export function checkValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number): number { |
||||
return assertion.check(currentHitPoints, |
||||
(currentHitPoints) => isValidCurrentHitPoints(currentHitPoints, maxHitPoints, minHitPoints), |
||||
(currentHitPoints) => `Invalid current hit points: ${minHitPoints} <= ${currentHitPoints} <= ${maxHitPoints}`) |
||||
} |
||||
|
||||
/** Asserts that the given HitPointsWithoutConfidence object lives up to all its requirements. */ |
||||
export function checkValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): HitPointsWithoutConfidence { |
||||
return assertion.check(hp, isValidHitPointsWithoutConfidence, (hp) => `Invalid hit points: ${hitPointsWithoutConfidenceToString(hp)}`) |
||||
} |
||||
|
||||
/** Asserts that the given HitPoints object lives up to all its requirements. */ |
||||
export function checkValidHitPoints(hp: HitPoints): HitPoints { |
||||
return assertion.check(hp, isValidHitPoints, (hp) => `Invalid hit points object: ${hitPointsToString(hp)}`) |
||||
} |
||||
|
||||
/** Asserts that the given confidenceLimit value is at least plausible. */ |
||||
export function checkValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): number { |
||||
return assertion.check(limit, (limit) => isValidConfidenceLimit(limit, hp), (limit) => { |
||||
return `Invalid confidence limit: ${limit} for ${hp.currentHealth}/${hp.maxHealth} Health, ${hp.maxConfidence} Max Confidence` |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Asserts that the given change in health has resulted in a congruent change in confidence - i.e., that the new |
||||
* confidence is the same distance below the new confidence limit as the old confidence was below the old one. |
||||
* A delta of 1 is permitted due to the fact that the confidence change will be rounded (up in the case of damage, |
||||
* down in the case of healing). |
||||
* |
||||
* Used by the damageHealth and recoverHealth functions. |
||||
*/ |
||||
export function checkConsistentConfidenceDelta(newConfidenceUnclamped: number, action: string, oldHp: HitPoints, newHealth: number): number { |
||||
return assertion.check(newConfidenceUnclamped, (newConfidenceUnclamped) => { |
||||
return Math.abs((confidenceLimit({...oldHp, currentHealth: newHealth}) - newConfidenceUnclamped) - (confidenceLimit(oldHp) - oldHp.currentConfidence)) <= 1 |
||||
}, (newConfidenceUnclamped) => { |
||||
const newLimit = confidenceLimit({...oldHp, currentHealth: newHealth}) |
||||
const oldLimit = confidenceLimit(oldHp) |
||||
return (`${action} from ${hitPointsToString(oldHp)} to ${hitPointsToString({...oldHp, currentHealth: newHealth, currentConfidence: newConfidenceUnclamped})} ` |
||||
+ `resulted in a change in the difference between the confidenceLimit and the currentConfidenceUnclamped ` |
||||
+ `from ${oldHp.currentConfidence}/${oldLimit} = ${oldLimit - oldHp.currentConfidence} ` |
||||
+ `to ${newConfidenceUnclamped}/${newLimit} = ${newLimit - newConfidenceUnclamped}`) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* The effective maximum confidence, based on scaling maxConfidence by the character's current health percentage. |
||||
* Always an integer, rounding down if the result is fractional. |
||||
* Is 0 if currentHealth <= 0. |
||||
* Is maxConfidence if and only if currentHealth === maxHealth, due to rounding down (floor). |
||||
* Otherwise, scaled to somewhere between them. |
||||
*/ |
||||
export function confidenceLimit(hp: HitPointsWithoutConfidence): number { |
||||
checkValidHitPointsWithoutConfidence(hp) |
||||
|
||||
const result = Math.max(0, Math.floor(hp.currentHealth * hp.maxConfidence / hp.maxHealth)) |
||||
|
||||
return checkValidConfidenceLimit(result, hp) |
||||
} |
||||
|
||||
/** |
||||
* Inflicts the given amount of damage on the character's health. |
||||
* The character's health will never be reduced below -maxHealth this way. |
||||
* If damageConfidence is true, confidence will be reduced proportionately. |
||||
* e.g., if Confidence is 50/80/100 and Health is 40/50, damaging Health by 10 with damageConfidence off |
||||
* results in Health 30/50 and Confidence 50/60/100 - the currentConfidence has not changed. |
||||
* On the other hand, with damageConfidence on, that same scenario |
||||
* results in Health 30/50 and Confidence 30/60/100 - the currentConfidence has fallen by 20 due to scaling the |
||||
* 10 Health damage to 20 Confidence damage. The character's confidence will not be reduced below 0 this way. |
||||
* If damageConfidence is off, currentConfidence will still change if the health change causes the confidenceLimit to |
||||
* drop below the currentConfidence. In this case, currentConfidence will be clamped to the new confidenceLimit. |
||||
*/ |
||||
export function damageHealth(hp: HitPoints, damage: number, options: {damageConfidence: boolean}): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage health by negative or non-integer value ${damage}`) |
||||
|
||||
const {damageConfidence} = options |
||||
let currentHealth = Math.max(-hp.maxHealth,hp.currentHealth - damage) |
||||
let currentConfidence = damageConfidence |
||||
? Math.max(0, |
||||
checkConsistentConfidenceDelta( |
||||
hp.currentConfidence - Math.ceil((Math.max(0, hp.currentHealth) - Math.max(0, currentHealth)) * hp.maxConfidence / hp.maxHealth), |
||||
"damageHealth", |
||||
hp, |
||||
currentHealth)) |
||||
: Math.min(hp.currentConfidence, confidenceLimit({ |
||||
...hp, |
||||
currentHealth: currentHealth |
||||
})) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentHealth, |
||||
currentConfidence, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Heals the character's health by the given amount. |
||||
* The character's health will never be increased above maxHealth this way. |
||||
* If recoverConfidence is true, confidence will be increased proportionately to the increase in health. |
||||
* e.g., if Confidence is 50/60/100 and Health is 30/50, recovering Health by 10 with recoverConfidence off |
||||
* results in Health 40/50 and Confidence 50/80/100 - the currentConfidence has not changed. |
||||
* On the other hand, with recoverConfidence on, that same scenario |
||||
* results in Health 40/50 and Confidence 70/80/100 - the currentConfidence has risen by 20 due to scaling the |
||||
* 10 Health recovery to 20 Confidence recovery. |
||||
*/ |
||||
export function recoverHealth(hp: HitPoints, healing: number, options: {recoverConfidence: boolean}): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal health by negative or non-integer value ${healing}`) |
||||
|
||||
const {recoverConfidence} = options |
||||
const currentHealth = Math.min(hp.maxHealth, hp.currentHealth + healing) |
||||
// We don't need to check against the confidenceLimit here. When recoverConfidence is on, we're raising confidence
|
||||
// by the same amount the confidenceLimit went up. So if it was under the limit before, it should be the same amount
|
||||
// under the limit now. And if recoverConfidence is off, we're not raising confidence at all, so we're even more
|
||||
// under the limit than we were before.
|
||||
// The assertion checks that the former case holds.
|
||||
const currentConfidence = recoverConfidence |
||||
? checkConsistentConfidenceDelta( |
||||
hp.currentConfidence + Math.floor((Math.max(0, currentHealth) - Math.max(0, hp.currentHealth)) * hp.maxConfidence / hp.maxHealth), |
||||
"recoverHealth", |
||||
hp, |
||||
currentHealth) |
||||
: hp.currentConfidence |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentHealth, |
||||
currentConfidence, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Sets the character's new maximum health. |
||||
* If the new value is below the character's current absolute-value health, the character's current health will be clamped to that value. |
||||
* If the new value is above the character's previous maximum health, the character's confidence will be clamped to the new confidence limit, which will be lower. |
||||
*/ |
||||
export function setMaxHealth(hp: HitPoints, maxHealth: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
checkValidMaxHitPoints(maxHealth) |
||||
|
||||
let {currentHealth, currentConfidence} = hp |
||||
if (maxHealth <= Math.abs(currentHealth)) { |
||||
currentHealth = Math.sign(currentHealth) * maxHealth |
||||
} |
||||
if (maxHealth > hp.maxHealth) { |
||||
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxHealth})) |
||||
} |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
maxHealth, |
||||
currentHealth, |
||||
currentConfidence |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Sets the character's new current health. |
||||
* The character's confidence will be clamped to the new confidence limit. |
||||
*/ |
||||
export function setCurrentHealth(hp: HitPoints, currentHealth: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
checkValidCurrentHitPoints(currentHealth, hp.maxHealth, -hp.maxHealth) |
||||
|
||||
const currentConfidence = Math.min(confidenceLimit({...hp, currentHealth}), hp.currentConfidence) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentHealth, |
||||
currentConfidence |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Inflicts the given amount of damage on the character's confidence. |
||||
* The character's confidence will never be reduced below 0 this way. |
||||
*/ |
||||
export function damageConfidence(hp: HitPoints, damage: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage confidence by negative or non-integer value ${damage}`) |
||||
|
||||
let newConfidence = Math.max(0, hp.currentConfidence - damage) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentConfidence: newConfidence, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Heals the given amount of damage to the character's confidence. |
||||
* The character's confidence will never be increased above the current confidenceLimit this way. |
||||
*/ |
||||
export function recoverConfidence(hp: HitPoints, healing: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal confidence by negative or non-integer value ${healing}`) |
||||
|
||||
let newConfidence = Math.min(confidenceLimit(hp), hp.currentConfidence + healing) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentConfidence: newConfidence, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Sets the character's new maximum confidence. |
||||
* The current confidence will be clamped to the new confidenceLimit. |
||||
*/ |
||||
export function setMaxConfidence(hp: HitPoints, maxConfidence: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
checkValidMaxHitPoints(maxConfidence) |
||||
|
||||
let {currentConfidence} = hp |
||||
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxConfidence})) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
maxConfidence, |
||||
currentConfidence |
||||
}) |
||||
} |
||||
|
||||
/** Sets the character's new current confidence. */ |
||||
export function setCurrentConfidence(hp: HitPoints, currentConfidence: number): HitPoints { |
||||
checkValidHitPoints(hp) |
||||
checkValidCurrentHitPoints(currentConfidence, confidenceLimit(hp), 0) |
||||
|
||||
return checkValidHitPoints({ |
||||
...hp, |
||||
currentConfidence |
||||
}) |
||||
} |
Loading…
Reference in new issue