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