feat: Implement scene rulebook and validation engine
- Added a new SceneRulebook system to manage data-driven validation rules for actions. - Introduced rule checks for actions like "take", "open", "move", "introduce", and "describe". - Created a rulebook engine to evaluate conditions and enforce rules during action validation. - Enhanced action handling with support for scene entry and character descriptions. - Updated the architecture documentation to reflect the new rule-based validation approach. - Added new endpoints and improved the persistence layer for rulebooks.
This commit is contained in:
@@ -1,125 +1,21 @@
|
||||
import type { Action } from "./contracts/action";
|
||||
import type { Entity } from "./contracts/entity";
|
||||
import type { SceneRulebook } from "./contracts/rulebook";
|
||||
import type { ValidationResult } from "./contracts/validation";
|
||||
import type { WorldState } from "./contracts/world";
|
||||
import { createDefaultRulebook } from "./defaultRulebook";
|
||||
import { validateWithRulebook } from "./rulebookEngine";
|
||||
|
||||
function getEntity(worldState: WorldState, entityId: string | undefined): Entity | undefined {
|
||||
if (!entityId) {
|
||||
return undefined;
|
||||
}
|
||||
return worldState.entities[entityId];
|
||||
}
|
||||
|
||||
function hasKey(actor: Entity, requiredKeyId: string): boolean {
|
||||
return actor.attributes[`has_${requiredKeyId}`] === true;
|
||||
}
|
||||
|
||||
export function validateActions(actions: Action[], worldState: WorldState): ValidationResult[] {
|
||||
return actions.map((action, actionIndex): ValidationResult => {
|
||||
const actor = getEntity(worldState, action.actorId);
|
||||
if (!actor) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "actor_not_found",
|
||||
message: `Actor '${action.actorId}' does not exist.`,
|
||||
};
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case "inspect":
|
||||
return { actionIndex, success: true };
|
||||
|
||||
case "take": {
|
||||
const target = getEntity(worldState, action.targetId);
|
||||
if (!target) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "target_not_found",
|
||||
message: `Target '${action.targetId ?? "(missing)"}' does not exist.`,
|
||||
};
|
||||
}
|
||||
|
||||
const actorLocation = String(actor.attributes.location ?? "");
|
||||
const targetLocation = String(target.attributes.location ?? "");
|
||||
if (actorLocation !== targetLocation) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "not_in_same_location",
|
||||
message: `Target '${target.id}' is not in the same location as '${actor.id}'.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (target.attributes.takeable !== true) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "not_takeable",
|
||||
message: `Target '${target.id}' cannot be taken.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { actionIndex, success: true };
|
||||
}
|
||||
|
||||
case "open": {
|
||||
const target = getEntity(worldState, action.targetId);
|
||||
if (!target) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "target_not_found",
|
||||
message: `Target '${action.targetId ?? "(missing)"}' does not exist.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (target.attributes.openable !== true) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "not_openable",
|
||||
message: `Target '${target.id}' is not openable.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (target.attributes.locked === true) {
|
||||
const requiredKey = String(target.attributes.requiredKey ?? "key_1");
|
||||
if (!hasKey(actor, requiredKey)) {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "locked_requires_key",
|
||||
message: `Target '${target.id}' is locked and requires '${requiredKey}'.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { actionIndex, success: true };
|
||||
}
|
||||
|
||||
case "move": {
|
||||
const target = getEntity(worldState, action.targetId);
|
||||
if (!target || target.type !== "room") {
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "target_not_found",
|
||||
message: `Move target '${action.targetId ?? "(missing)"}' is not a valid room.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { actionIndex, success: true };
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
actionIndex,
|
||||
success: false,
|
||||
reason: "unknown_action",
|
||||
message: `Action type '${action.type}' is not supported.`,
|
||||
};
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Validate a list of parsed actions against the world state.
|
||||
*
|
||||
* Pass a SceneRulebook to use data-driven scene rules.
|
||||
* Falls back to the built-in default rulebook when none is provided.
|
||||
*/
|
||||
export function validateActions(
|
||||
actions: Action[],
|
||||
worldState: WorldState,
|
||||
rulebook?: SceneRulebook
|
||||
): ValidationResult[] {
|
||||
const activeRulebook = rulebook ?? createDefaultRulebook(worldState.id);
|
||||
return validateWithRulebook(actions, worldState, activeRulebook);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user