feat: add condition evaluator with full operator support
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user