Wire TaskStack into brain — tasks persist and resume
Brain rewritten to use TaskStack instead of TaskQueue: - Combat/flee use interrupt() — temporary, don't affect the stack - Player commands use push() — go on top, pause current task - When task completes, previous task RESUMES automatically - Self-directed goals push at SELF_DIRECTED priority (bottom) - "follow me" is open_ended — stays active until cancelled or new command Flow example: Doug exploring (self-directed) → Player says "follow me" → exploring paused, following starts → Zombie attacks → interrupt: fight → following resumes → Player says "stop" → following cancelled, exploring resumes Command parser updated to create PrimaryTask objects via make_task() All command tasks marked source="player" for priority handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61c4a919d2
commit
1c8e2a7c90
3 changed files with 288 additions and 586 deletions
|
|
@ -1,28 +1,33 @@
|
||||||
"""
|
"""
|
||||||
Doug's Brain — goal-based autonomous decision engine.
|
Doug's Brain — stack-based autonomous decision engine.
|
||||||
|
|
||||||
Instead of randomly generating tasks each tick, Doug now has:
|
Tasks are a STACK:
|
||||||
- A NEEDS system (safety, hunger, social, shelter, boredom) that drives urgency
|
- Player commands push on top (highest priority)
|
||||||
- A GOALS system (long-term objectives broken into steps)
|
- Self-directed goals sit at the bottom
|
||||||
- A MEMORY system (remembers locations of things he's seen)
|
- Combat/flee are temporary INTERRUPTIONS that don't affect the stack
|
||||||
- A DAILY ROUTINE that adapts to time of day and persona traits
|
- When a task completes, Doug resumes the one below it
|
||||||
|
- Each task can have subtasks (get materials → return to building)
|
||||||
|
|
||||||
The brain ticks every 2 seconds:
|
The brain ticks every 2 seconds:
|
||||||
1. Update needs (decay over time)
|
1. Update needs (decay over time)
|
||||||
2. Process scan results into memory
|
2. Scan surroundings → update memory
|
||||||
3. Check if current task is still running — if so, WAIT
|
3. If busy executing, WAIT
|
||||||
4. Pick the most urgent need or goal and execute the next step
|
4. Check for interrupts (combat, flee)
|
||||||
|
5. Get next subtask from the stack and execute it
|
||||||
|
6. If stack is empty, generate self-directed tasks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from enum import IntEnum
|
|
||||||
from PySide6.QtCore import QObject, QTimer, Signal
|
from PySide6.QtCore import QObject, QTimer, Signal
|
||||||
|
|
||||||
from dougbot.bridge.ws_client import BridgeWSClient
|
from dougbot.bridge.ws_client import BridgeWSClient
|
||||||
from dougbot.bridge.protocol import ResponseMessage
|
from dougbot.bridge.protocol import ResponseMessage
|
||||||
from dougbot.core.task_queue import TaskQueue, Task, Priority
|
from dougbot.core.task_queue import (
|
||||||
|
TaskStack, PrimaryTask, SubTask, Priority, TaskStatus,
|
||||||
|
make_task, make_interrupt,
|
||||||
|
)
|
||||||
from dougbot.core.behaviors import (
|
from dougbot.core.behaviors import (
|
||||||
NeedsSystem, GoalManager, SpatialMemory, DailyRoutine, BehaviorEngine,
|
NeedsSystem, GoalManager, SpatialMemory, DailyRoutine, BehaviorEngine,
|
||||||
)
|
)
|
||||||
|
|
@ -31,19 +36,11 @@ from dougbot.utils.logging import get_logger
|
||||||
log = get_logger("core.brain")
|
log = get_logger("core.brain")
|
||||||
|
|
||||||
|
|
||||||
class BrainState(IntEnum):
|
|
||||||
"""What the brain is doing right now."""
|
|
||||||
IDLE = 0
|
|
||||||
EXECUTING_TASK = 1
|
|
||||||
WAITING_FOR_SCAN = 2
|
|
||||||
|
|
||||||
|
|
||||||
class DougBrain(QObject):
|
class DougBrain(QObject):
|
||||||
"""Goal-based autonomous decision engine with needs and memory."""
|
"""Stack-based autonomous decision engine."""
|
||||||
|
|
||||||
# Signals (same interface as before)
|
wants_to_chat = Signal(str)
|
||||||
wants_to_chat = Signal(str) # Unprompted chat message
|
wants_ai_chat = Signal(str, str)
|
||||||
wants_ai_chat = Signal(str, str) # (context, prompt) — ask AI what to say
|
|
||||||
|
|
||||||
def __init__(self, ws_client: BridgeWSClient, doug_name: str,
|
def __init__(self, ws_client: BridgeWSClient, doug_name: str,
|
||||||
traits: dict = None, age: int = 30, parent=None):
|
traits: dict = None, age: int = 30, parent=None):
|
||||||
|
|
@ -55,46 +52,45 @@ class DougBrain(QObject):
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
# Core systems
|
# Core systems
|
||||||
self._tasks = TaskQueue()
|
self._tasks = TaskStack()
|
||||||
traits = traits or {}
|
traits = traits or {}
|
||||||
self._needs = NeedsSystem(traits)
|
self._needs = NeedsSystem(traits)
|
||||||
self._goals = GoalManager(traits, age)
|
self._goals = GoalManager(traits, age)
|
||||||
self._memory = SpatialMemory()
|
self._memory = SpatialMemory()
|
||||||
self._routine = DailyRoutine(traits, age)
|
self._routine = DailyRoutine(traits, age)
|
||||||
# BehaviorEngine is kept for compatibility (main_window accesses it)
|
|
||||||
self._behaviors = BehaviorEngine(traits, age, doug_name)
|
self._behaviors = BehaviorEngine(traits, age, doug_name)
|
||||||
|
self._traits = traits
|
||||||
|
|
||||||
# Brain state
|
# Scan state
|
||||||
self._state = BrainState.IDLE
|
|
||||||
self._action_sent_time = 0.0
|
|
||||||
self._pending_scan = False
|
self._pending_scan = False
|
||||||
self._last_scan_time = 0.0
|
self._last_scan_time = 0.0
|
||||||
self._scan_interval = 4.0 # seconds between scans
|
self._scan_interval = 4.0
|
||||||
|
|
||||||
# Tick counter for staggering updates
|
# Tick counter
|
||||||
self._tick_count = 0
|
self._tick_count = 0
|
||||||
|
|
||||||
# Chat throttle
|
# Chat throttle
|
||||||
self._last_chat_time = 0.0
|
self._last_chat_time = 0.0
|
||||||
|
|
||||||
|
# Action tracking
|
||||||
|
self._action_sent_time = 0.0
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self._running = True
|
self._running = True
|
||||||
self._tick_timer.start(2000)
|
self._tick_timer.start(2000)
|
||||||
# Seed some initial goals based on persona
|
|
||||||
self._goals.seed_initial_goals(self._memory, self._behaviors)
|
self._goals.seed_initial_goals(self._memory, self._behaviors)
|
||||||
log.info("Brain started — Doug is thinking (goal-based)")
|
log.info("Brain started — Doug is thinking (stack-based)")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
self._tick_timer.stop()
|
self._tick_timer.stop()
|
||||||
self._tasks.clear()
|
self._tasks.cancel_all()
|
||||||
self._ws.send_request("stop", {})
|
self._ws.send_request("stop", {})
|
||||||
log.info("Brain stopped")
|
log.info("Brain stopped")
|
||||||
|
|
||||||
# ── Event handling (same interface) ──
|
# ── Event handling ──
|
||||||
|
|
||||||
def update_from_event(self, event: str, data: dict):
|
def update_from_event(self, event: str, data: dict):
|
||||||
"""Update brain state from bridge events."""
|
|
||||||
if event == "spawn_complete":
|
if event == "spawn_complete":
|
||||||
pos = data.get("position", {})
|
pos = data.get("position", {})
|
||||||
self._behaviors.position = {
|
self._behaviors.position = {
|
||||||
|
|
@ -106,26 +102,22 @@ class DougBrain(QObject):
|
||||||
elif event == "health_changed":
|
elif event == "health_changed":
|
||||||
self._behaviors.health = data.get("health", 20)
|
self._behaviors.health = data.get("health", 20)
|
||||||
self._behaviors.food = data.get("food", 20)
|
self._behaviors.food = data.get("food", 20)
|
||||||
# Update safety need immediately on damage
|
|
||||||
self._needs.on_health_change(self._behaviors.health, self._behaviors.food)
|
self._needs.on_health_change(self._behaviors.health, self._behaviors.food)
|
||||||
|
|
||||||
elif event == "time_update":
|
elif event == "time_update":
|
||||||
self._behaviors.day_time = data.get("dayTime", 0)
|
self._behaviors.day_time = data.get("dayTime", 0)
|
||||||
|
|
||||||
elif event == "movement_complete":
|
elif event == "movement_complete":
|
||||||
self._state = BrainState.IDLE
|
self._tasks.complete_current()
|
||||||
self._tasks.complete()
|
|
||||||
|
|
||||||
elif event == "movement_failed":
|
elif event == "movement_failed":
|
||||||
self._state = BrainState.IDLE
|
self._tasks.fail_current()
|
||||||
self._tasks.cancel()
|
|
||||||
|
|
||||||
elif event == "death":
|
elif event == "death":
|
||||||
self._state = BrainState.IDLE
|
self._tasks.cancel_all()
|
||||||
self._tasks.clear()
|
|
||||||
self._needs.on_death()
|
self._needs.on_death()
|
||||||
self._goals.on_death()
|
self._goals.on_death()
|
||||||
log.info("Doug died — clearing all tasks and resetting needs")
|
log.info("Doug died — all tasks cleared")
|
||||||
|
|
||||||
elif event == "player_joined":
|
elif event == "player_joined":
|
||||||
username = data.get("username", "")
|
username = data.get("username", "")
|
||||||
|
|
@ -141,7 +133,6 @@ class DougBrain(QObject):
|
||||||
# ── Main tick ──
|
# ── Main tick ──
|
||||||
|
|
||||||
def _tick(self):
|
def _tick(self):
|
||||||
"""Main brain tick — needs → scan → decide → act."""
|
|
||||||
from PySide6.QtNetwork import QAbstractSocket
|
from PySide6.QtNetwork import QAbstractSocket
|
||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
|
|
@ -150,20 +141,19 @@ class DougBrain(QObject):
|
||||||
|
|
||||||
self._tick_count += 1
|
self._tick_count += 1
|
||||||
|
|
||||||
# Safety: unstick action timeout (20s)
|
# If executing a subtask, wait (with timeout safety)
|
||||||
if self._state == BrainState.EXECUTING_TASK:
|
if self._tasks.is_busy:
|
||||||
if time.time() - self._action_sent_time > 20:
|
if time.time() - self._action_sent_time > 25:
|
||||||
log.debug("Action timed out — unsticking")
|
log.debug("Action timed out — unsticking")
|
||||||
self._state = BrainState.IDLE
|
self._tasks.fail_current()
|
||||||
self._tasks.cancel()
|
|
||||||
else:
|
else:
|
||||||
return # WAIT for current task to finish — don't pile on
|
return
|
||||||
|
|
||||||
# Safety: unstick pending scan
|
# Unstick pending scan
|
||||||
if self._pending_scan and (time.time() - self._last_scan_time > 10):
|
if self._pending_scan and (time.time() - self._last_scan_time > 10):
|
||||||
self._pending_scan = False
|
self._pending_scan = False
|
||||||
|
|
||||||
# Step 1: Decay needs every tick (every 2s)
|
# Step 1: Decay needs
|
||||||
self._needs.decay(
|
self._needs.decay(
|
||||||
health=self._behaviors.health,
|
health=self._behaviors.health,
|
||||||
food=self._behaviors.food,
|
food=self._behaviors.food,
|
||||||
|
|
@ -179,23 +169,24 @@ class DougBrain(QObject):
|
||||||
self._last_scan_time = time.time()
|
self._last_scan_time = time.time()
|
||||||
self._ws.send_request("scan_surroundings", {"radius": 16}, self._on_scan)
|
self._ws.send_request("scan_surroundings", {"radius": 16}, self._on_scan)
|
||||||
self._ws.send_request("get_inventory", {}, self._on_inventory)
|
self._ws.send_request("get_inventory", {}, self._on_inventory)
|
||||||
return # Wait for scan before deciding
|
return
|
||||||
|
|
||||||
if self._pending_scan:
|
if self._pending_scan:
|
||||||
return # Still waiting for scan
|
return
|
||||||
|
|
||||||
# Step 3: Decide what to do
|
# Step 3: Check for interrupts (combat/flee)
|
||||||
task = self._decide()
|
self._check_interrupts()
|
||||||
if task:
|
|
||||||
self._tasks.add(task)
|
|
||||||
|
|
||||||
# Step 4: Execute top task from queue
|
# Step 4: If stack is empty, generate self-directed tasks
|
||||||
self._execute_next_task()
|
if not self._tasks.current_task:
|
||||||
|
self._generate_self_directed()
|
||||||
|
|
||||||
# ── Scanning & Memory ──
|
# Step 5: Execute next subtask from the stack
|
||||||
|
self._execute_next()
|
||||||
|
|
||||||
|
# ── Scanning ──
|
||||||
|
|
||||||
def _on_scan(self, response: ResponseMessage):
|
def _on_scan(self, response: ResponseMessage):
|
||||||
"""Process scan results and update memory."""
|
|
||||||
self._pending_scan = False
|
self._pending_scan = False
|
||||||
if response.status != "success":
|
if response.status != "success":
|
||||||
return
|
return
|
||||||
|
|
@ -215,7 +206,6 @@ class DougBrain(QObject):
|
||||||
self._behaviors.nearby_entities = entities
|
self._behaviors.nearby_entities = entities
|
||||||
self._behaviors.nearby_hostiles = [e for e in entities if e.get("isHostile", False)]
|
self._behaviors.nearby_hostiles = [e for e in entities if e.get("isHostile", False)]
|
||||||
|
|
||||||
# Update memory with what we see
|
|
||||||
self._memory.update_from_scan(
|
self._memory.update_from_scan(
|
||||||
position=self._behaviors.position,
|
position=self._behaviors.position,
|
||||||
blocks=self._behaviors.nearby_blocks,
|
blocks=self._behaviors.nearby_blocks,
|
||||||
|
|
@ -223,8 +213,6 @@ class DougBrain(QObject):
|
||||||
players=self._behaviors.nearby_players,
|
players=self._behaviors.nearby_players,
|
||||||
hostiles=self._behaviors.nearby_hostiles,
|
hostiles=self._behaviors.nearby_hostiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update needs based on what we see
|
|
||||||
self._needs.on_scan(
|
self._needs.on_scan(
|
||||||
hostiles_nearby=len(self._behaviors.nearby_hostiles),
|
hostiles_nearby=len(self._behaviors.nearby_hostiles),
|
||||||
players_nearby=len(self._behaviors.nearby_players),
|
players_nearby=len(self._behaviors.nearby_players),
|
||||||
|
|
@ -235,479 +223,242 @@ class DougBrain(QObject):
|
||||||
return
|
return
|
||||||
self._behaviors.inventory = response.data.get("items", [])
|
self._behaviors.inventory = response.data.get("items", [])
|
||||||
|
|
||||||
# ── Decision Engine ──
|
# ── Interrupts (combat/flee — temporary, don't affect stack) ──
|
||||||
|
|
||||||
def _decide(self) -> Task | None:
|
def _check_interrupts(self):
|
||||||
"""
|
"""Check for immediate threats that need a temporary interrupt."""
|
||||||
The core decision: what should Doug do RIGHT NOW?
|
|
||||||
|
|
||||||
Priority order:
|
|
||||||
1. Critical survival (flee, eat) — if safety/hunger need is critical
|
|
||||||
2. Player commands (already in queue at HIGH priority)
|
|
||||||
3. Urgent needs (shelter at night, social when lonely)
|
|
||||||
4. Current goal step (work toward long-term goals)
|
|
||||||
5. Daily routine activity
|
|
||||||
6. Idle behavior (look around, chat)
|
|
||||||
"""
|
|
||||||
b = self._behaviors
|
b = self._behaviors
|
||||||
|
bravery = self._traits.get("bravery", 50)
|
||||||
# 1. CRITICAL SURVIVAL — safety need below 20
|
|
||||||
if self._needs.safety < 20:
|
|
||||||
task = self._survival_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 2. HUNGER — food need below 25
|
|
||||||
if self._needs.hunger < 25:
|
|
||||||
task = self._eat_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 3. SHELTER — at night with low shelter need
|
|
||||||
if b.is_night and self._needs.shelter < 30:
|
|
||||||
task = self._shelter_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 4. SOCIAL — respond to nearby players if social need is low
|
|
||||||
if self._needs.social < 30 and b.nearby_players:
|
|
||||||
task = self._social_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 5. GOAL PROGRESS — work on the current goal
|
|
||||||
task = self._goal_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 6. DAILY ROUTINE — what should Doug be doing at this time of day?
|
|
||||||
task = self._routine_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 7. BOREDOM — do something interesting
|
|
||||||
if self._needs.boredom < 30:
|
|
||||||
task = self._boredom_task()
|
|
||||||
if task:
|
|
||||||
return task
|
|
||||||
|
|
||||||
# 8. IDLE — look around, maybe chat
|
|
||||||
return self._idle_task()
|
|
||||||
|
|
||||||
# ── Need-driven tasks ──
|
|
||||||
|
|
||||||
def _survival_task(self) -> Task | None:
|
|
||||||
"""Handle immediate survival threats — fight or flee based on bravery."""
|
|
||||||
b = self._behaviors
|
|
||||||
bravery = b._traits.get("bravery", 50)
|
|
||||||
|
|
||||||
hostile = self._nearest_hostile(10)
|
hostile = self._nearest_hostile(10)
|
||||||
if hostile:
|
if not hostile:
|
||||||
dist = hostile.get("distance", 99)
|
return
|
||||||
|
|
||||||
# FIGHT if: brave enough AND health is ok AND mob is close
|
dist = hostile.get("distance", 99)
|
||||||
should_fight = (
|
mob_type = hostile.get("type", "mob")
|
||||||
bravery > 30
|
|
||||||
and b.health > 8
|
|
||||||
and dist < 6
|
|
||||||
)
|
|
||||||
|
|
||||||
if should_fight:
|
# FIGHT if brave enough and healthy
|
||||||
return Task(
|
should_fight = bravery > 30 and b.health > 8 and dist < 6
|
||||||
name="combat",
|
# FLEE if scared or hurt
|
||||||
priority=Priority.CRITICAL,
|
should_flee = (not should_fight) and (dist < 8) and (b.health < 14 or bravery < 30)
|
||||||
action="attack_nearest_hostile",
|
|
||||||
params={"range": 6},
|
|
||||||
description=f"Fighting {hostile.get('type', 'mob')}!",
|
|
||||||
timeout=12,
|
|
||||||
interruptible=False,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self._flee_task(hostile)
|
|
||||||
|
|
||||||
# Critical health with no hostiles — eat if possible
|
if should_fight:
|
||||||
if b.health <= 6:
|
self._tasks.interrupt(make_interrupt(
|
||||||
|
"combat", f"Fighting {mob_type}!",
|
||||||
|
"attack_nearest_hostile", {"range": 6}, timeout=12,
|
||||||
|
))
|
||||||
|
elif should_flee:
|
||||||
|
hpos = hostile.get("position", b.position)
|
||||||
|
dx = b.position["x"] - hpos.get("x", 0)
|
||||||
|
dz = b.position["z"] - hpos.get("z", 0)
|
||||||
|
d = max(0.1, math.sqrt(dx * dx + dz * dz))
|
||||||
|
flee_x = b.position["x"] + (dx / d) * 15
|
||||||
|
flee_z = b.position["z"] + (dz / d) * 15
|
||||||
|
|
||||||
|
self._tasks.interrupt(make_interrupt(
|
||||||
|
"flee", f"Fleeing from {mob_type}!",
|
||||||
|
"move_to", {"x": flee_x, "y": b.position["y"], "z": flee_z, "range": 3},
|
||||||
|
timeout=15,
|
||||||
|
))
|
||||||
|
|
||||||
|
# ── Self-directed task generation ──
|
||||||
|
|
||||||
|
def _generate_self_directed(self):
|
||||||
|
"""Generate a task when the stack is empty."""
|
||||||
|
b = self._behaviors
|
||||||
|
|
||||||
|
# HUNGER — eat if we have food
|
||||||
|
if self._needs.hunger < 25:
|
||||||
food = self._find_food()
|
food = self._find_food()
|
||||||
if food:
|
if food:
|
||||||
return Task(
|
self._tasks.push(make_task(
|
||||||
name="emergency_eat",
|
"eat", Priority.URGENT, f"Eating {food}",
|
||||||
priority=Priority.CRITICAL,
|
"equip_item", {"name": food, "destination": "hand"}, timeout=10,
|
||||||
action="equip_item",
|
))
|
||||||
params={"name": food, "destination": "hand"},
|
return
|
||||||
description=f"Emergency eating {food}",
|
|
||||||
timeout=10,
|
# SHELTER at night
|
||||||
|
if b.is_night and self._needs.shelter < 30:
|
||||||
|
home = self._memory.home
|
||||||
|
if home and self._distance_to(home) > 10:
|
||||||
|
self._tasks.push(make_task(
|
||||||
|
"go_home", Priority.URGENT, "Heading home for the night",
|
||||||
|
"move_to", {"x": home["x"], "y": home["y"], "z": home["z"], "range": 3},
|
||||||
|
timeout=30,
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
# SOCIAL — approach nearby players
|
||||||
|
if self._needs.social < 30 and b.nearby_players:
|
||||||
|
player = b.nearby_players[0]
|
||||||
|
dist = player.get("distance", 99)
|
||||||
|
name = player.get("name", "player")
|
||||||
|
if dist > 5:
|
||||||
|
self._tasks.push(make_task(
|
||||||
|
f"approach_{name}", Priority.NORMAL, f"Walking toward {name}",
|
||||||
|
"move_to", {**player["position"], "range": 3}, timeout=15,
|
||||||
|
))
|
||||||
|
return
|
||||||
|
# Close — say hi (throttled)
|
||||||
|
if time.time() - self._last_chat_time > 45:
|
||||||
|
self._last_chat_time = time.time()
|
||||||
|
self._needs.social = min(100, self._needs.social + 40)
|
||||||
|
self.wants_ai_chat.emit(
|
||||||
|
self._build_chat_context(),
|
||||||
|
f"You notice {name} nearby. Say something friendly and short."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
return None
|
# GOAL PROGRESS
|
||||||
|
|
||||||
def _eat_task(self) -> Task | None:
|
|
||||||
"""Find and eat food."""
|
|
||||||
food = self._find_food()
|
|
||||||
if food:
|
|
||||||
return Task(
|
|
||||||
name="eat",
|
|
||||||
priority=Priority.URGENT,
|
|
||||||
action="equip_item",
|
|
||||||
params={"name": food, "destination": "hand"},
|
|
||||||
description=f"Eating {food}",
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
# No food — add a goal to find some
|
|
||||||
if not self._goals.has_goal("find_food"):
|
|
||||||
self._goals.add_goal("find_food", priority=8)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _shelter_task(self) -> Task | None:
|
|
||||||
"""Seek shelter at night."""
|
|
||||||
home = self._memory.home
|
|
||||||
if not home:
|
|
||||||
return None
|
|
||||||
|
|
||||||
dist = self._distance_to(home)
|
|
||||||
if dist > 10:
|
|
||||||
return Task(
|
|
||||||
name="go_home",
|
|
||||||
priority=Priority.URGENT,
|
|
||||||
action="move_to",
|
|
||||||
params={"x": home["x"], "y": home["y"], "z": home["z"], "range": 3},
|
|
||||||
description="Heading home for the night",
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
# Already near home — shelter need satisfied
|
|
||||||
self._needs.shelter = min(100, self._needs.shelter + 30)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _social_task(self) -> Task | None:
|
|
||||||
"""Interact with nearby players."""
|
|
||||||
b = self._behaviors
|
|
||||||
if not b.nearby_players:
|
|
||||||
return None
|
|
||||||
|
|
||||||
player = b.nearby_players[0]
|
|
||||||
dist = player.get("distance", 99)
|
|
||||||
name = player.get("name", "player")
|
|
||||||
|
|
||||||
# Walk toward player if far
|
|
||||||
if dist > 5:
|
|
||||||
return Task(
|
|
||||||
name=f"approach_{name}",
|
|
||||||
priority=Priority.NORMAL,
|
|
||||||
action="move_to",
|
|
||||||
params={**player["position"], "range": 3},
|
|
||||||
description=f"Walking toward {name}",
|
|
||||||
timeout=15,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Close enough — say hi (throttled)
|
|
||||||
if time.time() - self._last_chat_time > 45:
|
|
||||||
self._last_chat_time = time.time()
|
|
||||||
self._needs.social = min(100, self._needs.social + 40)
|
|
||||||
context = self._build_chat_context()
|
|
||||||
self.wants_ai_chat.emit(
|
|
||||||
context,
|
|
||||||
f"You notice {name} nearby. Say something friendly and short."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Social need partially satisfied just by being near people
|
|
||||||
self._needs.social = min(100, self._needs.social + 10)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _goal_task(self) -> Task | None:
|
|
||||||
"""Get the next step from the active goal."""
|
|
||||||
goal = self._goals.get_active_goal()
|
goal = self._goals.get_active_goal()
|
||||||
if not goal:
|
if goal:
|
||||||
return None
|
step = self._goals.get_next_step(goal, b, self._memory)
|
||||||
|
if step:
|
||||||
|
# Wrap the old Task as a PrimaryTask
|
||||||
|
task = make_task(
|
||||||
|
step.name, Priority.SELF_DIRECTED, step.description,
|
||||||
|
step.action, step.params, timeout=step.timeout,
|
||||||
|
)
|
||||||
|
self._tasks.push(task)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self._goals.complete_goal(goal["name"])
|
||||||
|
self._needs.boredom = min(100, self._needs.boredom + 20)
|
||||||
|
|
||||||
step = self._goals.get_next_step(goal, self._behaviors, self._memory)
|
# DAILY ROUTINE
|
||||||
if not step:
|
|
||||||
# Goal complete or stuck — mark it done
|
|
||||||
self._goals.complete_goal(goal["name"])
|
|
||||||
self._needs.boredom = min(100, self._needs.boredom + 20)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return step
|
|
||||||
|
|
||||||
def _routine_task(self) -> Task | None:
|
|
||||||
"""Get a task from the daily routine."""
|
|
||||||
b = self._behaviors
|
|
||||||
phase = self._routine.get_phase(b.day_time)
|
phase = self._routine.get_phase(b.day_time)
|
||||||
|
if phase == "morning" and not self._goals.has_any_goals():
|
||||||
|
self._goals.seed_initial_goals(self._memory, b)
|
||||||
|
|
||||||
if phase == "morning":
|
if phase == "evening":
|
||||||
# Morning: look around, plan the day
|
|
||||||
if not self._goals.has_any_goals():
|
|
||||||
self._goals.seed_initial_goals(self._memory, b)
|
|
||||||
# Look around to survey
|
|
||||||
if random.random() < 0.3:
|
|
||||||
return self._look_around_task()
|
|
||||||
|
|
||||||
elif phase == "day":
|
|
||||||
# Daytime: if no active goal, pick one based on what we know
|
|
||||||
if not self._goals.has_any_goals():
|
|
||||||
self._goals.generate_goal_from_environment(self._memory, b)
|
|
||||||
# Fall through to goal_task on next tick
|
|
||||||
|
|
||||||
elif phase == "evening":
|
|
||||||
# Evening: head toward home
|
|
||||||
home = self._memory.home
|
home = self._memory.home
|
||||||
if home and self._distance_to(home) > 15:
|
if home and self._distance_to(home) > 15:
|
||||||
return Task(
|
self._tasks.push(make_task(
|
||||||
name="evening_return",
|
"evening_return", Priority.NORMAL, "Heading home for the evening",
|
||||||
priority=Priority.NORMAL,
|
"move_to", {"x": home["x"], "y": home["y"], "z": home["z"], "range": 3},
|
||||||
action="move_to",
|
|
||||||
params={"x": home["x"], "y": home["y"], "z": home["z"], "range": 3},
|
|
||||||
description="Heading home for the evening",
|
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
))
|
||||||
|
return
|
||||||
|
|
||||||
elif phase == "night":
|
# BOREDOM — explore
|
||||||
# Night: stay near home, look around cautiously
|
if self._needs.boredom < 30:
|
||||||
if random.random() < 0.2:
|
interesting = self._memory.get_nearest_unexplored(b.position, max_dist=30)
|
||||||
return self._look_around_task()
|
if interesting:
|
||||||
|
self._needs.boredom = min(100, self._needs.boredom + 15)
|
||||||
|
self._tasks.push(make_task(
|
||||||
|
"explore", Priority.SELF_DIRECTED, f"Checking out a {interesting['type']}",
|
||||||
|
"move_to", {"x": interesting["x"], "y": interesting["y"],
|
||||||
|
"z": interesting["z"], "range": 2},
|
||||||
|
timeout=20,
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
return None
|
# Explore new direction
|
||||||
|
angle = self._memory.suggest_explore_angle(b.position)
|
||||||
def _boredom_task(self) -> Task | None:
|
curiosity = self._traits.get("curiosity", 50)
|
||||||
"""Do something interesting to combat boredom."""
|
dist = random.uniform(8, 12 + curiosity // 10)
|
||||||
b = self._behaviors
|
target_x = b.position["x"] + math.cos(angle) * dist
|
||||||
|
target_z = b.position["z"] + math.sin(angle) * dist
|
||||||
# Check nearby interesting things from memory
|
self._needs.boredom = min(100, self._needs.boredom + 10)
|
||||||
interesting = self._memory.get_nearest_unexplored(b.position, max_dist=30)
|
self._tasks.push(make_task(
|
||||||
if interesting:
|
"explore", Priority.SELF_DIRECTED, "Exploring",
|
||||||
self._needs.boredom = min(100, self._needs.boredom + 15)
|
"move_to", {"x": target_x, "y": b.position["y"], "z": target_z, "range": 2},
|
||||||
return Task(
|
|
||||||
name=f"explore_{interesting['type']}",
|
|
||||||
priority=Priority.LOW,
|
|
||||||
action="move_to",
|
|
||||||
params={"x": interesting["x"], "y": interesting["y"],
|
|
||||||
"z": interesting["z"], "range": 2},
|
|
||||||
description=f"Checking out a {interesting['type']}",
|
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
))
|
||||||
|
return
|
||||||
|
|
||||||
# Nothing interesting — explore in a new direction
|
# IDLE — look around, maybe chat
|
||||||
return self._explore_task()
|
|
||||||
|
|
||||||
def _idle_task(self) -> Task | None:
|
|
||||||
"""Idle behavior — look around or chat."""
|
|
||||||
b = self._behaviors
|
|
||||||
|
|
||||||
# Chatty behavior near players
|
|
||||||
if b.nearby_players and time.time() - self._last_chat_time > 60:
|
if b.nearby_players and time.time() - self._last_chat_time > 60:
|
||||||
if random.random() < 0.15:
|
if random.random() < 0.1:
|
||||||
self._last_chat_time = time.time()
|
self._last_chat_time = time.time()
|
||||||
context = self._build_chat_context()
|
self.wants_ai_chat.emit(self._build_chat_context(), "Say something casual.")
|
||||||
self.wants_ai_chat.emit(context, "Say something casual and short.")
|
return
|
||||||
return None
|
|
||||||
|
|
||||||
# Look around
|
if random.random() < 0.3:
|
||||||
if random.random() < 0.4:
|
self._tasks.push(make_task(
|
||||||
return self._look_around_task()
|
"look_around", Priority.IDLE, "",
|
||||||
|
"look_at", {
|
||||||
|
"x": b.position["x"] + random.uniform(-20, 20),
|
||||||
|
"y": b.position["y"] + random.uniform(-3, 5),
|
||||||
|
"z": b.position["z"] + random.uniform(-20, 20),
|
||||||
|
}, timeout=3,
|
||||||
|
))
|
||||||
|
|
||||||
return None
|
# ── Execute next subtask from the stack ──
|
||||||
|
|
||||||
# ── Task factories ──
|
def _execute_next(self):
|
||||||
|
"""Get the next subtask from the stack and send it to the bridge."""
|
||||||
def _flee_task(self, hostile: dict) -> Task:
|
subtask = self._tasks.get_next_action()
|
||||||
"""Run away from a hostile mob."""
|
if not subtask:
|
||||||
hpos = hostile.get("position", self._behaviors.position)
|
|
||||||
dx = self._behaviors.position["x"] - hpos.get("x", 0)
|
|
||||||
dz = self._behaviors.position["z"] - hpos.get("z", 0)
|
|
||||||
dist = max(0.1, math.sqrt(dx * dx + dz * dz))
|
|
||||||
|
|
||||||
flee_dist = 15
|
|
||||||
flee_x = self._behaviors.position["x"] + (dx / dist) * flee_dist
|
|
||||||
flee_z = self._behaviors.position["z"] + (dz / dist) * flee_dist
|
|
||||||
|
|
||||||
return Task(
|
|
||||||
name="flee",
|
|
||||||
priority=Priority.CRITICAL,
|
|
||||||
action="move_to",
|
|
||||||
params={"x": flee_x, "y": self._behaviors.position["y"],
|
|
||||||
"z": flee_z, "range": 3},
|
|
||||||
description=f"Fleeing from {hostile.get('type', 'mob')}!",
|
|
||||||
timeout=15,
|
|
||||||
interruptible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _explore_task(self) -> Task:
|
|
||||||
"""Explore in a direction we haven't been."""
|
|
||||||
b = self._behaviors
|
|
||||||
curiosity = b._traits.get("curiosity", 50)
|
|
||||||
|
|
||||||
# Pick a direction biased away from explored areas
|
|
||||||
angle = self._memory.suggest_explore_angle(b.position)
|
|
||||||
dist = random.uniform(8, 12 + curiosity // 10)
|
|
||||||
|
|
||||||
target_x = b.position["x"] + math.cos(angle) * dist
|
|
||||||
target_z = b.position["z"] + math.sin(angle) * dist
|
|
||||||
|
|
||||||
# Don't go too far from home
|
|
||||||
home = self._memory.home or b.spawn_pos
|
|
||||||
max_radius = 40 + curiosity // 2
|
|
||||||
dx = target_x - home["x"]
|
|
||||||
dz = target_z - home["z"]
|
|
||||||
if math.sqrt(dx * dx + dz * dz) > max_radius:
|
|
||||||
angle = math.atan2(home["z"] - b.position["z"],
|
|
||||||
home["x"] - b.position["x"])
|
|
||||||
target_x = b.position["x"] + math.cos(angle) * 8
|
|
||||||
target_z = b.position["z"] + math.sin(angle) * 8
|
|
||||||
|
|
||||||
self._needs.boredom = min(100, self._needs.boredom + 10)
|
|
||||||
return Task(
|
|
||||||
name="explore",
|
|
||||||
priority=Priority.IDLE,
|
|
||||||
action="move_to",
|
|
||||||
params={"x": target_x, "y": b.position["y"], "z": target_z, "range": 2},
|
|
||||||
description="Exploring",
|
|
||||||
timeout=20,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _look_around_task(self) -> Task:
|
|
||||||
"""Look in a random direction."""
|
|
||||||
b = self._behaviors
|
|
||||||
return Task(
|
|
||||||
name="look_around",
|
|
||||||
priority=Priority.IDLE,
|
|
||||||
action="look_at",
|
|
||||||
params={
|
|
||||||
"x": b.position["x"] + random.uniform(-20, 20),
|
|
||||||
"y": b.position["y"] + random.uniform(-3, 5),
|
|
||||||
"z": b.position["z"] + random.uniform(-20, 20),
|
|
||||||
},
|
|
||||||
description="",
|
|
||||||
timeout=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Task execution ──
|
|
||||||
|
|
||||||
def _execute_next_task(self):
|
|
||||||
"""Execute the highest priority task from the queue."""
|
|
||||||
task = self._tasks.next()
|
|
||||||
if not task:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle special callbacks
|
|
||||||
if task.callback == "on_idle_chat":
|
|
||||||
self._handle_idle_chat(task)
|
|
||||||
self._tasks.complete()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Callbacks that need special response handling
|
|
||||||
if task.callback in ("on_equip_result", "on_inventory_report"):
|
|
||||||
# Execute then report result directly to chat (no AI)
|
|
||||||
self._execute_action_with_report(task)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Skip placeholder actions
|
|
||||||
if task.action == "status":
|
|
||||||
self._tasks.complete()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Log significant actions
|
# Log significant actions
|
||||||
if task.description and task.priority >= Priority.LOW:
|
if subtask.description:
|
||||||
log.info(f"[{task.priority.name}] {task.description}")
|
current = self._tasks.current_task
|
||||||
|
priority_name = current.priority.name if current else "?"
|
||||||
|
log.info(f"[{priority_name}] {subtask.description}")
|
||||||
|
|
||||||
self._execute_action(task)
|
|
||||||
|
|
||||||
def _execute_action(self, task: Task):
|
|
||||||
"""Send an action to the bridge and mark brain as busy."""
|
|
||||||
self._state = BrainState.EXECUTING_TASK
|
|
||||||
self._action_sent_time = time.time()
|
self._action_sent_time = time.time()
|
||||||
|
|
||||||
def on_response(resp: ResponseMessage):
|
def on_response(resp: ResponseMessage):
|
||||||
if resp.status == "success":
|
if resp.status == "success":
|
||||||
data = resp.data or {}
|
data = resp.data or {}
|
||||||
|
|
||||||
# Craft results
|
# Craft results — report to chat
|
||||||
if task.action == "craft_item":
|
if subtask.action == "craft_item":
|
||||||
self._state = BrainState.IDLE
|
|
||||||
if data.get("crafted"):
|
if data.get("crafted"):
|
||||||
item = data.get("item", "item").replace("_", " ")
|
item = data.get("item", "item").replace("_", " ")
|
||||||
self._ws.send_request("send_chat", {
|
self._ws.send_request("send_chat", {
|
||||||
"message": f"Done! Crafted {data.get('count', 1)} {item}."
|
"message": f"Done! Crafted {data.get('count', 1)} {item}."
|
||||||
})
|
})
|
||||||
self._tasks.complete()
|
|
||||||
else:
|
else:
|
||||||
error = data.get("error", "Something went wrong.")
|
error = data.get("error", "Something went wrong.")
|
||||||
self._ws.send_request("send_chat", {"message": error})
|
self._ws.send_request("send_chat", {"message": error})
|
||||||
self._tasks.cancel()
|
self._tasks.complete_current()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Instant-complete actions
|
# Equip results — report to chat
|
||||||
if task.action in ("open_chest", "dig_block", "equip_item",
|
if subtask.action == "equip_item":
|
||||||
"look_at", "send_chat", "attack_nearest_hostile"):
|
item = subtask.params.get("name", "item").replace("_", " ")
|
||||||
self._state = BrainState.IDLE
|
|
||||||
self._tasks.complete()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement actions wait for movement_complete event
|
|
||||||
if task.action not in ("move_to", "move_relative", "follow_player"):
|
|
||||||
self._state = BrainState.IDLE
|
|
||||||
self._tasks.complete()
|
|
||||||
else:
|
|
||||||
self._state = BrainState.IDLE
|
|
||||||
self._tasks.cancel()
|
|
||||||
error = resp.error or "Something went wrong"
|
|
||||||
log.debug(f"Action failed: {error}")
|
|
||||||
if task.priority >= Priority.HIGH:
|
|
||||||
self._ws.send_request("send_chat", {"message": error})
|
|
||||||
|
|
||||||
self._ws.send_request(task.action, task.params, on_response)
|
|
||||||
|
|
||||||
def _execute_action_with_report(self, task: Task):
|
|
||||||
"""Execute an action and report the result directly to chat (no AI)."""
|
|
||||||
self._state = BrainState.EXECUTING_TASK
|
|
||||||
self._action_sent_time = time.time()
|
|
||||||
|
|
||||||
if task.description and task.priority >= Priority.LOW:
|
|
||||||
log.info(f"[{task.priority.name}] {task.description}")
|
|
||||||
|
|
||||||
def on_response(resp: ResponseMessage):
|
|
||||||
self._state = BrainState.IDLE
|
|
||||||
|
|
||||||
if task.callback == "on_equip_result":
|
|
||||||
if resp.status == "success":
|
|
||||||
item = task.params.get("name", "item").replace("_", " ")
|
|
||||||
self._ws.send_request("send_chat", {"message": f"Equipped {item}."})
|
self._ws.send_request("send_chat", {"message": f"Equipped {item}."})
|
||||||
self._tasks.complete()
|
self._tasks.complete_current()
|
||||||
else:
|
return
|
||||||
error = resp.error or "I don't have that item."
|
|
||||||
self._ws.send_request("send_chat", {"message": error})
|
|
||||||
self._tasks.cancel()
|
|
||||||
|
|
||||||
elif task.callback == "on_inventory_report":
|
# Inventory check — report to chat
|
||||||
if resp.status == "success":
|
if subtask.action == "get_inventory":
|
||||||
items = resp.data.get("items", [])
|
items = data.get("items", [])
|
||||||
if items:
|
if items:
|
||||||
# List items concisely
|
|
||||||
item_strs = [f"{i['count']}x {i['name'].replace('_',' ')}" for i in items[:8]]
|
item_strs = [f"{i['count']}x {i['name'].replace('_',' ')}" for i in items[:8]]
|
||||||
msg = "I have: " + ", ".join(item_strs)
|
msg = "I have: " + ", ".join(item_strs)
|
||||||
if len(items) > 8:
|
if len(items) > 8:
|
||||||
msg += f" and {len(items) - 8} more items"
|
msg += f" and {len(items) - 8} more"
|
||||||
self._ws.send_request("send_chat", {"message": msg})
|
self._ws.send_request("send_chat", {"message": msg})
|
||||||
else:
|
else:
|
||||||
self._ws.send_request("send_chat", {"message": "My inventory is empty."})
|
self._ws.send_request("send_chat", {"message": "My inventory is empty."})
|
||||||
self._tasks.complete()
|
self._tasks.complete_current()
|
||||||
else:
|
return
|
||||||
self._ws.send_request("send_chat", {"message": "Can't check inventory right now."})
|
|
||||||
self._tasks.cancel()
|
|
||||||
else:
|
|
||||||
self._tasks.complete()
|
|
||||||
|
|
||||||
self._ws.send_request(task.action, task.params, on_response)
|
# Movement actions wait for movement_complete event
|
||||||
|
if subtask.action in ("move_to", "move_relative", "follow_player"):
|
||||||
|
return # Don't complete yet — wait for event
|
||||||
|
|
||||||
|
# Everything else completes immediately
|
||||||
|
self._tasks.complete_current()
|
||||||
|
|
||||||
|
else:
|
||||||
|
error = resp.error or "Something went wrong"
|
||||||
|
log.debug(f"Action failed: {error}")
|
||||||
|
# Report HIGH priority failures to chat
|
||||||
|
current = self._tasks.current_task
|
||||||
|
if current and current.priority >= Priority.HIGH:
|
||||||
|
self._ws.send_request("send_chat", {"message": error})
|
||||||
|
self._tasks.fail_current()
|
||||||
|
|
||||||
|
self._ws.send_request(subtask.action, subtask.params, on_response)
|
||||||
|
|
||||||
# ── Helpers ──
|
# ── Helpers ──
|
||||||
|
|
||||||
def _handle_idle_chat(self, task: Task):
|
|
||||||
"""Handle unprompted chat."""
|
|
||||||
context = self._build_chat_context()
|
|
||||||
self.wants_ai_chat.emit(
|
|
||||||
context,
|
|
||||||
"Say something to the players nearby. Keep it natural and short."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_chat_context(self) -> str:
|
def _build_chat_context(self) -> str:
|
||||||
"""Build a context string describing what's happening."""
|
|
||||||
parts = []
|
parts = []
|
||||||
b = self._behaviors
|
b = self._behaviors
|
||||||
if b.nearby_players:
|
if b.nearby_players:
|
||||||
|
|
@ -723,19 +474,19 @@ class DougBrain(QObject):
|
||||||
types = [h["type"] for h in b.nearby_hostiles[:3]]
|
types = [h["type"] for h in b.nearby_hostiles[:3]]
|
||||||
parts.append(f"Nearby mobs: {', '.join(types)}")
|
parts.append(f"Nearby mobs: {', '.join(types)}")
|
||||||
|
|
||||||
# Add goal context
|
# Current task context
|
||||||
goal = self._goals.get_active_goal()
|
current = self._tasks.current_task
|
||||||
if goal:
|
if current and current.description:
|
||||||
parts.append(f"Currently working on: {goal['description']}")
|
parts.append(f"Currently: {current.description}")
|
||||||
|
|
||||||
# Add need context
|
# Stack depth
|
||||||
critical_needs = self._needs.get_critical_needs()
|
depth = self._tasks.stack_depth
|
||||||
if critical_needs:
|
if depth > 1:
|
||||||
parts.append(f"Feeling: {', '.join(critical_needs)}")
|
parts.append(f"Tasks queued: {depth}")
|
||||||
|
|
||||||
return "; ".join(parts) if parts else "Nothing special happening"
|
return "; ".join(parts) if parts else "Nothing special happening"
|
||||||
|
|
||||||
def _nearest_hostile(self, max_dist: float) -> dict | None:
|
def _nearest_hostile(self, max_dist: float):
|
||||||
closest = None
|
closest = None
|
||||||
closest_dist = max_dist
|
closest_dist = max_dist
|
||||||
for h in self._behaviors.nearby_hostiles:
|
for h in self._behaviors.nearby_hostiles:
|
||||||
|
|
@ -748,18 +499,16 @@ class DougBrain(QObject):
|
||||||
def _distance_to(self, pos: dict) -> float:
|
def _distance_to(self, pos: dict) -> float:
|
||||||
b = self._behaviors.position
|
b = self._behaviors.position
|
||||||
dx = b["x"] - pos.get("x", 0)
|
dx = b["x"] - pos.get("x", 0)
|
||||||
dy = b["y"] - pos.get("y", 0)
|
|
||||||
dz = b["z"] - pos.get("z", 0)
|
dz = b["z"] - pos.get("z", 0)
|
||||||
return math.sqrt(dx * dx + dy * dy + dz * dz)
|
return math.sqrt(dx * dx + dz * dz)
|
||||||
|
|
||||||
def _find_food(self) -> str | None:
|
def _find_food(self):
|
||||||
"""Find food in inventory."""
|
|
||||||
food_items = {
|
food_items = {
|
||||||
"cooked_beef", "cooked_porkchop", "cooked_chicken", "cooked_mutton",
|
"cooked_beef", "cooked_porkchop", "cooked_chicken", "cooked_mutton",
|
||||||
"cooked_rabbit", "cooked_salmon", "cooked_cod", "bread", "apple",
|
"cooked_rabbit", "cooked_salmon", "cooked_cod", "bread", "apple",
|
||||||
"golden_apple", "melon_slice", "sweet_berries", "baked_potato",
|
"golden_apple", "melon_slice", "sweet_berries", "baked_potato",
|
||||||
"mushroom_stew", "beetroot_soup", "rabbit_stew", "cookie",
|
"mushroom_stew", "rabbit_stew", "cookie", "pumpkin_pie",
|
||||||
"pumpkin_pie", "cake", "dried_kelp", "carrot", "potato",
|
"carrot", "potato", "dried_kelp",
|
||||||
}
|
}
|
||||||
for item in self._behaviors.inventory:
|
for item in self._behaviors.inventory:
|
||||||
if item.get("name", "").replace("minecraft:", "") in food_items:
|
if item.get("name", "").replace("minecraft:", "") in food_items:
|
||||||
|
|
@ -767,22 +516,17 @@ class DougBrain(QObject):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_sheltered(self) -> bool:
|
def _is_sheltered(self) -> bool:
|
||||||
"""Check if near home/shelter."""
|
|
||||||
home = self._memory.home
|
home = self._memory.home
|
||||||
if not home:
|
if not home:
|
||||||
return False
|
return False
|
||||||
return self._distance_to(home) < 15
|
return self._distance_to(home) < 15
|
||||||
|
|
||||||
# ── Public interface (same as before) ──
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_action(self) -> str:
|
def current_action(self) -> str:
|
||||||
task = self._tasks.current_task
|
task = self._tasks.current_task
|
||||||
if task:
|
if task:
|
||||||
return task.description or task.name
|
st = task.current_subtask()
|
||||||
goal = self._goals.get_active_goal()
|
return st.description if st and st.description else task.description
|
||||||
if goal:
|
|
||||||
return goal["description"]
|
|
||||||
return "idle"
|
return "idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from dougbot.core.task_queue import Task, Priority
|
from dougbot.core.task_queue import PrimaryTask, Priority, make_task
|
||||||
from dougbot.utils.logging import get_logger
|
from dougbot.utils.logging import get_logger
|
||||||
|
|
||||||
log = get_logger("core.commands")
|
log = get_logger("core.commands")
|
||||||
|
|
@ -297,129 +297,87 @@ class CommandParser:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def command_to_task(cmd: ParsedCommand, behaviors) -> Optional[Task]:
|
def command_to_task(cmd: ParsedCommand, behaviors) -> Optional[PrimaryTask]:
|
||||||
"""Convert a parsed command into a Task for the queue."""
|
"""Convert a parsed command into a PrimaryTask for the stack."""
|
||||||
|
|
||||||
if cmd.action == "follow_player":
|
if cmd.action == "follow_player":
|
||||||
return Task(
|
task = make_task(
|
||||||
name=f"follow_{cmd.target}",
|
f"follow_{cmd.target}", Priority.HIGH, f"Following {cmd.target}",
|
||||||
priority=Priority.HIGH,
|
"follow_player", {"name": cmd.target, "range": 3}, timeout=60,
|
||||||
action="follow_player",
|
source="player", open_ended=True,
|
||||||
params={"name": cmd.target, "range": 3},
|
|
||||||
description=f"Following {cmd.target}",
|
|
||||||
timeout=60,
|
|
||||||
)
|
)
|
||||||
|
return task
|
||||||
|
|
||||||
elif cmd.action == "stop":
|
elif cmd.action == "stop":
|
||||||
return Task(
|
return make_task(
|
||||||
name="stop",
|
"stop", Priority.HIGH, "Stopping",
|
||||||
priority=Priority.HIGH,
|
"stop", {}, timeout=5, source="player",
|
||||||
action="stop",
|
|
||||||
params={},
|
|
||||||
description="Stopping",
|
|
||||||
timeout=5,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "check_chest":
|
elif cmd.action == "check_chest":
|
||||||
# Find nearest container
|
|
||||||
if behaviors.nearby_containers:
|
if behaviors.nearby_containers:
|
||||||
container = behaviors.nearby_containers[0]
|
container = behaviors.nearby_containers[0]
|
||||||
return Task(
|
return make_task(
|
||||||
name="check_chest",
|
"check_chest", Priority.HIGH, "Checking the chest",
|
||||||
priority=Priority.HIGH,
|
"open_chest", container["position"], timeout=15, source="player",
|
||||||
action="open_chest",
|
|
||||||
params=container["position"],
|
|
||||||
description="Checking the chest",
|
|
||||||
timeout=15,
|
|
||||||
callback="on_container_opened",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "sort_chest":
|
elif cmd.action == "sort_chest":
|
||||||
if behaviors.nearby_containers:
|
if behaviors.nearby_containers:
|
||||||
container = behaviors.nearby_containers[0]
|
container = behaviors.nearby_containers[0]
|
||||||
return Task(
|
return make_task(
|
||||||
name="sort_chest",
|
"sort_chest", Priority.HIGH, "Sorting the chest",
|
||||||
priority=Priority.HIGH,
|
"open_chest", container["position"], timeout=30, source="player",
|
||||||
action="open_chest",
|
|
||||||
params=container["position"],
|
|
||||||
description="Sorting the chest",
|
|
||||||
timeout=30,
|
|
||||||
callback="on_sort_container",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "craft":
|
elif cmd.action == "craft":
|
||||||
count = cmd.params.get("count", 1) if cmd.params else 1
|
count = cmd.params.get("count", 1) if cmd.params else 1
|
||||||
return Task(
|
return make_task(
|
||||||
name=f"craft_{cmd.target}",
|
f"craft_{cmd.target}", Priority.HIGH,
|
||||||
priority=Priority.HIGH,
|
f"Crafting {count}x {cmd.target.replace('_', ' ')}",
|
||||||
action="craft_item",
|
"craft_item", {"itemName": cmd.target, "count": count},
|
||||||
params={"itemName": cmd.target, "count": count},
|
timeout=30, source="player",
|
||||||
description=f"Crafting {count}x {cmd.target.replace('_', ' ')}",
|
|
||||||
timeout=30,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "mine":
|
elif cmd.action == "mine":
|
||||||
return Task(
|
return make_task(
|
||||||
name=f"mine_{cmd.target}",
|
f"mine_{cmd.target}", Priority.HIGH,
|
||||||
priority=Priority.HIGH,
|
f"Looking for {cmd.target.replace('_', ' ')} to mine",
|
||||||
action="find_blocks",
|
"find_blocks", {"blockName": cmd.target, "radius": 16, "count": 1},
|
||||||
params={"blockName": cmd.target, "radius": 16, "count": 1},
|
timeout=30, source="player",
|
||||||
description=f"Looking for {cmd.target.replace('_', ' ')} to mine",
|
|
||||||
timeout=30,
|
|
||||||
callback="on_found_blocks_to_mine",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "attack":
|
elif cmd.action == "attack":
|
||||||
return Task(
|
return make_task(
|
||||||
name="attack_command",
|
"attack_command", Priority.HIGH, "Attacking!",
|
||||||
priority=Priority.HIGH,
|
"attack_nearest_hostile", {"range": 8}, timeout=10, source="player",
|
||||||
action="attack_nearest_hostile",
|
|
||||||
params={"range": 8},
|
|
||||||
description=f"Attacking!",
|
|
||||||
timeout=10,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "look_around":
|
elif cmd.action == "look_around":
|
||||||
return Task(
|
return make_task(
|
||||||
name="look_around_command",
|
"look_around_command", Priority.HIGH, "Looking around",
|
||||||
priority=Priority.HIGH,
|
"scan_surroundings", {"radius": 16}, timeout=10, source="player",
|
||||||
action="scan_surroundings",
|
|
||||||
params={"radius": 16},
|
|
||||||
description="Looking around",
|
|
||||||
timeout=10,
|
|
||||||
callback="on_look_around_report",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "equip":
|
elif cmd.action == "equip":
|
||||||
return Task(
|
return make_task(
|
||||||
name=f"equip_{cmd.target}",
|
f"equip_{cmd.target}", Priority.HIGH,
|
||||||
priority=Priority.HIGH,
|
f"Equipping {cmd.target.replace('_', ' ')}",
|
||||||
action="equip_item",
|
"equip_item", {"name": cmd.target, "destination": "hand"},
|
||||||
params={"name": cmd.target, "destination": "hand"},
|
timeout=10, source="player",
|
||||||
description=f"Equipping {cmd.target.replace('_', ' ')}",
|
|
||||||
timeout=10,
|
|
||||||
callback="on_equip_result",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "check_inventory":
|
elif cmd.action == "check_inventory":
|
||||||
return Task(
|
return make_task(
|
||||||
name="check_inventory",
|
"check_inventory", Priority.HIGH, "Checking inventory",
|
||||||
priority=Priority.HIGH,
|
"get_inventory", {}, timeout=10, source="player",
|
||||||
action="get_inventory",
|
|
||||||
params={},
|
|
||||||
description="Checking inventory",
|
|
||||||
timeout=10,
|
|
||||||
callback="on_inventory_report",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif cmd.action == "go_to":
|
elif cmd.action == "go_to":
|
||||||
return Task(
|
return make_task(
|
||||||
name=f"go_to_{cmd.target}",
|
f"go_to_{cmd.target}", Priority.HIGH, f"Going to {cmd.target}",
|
||||||
priority=Priority.HIGH,
|
"follow_player", {"name": cmd.target, "range": 3},
|
||||||
action="follow_player",
|
timeout=30, source="player",
|
||||||
params={"name": cmd.target, "range": 3},
|
|
||||||
description=f"Going to {cmd.target}",
|
|
||||||
timeout=30,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,7 @@ class MainWindow(QMainWindow):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Add task to brain's queue — task runs first, then Doug responds
|
# Add task to brain's queue — task runs first, then Doug responds
|
||||||
self._brain._tasks.add(task)
|
self._brain._tasks.push(task)
|
||||||
log.info(f"Command from {sender}: {cmd.action} → task '{task.name}'")
|
log.info(f"Command from {sender}: {cmd.action} → task '{task.name}'")
|
||||||
|
|
||||||
# Short acknowledgment ONLY — no AI call, just a quick "on it" type response
|
# Short acknowledgment ONLY — no AI call, just a quick "on it" type response
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue