Thursday, February 26, 2026

a game engine, of sorts

 license: public domain CC0

Design Doc: The Behavioral Context Pipeline (BCP)
1. Core Philosophy
  • Data is Passive: Entities are "dumb" records. They do not have methods or internal logic.
  • Behavior is External: Logic lives in Sagas (Scenario-based scripts) that observe the state and request changes.
  • Conflict is First-Class: Coordination between overlapping rules (e.g., a Power-up vs. a Stun) is handled by an explicit Arbitrator, not by nested if/else blocks.
  • One-Way Flow: The system state transforms exactly once per frame:
    .

2. System Primitives
A. The World (State)
A single, immutable tree containing three primary keys:
  1. Context: The high-level mode (e.g., MENU, BATTLE, CUTSCENE).
  2. Entities: The domain data (e.g., players, items).
  3. Active Sagas: A list of currently running behavioral scripts.
B. The Intent (Communication)
Instead of direct mutations, Sagas emit an Intent object:
  • Request: "I would like X to happen."
  • Block: "I forbid Y from happening this frame."
C. The Saga (Behavior)
A pure function: (World) => Intent. It describes a single rule or scenario.

3. The Frame Pipeline (The Kernel)
Every frame (or "tick"), the system executes these four discrete stages in order:
Stage 1: Layer Filtering (Context)
Determine which "Layer" of the system is active. If the World.Context is PAUSED, the kernel skips Game-logic Sagas and only runs System-level Sagas. This creates a natural hierarchy without complex state machines.
Stage 2: Intent Collection (Behavioral)
The kernel iterates through all Active Sagas and calls them.
  • Input: Current World State.
  • Output: A massive list of all Requests and Blocks from every corner of the system.
Stage 3: Arbitration (Conflict Resolution)
The "Heart" of the system. It resolves overlaps using a simple rule: Blocks override Requests.
  1. Aggregate all Blocks into a "Forbidden List."
  2. Filter the Requests against this list.
  3. Optional: If multiple requests for the same attribute exist (e.g., two speed buffs), apply a mathematical "Merge" (e.g.,
    ).
Stage 4: Reduction (Data Update)
The remaining "Allowed Requests" are passed to a standard Reducer.
  • Result: A brand new World State.

4. Addressing Identity & Overlap (DCI Integration)
To prevent Sagas from becoming "Global Soup," we use Contextual Binding. When a Saga is spawned, it is given Roles (references to specific Entity IDs).
Example: The Poison Debuff
  • Data: Entity(id: 7, name: "Spider"), Entity(id: 1, name: "Hero").
  • Context: Combat.
  • Saga Instance: PoisonSaga(source: 7, target: 1).
  • Logic: This specific instance only emits Block(MOVE) for target: 1. It doesn't need to know about other players.

5. Debugging & Traceability
This architecture is "Self-Documenting" for debuggers:
Debugger QuestionAnswer Source
"Why didn't the player move?"Check the Arbitration Table for a Block on the MOVE action.
"Which rule caused the block?"The Arbitration Table tracks the Saga ID that returned the block.
"What is the current system state?"Inspect the Context Stack and Active Sagas list.
"How do I reproduce a bug?"Replay the Initial State + Input Sequence. Since the pipeline is pure, it is 100% deterministic.

6. Minimal Implementation Template (JavaScript)
javascript
// 1. The Kernel
function frame(world, input) {
  // Layering
  if (world.context === 'PAUSED') return systemOnly(world, input);

  // Intent Collection
  const intents = world.sagas.map(s => s.run(world));
  intents.push({ request: [input] });

  // Arbitration
  const blocks = intents.flatMap(i => i.block);
  const allowed = intents.flatMap(i => i.request)
                         .filter(req => !blocks.includes(req.type));

  // Reduction
  return allowed.reduce(reducer, world);
}

// 2. A Concrete Saga (Role-based)
const StunSaga = (targetId) => ({
  run: (world) => ({
    block: [`MOVE_${targetId}`, `ATTACK_${targetId}`]
  })
});
Use code with caution.

7. Summary of Benefits
  1. Additive Features: Want a "Low Gravity" mode? Add a Saga that blocks standard gravity and requests a lower value. You don't touch the Player code.
  2. No Hidden Dependencies: You can't "sneak" a change in. It must go through the Intent -> Arbitration -> Reducer pipeline.
  3. Simple Complexity: "Layers" are just if statements at the top of the frame. "Sagas" are just functions in a list.

No comments:

Post a Comment