update bot

This commit is contained in:
2026-01-06 08:55:14 +08:00
parent cbbe95e108
commit 059f152374
12 changed files with 353 additions and 148 deletions
+158 -116
View File
@@ -5,8 +5,8 @@ class_name BotController
# Handles all bot decision-making: movement, grabbing, putting, arranging, and sabotage
# Configuration
@export var tick_rate: int = 60 # Ticks between AI updates (in frames)
@export var action_delay: float = 0.5 # Delay between actions
@export var tick_rate: int = 12 # Ticks between AI updates (in frames)
@export var action_delay: float = 0.15 # Delay between actions
# References
var actor: Node3D # The player character this controller is attached to
@@ -17,6 +17,8 @@ var strategic_planner: RefCounted
var _tick_counter: int = 0
var _is_processing_action: bool = false
var _current_action: String = "idle"
var _stuck_timer: float = 0.0
var rng = RandomNumberGenerator.new()
# Tile constants
const GOAL_TILES = [7, 8, 9, 10]
@@ -27,29 +29,30 @@ func _ready():
if Engine.is_editor_hint():
return
# Desync bots by randomizing their tick counter start
rng.seed = name.hash()
_tick_counter = rng.randi() % tick_rate
# Get parent (should be player character)
actor = get_parent()
# ... (rest of _ready) ...
if not actor:
push_error("[BotController] No parent node found")
queue_free()
return
# Only run for bots
# DEBUG: Print exact state of checks
# print("[BotController] Checking if %s is bot. in_group(Bots): %s, is_bot: %s" % [actor.name, actor.is_in_group("Bots"), actor.get("is_bot")])
if not actor.is_in_group("Bots") and not actor.get("is_bot"):
# print("[BotController] Actor is not a bot, removing controller.")
queue_free()
return
# Wait for actor to be fully ready
await get_tree().create_timer(1.0).timeout # Increased wait time to 1.0s to be safe
await get_tree().create_timer(1.0).timeout
enhanced_gridmap = actor.enhanced_gridmap
if not enhanced_gridmap:
push_error("[BotController] EnhancedGridMap not found for " + actor.name)
return # Don't crash, just stop
return
# Initialize strategic planner
var BotStrategicPlanner = load("res://scripts/bot_strategic_planner.gd")
@@ -67,14 +70,24 @@ func _exit_tree():
actor = null
enhanced_gridmap = null
func _physics_process(_delta):
func _physics_process(delta):
if not is_instance_valid(actor) or not strategic_planner:
return
# Only run on server/authority (Authority 1)
# NOTE: If we are not the server, we should not run logic
if not multiplayer.is_server():
return
# STUCK PREVENTION
if actor.is_player_moving:
_stuck_timer += delta
if _stuck_timer > 2.0:
print("[BotController] %s stuck in moving state! Force resetting." % actor.name)
actor.is_player_moving = false
_stuck_timer = 0.0
return
else:
_stuck_timer = 0.0
# Rate limiting
_tick_counter += 1
@@ -87,22 +100,39 @@ func _physics_process(_delta):
return
# Run AI decision loop
print("[BotController] Running AI Tick for ", actor.name)
# print("[BotController] Running AI Tick for ", actor.name)
_run_ai_tick()
func _run_ai_tick():
"""Main AI decision loop - replaces Beehave behavior tree."""
if not is_instance_valid(actor) or _is_processing_action:
return
# Don't make new decisions while moving
if actor.is_player_moving:
return
print("[BotController] AI Tick: evaluating priorities...")
# Evaluate board status
var board_fullness = _get_board_fullness_ratio()
var full_board_priority_mode = board_fullness > 0.6
# PRIORITY OVERRIDE: If board is getting full, prioritize clearing space!
if full_board_priority_mode:
print("[BotController] Board fullness %.2f > 0.6! Prioritizing PUT." % board_fullness)
if await _try_put(true): # Pass true to indicate high priority/panic check
print("[BotController] Action Taken: Put (Priority)")
return
# Priority 1: Use power-up sabotage if conditions are met
if await _try_use_powerup():
print("[BotController] Action Taken: PowerUp")
return
# Priority 2: Grab tiles (goal tiles or holo tiles)
# ONLY if not in critical full mode (unless it's a critical needed tile?)
# Refined: If > 80% full, disable grabbing completely unless strict conditions met inner function
if await _try_grab():
print("[BotController] Action Taken: Grab")
return
@@ -112,10 +142,11 @@ func _run_ai_tick():
print("[BotController] Action Taken: Move")
return
# Priority 4: Put tiles back on grid (rarely needed)
if await _try_put():
print("[BotController] Action Taken: Put")
return
# Priority 4: Put tiles back on grid (Standard priority)
if not full_board_priority_mode:
if await _try_put():
print("[BotController] Action Taken: Put")
return
# Priority 5: Arrange playerboard
if await _try_arrange():
@@ -150,7 +181,7 @@ func _try_use_powerup() -> bool:
if main and main.has_method("broadcast_message"):
main.rpc("broadcast_message", actor.display_name, "Used a special power!")
await get_tree().create_timer(action_delay).timeout
await _wait_with_variance(action_delay)
if not is_instance_valid(self): return true # Early exit if deleted
_is_processing_action = false
@@ -162,15 +193,26 @@ func _try_use_powerup() -> bool:
# =============================================================================
func _try_grab() -> bool:
"""Try to grab a tile from the grid."""
"""Try to grab a tile from the grid using direct player control."""
# Check AP only if turn-based
if TurnManager.turn_based_mode and actor.action_points <= 0:
return false
# PANIC MODE CHECK
var main = get_tree().get_root().get_node_or_null("Main")
if main:
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
if goals_cycle_manager and goals_cycle_manager.get_time_remaining() < 5.0:
return false
if _is_playerboard_full():
# print("[BotController] Grab failed: Board full")
return false
# Check fullness - if >80% full, be picky
var empty_slots = actor.playerboard.count(-1)
if empty_slots <= 2:
pass
# Check if goals already achieved
if _is_goals_achieved():
return false
@@ -178,54 +220,27 @@ func _try_grab() -> bool:
# Get tiles we need
var tiles_needed = strategic_planner.get_tiles_needed()
# Check current position and adjacent cells for tiles
# Find tile to grab
var grab_info = _find_tile_to_grab(tiles_needed)
if not grab_info.position:
# print("[BotController] Grab failed: No valid tile found in range")
return false
# Execute grab
_is_processing_action = true
_current_action = "grabbing"
# Execute grab using PLAYER method (mimic human input)
var success = actor.grab_item(grab_info.position)
var cell = Vector3i(grab_info.position.x, 1, grab_info.position.y)
var item = enhanced_gridmap.get_cell_item(cell)
# Handle holo tiles (power-up)
if item in HOLO_TILES:
if actor.is_multiplayer_authority():
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
var powerup_manager = actor.get_node_or_null("PowerUpManager")
if powerup_manager:
powerup_manager.add_holo_pickup()
# Only consume AP in turn-based mode
if TurnManager.turn_based_mode:
actor.action_points -= 1
print("[BotController] %s collected holo tile!" % actor.name)
else:
# Regular tile - place in playerboard
var target_slot = strategic_planner.find_best_slot_for_tile(item)
if target_slot != -1 and actor.is_multiplayer_authority():
actor.playerboard[target_slot] = item
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
actor.rpc("sync_playerboard", actor.playerboard)
if TurnManager.turn_based_mode:
actor.action_points -= 1
print("[BotController] %s grabbed tile %d -> slot %d" % [actor.name, item, target_slot])
# Check goal completion
if _is_goals_achieved():
_handle_goal_completion()
await get_tree().create_timer(action_delay).timeout
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
return true
if success:
_is_processing_action = true
_current_action = "grabbing"
print("[BotController] %s grabbed tile via player input!" % actor.name)
# Wait for animation
await _wait_with_variance(action_delay)
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
return true
return false
func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
"""Find best tile to grab from current or adjacent positions."""
@@ -256,16 +271,6 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
if item in HOLO_TILES and not result.position:
result = {"position": neighbor.position, "type": item}
# Third pass: any goal tile
if not result.position:
for neighbor in neighbors:
if not neighbor.is_walkable:
continue
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
item = enhanced_gridmap.get_cell_item(cell)
if item in actor.goals and item != -1:
return {"position": neighbor.position, "type": item}
return result
# =============================================================================
@@ -273,7 +278,7 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
# =============================================================================
func _try_move() -> bool:
"""Try to move toward needed tiles."""
"""Try to move toward needed tiles taking single steps like a player."""
if TurnManager.turn_based_mode and actor.action_points <= 0:
return false
@@ -281,59 +286,82 @@ func _try_move() -> bool:
return false
# Find optimal movement target
var target_pos = strategic_planner.find_optimal_move_target()
var final_target = strategic_planner.find_optimal_move_target()
if target_pos == Vector2i(-1, -1) or target_pos == actor.current_position:
print("[BotController] Move failed: No valid target or already at target. Pos: %s" % actor.current_position)
if final_target == Vector2i(-1, -1) or final_target == actor.current_position:
return false
# Calculate path to target
var path = enhanced_gridmap.find_path(
Vector2(actor.current_position),
Vector2(final_target),
0,
false
)
# Check if within movement range
if not actor.is_within_movement_range(target_pos):
print("[BotController] Move failed: Target %s out of range" % target_pos)
var next_step = Vector2i(-1, -1)
# Need at least current pos + next step
if path.size() >= 2:
# Extract immediate next step from path
next_step = Vector2i(path[1].x, path[1].y)
else:
# Fallback: Pathfinding failed or target is too close?
# Check if target is adjacent and we can move directly
var dist = abs(final_target.x - actor.current_position.x) + abs(final_target.y - actor.current_position.y)
if dist == 1:
next_step = final_target
else:
return false
# Redundant safety check (simple_move_to also checks this)
if actor.is_position_occupied(next_step):
return false
# Execute movement
_is_processing_action = true
_current_action = "moving"
if actor.is_multiplayer_authority():
var path = enhanced_gridmap.find_path(
Vector2(actor.current_position),
Vector2(target_pos),
0,
false
)
if path.size() > 1:
path.pop_front()
actor.rpc("start_movement_along_path", path, false)
if TurnManager.turn_based_mode:
actor.action_points -= 1
var tiles_needed = strategic_planner.get_tiles_needed()
print("[BotController] %s moving toward tiles %s (Target: %s)" % [actor.name, tiles_needed, target_pos])
await get_tree().create_timer(action_delay * 2).timeout # Movement takes longer
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
return true
# Execute SINGLE STEP movement using player manager
if actor.movement_manager.simple_move_to(next_step):
_is_processing_action = true
_current_action = "moving"
# Wait for movement tween (approx 0.25s) plus small delay
await get_tree().create_timer(0.3).timeout
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
return true
return false
# =============================================================================
# Put Tiles Back
# =============================================================================
func _try_put() -> bool:
func _try_put(high_priority: bool = false) -> bool:
"""Try to put a tile from playerboard onto grid."""
if actor.action_points <= 0:
return false
# Find a tile in playerboard that we could put
var put_slot = -1
for i in range(actor.playerboard.size()):
if actor.playerboard[i] in GOAL_TILES:
put_slot = i
break
# Check for Panic Mode
var is_panic = false
var main = get_tree().get_root().get_node_or_null("Main")
if main:
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
if goals_cycle_manager and goals_cycle_manager.get_time_remaining() < 5.0:
is_panic = true
# Also panic if board is critically full (>80%) or high priority flag set
if _get_board_fullness_ratio() > 0.8 or high_priority:
is_panic = true
if is_panic:
# Aggressive dumping
put_slot = strategic_planner.get_unneeded_tile_slot_panic()
else:
# Normal smart dumping
put_slot = strategic_planner.get_unneeded_tile_slot()
if put_slot == -1:
return false
@@ -350,13 +378,13 @@ func _try_put() -> bool:
if actor.is_multiplayer_authority():
var item = actor.playerboard[put_slot]
var cell = Vector3i(put_position.x, 1, put_position.y)
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
actor.playerboard[put_slot] = -1
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
actor.rpc("sync_playerboard", actor.playerboard)
actor.action_points -= 1
print("[BotController] %s put tile %d at %s" % [actor.name, item, put_position])
print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic])
await get_tree().create_timer(action_delay).timeout
await _wait_with_variance(action_delay)
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
@@ -407,7 +435,7 @@ func _try_arrange() -> bool:
actor.action_points -= 2
print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot])
await get_tree().create_timer(action_delay).timeout
await _wait_with_variance(action_delay)
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
@@ -444,6 +472,14 @@ func _is_playerboard_full() -> bool:
return false
return true
func _get_board_fullness_ratio() -> float:
"""Returns ratio of occupied slots (0.0 to 1.0)."""
var occupied = 0
for item in actor.playerboard:
if item != -1:
occupied += 1
return float(occupied) / float(actor.playerboard.size())
func _is_goals_achieved() -> bool:
"""Check if goal pattern is complete in any 3x3 region of playerboard."""
var goals_2d = []
@@ -492,4 +528,10 @@ func _handle_goal_completion():
if powerup_manager:
powerup_manager.add_goal_completion_reward()
print("[BotController] %s COMPLETED GOAL!" % actor.name)
func _wait_with_variance(base_delay: float):
var variance = rng.randf_range(-0.05, 0.05)
var final_delay = max(0.05, base_delay + variance)
await get_tree().create_timer(final_delay).timeout