Files
CharacterGardenStack/charactergarden/app/src/defaultRulebook.ts
spencer ff9b86c3e9 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.
2026-04-26 13:33:05 -04:00

248 lines
9.0 KiB
TypeScript

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}'.",
},
],
},
],
};
}