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:
247
charactergarden/app/src/defaultRulebook.ts
Normal file
247
charactergarden/app/src/defaultRulebook.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import type { SceneRulebook } from "./contracts/rulebook";
|
||||
|
||||
export const DEFAULT_RULEBOOK_ID = "rulebook_default";
|
||||
|
||||
/**
|
||||
* Builds the default SceneRulebook, encoding all validation logic that was
|
||||
* previously hardcoded in truthEngine.ts as editable, data-driven rules.
|
||||
*/
|
||||
export function createDefaultRulebook(worldId: string): SceneRulebook {
|
||||
const now = Date.now();
|
||||
return {
|
||||
id: DEFAULT_RULEBOOK_ID,
|
||||
worldId,
|
||||
name: "Default Rulebook",
|
||||
description: "Built-in scene rules for the CharacterGarden engine. Edit freely — the engine re-evaluates on every turn.",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
rules: [
|
||||
{
|
||||
actionType: "inspect",
|
||||
enabled: true,
|
||||
checks: [],
|
||||
},
|
||||
|
||||
{
|
||||
actionType: "take",
|
||||
enabled: true,
|
||||
checks: [
|
||||
{
|
||||
id: "take_target_exists",
|
||||
description: "Target entity must exist in the world",
|
||||
condition: { op: "entityExists", role: "target" },
|
||||
failReason: "target_not_found",
|
||||
failMessage: "Target '{target.id}' does not exist.",
|
||||
},
|
||||
{
|
||||
id: "take_same_location",
|
||||
description: "Actor and target must be in the same location",
|
||||
condition: { op: "sameLocation", roleA: "actor", roleB: "target" },
|
||||
failReason: "not_in_same_location",
|
||||
failMessage: "Target '{target.id}' is not in the same location as '{actor.id}'.",
|
||||
},
|
||||
{
|
||||
id: "take_takeable",
|
||||
description: "Target must have takeable attribute set to true",
|
||||
condition: { op: "eq", role: "target", attribute: "takeable", value: true },
|
||||
failReason: "not_takeable",
|
||||
failMessage: "Target '{target.id}' cannot be taken.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
actionType: "open",
|
||||
enabled: true,
|
||||
checks: [
|
||||
{
|
||||
id: "open_target_exists",
|
||||
description: "Target entity must exist in the world",
|
||||
condition: { op: "entityExists", role: "target" },
|
||||
failReason: "target_not_found",
|
||||
failMessage: "Target '{target.id}' does not exist.",
|
||||
},
|
||||
{
|
||||
id: "open_openable",
|
||||
description: "Target must have openable attribute set to true",
|
||||
condition: { op: "eq", role: "target", attribute: "openable", value: true },
|
||||
failReason: "not_openable",
|
||||
failMessage: "Target '{target.id}' is not openable.",
|
||||
},
|
||||
{
|
||||
id: "open_lock_check",
|
||||
description: "If target is locked, actor must possess the required key (has_<requiredKey> attribute)",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{
|
||||
op: "not",
|
||||
condition: { op: "eq", role: "target", attribute: "locked", value: true },
|
||||
},
|
||||
{
|
||||
op: "attributeRef",
|
||||
checkRole: "actor",
|
||||
prefix: "has_",
|
||||
refRole: "target",
|
||||
refAttribute: "requiredKey",
|
||||
},
|
||||
],
|
||||
},
|
||||
failReason: "locked_requires_key",
|
||||
failMessage: "Target '{target.id}' is locked and requires a key.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
actionType: "move",
|
||||
enabled: true,
|
||||
checks: [
|
||||
{
|
||||
id: "move_target_is_room",
|
||||
description: "Target must be an existing entity of type 'room'",
|
||||
condition: {
|
||||
op: "and",
|
||||
conditions: [
|
||||
{ op: "entityExists", role: "target" },
|
||||
{ op: "entityType", role: "target", requiredType: "room" },
|
||||
],
|
||||
},
|
||||
failReason: "target_not_found",
|
||||
failMessage: "Move target '{target.id}' is not a valid room.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
actionType: "introduce",
|
||||
enabled: true,
|
||||
checks: [
|
||||
{
|
||||
id: "introduce_actor_authorized",
|
||||
description: "Only approved characters can introduce/create characters in-scene",
|
||||
condition: {
|
||||
op: "actorIdIn",
|
||||
allowedIds: ["player"],
|
||||
},
|
||||
failReason: "actor_not_authorized",
|
||||
failMessage: "Actor '{actor.id}' is not allowed to introduce new characters.",
|
||||
},
|
||||
{
|
||||
id: "introduce_actor_in_room",
|
||||
description: "Actor must be located in a valid room entity",
|
||||
condition: {
|
||||
op: "and",
|
||||
conditions: [
|
||||
{ op: "entityExists", role: "actorRoom" },
|
||||
{ op: "entityType", role: "actorRoom", requiredType: "room" },
|
||||
],
|
||||
},
|
||||
failReason: "room_not_found",
|
||||
failMessage: "Actor '{actor.id}' is not currently in a valid room.",
|
||||
},
|
||||
{
|
||||
id: "introduce_room_joinable",
|
||||
description: "Actor's room must allow new arrivals (is_joinable: true)",
|
||||
condition: { op: "eq", role: "actorRoom", attribute: "is_joinable", value: true },
|
||||
failReason: "room_not_joinable",
|
||||
failMessage: "Room is not available for new arrivals.",
|
||||
},
|
||||
{
|
||||
id: "introduce_target_is_character",
|
||||
description: "If target entity exists, it must be of type 'character'",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
||||
{ op: "entityType", role: "target", requiredType: "character" },
|
||||
],
|
||||
},
|
||||
failReason: "target_not_character",
|
||||
failMessage: "Target '{target.id}' is not a character and cannot join the scene.",
|
||||
},
|
||||
{
|
||||
id: "introduce_target_social",
|
||||
description: "If target exists, it must be socially available (is_social: true)",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
||||
{ op: "eq", role: "target", attribute: "is_social", value: true },
|
||||
],
|
||||
},
|
||||
failReason: "target_not_social",
|
||||
failMessage: "Target '{target.id}' is not socially available to join the scene.",
|
||||
},
|
||||
{
|
||||
id: "introduce_not_already_present",
|
||||
description: "If target exists, it must not already be in the same room as the actor",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
||||
{
|
||||
op: "not",
|
||||
condition: { op: "sameLocation", roleA: "actor", roleB: "target" },
|
||||
},
|
||||
],
|
||||
},
|
||||
failReason: "already_in_scene",
|
||||
failMessage: "Target '{target.id}' is already present in the scene.",
|
||||
},
|
||||
{
|
||||
id: "introduce_no_name_duplicate",
|
||||
description: "When introducing a new character by name, no character with that name may already be in the room",
|
||||
condition: {
|
||||
op: "metaValueNotInRoom",
|
||||
metaKey: "characterName",
|
||||
entityType: "character",
|
||||
},
|
||||
failReason: "already_in_scene",
|
||||
failMessage: "A character with this name is already present in the scene.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
actionType: "describe",
|
||||
enabled: true,
|
||||
checks: [
|
||||
{
|
||||
id: "describe_target_exists",
|
||||
description: "Target must exist in the world or be created earlier in this turn",
|
||||
condition: { op: "entityExistsOrWillBeCreated", role: "target" },
|
||||
failReason: "target_not_found",
|
||||
failMessage: "Target '{target.id}' does not exist.",
|
||||
},
|
||||
{
|
||||
id: "describe_target_is_character",
|
||||
description: "If target exists, it must be of type 'character'",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
||||
{ op: "entityType", role: "target", requiredType: "character" },
|
||||
],
|
||||
},
|
||||
failReason: "target_not_character",
|
||||
failMessage: "Target '{target.id}' is not a character and cannot be described.",
|
||||
},
|
||||
{
|
||||
id: "describe_same_location",
|
||||
description: "If target exists, actor and target must be in the same location",
|
||||
condition: {
|
||||
op: "or",
|
||||
conditions: [
|
||||
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
||||
{ op: "sameLocation", roleA: "actor", roleB: "target" },
|
||||
],
|
||||
},
|
||||
failReason: "not_in_same_location",
|
||||
failMessage: "Target '{target.id}' is not in the same location as '{actor.id}'.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user