Behavior data struct and primitive implementations #300

Closed
opened 2026-03-15 00:31:11 +00:00 by john · 1 comment
Owner

Summary

Implement the behavior system as a tagged union/variant data struct on entities. Each behavior type determines what action an entity takes on its turn.

Data structure

// EntityBehavior.h
struct EntityBehavior {
    BehaviorType type = BehaviorType::IDLE;
    
    // Shared
    std::string collide_label;  // entity label to treat as impassable
    
    // PATH / WAYPOINT / PATROL / LOOP
    std::vector<sf::Vector2i> waypoints;  // point list
    int current_waypoint_index = 0;
    int patrol_direction = 1;             // +1 or -1 for patrol
    std::vector<sf::Vector2i> current_path; // A* steps to current target
    int path_step_index = 0;
    
    // SLEEP
    int sleep_turns_remaining = 0;
    
    // SEEK / FLEE
    std::shared_ptr<DiscreteMap> dijkstra_map;  // reference to distance field
};

Python API

entity.behavior          # current BehaviorType enum value (read-only)
entity.set_behavior(mcrfpy.Behavior.IDLE)
entity.set_behavior(mcrfpy.Behavior.CUSTOM)
entity.set_behavior(mcrfpy.Behavior.NOISE4)
entity.set_behavior(mcrfpy.Behavior.NOISE8)
entity.set_behavior(mcrfpy.Behavior.PATH, destination=(10, 15), collide="solid")
entity.set_behavior(mcrfpy.Behavior.WAYPOINT, points=[(5,5), (10,10), (15,5)], collide="solid")
entity.set_behavior(mcrfpy.Behavior.PATROL, points=[(5,5), (15,5), (15,15)], collide="solid")
entity.set_behavior(mcrfpy.Behavior.LOOP, points=[(5,5), (15,5), (15,15)], collide="solid")
entity.set_behavior(mcrfpy.Behavior.SLEEP, turns=5)
entity.set_behavior(mcrfpy.Behavior.SEEK, dijkstra=dmap, collide="solid")
entity.set_behavior(mcrfpy.Behavior.FLEE, dijkstra=dmap, collide="solid")

Behavior details

Type Per-turn action Calls step() when
IDLE No-op. Does not participate in turns. Never
CUSTOM Calls step(DONE, None) every turn Every turn
NOISE4 Move to random walkable cardinal neighbor Never (stuck = skip)
NOISE8 Move to random walkable 8-dir neighbor Never (stuck = skip)
PATH Move one step along A* path DONE: arrived. BLOCKED: next cell has collide-label entity
WAYPOINT Path to current waypoint, walk one step DONE: last point reached
PATROL Same as waypoint, reverses at endpoints DONE: full patrol cycle (back to start)
LOOP Same as waypoint, wraps to first point Never (infinite) — unless blocked
SLEEP Decrement counter, do nothing DONE: counter reaches 0 (auto-set to idle before calling step)
SEEK dijkstra.step_from(cell_pos) toward source DONE: at distance 0. BLOCKED: collide-entity
FLEE Step to maximize distance from source DONE: no moves increase distance (cornered/out of range). BLOCKED: collide-entity

Path computation

  • PATH: A* computed once when behavior is assigned
  • WAYPOINT/PATROL/LOOP: A* computed per segment (to current target point), recomputed on arrival at each waypoint
  • Recalculation: When BLOCKED fires and step() reassigns the same behavior

Seek/Flee with DiscreteMap

Seek/flee hold a std::shared_ptr<DiscreteMap> — this could represent a multi-polar Dijkstra map (e.g., "all exits", "player + allies"). The DiscreteMap is an external reference; the behavior does not own or recompute it.

  • Seek: each turn calls step_from() to move toward the nearest source (distance 0)
  • Flee: each turn picks the neighbor cell that maximizes distance. If no neighbor has greater distance, DONE fires (cornered or out of calculated bounds)

Dependencies

  • #295 (cell_pos — behaviors move entities by updating cell_pos)
  • #296 (entity labels — collide parameter)
  • #297 (mcrfpy.Behavior enum)
  • #298 (mcrfpy.Trigger enum)
  • #299 (step() callback)
  • #294 (DiscreteMap for seek/flee — partial dependency, seek/flee can be implemented last)

Files to create/modify

  • src/EntityBehavior.h — new file, behavior data struct
  • src/EntityBehavior.cpp — new file, per-behavior turn logic
  • src/UIEntity.h/cpp — add EntityBehavior behavior member, set_behavior() method
  • src/UIEntityPyMethods.h — Python bindings for set_behavior()
## Summary Implement the behavior system as a tagged union/variant data struct on entities. Each behavior type determines what action an entity takes on its turn. ## Data structure ```cpp // EntityBehavior.h struct EntityBehavior { BehaviorType type = BehaviorType::IDLE; // Shared std::string collide_label; // entity label to treat as impassable // PATH / WAYPOINT / PATROL / LOOP std::vector<sf::Vector2i> waypoints; // point list int current_waypoint_index = 0; int patrol_direction = 1; // +1 or -1 for patrol std::vector<sf::Vector2i> current_path; // A* steps to current target int path_step_index = 0; // SLEEP int sleep_turns_remaining = 0; // SEEK / FLEE std::shared_ptr<DiscreteMap> dijkstra_map; // reference to distance field }; ``` ## Python API ```python entity.behavior # current BehaviorType enum value (read-only) entity.set_behavior(mcrfpy.Behavior.IDLE) entity.set_behavior(mcrfpy.Behavior.CUSTOM) entity.set_behavior(mcrfpy.Behavior.NOISE4) entity.set_behavior(mcrfpy.Behavior.NOISE8) entity.set_behavior(mcrfpy.Behavior.PATH, destination=(10, 15), collide="solid") entity.set_behavior(mcrfpy.Behavior.WAYPOINT, points=[(5,5), (10,10), (15,5)], collide="solid") entity.set_behavior(mcrfpy.Behavior.PATROL, points=[(5,5), (15,5), (15,15)], collide="solid") entity.set_behavior(mcrfpy.Behavior.LOOP, points=[(5,5), (15,5), (15,15)], collide="solid") entity.set_behavior(mcrfpy.Behavior.SLEEP, turns=5) entity.set_behavior(mcrfpy.Behavior.SEEK, dijkstra=dmap, collide="solid") entity.set_behavior(mcrfpy.Behavior.FLEE, dijkstra=dmap, collide="solid") ``` ## Behavior details | Type | Per-turn action | Calls `step()` when | |------|-----------------|---------------------| | `IDLE` | No-op. Does not participate in turns. | Never | | `CUSTOM` | Calls `step(DONE, None)` every turn | Every turn | | `NOISE4` | Move to random walkable cardinal neighbor | Never (stuck = skip) | | `NOISE8` | Move to random walkable 8-dir neighbor | Never (stuck = skip) | | `PATH` | Move one step along A* path | `DONE`: arrived. `BLOCKED`: next cell has collide-label entity | | `WAYPOINT` | Path to current waypoint, walk one step | `DONE`: last point reached | | `PATROL` | Same as waypoint, reverses at endpoints | `DONE`: full patrol cycle (back to start) | | `LOOP` | Same as waypoint, wraps to first point | Never (infinite) — unless blocked | | `SLEEP` | Decrement counter, do nothing | `DONE`: counter reaches 0 (auto-set to idle before calling step) | | `SEEK` | `dijkstra.step_from(cell_pos)` toward source | `DONE`: at distance 0. `BLOCKED`: collide-entity | | `FLEE` | Step to maximize distance from source | `DONE`: no moves increase distance (cornered/out of range). `BLOCKED`: collide-entity | ## Path computation - **PATH**: A* computed once when behavior is assigned - **WAYPOINT/PATROL/LOOP**: A* computed per segment (to current target point), recomputed on arrival at each waypoint - **Recalculation**: When `BLOCKED` fires and `step()` reassigns the same behavior ## Seek/Flee with DiscreteMap Seek/flee hold a `std::shared_ptr<DiscreteMap>` — this could represent a multi-polar Dijkstra map (e.g., "all exits", "player + allies"). The DiscreteMap is an external reference; the behavior does not own or recompute it. - **Seek**: each turn calls `step_from()` to move toward the nearest source (distance 0) - **Flee**: each turn picks the neighbor cell that maximizes distance. If no neighbor has greater distance, `DONE` fires (cornered or out of calculated bounds) ## Dependencies - #295 (`cell_pos` — behaviors move entities by updating `cell_pos`) - #296 (entity labels — `collide` parameter) - #297 (`mcrfpy.Behavior` enum) - #298 (`mcrfpy.Trigger` enum) - #299 (`step()` callback) - #294 (DiscreteMap for seek/flee — partial dependency, seek/flee can be implemented last) ## Files to create/modify - `src/EntityBehavior.h` — new file, behavior data struct - `src/EntityBehavior.cpp` — new file, per-behavior turn logic - `src/UIEntity.h/cpp` — add `EntityBehavior behavior` member, `set_behavior()` method - `src/UIEntityPyMethods.h` — Python bindings for `set_behavior()`
Author
Owner

Roadmap context

Part of the Grid & Entity Overhaul Roadmap (docs/GRID_ENTITY_OVERHAUL_ROADMAP.md), Phase 3a (first in behavior system implementation).

Convergence point for all Phase 2 dependencies. Recommended implementation order within this issue: idle/custom first, then noise4/noise8, then path, then waypoint/patrol/loop, then sleep, then seek/flee last (seek/flee depend on DijkstraMap/DiscreteMap).

Seek/flee will initially use current DijkstraMap type, migrating to DiscreteMap after #294 lands.

## Roadmap context Part of the Grid & Entity Overhaul Roadmap (`docs/GRID_ENTITY_OVERHAUL_ROADMAP.md`), **Phase 3a** (first in behavior system implementation). Convergence point for all Phase 2 dependencies. Recommended implementation order within this issue: idle/custom first, then noise4/noise8, then path, then waypoint/patrol/loop, then sleep, then seek/flee last (seek/flee depend on DijkstraMap/DiscreteMap). Seek/flee will initially use current `DijkstraMap` type, migrating to `DiscreteMap` after #294 lands.
john closed this issue 2026-03-16 11:21:02 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
john/McRogueFace#300
No description provided.