commit 8ff8e8f52257668907d3c94f344ad0f07d390141 Author: congsh <2452821485@qq.com> Date: Wed May 13 03:26:41 2026 +0000 feat: add condition evaluator with full operator support diff --git a/src/engine/conditionEvaluator.js b/src/engine/conditionEvaluator.js new file mode 100644 index 0000000..a84a5e4 --- /dev/null +++ b/src/engine/conditionEvaluator.js @@ -0,0 +1,111 @@ +/** + * Condition evaluator for the Rebirth Incremental game. + * Evaluates condition objects against the current game state. + */ + +/** + * Operator functions for comparing values. + * All operators perform loose equality/inequality checks where applicable. + */ +export const OPS = { + '==': (a, b) => a == b, + '===': (a, b) => a === b, + '!=': (a, b) => a != b, + '!==': (a, b) => a !== b, + '>': (a, b) => a > b, + '>=': (a, b) => a >= b, + '<': (a, b) => a < b, + '<=': (a, b) => a <= b, +}; + +/** + * Evaluates a condition object against the game state. + * @param {Object} condition - The condition to evaluate + * @param {Object} state - The current game state + * @returns {boolean} - Whether the condition is satisfied + */ +export function evaluateCondition(condition, state) { + if (!condition || typeof condition !== 'object') { + return false; + } + + const { type } = condition; + + switch (type) { + case 'age': { + const opFn = OPS[condition.op]; + if (!opFn) return false; + return opFn(state.age ?? 0, condition.value); + } + + case 'stat': { + const opFn = OPS[condition.op]; + if (!opFn) return false; + const statValue = state.stats?.[condition.stat] ?? 0; + return opFn(statValue, condition.value); + } + + case 'careerLevel': { + const opFn = OPS[condition.op]; + if (!opFn) return false; + const level = state.careers?.[condition.careerId]?.level ?? 0; + return opFn(level, condition.value); + } + + case 'money': { + const opFn = OPS[condition.op]; + if (!opFn) return false; + return opFn(state.money ?? 0, condition.value); + } + + case 'identity': { + return state.identity === condition.value; + } + + case 'worldFlag': { + const flagValue = state.worldFlags?.[condition.flag]; + if (condition.value !== undefined) { + return flagValue === condition.value; + } + return !!flagValue; + } + + case 'hasItem': { + const itemCount = state.shopItems?.[condition.itemId] ?? 0; + return itemCount > 0; + } + + case 'hasArtifact': { + const artifactValue = state.artifacts?.[condition.artifactId] ?? 0; + if (condition.op) { + const opFn = OPS[condition.op]; + if (!opFn) return false; + return opFn(artifactValue, condition.value); + } + return artifactValue > 0; + } + + case 'reincarnation': { + const opFn = OPS[condition.op]; + if (!opFn) return false; + return opFn(state.reincarnation ?? 0, condition.value); + } + + case 'and': { + if (!Array.isArray(condition.conditions)) return false; + return condition.conditions.every(sub => evaluateCondition(sub, state)); + } + + case 'or': { + if (!Array.isArray(condition.conditions)) return false; + return condition.conditions.some(sub => evaluateCondition(sub, state)); + } + + case 'not': { + return !evaluateCondition(condition.condition, state); + } + + default: + return false; + } +} diff --git a/test/conditionEvaluator.test.js b/test/conditionEvaluator.test.js new file mode 100644 index 0000000..9f512b8 --- /dev/null +++ b/test/conditionEvaluator.test.js @@ -0,0 +1,156 @@ +import { evaluateCondition, OPS } from '../src/engine/conditionEvaluator.js'; + +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +// Test state matching the reference +const state = { + age: 15, + reincarnation: 2, + money: 300, + stats: { body: 30, wisdom: 20, charm: 10, destiny: 5, business: 8, intelligence: 15 }, + careers: { student: { level: 120 } }, + identity: 'orphan', + worldFlags: { met_taoist: true }, + shopItems: { taoist_license: 1 }, + artifacts: { immortal_sword: 3 } +}; + +// Test OPS constant exists +assert(typeof OPS === 'object', 'OPS should be an object'); +assert(typeof OPS['>='] === 'function', 'OPS[">="] should be a function'); + +// === age conditions === +assert(evaluateCondition({ type: 'age', op: '>=', value: 14 }, state) === true, 'age >= 14 should be true'); +assert(evaluateCondition({ type: 'age', op: '>=', value: 16 }, state) === false, 'age >= 16 should be false'); +assert(evaluateCondition({ type: 'age', op: '==', value: 15 }, state) === true, 'age == 15 should be true'); +assert(evaluateCondition({ type: 'age', op: '>', value: 10 }, state) === true, 'age > 10 should be true'); +assert(evaluateCondition({ type: 'age', op: '<', value: 20 }, state) === true, 'age < 20 should be true'); +assert(evaluateCondition({ type: 'age', op: '<=', value: 15 }, state) === true, 'age <= 15 should be true'); +assert(evaluateCondition({ type: 'age', op: '!=', value: 10 }, state) === true, 'age != 10 should be true'); + +// === stat conditions === +assert(evaluateCondition({ type: 'stat', stat: 'body', op: '>=', value: 20 }, state) === true, 'stat body >= 20 should be true'); +assert(evaluateCondition({ type: 'stat', stat: 'body', op: '>=', value: 40 }, state) === false, 'stat body >= 40 should be false'); +assert(evaluateCondition({ type: 'stat', stat: 'wisdom', op: '==', value: 20 }, state) === true, 'stat wisdom == 20 should be true'); +assert(evaluateCondition({ type: 'stat', stat: 'charm', op: '<', value: 20 }, state) === true, 'stat charm < 20 should be true'); +assert(evaluateCondition({ type: 'stat', stat: 'destiny', op: '>', value: 0 }, state) === true, 'stat destiny > 0 should be true'); + +// Missing stat defaults to 0 +assert(evaluateCondition({ type: 'stat', stat: 'nonexistent', op: '==', value: 0 }, state) === true, 'missing stat == 0 should be true'); + +// === careerLevel conditions === +assert(evaluateCondition({ type: 'careerLevel', careerId: 'student', op: '>=', value: 100 }, state) === true, 'careerLevel student >= 100 should be true'); +assert(evaluateCondition({ type: 'careerLevel', careerId: 'student', op: '>=', value: 200 }, state) === false, 'careerLevel student >= 200 should be false'); +assert(evaluateCondition({ type: 'careerLevel', careerId: 'student', op: '==', value: 120 }, state) === true, 'careerLevel student == 120 should be true'); + +// Missing career defaults to 0 +assert(evaluateCondition({ type: 'careerLevel', careerId: 'missing', op: '==', value: 0 }, state) === true, 'missing career level == 0 should be true'); + +// === money conditions === +assert(evaluateCondition({ type: 'money', op: '>=', value: 200 }, state) === true, 'money >= 200 should be true'); +assert(evaluateCondition({ type: 'money', op: '>=', value: 500 }, state) === false, 'money >= 500 should be false'); +assert(evaluateCondition({ type: 'money', op: '==', value: 300 }, state) === true, 'money == 300 should be true'); +assert(evaluateCondition({ type: 'money', op: '<', value: 500 }, state) === true, 'money < 500 should be true'); + +// === identity conditions === +assert(evaluateCondition({ type: 'identity', value: 'orphan' }, state) === true, 'identity orphan should be true'); +assert(evaluateCondition({ type: 'identity', value: 'noble' }, state) === false, 'identity noble should be false'); + +// === worldFlag conditions === +assert(evaluateCondition({ type: 'worldFlag', flag: 'met_taoist', value: true }, state) === true, 'worldFlag met_taoist == true should be true'); +assert(evaluateCondition({ type: 'worldFlag', flag: 'met_taoist' }, state) === true, 'worldFlag met_taoist (no value) should be true'); +assert(evaluateCondition({ type: 'worldFlag', flag: 'met_king', value: true }, state) === false, 'worldFlag met_king == true should be false'); +assert(evaluateCondition({ type: 'worldFlag', flag: 'met_king' }, state) === false, 'worldFlag met_king (no value) should be false'); + +// === hasItem conditions === +assert(evaluateCondition({ type: 'hasItem', itemId: 'taoist_license' }, state) === true, 'hasItem taoist_license should be true'); +assert(evaluateCondition({ type: 'hasItem', itemId: 'missing_item' }, state) === false, 'hasItem missing_item should be false'); + +// === hasArtifact conditions === +assert(evaluateCondition({ type: 'hasArtifact', artifactId: 'immortal_sword' }, state) === true, 'hasArtifact immortal_sword should be true'); +assert(evaluateCondition({ type: 'hasArtifact', artifactId: 'immortal_sword', op: '>=', value: 3 }, state) === true, 'hasArtifact immortal_sword >= 3 should be true'); +assert(evaluateCondition({ type: 'hasArtifact', artifactId: 'immortal_sword', op: '>', value: 5 }, state) === false, 'hasArtifact immortal_sword > 5 should be false'); +assert(evaluateCondition({ type: 'hasArtifact', artifactId: 'missing_artifact' }, state) === false, 'hasArtifact missing_artifact should be false'); +assert(evaluateCondition({ type: 'hasArtifact', artifactId: 'missing_artifact', op: '==', value: 0 }, state) === true, 'hasArtifact missing_artifact == 0 should be true'); + +// === reincarnation conditions === +assert(evaluateCondition({ type: 'reincarnation', op: '>=', value: 1 }, state) === true, 'reincarnation >= 1 should be true'); +assert(evaluateCondition({ type: 'reincarnation', op: '>=', value: 5 }, state) === false, 'reincarnation >= 5 should be false'); +assert(evaluateCondition({ type: 'reincarnation', op: '==', value: 2 }, state) === true, 'reincarnation == 2 should be true'); + +// === and conditions === +assert(evaluateCondition({ + type: 'and', + conditions: [ + { type: 'age', op: '>=', value: 14 }, + { type: 'stat', stat: 'body', op: '>=', value: 20 } + ] +}, state) === true, 'and with two true conditions should be true'); + +assert(evaluateCondition({ + type: 'and', + conditions: [ + { type: 'age', op: '>=', value: 14 }, + { type: 'stat', stat: 'body', op: '>=', value: 50 } + ] +}, state) === false, 'and with one false condition should be false'); + +assert(evaluateCondition({ + type: 'and', + conditions: [] +}, state) === true, 'and with empty conditions should be true (vacuous truth)'); + +// === or conditions === +assert(evaluateCondition({ + type: 'or', + conditions: [ + { type: 'age', op: '>=', value: 20 }, + { type: 'stat', stat: 'body', op: '>=', value: 20 } + ] +}, state) === true, 'or with one true condition should be true'); + +assert(evaluateCondition({ + type: 'or', + conditions: [ + { type: 'age', op: '>=', value: 20 }, + { type: 'stat', stat: 'body', op: '>=', value: 50 } + ] +}, state) === false, 'or with all false conditions should be false'); + +assert(evaluateCondition({ + type: 'or', + conditions: [] +}, state) === false, 'or with empty conditions should be false'); + +// === not conditions === +assert(evaluateCondition({ + type: 'not', + condition: { type: 'age', op: '>=', value: 20 } +}, state) === true, 'not of false condition should be true'); + +assert(evaluateCondition({ + type: 'not', + condition: { type: 'age', op: '>=', value: 14 } +}, state) === false, 'not of true condition should be false'); + +// === nested compound conditions === +assert(evaluateCondition({ + type: 'and', + conditions: [ + { type: 'or', conditions: [ + { type: 'age', op: '>=', value: 20 }, + { type: 'identity', value: 'orphan' } + ]}, + { type: 'not', condition: { type: 'stat', stat: 'body', op: '<', value: 10 }} + ] +}, state) === true, 'nested compound condition should be true'); + +// === invalid/edge cases === +assert(evaluateCondition(null, state) === false, 'null condition should be false'); +assert(evaluateCondition({}, state) === false, 'empty object should be false'); +assert(evaluateCondition({ type: 'unknown' }, state) === false, 'unknown type should be false'); +assert(evaluateCondition({ type: 'age', op: 'invalid', value: 10 }, state) === false, 'invalid operator should be false'); + +console.log('All conditionEvaluator tests PASS');