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:
2026-04-26 13:33:05 -04:00
parent 998635f542
commit ff9b86c3e9
16 changed files with 2013 additions and 412 deletions

View File

@@ -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);
}