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:
@@ -21,6 +21,46 @@ function cloneWorldState(worldState: WorldState): WorldState {
|
||||
};
|
||||
}
|
||||
|
||||
function slugify(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "") || "character";
|
||||
}
|
||||
|
||||
function createCharacterId(worldState: WorldState, baseName: string): string {
|
||||
const baseId = `character_${slugify(baseName)}`;
|
||||
if (!worldState.entities[baseId]) {
|
||||
return baseId;
|
||||
}
|
||||
|
||||
let suffix = 2;
|
||||
while (worldState.entities[`${baseId}_${suffix}`]) {
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
return `${baseId}_${suffix}`;
|
||||
}
|
||||
|
||||
function getActionCharacterName(action: Action): string | undefined {
|
||||
const displayName = action.metadata?.displayName;
|
||||
if (typeof displayName === "string" && displayName.trim()) {
|
||||
return displayName.trim();
|
||||
}
|
||||
|
||||
const characterName = action.metadata?.characterName;
|
||||
if (typeof characterName === "string" && characterName.trim()) {
|
||||
return characterName
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function applyActions(
|
||||
actions: Action[],
|
||||
results: ValidationResult[],
|
||||
@@ -60,6 +100,43 @@ export function applyActions(
|
||||
target.attributes.open = true;
|
||||
}
|
||||
break;
|
||||
case "introduce":
|
||||
if (actor && target) {
|
||||
target.attributes.location = actor.attributes.location;
|
||||
target.attributes.in_scene = true;
|
||||
target.attributes.last_introduced_by = actor.id;
|
||||
} else if (actor) {
|
||||
const characterName = getActionCharacterName(action);
|
||||
if (!characterName) {
|
||||
break;
|
||||
}
|
||||
|
||||
const characterId = createCharacterId(nextState, characterName);
|
||||
nextState.entities[characterId] = {
|
||||
id: characterId,
|
||||
name: characterName,
|
||||
type: "character",
|
||||
attributes: {
|
||||
location: actor.attributes.location,
|
||||
is_social: true,
|
||||
in_scene: true,
|
||||
created_by_action: "introduce",
|
||||
last_introduced_by: actor.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "describe":
|
||||
if (target) {
|
||||
const trait = action.metadata?.trait;
|
||||
if (typeof trait === "string" && trait.trim()) {
|
||||
const traits = Array.isArray(target.attributes.traits)
|
||||
? target.attributes.traits
|
||||
: [];
|
||||
target.attributes.traits = [...traits, trait.trim()];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "inspect":
|
||||
default:
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user