feat: add event engine with age trigger, random pool, choice filtering
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
import {
|
||||
setEventConfig,
|
||||
getEventConfig,
|
||||
checkAgeEvents,
|
||||
pickRandomEvent,
|
||||
getAvailableChoices,
|
||||
executeEvent,
|
||||
} from '../src/engine/eventEngine.js';
|
||||
import { evaluateCondition } from '../src/engine/conditionEvaluator.js';
|
||||
import { applyEffect } from '../src/engine/effectApplier.js';
|
||||
|
||||
// ==================== 测试数据 ====================
|
||||
|
||||
const testConfig = {
|
||||
events: [
|
||||
{
|
||||
id: 'daily_training',
|
||||
name: '日常修炼',
|
||||
trigger: { type: 'random', pool: 'normal' },
|
||||
weight: 50,
|
||||
effects: [{ type: 'addStat', stat: '体质', value: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'study_hard',
|
||||
name: '刻苦学习',
|
||||
trigger: { type: 'random', pool: 'normal' },
|
||||
weight: 50,
|
||||
effects: [{ type: 'addStat', stat: '悟性', value: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'invasion_military_40',
|
||||
name: '蛮族入侵',
|
||||
trigger: { type: 'age', value: 40 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'resist',
|
||||
text: '率军抵抗',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'soldier',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你率领大军击退了蛮族入侵!' }],
|
||||
},
|
||||
{
|
||||
id: 'flee',
|
||||
text: '弃城逃跑',
|
||||
effects: [{ type: 'addLog', text: '你选择了弃城逃跑。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'invasion_spiritual_45',
|
||||
name: '天魔降世',
|
||||
trigger: { type: 'age', value: 45 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'suppress',
|
||||
text: '出手镇压',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'taoist',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以无上道法镇压天魔!' }],
|
||||
},
|
||||
{
|
||||
id: 'hide',
|
||||
text: '躲藏起来',
|
||||
effects: [{ type: 'addLog', text: '你选择了躲藏起来。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'invasion_political_50',
|
||||
name: '王朝崩坏',
|
||||
trigger: { type: 'age', value: 50 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'assist',
|
||||
text: '辅佐明君',
|
||||
requirements: {
|
||||
type: 'stat',
|
||||
stat: '智力',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以智谋辅佐明君!' }],
|
||||
},
|
||||
{
|
||||
id: 'selfish',
|
||||
text: '独善其身',
|
||||
effects: [{ type: 'addLog', text: '你选择了独善其身。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'final_crisis_60',
|
||||
name: '最终危机',
|
||||
trigger: { type: 'age', value: 60 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'general',
|
||||
text: '以将军之力平定天下',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'soldier',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以将军之力平定天下!' }],
|
||||
},
|
||||
{
|
||||
id: 'chancellor',
|
||||
text: '以宰相之智治理国家',
|
||||
requirements: {
|
||||
type: 'stat',
|
||||
stat: '智力',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以宰相之智治理国家!' }],
|
||||
},
|
||||
{
|
||||
id: 'cultivator',
|
||||
text: '以修仙之道超脱轮回',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'taoist',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以修仙之道超脱轮回!' }],
|
||||
},
|
||||
{
|
||||
id: 'powerless',
|
||||
text: '无力回天',
|
||||
effects: [{ type: 'addLog', text: '你感到无力回天。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ==================== 测试辅助函数 ====================
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
const actualStr = JSON.stringify(actual);
|
||||
const expectedStr = JSON.stringify(expected);
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message}\n 期望: ${expectedStr}\n 实际: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertTrue(value, message) {
|
||||
if (!value) {
|
||||
throw new Error(message || `期望为 true,实际为 ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertFalse(value, message) {
|
||||
if (value) {
|
||||
throw new Error(message || `期望为 false,实际为 ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 测试用例 ====================
|
||||
|
||||
function testSetAndGetEventConfig() {
|
||||
setEventConfig(testConfig);
|
||||
const config = getEventConfig();
|
||||
assertEqual(config.events.length, 6, '配置应有6个事件');
|
||||
}
|
||||
|
||||
function testCheckAgeEvents() {
|
||||
// 测试40岁触发入侵事件
|
||||
const state = {
|
||||
age: 40,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
const triggered = checkAgeEvents(testConfig, state);
|
||||
assertEqual(triggered.length, 1, '40岁应触发1个事件');
|
||||
assertEqual(triggered[0].id, 'invasion_military_40', '40岁应触发蛮族入侵事件');
|
||||
assertTrue(
|
||||
state.triggeredEvents.has('invasion_military_40'),
|
||||
'事件ID应加入triggeredEvents'
|
||||
);
|
||||
|
||||
// 再次检查,不应重复触发
|
||||
const triggeredAgain = checkAgeEvents(testConfig, state);
|
||||
assertEqual(triggeredAgain.length, 0, '已触发的事件不应重复触发');
|
||||
|
||||
// 测试45岁触发
|
||||
const state45 = {
|
||||
age: 45,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered45 = checkAgeEvents(testConfig, state45);
|
||||
assertEqual(triggered45.length, 1, '45岁应触发1个事件');
|
||||
assertEqual(triggered45[0].id, 'invasion_spiritual_45', '45岁应触发天魔降世事件');
|
||||
|
||||
// 测试50岁触发
|
||||
const state50 = {
|
||||
age: 50,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered50 = checkAgeEvents(testConfig, state50);
|
||||
assertEqual(triggered50.length, 1, '50岁应触发1个事件');
|
||||
assertEqual(triggered50[0].id, 'invasion_political_50', '50岁应触发王朝崩坏事件');
|
||||
|
||||
// 测试60岁触发
|
||||
const state60 = {
|
||||
age: 60,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered60 = checkAgeEvents(testConfig, state60);
|
||||
assertEqual(triggered60.length, 1, '60岁应触发1个事件');
|
||||
assertEqual(triggered60[0].id, 'final_crisis_60', '60岁应触发最终危机事件');
|
||||
|
||||
// 测试非触发年龄
|
||||
const state30 = {
|
||||
age: 30,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered30 = checkAgeEvents(testConfig, state30);
|
||||
assertEqual(triggered30.length, 0, '30岁不应触发任何事件');
|
||||
}
|
||||
|
||||
function testPickRandomEvent() {
|
||||
// 从normal池抽取
|
||||
const state = {
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
const event = pickRandomEvent(testConfig, state, 'normal');
|
||||
assertTrue(event !== null, '应从normal池抽到一个事件');
|
||||
assertTrue(
|
||||
event.id === 'daily_training' || event.id === 'study_hard',
|
||||
'抽到的事件应在normal池中'
|
||||
);
|
||||
|
||||
// 测试空池
|
||||
const emptyEvent = pickRandomEvent(testConfig, state, 'nonexistent');
|
||||
assertEqual(emptyEvent, null, '不存在的池应返回null');
|
||||
|
||||
// 测试所有事件都被触发后
|
||||
state.triggeredEvents.add('daily_training');
|
||||
state.triggeredEvents.add('study_hard');
|
||||
const noEvent = pickRandomEvent(testConfig, state, 'normal');
|
||||
assertEqual(noEvent, null, '所有事件触发后应返回null');
|
||||
}
|
||||
|
||||
function testGetAvailableChoices() {
|
||||
// 测试无requirement的选项始终可用
|
||||
const event = testConfig.events.find(e => e.id === 'invasion_military_40');
|
||||
const stateNoSoldier = {
|
||||
careers: {},
|
||||
};
|
||||
const choicesNoSoldier = getAvailableChoices(event, stateNoSoldier);
|
||||
assertEqual(choicesNoSoldier.length, 1, '无士兵职业时应有1个可用选项');
|
||||
assertEqual(choicesNoSoldier[0].id, 'flee', '可用选项应为逃跑');
|
||||
|
||||
// 测试满足requirement
|
||||
const stateWithSoldier = {
|
||||
careers: {
|
||||
soldier: { level: 50 },
|
||||
},
|
||||
};
|
||||
const choicesWithSoldier = getAvailableChoices(event, stateWithSoldier);
|
||||
assertEqual(choicesWithSoldier.length, 2, '有士兵50级时应有2个可用选项');
|
||||
|
||||
// 测试最终危机事件
|
||||
const finalEvent = testConfig.events.find(e => e.id === 'final_crisis_60');
|
||||
const stateAll = {
|
||||
careers: {
|
||||
soldier: { level: 50 },
|
||||
taoist: { level: 50 },
|
||||
},
|
||||
stats: {
|
||||
智力: 50,
|
||||
},
|
||||
};
|
||||
const allChoices = getAvailableChoices(finalEvent, stateAll);
|
||||
assertEqual(allChoices.length, 4, '满足所有条件时应有4个可用选项');
|
||||
|
||||
const stateNone = {
|
||||
careers: {},
|
||||
stats: {},
|
||||
};
|
||||
const noChoices = getAvailableChoices(finalEvent, stateNone);
|
||||
assertEqual(noChoices.length, 1, '不满足任何条件时应有1个可用选项(无力回天)');
|
||||
assertEqual(noChoices[0].id, 'powerless', '唯一可用选项应为无力回天');
|
||||
}
|
||||
|
||||
function testExecuteEvent() {
|
||||
const event = testConfig.events.find(e => e.id === 'daily_training');
|
||||
const state = {
|
||||
stats: {},
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
executeEvent(event, state, applyEffect);
|
||||
assertEqual(state.stats['体质'], 1, '执行事件后体质应增加1');
|
||||
assertTrue(
|
||||
state.triggeredEvents.has('daily_training'),
|
||||
'事件ID应加入triggeredEvents'
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 运行测试 ====================
|
||||
|
||||
function runTests() {
|
||||
const tests = [
|
||||
{ name: 'testSetAndGetEventConfig', fn: testSetAndGetEventConfig },
|
||||
{ name: 'testCheckAgeEvents', fn: testCheckAgeEvents },
|
||||
{ name: 'testPickRandomEvent', fn: testPickRandomEvent },
|
||||
{ name: 'testGetAvailableChoices', fn: testGetAvailableChoices },
|
||||
{ name: 'testExecuteEvent', fn: testExecuteEvent },
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
test.fn();
|
||||
console.log(` PASS: ${test.name}`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.log(` FAIL: ${test.name}`);
|
||||
console.log(` ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('All eventEngine tests PASS');
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in New Issue
Block a user