Started with async inputs to actions

This commit is contained in:
Thaum Rystra
2024-02-19 22:31:18 +02:00
parent 55a6b16c31
commit 3ea492ee78
15 changed files with 180 additions and 57 deletions

View File

@@ -1,5 +1,6 @@
{
"cSpell.words": [
"alea",
"autorun",
"blackbox",
"Crits",

View File

@@ -7,12 +7,12 @@ import propertySchemasIndex from '/imports/api/properties/computedPropertySchema
import { storedIconsSchema } from '/imports/api/icons/Icons';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
const CreatureProperties: Mongo.Collection<CreatureProperty> = new Mongo.Collection('creatureProperties');
// TODO make this a union type of all CreatureProperty types
const CreatureProperties: Mongo.Collection<any> = new Mongo.Collection('creatureProperties');
export interface CreatureProperty extends TreeDoc {
export interface CreatureProperty {
_id: string
_migrationError?: string
type: string
tags: string[]
disabled?: boolean
icon?: {

View File

@@ -9,6 +9,7 @@ import { loadCreature } from '/imports/api/engine/loadCreatures';
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
import { applyAction } from '/imports/api/engine/action/functions/applyAction';
import { LogContent, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult';
import inputProvider from '/imports/api/engine/action/functions/inputProviderForTests.testFn';
const creatureId = Random.id();
const targetId = Random.id();
@@ -81,11 +82,12 @@ describe('Interrupt action system', function () {
[{ value: 'child 2 of index branch' }]
);
});
it('Halts execution of choice branches', async function () {
let userInputRequested = false;
const requestUserInput = () => { userInputRequested = true; return 0 };
await runActionById(choiceBranchId, requestUserInput);
assert.isTrue(userInputRequested, 'User input should be requested when a choice branch is applied');
it('Gets choices from choice branches', async function () {
const action = await runActionById(choiceBranchId);
assert.deepEqual(
allLogContent(action),
[{ value: 'child 1 of choice branch' }]
);
});
it('Applies adjustments', async function () {
let action = await runActionById(adjustmentSetId);
@@ -205,12 +207,12 @@ function createAction(prop, targetIds?) {
return EngineActions.insertAsync(action);
}
async function runActionById(propId, userInputFn = () => 0) {
async function runActionById(propId) {
const prop = await CreatureProperties.findOneAsync(propId);
const actionId = await createAction(prop);
const action = await EngineActions.findOneAsync(actionId);
if (!action) throw 'Action is expected to exist';
await applyAction(action, userInputFn, { simulate: true });
await applyAction(action, inputProvider, { simulate: true });
return action;
}

View File

@@ -8,6 +8,7 @@ import {
runActionById
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
import Alea from 'alea';
const [
creatureId, targetCreatureId, targetCreature2Id,
@@ -93,6 +94,14 @@ describe('Apply Action Properties', function () {
await createTestCreature(actionTargetCreature2);
});
it('should generate random numbers reliably given consistent seeds', function () {
const aleaFraction = Alea('test', 'seeds');
const randomNumbers = [aleaFraction(), aleaFraction(), aleaFraction()];
assert.deepEqual(randomNumbers, [
0.19889510236680508, 0.9176857066340744, 0.042551583144813776
]);
});
it('should run empty actions', async function () {
const action = await runActionById(emptyActionId);
assert.exists(action);

View File

@@ -24,7 +24,7 @@ export default async function applyAdjustmentProperty(
}
// Evaluate the amount
await recalculateCalculation(prop.amount, action, 'reduce');
await recalculateCalculation(prop.amount, action, 'reduce', userInput);
const value = +prop.amount.value;
if (!isFinite(value)) {
return;

View File

@@ -1,6 +1,7 @@
import { filter } from 'lodash';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import { applyAfterPropTasksForSingleChild, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
import { applyAfterPropTasksForSingleChild, applyAfterPropTasksForSomeChildren, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
@@ -16,7 +17,7 @@ export default async function applyBranchProperty(
switch (prop.branchType) {
case 'if': {
await recalculateCalculation(prop.condition, action, 'reduce');
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
if (prop.condition?.value) {
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
@@ -28,7 +29,7 @@ export default async function applyBranchProperty(
if (!children.length) {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
await recalculateCalculation(prop.condition, action, 'reduce');
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
if (!isFinite(prop.condition?.value)) {
result.appendLog({
name: 'Branch Error',
@@ -110,21 +111,17 @@ export default async function applyBranchProperty(
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
case 'choice': {
let index;
if (action._isSimulation) {
index = await userInput(prop);
} else {
// TODO
throw 'Reading stored user input not implemented'
}
const children = await getPropertyChildren(action.creatureId, prop);
if (!children.length) {
let choices: string[];
let chosenChildren: typeof children = [];
if (children.length) {
choices = await userInput.choose(action, children);
chosenChildren = filter(children, child => choices.includes(child._id));
}
if (!children.length || !chosenChildren.length) {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
if (!isFinite(index) || index < 0) index = 0;
if (index > children.length - 1) index = children.length - 1;
const child = children[index];
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
return applyAfterPropTasksForSomeChildren(action, prop, chosenChildren, targets, userInput);
}
}
}

View File

@@ -4,6 +4,17 @@ type InputProvider = {
rollDice(
action: EngineAction, dice: { number: number, diceSize: number }[]
): Promise<number[][]>;
/**
* Choose from a provided selection
* @param action
* @param choices Options to choose from
* @param quantity Number of choices to make [min, max] inclusive, where -1 means no limit
*/
choose(
action: EngineAction,
choices: ({ _id: string } & Record<string, any>)[],
quantity?: [min: number, max: number],
): Promise<string[]>;
}
export default InputProvider;

View File

@@ -8,8 +8,7 @@ import { loadCreature } from '/imports/api/engine/loadCreatures';
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
import { applyAction } from '/imports/api/engine/action/functions/applyAction';
import { LogContent, Mutation, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import inputProvider from '/imports/api/engine/action/functions/inputProviderForTests.testFn';
/**
* Removes all creatures, properties, and creatureVariable documents from the database
*/
@@ -59,7 +58,7 @@ export const randomIds = new Array(100).fill(undefined).map(() => Random.id());
* @param userInputFn A function that simulates user input
* @returns The Engine Action with mutations resulting from running the action
*/
export async function runActionById(propId, targetIds?, userInput = testInputProvider) {
export async function runActionById(propId, targetIds?, userInput = inputProvider) {
const prop = await CreatureProperties.findOneAsync(propId);
const actionId = await createAction(prop, targetIds);
const action = await EngineActions.findOneAsync(actionId);
@@ -148,25 +147,3 @@ export function allLogContent(action: EngineAction) {
});
return contents;
}
const testInputProvider: InputProvider = {
/**
* For testing, randomness is hard to deal with
* rollDice function returns the average roll for every dice rolled
* [5d10, 1d4] => [[6,6,6,6,6], [3]]
*/
async rollDice(action, dice) {
const result: number[][] = [];
for (const diceRoll of dice) {
const averageRoll = Math.round(diceRoll.diceSize / 2);
// Return an array full of averagely rolled dice
result.push(
new Array(diceRoll.number)
.fill(averageRoll)
)
}
return result;
}
}
export { testInputProvider }

View File

@@ -16,7 +16,6 @@ export async function applyChildren(
action: EngineAction, prop, targetIds: string[], userInput
) {
const children = await getPropertyChildren(action.creatureId, prop);
// Push the child tasks and related triggers to the stack
for (const childProp of children) {
await applyTask(action, { prop: childProp, targetIds }, userInput);
}
@@ -101,6 +100,25 @@ export async function applyAfterPropTasksForSingleChild(
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
}
/**
* Returns a list of tasks containing the following:
* After triggers
* After-children triggers
* @param action
* @param prop
* @param targetIds
* @returns
*/
export async function applyAfterPropTasksForSomeChildren(
action: EngineAction, prop, children, targetIds: string[], userInput
) {
await applyAfterTriggers(action, prop, targetIds, userInput);
for (const childProp of children) {
await applyTask(action, { prop: childProp, targetIds }, userInput);
}
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
}
/**
* Get all the trigger tasks for a given trigger path
* @param action

View File

@@ -0,0 +1,34 @@
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
const inputProviderForTests: InputProvider = {
/**
* For testing, randomness is hard to deal with
* rollDice function returns the average roll for every dice rolled
* [5d10, 1d4] => [[6,6,6,6,6], [3]]
*/
async rollDice(action, dice) {
const result: number[][] = [];
for (const diceRoll of dice) {
const averageRoll = Math.round(diceRoll.diceSize / 2);
// Return an array full of averagely rolled dice
result.push(
new Array(diceRoll.number)
.fill(averageRoll)
)
}
return result;
},
/**
* For testing, always return the minimum number of choices, always choosing the first options
*/
async choose(action, choices, quantity = [1, 1]) {
const chosen: string[] = [];
const choiceQuantity = quantity[0] <= 0 ? 1 : quantity[0];
for (let i = 0; i < choiceQuantity && i < choices.length; i += 1) {
chosen.push(choices[i]._id);
}
return chosen;
}
}
export default inputProviderForTests;

View File

@@ -6,12 +6,24 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX';
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField';
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
import Property from '/imports/api/properties/Properties.type';
export type CreatureAction = Action & CreatureProperty & {
overridden?: boolean
insufficientResources?: boolean
}
/*
* Actions are things a character can do
*/
export interface Action extends ActionBase {
type: 'action'
}
export interface ActionBase extends CreatureProperty {
/**
* Base property type for both spells and actions
*/
export interface ActionBase extends Property {
name?: string
summary?: InlineCalculation
description?: InlineCalculation
@@ -23,9 +35,7 @@ export interface ActionBase extends CreatureProperty {
usesUsed?: number
reset?: string
silent?: boolean
insufficientResources?: boolean
usesLeft?: number
overridden?: boolean
// Resources
resources: {
itemsConsumed: {

View File

@@ -2,11 +2,57 @@ import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema';
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField';
import { ConstantValueType } from '/imports/parser/parseTree/constant';
import Property from '/imports/api/properties/Properties.type';
export type CreatureAttribute = Attribute & CreatureProperty & {
total?: ConstantValueType;
value?: ConstantValueType;
modifier?: number;
proficiency?: 0 | 0.49 | 0.5 | 1 | 2;
advantage?: -1 | 0 | 1;
constitutionMod?: number;
hide?: true;
overridden?: true;
effectIds?: string[];
proficiencyIds?: string[];
definitions?: { _id: string, type: string, row?: number }[];
}
export interface Attribute extends Property {
type: 'attribute';
name?: string;
variableName?: string;
attributeType: 'ability' | 'stat' | 'modifier' | 'hitDice' | 'healthBar' | 'resource' |
'spellSlot' | 'utility';
hitDiceSize?: 'd1' | 'd2' | 'd4' | 'd6' | 'd8' | 'd10' | 'd12' | 'd20' | 'd100';
spellSlotLevel?: CalculatedField;
healthBarColorMid?: string;
healthBarColorLow?: string;
healthBarNoDamage?: true;
healthBarNoHealing?: true;
healthBarNoDamageOverflow?: true;
healthBarNoHealingOverflow?: true;
healthBarDamageOrder?: number;
healthBarHealingOrder?: number;
baseValue?: CalculatedField;
description?: InlineCalculation;
damage?: number;
decimal?: true;
ignoreLowerLimit?: true;
ignoreUpperLimit?: true;
hideWhenTotalZero?: true;
hideWhenValueZero?: true;
reset?: string;
}
/*
* Attributes are numbered stats of a character
*/
let AttributeSchema = createPropertySchema({
const AttributeSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -134,7 +180,7 @@ let AttributeSchema = createPropertySchema({
},
});
let ComputedOnlyAttributeSchema = createPropertySchema({
const ComputedOnlyAttributeSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,

View File

@@ -0,0 +1,12 @@
import { TreeDoc } from '/imports/api/parenting/ChildSchema';
export default interface Property extends TreeDoc {
_id: string
_migrationError?: string
tags: string[]
icon?: {
name: string
shape: string
},
slotQuantityFilled?: number
}

5
app/package-lock.json generated
View File

@@ -1790,6 +1790,11 @@
"uri-js": "^4.2.2"
}
},
"alea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
"integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA=="
},
"ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",

View File

@@ -24,6 +24,7 @@
"@chenfengyuan/vue-countdown": "^1.1.5",
"@tozd/vue-observer-utils": "^0.5.0",
"@types/meteor": "^2.9.8",
"alea": "^1.0.1",
"aws-sdk": "^2.1559.0",
"bcrypt": "^5.1.1",
"chroma-js": "^2.4.2",