More testing of action system, now with test coverage

This commit is contained in:
Thaum Rystra
2024-02-27 07:30:40 +02:00
parent 5141704e23
commit b13ca8c64b
20 changed files with 236 additions and 249 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -8,7 +8,9 @@
"EJSON",
"healthbar",
"healthbars",
"meteortesting",
"nearley",
"ngraph",
"uncomputed",
"walkdown"
]

View File

@@ -1,16 +0,0 @@
# Checklists
## Adding features
- [ ] My new pull request has zero code changes
- [ ] I have described the feature I intend to work on
- [ ] I have described how I intend to implement the feature
- [ ] I will wait for comment from the project's maintainers before submitting code changes
## Fixing bugs
- [ ] I have performed a self-review of my code
- [ ] I have included a link to the relevant github issue or discord post in the description
# Description
`Detailed description of your changes`

3
app/.gitignore vendored
View File

@@ -3,6 +3,9 @@
.demeteorized
.cache
.vscode
.coverage
.nyc_output
.DS_Store
fileStorage
settings.json
public/components

View File

@@ -51,3 +51,5 @@ zodern:types
zodern:fix-async-stubs
typescript
ecmascript
lmieulet:meteor-legacy-coverage
lmieulet:meteor-coverage

View File

@@ -52,6 +52,8 @@ inter-process-messaging@0.1.1
lai:collection-extensions@0.4.0
launch-screen@2.0.0
littledata:synced-cron@1.5.1
lmieulet:meteor-coverage@4.1.0
lmieulet:meteor-legacy-coverage@0.2.0
localstorage@1.2.0
logging@1.3.3
mdg:validated-method@1.3.0

View File

@@ -1,5 +1,6 @@
import { assert } from 'chai';
import {
allLogContent,
allMutations,
allUpdates,
createTestCreature,
@@ -7,12 +8,13 @@ import {
removeAllCreaturesAndProps,
runActionById
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
import { LogContent, Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
import Alea from 'alea';
const [
creatureId, targetCreatureId, targetCreature2Id,
emptyActionId, attackActionId, usesActionId, attackMissId,
creatureId, targetCreatureId, targetCreature2Id, emptyActionId, selfActionId, attackActionId,
usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId, consumeAmmoId,
consumeResourceId, noUsesActionId, insufficientResourcesActionId
] = randomIds;
const actionTestCreature = {
@@ -22,6 +24,13 @@ const actionTestCreature = {
{
_id: emptyActionId,
type: 'action',
summary: { text: 'Summary text 1 + 1 = {1 + 1}' }
},
// Attack that targets self
{
_id: selfActionId,
type: 'action',
target: 'self',
},
// Attack that hits
{
@@ -35,6 +44,12 @@ const actionTestCreature = {
type: 'action',
attackRoll: { calculation: '-5' },
},
// Attack that has no target
{
_id: attackNoTargetId,
type: 'action',
attackRoll: { calculation: '1' },
},
// Disable crits
{
type: 'attribute',
@@ -56,6 +71,57 @@ const actionTestCreature = {
usesUsed: 1,
reset: 'longRest',
},
// Not enough uses
{
_id: noUsesActionId,
type: 'action',
uses: { calculation: '5' },
usesUsed: 5,
reset: 'longRest',
},
// Uses Resources
{
_id: ammoId,
type: 'item',
quantity: 12,
tags: ['ammo']
},
{
_id: resourceAttId,
type: 'attribute',
name: 'Resource Name',
attributeType: 'stat',
baseValue: { calculation: '7' },
variableName: 'resourceVar',
},
{
_id: usesResourcesActionId,
type: 'action',
resources: {
itemsConsumed: [{
_id: consumeAmmoId,
tag: 'ammo',
quantity: { calculation: '3' },
itemId: ammoId,
}],
attributesConsumed: [{
_id: consumeResourceId,
variableName: 'resourceVar',
quantity: { calculation: '2' },
}]
}
},
{
_id: insufficientResourcesActionId,
type: 'action',
resources: {
attributesConsumed: [{
_id: consumeResourceId,
variableName: 'resourceVar',
quantity: { calculation: '9001' },
}]
}
}
],
}
@@ -106,11 +172,43 @@ describe('Apply Action Properties', function () {
const action = await runActionById(emptyActionId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [{ name: 'Action' }],
contents: [{
name: 'Action',
value: 'Summary text 1 + 1 = 2',
}],
targetIds: [],
}]);
});
it('should target self when set', async function () {
const action = await runActionById(selfActionId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [{
name: 'Action',
}],
targetIds: [creatureId],
}]);
});
it('should make attack rolls against no targets', async function () {
const action = await runActionById(attackNoTargetId, []);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [],
}, {
contents: [{
name: 'To Hit',
value: '1d20 [10] + 1\n**11**',
inline: true,
}],
targetIds: [],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
})
it('should make attack rolls against multiple creatures', async function () {
const action = await runActionById(attackActionId, [
targetCreatureId,
@@ -151,6 +249,19 @@ describe('Apply Action Properties', function () {
assert.deepEqual(allUpdates(action), expectedUpdates);
});
it('should fail to make attacks that have no uses left', async function () {
const action = await runActionById(noUsesActionId, [targetCreatureId]);
const expectedContent: LogContent[] = [
{
name: 'Action'
}, {
name: 'Error',
value: 'Action does not have enough uses left'
}
]
assert.deepEqual(allLogContent(action), expectedContent);
});
it('should make attack rolls that miss', async function () {
const action = await runActionById(attackMissId, [targetCreatureId]);
const expectedMutations: Mutation[] = [
@@ -169,4 +280,59 @@ describe('Apply Action Properties', function () {
assert.deepEqual(allMutations(action), expectedMutations);
});
it('actions should consume resources', async function () {
const action = await runActionById(usesResourcesActionId, []);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: []
},
{
contents: [{
inline: true,
name: 'Attribute damaged',
value: '2 Resource Name',
}],
targetIds: [creatureId],
updates: [{
inc: {
damage: 2,
value: -2
},
propId: resourceAttId,
type: 'attribute'
}],
},
{
targetIds: [],
updates: [
{
inc: {
quantity: -3
},
propId: ammoId,
type: 'item',
}
]
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should handle insufficient resources', async function () {
const action = await runActionById(insufficientResourcesActionId, []);
const expectedMutations: Mutation[] = [
{
contents: [{
name: 'Action'
}, {
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
}],
targetIds: [],
},
];
assert.deepEqual(allMutations(action), expectedMutations);
});
});

View File

@@ -21,33 +21,36 @@ export default async function applyActionProperty(
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
//Log the name and summary, check that the property has enough resources to fire
const content: LogContent = { name: getPropertyTitle(prop) };
if (prop.summary?.text) {
await recalculateInlineCalculations(prop.summary, action, 'reduce', userInput);
content.value = prop.summary.value;
}
if (prop.silent) content.silenced = true;
result.appendLog(content, targetIds);
result.appendLog({
name: getPropertyTitle(prop),
...prop.summary && { value: prop.summary.value },
...prop.silent && { silenced: true },
}, targetIds);
// Check Uses
if (prop.usesLeft <= 0) {
if (!prop.silent) result.appendLog({
result.appendLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
value: `${getPropertyTitle(prop)} does not have enough uses left`,
...prop.silent && { silenced: true },
}, targetIds);
return;
}
// Check Resources
if (prop.insufficientResources) {
if (!prop.silent) result.appendLog({
result.appendLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
...prop.silent && { silenced: true },
}, targetIds);
return;
}
spendResources(action, prop, targetIds, result, userInput);
await spendResources(action, prop, targetIds, result, userInput);
const attack: CalculatedField = prop.attackRoll || prop.attackRollBonus;

View File

@@ -5,10 +5,12 @@ import { applyTriggers } from '/imports/api/engine/action/functions/applyTaskGro
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
export default async function applyDamagePropTask(
task: DamagePropTask, action: EngineAction, result: TaskResult, userInput
): Promise<number> {
const prop = task.prop;
if (task.targetIds.length > 1) {
@@ -22,7 +24,7 @@ export default async function applyDamagePropTask(
// Set the scope properties
result.pushScope = {};
if (prop.operation === 'increment') {
if (operation === 'increment') {
if (value >= 0) {
result.pushScope['~damage'] = { value };
} else {
@@ -67,7 +69,7 @@ export default async function applyDamagePropTask(
value: `${statName}${operation === 'set' ? ' set to' : ''}` +
` ${value}`,
inline: true,
silenced: prop.silent,
...prop.silent && { silenced: true },
}, task.targetIds);
}
@@ -98,7 +100,7 @@ export default async function applyDamagePropTask(
name: title,
value: `${getPropertyTitle(targetProp)} set to ${value}`,
inline: true,
silenced: prop.silent,
...prop.silent && { silenced: true },
}]
});
} else if (operation === 'increment') {
@@ -120,10 +122,10 @@ export default async function applyDamagePropTask(
type: targetProp.type,
}],
contents: [{
name: 'Attribute damage',
value: `${getPropertyTitle(targetProp)} ${value}`,
name: 'Attribute damaged',
value: `${numberToSignedString(-value)} ${getPropertyTitle(targetProp)}`,
inline: true,
silenced: prop.silent,
...prop.silent && { silenced: true },
}]
});
}

View File

@@ -43,11 +43,13 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action:
type: 'item',
}],
// Log the item name as a heading if it has child properties to apply
contents: itemChildren.length ? [{
name: getPropertyTitle(item) || 'Ammo',
inline: false,
silenced: prop.silent,
}] : undefined,
...itemChildren.length && {
contents: [{
name: getPropertyTitle(item) || 'Ammo',
inline: false,
silenced: prop.silent,
}]
},
});
await applyTriggers(action, item, [action.creatureId], 'ammo.after', userInput);

View File

@@ -53,7 +53,7 @@ export default async function applyTask(
// Create a result an push it to the action results, pass it to the apply function to modify
const result = new TaskResult(task.prop._id, task.targetIds);
result.scope[`#${prop.type}`] = prop;
result.scope[`#${prop.type}`] = { _propId: prop._id };
action.results.push(result);
// Apply the property

View File

@@ -1,4 +1,3 @@
console.log('call.ts imports')
import error from '/imports/parser/parseTree/error';
import constant from '/imports/parser/parseTree/constant';
import functions, { ParserFunction } from '/imports/parser/functions';

9
app/package-lock.json generated
View File

@@ -1512,6 +1512,15 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"@types/simpl-schema": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@types/simpl-schema/-/simpl-schema-1.12.7.tgz",
"integrity": "sha512-GhXOCJqKcDeawYoIe4Jly7C5ePR3Uh3jaswb1U+Ruh1x7EtZqOJMMyxnoVvJUIl5b+v7yoFs4RVny/ZowyXBDw==",
"dev": true,
"requires": {
"@types/meteor": "*"
}
},
"@types/sizzle": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",

View File

@@ -13,6 +13,8 @@
"debug": "meteor --inspect",
"bundle-viz": "meteor --extra-packages bundle-visualizer --production",
"test": "meteor test --driver-package meteortesting:mocha --port 3001",
"test:coverage": "COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_OUT_REMAP=1 COVERAGE_APP_FOLDER=$PWD/ meteor test --once --driver-package meteortesting:mocha",
"test:watch:coverage": "COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_OUT_REMAP=1 COVERAGE_APP_FOLDER=$PWD/ TEST_WATCH=1 meteor test --driver-package meteortesting:mocha",
"build": "meteor build ../build --architecture os.linux.x86_64"
},
"engines": {
@@ -68,6 +70,7 @@
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/mocha": "^10.0.6",
"@types/simpl-schema": "^1.12.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vue/compiler-dom": "^3.4.19",

View File

@@ -1,9 +1,11 @@
{
"compilerOptions": {
"module": "ESNext",
"module": "esNext",
"moduleResolution": "Node",
"target": "ES2020",
"jsx": "react",
"target": "es2018",
"lib": ["esnext", "dom"],
"jsx": "preserve",
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": ".",
@@ -22,15 +24,19 @@
"meteor/aldeed:collection2": [
"packages/collection2/collection2.js"
]
}
},
"resolveJsonModule": true,
"types": ["node", "mocha"],
"esModuleInterop": true,
},
"vueCompilerOptions": {
"target": 2 // For Vue version <= 2.6.14
"target": 2
},
"exclude": [
"node_modules",
"**/node_modules/*",
".meteor"
".meteor",
"./packages/**"
],
"typeAcquisition": {
"include": [

21
node_modules/@types/nearley/LICENSE generated vendored
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View File

@@ -1,15 +0,0 @@
# Installation
> `npm install --save @types/nearley`
# Summary
This package contains type definitions for nearley (https://github.com/Hardmath123/nearley#readme).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nearley.
### Additional Details
* Last updated: Tue, 07 Nov 2023 09:09:39 GMT
* Dependencies: none
# Credits
These definitions were written by [Nikita Litvin](https://github.com/deltaidea), and [BendingBender](https://github.com/BendingBender).

View File

@@ -1,108 +0,0 @@
export as namespace nearley;
export class Parser {
/**
* Reserved token for indicating a parse fail.
*/
static fail: {};
grammar: Grammar;
options: ParserOptions;
lexer: Lexer;
lexerState?: LexerState | undefined;
current: number;
/**
* An array of possible parsings. Each element is the thing returned by your grammar.
*
* Note that this is undefined before the first feed() call.
* It isn't typed as `any[] | undefined` to spare you the null checks when it's definitely an array.
*/
results: any[];
constructor(grammar: Grammar, options?: ParserOptions);
/**
* The Parser object can be fed data in parts with .feed(data).
* You can then find an array of parsings with the .results property.
* If results is empty, then there are no parsings.
* If results contains multiple values, then that combination is ambiguous.
*
* @throws If there are no possible parsings, nearley will throw an error
* whose offset property is the index of the offending token.
*/
feed(chunk: string): this;
finish(): any[];
restore(column: { [key: string]: any; lexerState: LexerState }): void;
save(): { [key: string]: any; lexerState: LexerState };
}
export interface ParserOptions {
keepHistory?: boolean | undefined;
lexer?: Lexer | undefined;
}
export class Rule {
static highestId: number;
id: number;
name: string;
symbols: any[];
postprocess?: Postprocessor | undefined;
constructor(name: string, symbols: any[], postprocess?: Postprocessor);
toString(withCursorAt?: number): string;
}
export class Grammar {
static fromCompiled(rules: CompiledRules): Grammar;
rules: Rule[];
start: string;
byName: { [ruleName: string]: Rule[] };
lexer?: Lexer | undefined;
constructor(rules: Rule[]);
}
export interface CompiledRules {
Lexer?: Lexer | undefined;
ParserStart: string;
ParserRules: ParserRule[];
}
export interface ParserRule {
name: string;
symbols: any[];
postprocess?: Postprocessor | undefined;
}
export type Postprocessor = (data: any[], reference?: number, wantedBy?: {}) => void;
export interface Lexer {
/**
* Sets the internal buffer to data, and restores line/col/state info taken from save().
*/
reset(data: string, state?: LexerState): void;
/**
* Returns e.g. {type, value, line, col, …}. Only the value attribute is required.
*/
next(): Token | undefined;
/**
* Returns an object describing the current line/col etc. This allows us
* to preserve this information between feed() calls, and also to support Parser#rewind().
* The exact structure is lexer-specific; nearley doesn't care what's in it.
*/
save(): LexerState;
/**
* Returns a string with an error message describing the line/col of the offending token.
* You might like to include a preview of the line in question.
*/
formatError(token: Token, message: string): string;
}
export type Token = string | { value: string };
export interface LexerState {
[x: string]: any;
}

View File

@@ -1,58 +0,0 @@
{
"_from": "@types/nearley",
"_id": "@types/nearley@2.11.5",
"_inBundle": false,
"_integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA==",
"_location": "/@types/nearley",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "@types/nearley",
"name": "@types/nearley",
"escapedName": "@types%2fnearley",
"scope": "@types",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz",
"_shasum": "9087e1634e1c90efb25d661390702381789685cb",
"_spec": "@types/nearley",
"_where": "/Users/stef/github/DiceCloud",
"bugs": {
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Nikita Litvin",
"url": "https://github.com/deltaidea"
},
{
"name": "BendingBender",
"url": "https://github.com/BendingBender"
}
],
"dependencies": {},
"deprecated": false,
"description": "TypeScript definitions for nearley",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nearley",
"license": "MIT",
"main": "",
"name": "@types/nearley",
"repository": {
"type": "git",
"url": "git+https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/nearley"
},
"scripts": {},
"typeScriptVersion": "4.5",
"types": "index.d.ts",
"typesPublisherContentHash": "2b82830a1a87ef19e588c4f6dcd1c00fde50afd7c9dc5bd8233b054f436578d4",
"version": "2.11.5"
}

6
package-lock.json generated
View File

@@ -2,6 +2,12 @@
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@types/lodash": {
"version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"@types/nearley": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz",