overhaul bot
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
extends ActionLeaf
|
||||
|
||||
func find_best_arrangement(actor) -> Dictionary:
|
||||
# Convert goals to 2D array for easier comparison
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
for j in range(3):
|
||||
row.append(actor.goals[i * 3 + j])
|
||||
goals_2d.append(row)
|
||||
|
||||
# Convert playerboard to 2D
|
||||
var board_2d = []
|
||||
for i in range(5):
|
||||
var row = []
|
||||
for j in range(5):
|
||||
row.append(actor.playerboard[i * 5 + j])
|
||||
board_2d.append(row)
|
||||
|
||||
# Find misplaced items
|
||||
for i in range(1, 4): # Check central 3x3 area
|
||||
for j in range(1, 4):
|
||||
var board_idx = i * 5 + j
|
||||
var goal_idx = (i - 1) * 3 + (j - 1)
|
||||
|
||||
var current_item = actor.playerboard[board_idx]
|
||||
var goal_item = actor.goals[goal_idx]
|
||||
|
||||
if current_item != goal_item and current_item != -1:
|
||||
# Find better position for this item
|
||||
for gi in range(3):
|
||||
for gj in range(3):
|
||||
if actor.goals[gi * 3 + gj] == current_item:
|
||||
var target_slot = (gi + 1) * 5 + (gj + 1)
|
||||
if actor.playerboard[target_slot] == -1:
|
||||
return {
|
||||
"source_slot": board_idx,
|
||||
"target_slot": target_slot
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
# Check only central 3x3 area of playerboard against goals
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
var goal_idx = i * 3 + j
|
||||
|
||||
if actor.goals[goal_idx] != -1 and actor.goals[goal_idx] != actor.playerboard[board_idx]:
|
||||
return false
|
||||
elif actor.goals[goal_idx] == -1 and actor.playerboard[board_idx] != -1:
|
||||
return false
|
||||
|
||||
# Also check outside the goal area
|
||||
if i == 0 or i == 4 or j == 0 or j == 4:
|
||||
if actor.playerboard[i * 5 + j] != -1:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# First check if goals are already achieved
|
||||
if is_goals_achieved(actor):
|
||||
blackboard.set_value("current_action", "idle")
|
||||
return FAILURE
|
||||
|
||||
if actor.action_points < 2:
|
||||
return FAILURE
|
||||
|
||||
var arrangement = find_best_arrangement(actor)
|
||||
if arrangement.is_empty():
|
||||
return FAILURE
|
||||
|
||||
blackboard.set_value("source_slot", arrangement.source_slot)
|
||||
blackboard.set_value("target_slot", arrangement.target_slot)
|
||||
|
||||
# Do the arrangement
|
||||
if actor.is_multiplayer_authority():
|
||||
var item = actor.playerboard[arrangement.source_slot]
|
||||
actor.playerboard[arrangement.target_slot] = item
|
||||
actor.playerboard[arrangement.source_slot] = -1
|
||||
actor.rpc("sync_playerboard", actor.playerboard)
|
||||
actor.has_performed_action = true
|
||||
actor.action_points -= 2
|
||||
|
||||
return SUCCESS
|
||||
@@ -1 +0,0 @@
|
||||
uid://b4fxorcb1yq17
|
||||
@@ -1,110 +0,0 @@
|
||||
extends ActionLeaf
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
# Convert goals to 2D for easier pattern matching
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
for j in range(3):
|
||||
row.append(actor.goals[i * 3 + j])
|
||||
goals_2d.append(row)
|
||||
|
||||
# Convert playerboard to 2D
|
||||
var board_2d = []
|
||||
for i in range(5):
|
||||
var row = []
|
||||
for j in range(5):
|
||||
row.append(actor.playerboard[i * 5 + j])
|
||||
board_2d.append(row)
|
||||
|
||||
# Check every possible 3x3 region in the 5x5 board
|
||||
for start_row in range(3): # 5-3+1 possible start rows
|
||||
for start_col in range(3): # 5-3+1 possible start columns
|
||||
var matches = true
|
||||
|
||||
# Check if this 3x3 region matches the goals
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_item = board_2d[start_row + i][start_col + j]
|
||||
var goal_item = goals_2d[i][j]
|
||||
|
||||
if goal_item != -1 and goal_item != board_item:
|
||||
matches = false
|
||||
break
|
||||
elif goal_item == -1 and board_item != -1:
|
||||
matches = false
|
||||
break
|
||||
|
||||
if not matches:
|
||||
break
|
||||
|
||||
if matches:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func find_target_slot(actor, item: int) -> int:
|
||||
# Get the goals in 2D format (3x3)
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
for j in range(3):
|
||||
row.append(actor.goals[i * 3 + j])
|
||||
goals_2d.append(row)
|
||||
|
||||
# Convert playerboard to 2D (5x5)
|
||||
var board_2d = []
|
||||
for i in range(5):
|
||||
var row = []
|
||||
for j in range(5):
|
||||
row.append(actor.playerboard[i * 5 + j])
|
||||
board_2d.append(row)
|
||||
|
||||
# Find matching position in goals
|
||||
var target_positions = []
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
if goals_2d[i][j] == item:
|
||||
# Convert to playerboard position (centered in 5x5 grid)
|
||||
target_positions.append(Vector2i(j + 1, i + 1))
|
||||
|
||||
# Find best empty slot that matches a goal position
|
||||
for pos in target_positions:
|
||||
var slot_index = (pos.y + 1) * 5 + (pos.x + 1)
|
||||
if actor.playerboard[slot_index] == -1:
|
||||
return slot_index
|
||||
|
||||
# If no matching position found, find any empty slot
|
||||
return actor.playerboard.find(-1)
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# Early validation
|
||||
if not actor.is_bot or actor.action_points <= 0:
|
||||
return FAILURE
|
||||
|
||||
var grab_position = blackboard.get_value("grab_position")
|
||||
if not grab_position:
|
||||
return FAILURE
|
||||
|
||||
# Check if there's an item
|
||||
var cell = Vector3i(grab_position.x, 1, grab_position.y)
|
||||
var item = actor.enhanced_gridmap.get_cell_item(cell)
|
||||
if item == -1:
|
||||
return FAILURE
|
||||
|
||||
# Find strategic slot
|
||||
var target_slot = find_target_slot(actor, item)
|
||||
if target_slot == -1:
|
||||
return FAILURE
|
||||
|
||||
# Execute grab
|
||||
if 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)
|
||||
actor.has_performed_action = true
|
||||
actor.action_points -= 1
|
||||
blackboard.set_value("current_action", "idle")
|
||||
actor._after_action_completed() # Add this line
|
||||
|
||||
return SUCCESS
|
||||
@@ -1 +0,0 @@
|
||||
uid://cuyorbwefmh0y
|
||||
@@ -1,46 +0,0 @@
|
||||
extends ActionLeaf
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
var goal_idx = i * 3 + j
|
||||
if actor.goals[goal_idx] != -1 and actor.goals[goal_idx] != actor.playerboard[board_idx]:
|
||||
return false
|
||||
elif actor.goals[goal_idx] == -1 and actor.playerboard[board_idx] != -1:
|
||||
return false
|
||||
return true
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# Don't move if goals are achieved
|
||||
if is_goals_achieved(actor):
|
||||
return FAILURE
|
||||
|
||||
# Get target from blackboard
|
||||
var target_pos = blackboard.get_value("move_target")
|
||||
if not target_pos:
|
||||
return FAILURE
|
||||
|
||||
if actor.action_points <= 0:
|
||||
return FAILURE
|
||||
|
||||
if not actor.is_bot == true and not actor.is_in_group("Bots"):
|
||||
return FAILURE
|
||||
|
||||
# Execute movement
|
||||
if actor.is_within_movement_range(target_pos):
|
||||
if actor.is_bot == true:
|
||||
var path = actor.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)
|
||||
actor.action_points -= 1
|
||||
blackboard.set_value("current_action", "moving")
|
||||
return SUCCESS
|
||||
|
||||
return FAILURE
|
||||
@@ -1 +0,0 @@
|
||||
uid://cireifbxafgf2
|
||||
@@ -1,47 +0,0 @@
|
||||
extends ActionLeaf
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
# Check only central 3x3 area of playerboard against goals
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
var goal_idx = i * 3 + j
|
||||
|
||||
if actor.goals[goal_idx] != -1 and actor.goals[goal_idx] != actor.playerboard[board_idx]:
|
||||
return false
|
||||
elif actor.goals[goal_idx] == -1 and actor.playerboard[board_idx] != -1:
|
||||
return false
|
||||
|
||||
# Also check outside the goal area
|
||||
if i == 0 or i == 4 or j == 0 or j == 4:
|
||||
if actor.playerboard[i * 5 + j] != -1:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
var put_position = blackboard.get_value("put_position")
|
||||
var put_slot = blackboard.get_value("put_slot")
|
||||
|
||||
if not put_position or put_slot == -1:
|
||||
return FAILURE
|
||||
|
||||
# Check if we still have the item and AP
|
||||
if actor.action_points <= 0 or actor.playerboard[put_slot] == -1:
|
||||
return FAILURE
|
||||
|
||||
# Check if target position is still empty
|
||||
var cell = Vector3i(put_position.x, 1, put_position.y)
|
||||
if actor.enhanced_gridmap.get_cell_item(cell) != -1:
|
||||
return FAILURE
|
||||
|
||||
# Execute put
|
||||
var item = actor.playerboard[put_slot]
|
||||
if actor.is_multiplayer_authority():
|
||||
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
|
||||
actor.playerboard[put_slot] = -1
|
||||
actor.rpc("sync_playerboard", actor.playerboard)
|
||||
actor.has_performed_action = true
|
||||
actor.action_points -= 1
|
||||
|
||||
return SUCCESS
|
||||
@@ -1 +0,0 @@
|
||||
uid://bdw5bwmr32h63
|
||||
@@ -1,16 +0,0 @@
|
||||
extends ConditionLeaf
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
if actor.action_points < 2:
|
||||
return FAILURE
|
||||
|
||||
# Look for items that match goals
|
||||
for i in range(actor.playerboard.size()):
|
||||
if actor.playerboard[i] in actor.goals:
|
||||
var neighbors = actor.get_adjacent_playerboard_slots(i)
|
||||
for adj_slot in neighbors:
|
||||
if actor.playerboard[adj_slot] == -1:
|
||||
blackboard.set_value("source_slot", i)
|
||||
blackboard.set_value("target_slot", adj_slot)
|
||||
return SUCCESS
|
||||
return FAILURE
|
||||
@@ -1 +0,0 @@
|
||||
uid://b25qg75d0xgkh
|
||||
@@ -1,68 +0,0 @@
|
||||
extends ConditionLeaf
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
# Convert goals to 2D for easier pattern matching
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
for j in range(3):
|
||||
row.append(actor.goals[i * 3 + j])
|
||||
goals_2d.append(row)
|
||||
|
||||
# Convert playerboard to 2D
|
||||
var board_2d = []
|
||||
for i in range(5):
|
||||
var row = []
|
||||
for j in range(5):
|
||||
row.append(actor.playerboard[i * 5 + j])
|
||||
board_2d.append(row)
|
||||
|
||||
# Check every possible 3x3 region in the 5x5 board
|
||||
for start_row in range(3):
|
||||
for start_col in range(3):
|
||||
var matches = true
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_item = board_2d[start_row + i][start_col + j]
|
||||
var goal_item = goals_2d[i][j]
|
||||
|
||||
if goal_item != -1 and goal_item != board_item:
|
||||
matches = false
|
||||
break
|
||||
elif goal_item == -1 and board_item != -1:
|
||||
matches = false
|
||||
break
|
||||
if not matches:
|
||||
break
|
||||
if matches:
|
||||
return true
|
||||
return false
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# First check if goals are achieved
|
||||
if is_goals_achieved(actor):
|
||||
blackboard.set_value("current_action", "idle")
|
||||
return FAILURE
|
||||
|
||||
if actor.playerboard_is_full():
|
||||
return FAILURE
|
||||
|
||||
# Check current position
|
||||
var current_cell = Vector3i(actor.current_position.x, 1, actor.current_position.y)
|
||||
var item = actor.enhanced_gridmap.get_cell_item(current_cell)
|
||||
if item in actor.goals:
|
||||
blackboard.set_value("grab_position", actor.current_position)
|
||||
return SUCCESS
|
||||
|
||||
# Check adjacent cells
|
||||
var neighbors = actor.enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
for neighbor in neighbors:
|
||||
if not neighbor.is_walkable:
|
||||
continue
|
||||
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
||||
item = actor.enhanced_gridmap.get_cell_item(cell)
|
||||
if item in actor.goals:
|
||||
blackboard.set_value("grab_position", neighbor.position)
|
||||
return SUCCESS
|
||||
|
||||
return FAILURE
|
||||
@@ -1 +0,0 @@
|
||||
uid://cui40g7qjf1y3
|
||||
@@ -1,40 +0,0 @@
|
||||
extends ConditionLeaf
|
||||
|
||||
func is_goals_achieved(actor) -> bool:
|
||||
# ... same is_goals_achieved function as in can_grab.gd ...
|
||||
return false
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# First check if goals are achieved
|
||||
if is_goals_achieved(actor):
|
||||
blackboard.set_value("current_action", "idle")
|
||||
return FAILURE
|
||||
|
||||
# Find an item in playerboard that matches goals
|
||||
var put_slot = -1
|
||||
for i in range(actor.playerboard.size()):
|
||||
if actor.playerboard[i] in actor.goals:
|
||||
put_slot = i
|
||||
break
|
||||
|
||||
if put_slot == -1:
|
||||
return FAILURE
|
||||
|
||||
# Find empty adjacent cell
|
||||
var current_cell = Vector3i(actor.current_position.x, 1, actor.current_position.y)
|
||||
if actor.enhanced_gridmap.get_cell_item(current_cell) == -1:
|
||||
blackboard.set_value("put_position", actor.current_position)
|
||||
blackboard.set_value("put_slot", put_slot)
|
||||
return SUCCESS
|
||||
|
||||
var neighbors = actor.enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
for neighbor in neighbors:
|
||||
if not neighbor.is_walkable:
|
||||
continue
|
||||
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
||||
if actor.enhanced_gridmap.get_cell_item(cell) == -1:
|
||||
blackboard.set_value("put_position", neighbor.position)
|
||||
blackboard.set_value("put_slot", put_slot)
|
||||
return SUCCESS
|
||||
|
||||
return FAILURE
|
||||
@@ -1 +0,0 @@
|
||||
uid://b7y30e5mxygj0
|
||||
@@ -1,18 +0,0 @@
|
||||
extends ConditionLeaf
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
var main = get_tree().get_root().get_node_or_null("Main")
|
||||
if not main:
|
||||
return FAILURE
|
||||
|
||||
# Always return SUCCESS for bots in non-turn-based mode
|
||||
if actor.is_bot and not TurnManager.turn_based_mode:
|
||||
return SUCCESS
|
||||
|
||||
# Update action points in blackboard
|
||||
blackboard.set_value("action_points", actor.action_points)
|
||||
|
||||
# Check if we have enough AP
|
||||
if actor.action_points >= 1:
|
||||
return SUCCESS
|
||||
return FAILURE
|
||||
@@ -1 +0,0 @@
|
||||
uid://b17qem72laaeb
|
||||
@@ -1,27 +0,0 @@
|
||||
extends ConditionLeaf
|
||||
|
||||
func tick(actor: Node, blackboard: Blackboard) -> int:
|
||||
# Find a valid movement target
|
||||
var target_pos = find_valid_target(actor)
|
||||
if target_pos:
|
||||
# Store target in blackboard
|
||||
blackboard.set_value("move_target", target_pos)
|
||||
return SUCCESS
|
||||
return FAILURE
|
||||
|
||||
func find_valid_target(actor: Node) -> Vector2i:
|
||||
# Get random position in range
|
||||
var valid_positions = []
|
||||
|
||||
for x in range(max(0, actor.current_position.x - actor.movement_range),
|
||||
min(actor.enhanced_gridmap.columns, actor.current_position.x + actor.movement_range + 1)):
|
||||
for z in range(max(0, actor.current_position.y - actor.movement_range),
|
||||
min(actor.enhanced_gridmap.rows, actor.current_position.y + actor.movement_range + 1)):
|
||||
var pos = Vector2i(x, z)
|
||||
if pos != actor.current_position and actor.is_within_movement_range(pos):
|
||||
if not actor.is_position_occupied(pos):
|
||||
valid_positions.append(pos)
|
||||
|
||||
if valid_positions.size() > 0:
|
||||
return valid_positions[randi() % valid_positions.size()]
|
||||
return Vector2i(-1, -1)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bc7jpc1bwy4dg
|
||||
@@ -1,28 +0,0 @@
|
||||
extends BeehaveTree
|
||||
|
||||
func _ready():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Get parent node safely
|
||||
var parent = get_parent()
|
||||
if not parent:
|
||||
push_error("BehaviorTree: No parent node found")
|
||||
return
|
||||
|
||||
# Only setup for bots
|
||||
if not parent.is_in_group("Bots"):
|
||||
queue_free() # Remove tree if not a bot
|
||||
return
|
||||
|
||||
# Set this tree's actor
|
||||
actor = parent
|
||||
|
||||
enabled = parent.is_multiplayer_authority() and parent.is_bot
|
||||
|
||||
# Set up blackboard with initial values
|
||||
if blackboard:
|
||||
blackboard.set_value("action_points", parent.action_points)
|
||||
blackboard.set_value("current_action", "idle")
|
||||
blackboard.set_value("grab_position", parent.current_position) # Default grab position
|
||||
blackboard.set_value("move_target", null)
|
||||
@@ -1 +0,0 @@
|
||||
uid://6g75rh3nj2s6
|
||||
@@ -1,19 +0,0 @@
|
||||
@tool
|
||||
extends Blackboard
|
||||
|
||||
var default_data = {
|
||||
"move_target": null,
|
||||
"current_action": "",
|
||||
"action_points": 3, # Adjust this value based on your game design
|
||||
"last_position": null,
|
||||
"path": [],
|
||||
"is_moving": false
|
||||
}
|
||||
|
||||
func _ready():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
for key in default_data:
|
||||
if not has_value(key):
|
||||
set_value(key, default_data[key])
|
||||
@@ -1 +0,0 @@
|
||||
uid://d2cr28ak2s1rr
|
||||
@@ -0,0 +1,495 @@
|
||||
extends Node
|
||||
class_name BotController
|
||||
|
||||
# BotController - Standalone modular bot AI system (no Beehave dependency)
|
||||
# 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
|
||||
|
||||
# References
|
||||
var actor: Node3D # The player character this controller is attached to
|
||||
var enhanced_gridmap: Node
|
||||
var strategic_planner: RefCounted
|
||||
|
||||
# State tracking
|
||||
var _tick_counter: int = 0
|
||||
var _is_processing_action: bool = false
|
||||
var _current_action: String = "idle"
|
||||
|
||||
# Tile constants
|
||||
const GOAL_TILES = [7, 8, 9, 10]
|
||||
const HOLO_TILES = [11, 12, 13, 14]
|
||||
|
||||
func _ready():
|
||||
# print("[BotController] _ready called for parent: ", get_parent().name)
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Get parent (should be player character)
|
||||
actor = get_parent()
|
||||
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
|
||||
|
||||
enhanced_gridmap = actor.enhanced_gridmap
|
||||
if not enhanced_gridmap:
|
||||
push_error("[BotController] EnhancedGridMap not found for " + actor.name)
|
||||
return # Don't crash, just stop
|
||||
|
||||
# Initialize strategic planner
|
||||
var BotStrategicPlanner = load("res://scripts/bot_strategic_planner.gd")
|
||||
strategic_planner = BotStrategicPlanner.new(actor, enhanced_gridmap)
|
||||
|
||||
# Disable input processing for bots
|
||||
actor.set_process_input(false)
|
||||
actor.set_process_unhandled_input(false)
|
||||
|
||||
print("[BotController] SUCCESFULLY STARTED for bot: %s (Authority: %s)" % [actor.name, actor.get_multiplayer_authority()])
|
||||
|
||||
func _exit_tree():
|
||||
# Ensure explicit cleanup to assist RefCounted cycle breaking
|
||||
strategic_planner = null
|
||||
actor = null
|
||||
enhanced_gridmap = null
|
||||
|
||||
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
|
||||
|
||||
# Rate limiting
|
||||
_tick_counter += 1
|
||||
if _tick_counter < tick_rate:
|
||||
return
|
||||
_tick_counter = 0
|
||||
|
||||
# Don't process if already doing something
|
||||
if _is_processing_action:
|
||||
return
|
||||
|
||||
# Run AI decision loop
|
||||
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
|
||||
|
||||
print("[BotController] AI Tick: evaluating priorities...")
|
||||
|
||||
# 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)
|
||||
if await _try_grab():
|
||||
print("[BotController] Action Taken: Grab")
|
||||
return
|
||||
|
||||
# Priority 3: Move toward needed tiles
|
||||
if await _try_move():
|
||||
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 5: Arrange playerboard
|
||||
if await _try_arrange():
|
||||
print("[BotController] Action Taken: Arrange")
|
||||
return
|
||||
|
||||
print("[BotController] No action taken (Idle). AP: %d, GoalsAchieved: %s" % [actor.action_points, _is_goals_achieved()])
|
||||
|
||||
# =============================================================================
|
||||
# Power-Up / Sabotage
|
||||
# =============================================================================
|
||||
|
||||
func _try_use_powerup() -> bool:
|
||||
"""Check and execute power-up sabotage."""
|
||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if not powerup_manager or not powerup_manager.can_use_special():
|
||||
return false
|
||||
|
||||
# Evaluate sabotage opportunity
|
||||
var eval = strategic_planner.evaluate_sabotage_opportunity()
|
||||
if not eval.should_sabotage:
|
||||
return false
|
||||
|
||||
# Execute sabotage
|
||||
_is_processing_action = true
|
||||
_current_action = "sabotaging"
|
||||
|
||||
var success = powerup_manager.use_special_effect()
|
||||
if success:
|
||||
print("[BotController] %s used power-up (reason: %s)" % [actor.name, eval.reason])
|
||||
var main = get_tree().get_root().get_node_or_null("Main")
|
||||
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
|
||||
if not is_instance_valid(self): return true # Early exit if deleted
|
||||
|
||||
_is_processing_action = false
|
||||
_current_action = "idle"
|
||||
return success
|
||||
|
||||
# =============================================================================
|
||||
# Grab Tiles
|
||||
# =============================================================================
|
||||
|
||||
func _try_grab() -> bool:
|
||||
"""Try to grab a tile from the grid."""
|
||||
# Check AP only if turn-based
|
||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||
return false
|
||||
|
||||
if _is_playerboard_full():
|
||||
# print("[BotController] Grab failed: Board full")
|
||||
return false
|
||||
|
||||
# Check if goals already achieved
|
||||
if _is_goals_achieved():
|
||||
return false
|
||||
|
||||
# Get tiles we need
|
||||
var tiles_needed = strategic_planner.get_tiles_needed()
|
||||
|
||||
# Check current position and adjacent cells for tiles
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
||||
"""Find best tile to grab from current or adjacent positions."""
|
||||
var result = {"position": null, "type": - 1}
|
||||
|
||||
# Check current position first
|
||||
var current_cell = Vector3i(actor.current_position.x, 1, actor.current_position.y)
|
||||
var item = enhanced_gridmap.get_cell_item(current_cell)
|
||||
|
||||
# Priority: needed tiles > holo tiles > any goal tile
|
||||
if item in tiles_needed:
|
||||
return {"position": actor.current_position, "type": item}
|
||||
if item in HOLO_TILES:
|
||||
result = {"position": actor.current_position, "type": item}
|
||||
|
||||
# Check adjacent cells
|
||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
|
||||
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 tiles_needed:
|
||||
return {"position": neighbor.position, "type": item}
|
||||
|
||||
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
|
||||
|
||||
# =============================================================================
|
||||
# Movement
|
||||
# =============================================================================
|
||||
|
||||
func _try_move() -> bool:
|
||||
"""Try to move toward needed tiles."""
|
||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||
return false
|
||||
|
||||
if _is_goals_achieved():
|
||||
return false
|
||||
|
||||
# Find optimal movement target
|
||||
var target_pos = 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)
|
||||
return 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)
|
||||
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
|
||||
|
||||
# =============================================================================
|
||||
# Put Tiles Back
|
||||
# =============================================================================
|
||||
|
||||
func _try_put() -> 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
|
||||
|
||||
if put_slot == -1:
|
||||
return false
|
||||
|
||||
# Find empty adjacent cell
|
||||
var put_position = _find_empty_adjacent_cell()
|
||||
if not put_position:
|
||||
return false
|
||||
|
||||
# Execute put
|
||||
_is_processing_action = true
|
||||
_current_action = "putting"
|
||||
|
||||
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_playerboard", actor.playerboard)
|
||||
actor.action_points -= 1
|
||||
print("[BotController] %s put tile %d at %s" % [actor.name, item, put_position])
|
||||
|
||||
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
|
||||
|
||||
func _find_empty_adjacent_cell() -> Vector2i:
|
||||
"""Find an empty cell adjacent to player."""
|
||||
var current_cell = Vector3i(actor.current_position.x, 1, actor.current_position.y)
|
||||
if enhanced_gridmap.get_cell_item(current_cell) == -1:
|
||||
return actor.current_position
|
||||
|
||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
for neighbor in neighbors:
|
||||
if not neighbor.is_walkable:
|
||||
continue
|
||||
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
||||
if enhanced_gridmap.get_cell_item(cell) == -1:
|
||||
return neighbor.position
|
||||
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
# =============================================================================
|
||||
# Arrange Playerboard
|
||||
# =============================================================================
|
||||
|
||||
func _try_arrange() -> bool:
|
||||
"""Try to rearrange tiles in playerboard."""
|
||||
if actor.action_points < 2:
|
||||
return false
|
||||
|
||||
if _is_goals_achieved():
|
||||
return false
|
||||
|
||||
# Find misplaced item and better position
|
||||
var arrangement = _find_best_arrangement()
|
||||
if arrangement.is_empty():
|
||||
return false
|
||||
|
||||
# Execute arrangement
|
||||
_is_processing_action = true
|
||||
_current_action = "arranging"
|
||||
|
||||
if actor.is_multiplayer_authority():
|
||||
var item = actor.playerboard[arrangement.source_slot]
|
||||
actor.playerboard[arrangement.target_slot] = item
|
||||
actor.playerboard[arrangement.source_slot] = -1
|
||||
actor.rpc("sync_playerboard", actor.playerboard)
|
||||
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
|
||||
if not is_instance_valid(self): return true
|
||||
_is_processing_action = false
|
||||
_current_action = "idle"
|
||||
return true
|
||||
|
||||
func _find_best_arrangement() -> Dictionary:
|
||||
"""Find a tile that can be moved to a better position."""
|
||||
for i in range(1, 4): # Check central 3x3
|
||||
for j in range(1, 4):
|
||||
var board_idx = i * 5 + j
|
||||
var goal_idx = (i - 1) * 3 + (j - 1)
|
||||
|
||||
var current_item = actor.playerboard[board_idx]
|
||||
var goal_item = actor.goals[goal_idx] if goal_idx < actor.goals.size() else -1
|
||||
|
||||
if current_item != goal_item and current_item != -1:
|
||||
# Find better position for this item
|
||||
for gi in range(3):
|
||||
for gj in range(3):
|
||||
if actor.goals[gi * 3 + gj] == current_item:
|
||||
var target_slot = (gi + 1) * 5 + (gj + 1)
|
||||
if actor.playerboard[target_slot] == -1:
|
||||
return {"source_slot": board_idx, "target_slot": target_slot}
|
||||
|
||||
return {}
|
||||
|
||||
# =============================================================================
|
||||
# Utility Functions
|
||||
# =============================================================================
|
||||
|
||||
func _is_playerboard_full() -> bool:
|
||||
for item in actor.playerboard:
|
||||
if item == -1:
|
||||
return false
|
||||
return true
|
||||
|
||||
func _is_goals_achieved() -> bool:
|
||||
"""Check if goal pattern is complete in any 3x3 region of playerboard."""
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
for j in range(3):
|
||||
row.append(actor.goals[i * 3 + j] if i * 3 + j < actor.goals.size() else -1)
|
||||
goals_2d.append(row)
|
||||
|
||||
var board_2d = []
|
||||
for i in range(5):
|
||||
var row = []
|
||||
for j in range(5):
|
||||
row.append(actor.playerboard[i * 5 + j] if i * 5 + j < actor.playerboard.size() else -1)
|
||||
board_2d.append(row)
|
||||
|
||||
for start_row in range(3):
|
||||
for start_col in range(3):
|
||||
var matches = true
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var board_item = board_2d[start_row + i][start_col + j]
|
||||
var goal_item = goals_2d[i][j]
|
||||
if goal_item != -1 and goal_item != board_item:
|
||||
matches = false
|
||||
break
|
||||
elif goal_item == -1 and board_item != -1:
|
||||
matches = false
|
||||
break
|
||||
if not matches:
|
||||
break
|
||||
if matches:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _handle_goal_completion():
|
||||
"""Handle goal completion - trigger scoring."""
|
||||
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:
|
||||
var time_remaining = goals_cycle_manager.get_time_remaining()
|
||||
goals_cycle_manager.on_goal_completed(actor, time_remaining)
|
||||
|
||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if powerup_manager:
|
||||
powerup_manager.add_goal_completion_reward()
|
||||
|
||||
print("[BotController] %s COMPLETED GOAL!" % actor.name)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwwwixc07jc86
|
||||
@@ -0,0 +1,318 @@
|
||||
extends RefCounted
|
||||
class_name BotStrategicPlanner
|
||||
|
||||
# BotStrategicPlanner - Strategic decision-making for bot AI
|
||||
# Evaluates tile needs, pathfinding targets, and sabotage opportunities
|
||||
|
||||
var actor: Node3D
|
||||
var enhanced_gridmap: Node
|
||||
|
||||
# Tile type constants
|
||||
const GOAL_TILES = [7, 8, 9, 10] # Heart, Diamond, Star, Coin
|
||||
const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles
|
||||
|
||||
func _init(p_actor: Node3D, p_gridmap: Node):
|
||||
actor = p_actor
|
||||
enhanced_gridmap = p_gridmap
|
||||
|
||||
# =============================================================================
|
||||
# Goal Analysis
|
||||
# =============================================================================
|
||||
|
||||
func calculate_goal_progress() -> float:
|
||||
"""Returns 0.0-1.0 representing how close bot is to completing their goal."""
|
||||
if not actor or actor.goals.size() == 0:
|
||||
return 0.0
|
||||
|
||||
var matches = 0
|
||||
var total_required = 0
|
||||
|
||||
# Check center 3x3 of playerboard against goals
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var goal_idx = i * 3 + j
|
||||
if goal_idx >= actor.goals.size():
|
||||
continue
|
||||
|
||||
var goal_value = actor.goals[goal_idx]
|
||||
if goal_value == -1:
|
||||
continue # Empty goal slot
|
||||
|
||||
total_required += 1
|
||||
var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board
|
||||
if board_idx < actor.playerboard.size() and actor.playerboard[board_idx] == goal_value:
|
||||
matches += 1
|
||||
|
||||
if total_required == 0:
|
||||
return 1.0 # All goals are -1 (empty)
|
||||
|
||||
return float(matches) / float(total_required)
|
||||
|
||||
func get_tiles_needed() -> Array:
|
||||
"""Returns array of tile types still needed to complete goal."""
|
||||
var needed = []
|
||||
|
||||
if not actor or actor.goals.size() == 0:
|
||||
return needed
|
||||
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var goal_idx = i * 3 + j
|
||||
if goal_idx >= actor.goals.size():
|
||||
continue
|
||||
|
||||
var goal_value = actor.goals[goal_idx]
|
||||
if goal_value == -1:
|
||||
continue
|
||||
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
if board_idx >= actor.playerboard.size() or actor.playerboard[board_idx] != goal_value:
|
||||
if not goal_value in needed:
|
||||
needed.append(goal_value)
|
||||
|
||||
return needed
|
||||
|
||||
func find_best_slot_for_tile(tile_type: int) -> int:
|
||||
"""Find the best playerboard slot for a given tile type."""
|
||||
# Check goals to find matching position
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var goal_idx = i * 3 + j
|
||||
if goal_idx < actor.goals.size() and actor.goals[goal_idx] == tile_type:
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
if board_idx < actor.playerboard.size() and actor.playerboard[board_idx] == -1:
|
||||
return board_idx
|
||||
|
||||
# Fallback: any empty slot
|
||||
return actor.playerboard.find(-1)
|
||||
|
||||
# =============================================================================
|
||||
# Tile Finding
|
||||
# =============================================================================
|
||||
|
||||
func find_best_tile_to_grab() -> Dictionary:
|
||||
"""Find the best tile to grab, prioritizing goal tiles then holo tiles."""
|
||||
var needed_tiles = get_tiles_needed()
|
||||
var best_tile = {"position": null, "type": - 1, "priority": 0}
|
||||
|
||||
if not enhanced_gridmap:
|
||||
return best_tile
|
||||
|
||||
# Search nearby area for tiles
|
||||
var search_radius = 5
|
||||
var current_pos = actor.current_position
|
||||
|
||||
for dx in range(-search_radius, search_radius + 1):
|
||||
for dz in range(-search_radius, search_radius + 1):
|
||||
var pos = Vector2i(current_pos.x + dx, current_pos.y + dz)
|
||||
if not enhanced_gridmap.is_position_valid(pos):
|
||||
continue
|
||||
|
||||
var cell = Vector3i(pos.x, 1, pos.y)
|
||||
var item = enhanced_gridmap.get_cell_item(cell)
|
||||
|
||||
if item == -1:
|
||||
continue
|
||||
|
||||
var priority = 0
|
||||
|
||||
# Priority 1: Tiles we need for goals
|
||||
if item in needed_tiles:
|
||||
priority = 10 - abs(dx) - abs(dz) # Closer = higher priority
|
||||
# Priority 2: Holo tiles for power-ups
|
||||
elif item in HOLO_TILES:
|
||||
priority = 5 - abs(dx) - abs(dz)
|
||||
elif item in GOAL_TILES:
|
||||
priority = 1 # Low priority - might be useful later
|
||||
|
||||
if priority > best_tile.priority:
|
||||
best_tile = {"position": pos, "type": item, "priority": priority}
|
||||
|
||||
return best_tile
|
||||
|
||||
func find_nearest_tile_of_type(tile_types: Array) -> Vector2i:
|
||||
"""Find nearest tile matching any type in array."""
|
||||
var current_pos = actor.current_position
|
||||
var nearest_pos = Vector2i(-1, -1)
|
||||
var nearest_dist = 999999
|
||||
|
||||
if not enhanced_gridmap:
|
||||
return nearest_pos
|
||||
|
||||
for x in range(enhanced_gridmap.columns):
|
||||
for z in range(enhanced_gridmap.rows):
|
||||
var pos = Vector2i(x, z)
|
||||
var cell = Vector3i(x, 1, z)
|
||||
var item = enhanced_gridmap.get_cell_item(cell)
|
||||
|
||||
if item in tile_types:
|
||||
var dist = abs(pos.x - current_pos.x) + abs(pos.y - current_pos.y)
|
||||
if dist < nearest_dist:
|
||||
nearest_dist = dist
|
||||
nearest_pos = pos
|
||||
|
||||
return nearest_pos
|
||||
|
||||
# =============================================================================
|
||||
# Movement Strategy
|
||||
# =============================================================================
|
||||
|
||||
func find_optimal_move_target() -> Vector2i:
|
||||
"""Calculate the best position to move towards."""
|
||||
var needed_tiles = get_tiles_needed()
|
||||
|
||||
# First: move toward tiles we need
|
||||
if needed_tiles.size() > 0:
|
||||
var target = find_nearest_tile_of_type(needed_tiles)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
|
||||
# Second: move toward holo tiles if we need power-ups
|
||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if powerup_manager and powerup_manager.current_points < powerup_manager.MAX_POINTS:
|
||||
var target = find_nearest_tile_of_type(HOLO_TILES)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
|
||||
# Third: move toward any goal tile that might be useful
|
||||
var target = find_nearest_tile_of_type(GOAL_TILES)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
|
||||
# Fallback: random valid position
|
||||
return _get_random_valid_position()
|
||||
|
||||
func _get_adjacent_position(target: Vector2i) -> Vector2i:
|
||||
"""Get a valid position adjacent to or at the target."""
|
||||
var current_pos = actor.current_position
|
||||
|
||||
# If we can reach the target directly, return it
|
||||
if _is_within_movement_range(target):
|
||||
return target
|
||||
|
||||
# Otherwise, move one step closer
|
||||
var dx = sign(target.x - current_pos.x)
|
||||
var dz = sign(target.y - current_pos.y)
|
||||
|
||||
var positions_to_try = [
|
||||
Vector2i(current_pos.x + dx, current_pos.y + dz),
|
||||
Vector2i(current_pos.x + dx, current_pos.y),
|
||||
Vector2i(current_pos.x, current_pos.y + dz)
|
||||
]
|
||||
|
||||
for pos in positions_to_try:
|
||||
if _is_valid_move_target(pos):
|
||||
return pos
|
||||
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
func _is_within_movement_range(pos: Vector2i) -> bool:
|
||||
var current_pos = actor.current_position
|
||||
var dist = max(abs(pos.x - current_pos.x), abs(pos.y - current_pos.y))
|
||||
return dist <= actor.movement_range
|
||||
|
||||
func _is_valid_move_target(pos: Vector2i) -> bool:
|
||||
if not enhanced_gridmap or not enhanced_gridmap.is_position_valid(pos):
|
||||
return false
|
||||
if actor.is_position_occupied(pos):
|
||||
return false
|
||||
return true
|
||||
|
||||
func _get_random_valid_position() -> Vector2i:
|
||||
var valid_positions = []
|
||||
var current_pos = actor.current_position
|
||||
var range_val = actor.movement_range
|
||||
|
||||
for dx in range(-range_val, range_val + 1):
|
||||
for dz in range(-range_val, range_val + 1):
|
||||
if dx == 0 and dz == 0:
|
||||
continue
|
||||
var pos = Vector2i(current_pos.x + dx, current_pos.y + dz)
|
||||
if _is_valid_move_target(pos):
|
||||
valid_positions.append(pos)
|
||||
|
||||
if valid_positions.size() > 0:
|
||||
return valid_positions[randi() % valid_positions.size()]
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
# =============================================================================
|
||||
# Sabotage Strategy
|
||||
# =============================================================================
|
||||
|
||||
func evaluate_sabotage_opportunity() -> Dictionary:
|
||||
"""Evaluate whether to use power-up for sabotage."""
|
||||
var result = {"should_sabotage": false, "reason": "", "target": null}
|
||||
|
||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if not powerup_manager or not powerup_manager.can_use_special():
|
||||
return result
|
||||
|
||||
# Get opponents
|
||||
var opponents = _get_opponents()
|
||||
if opponents.size() == 0:
|
||||
return result
|
||||
|
||||
# Check conditions for sabotage (balanced strategy)
|
||||
|
||||
# Condition 1: Power-ups are maxed - use it or lose potential gains
|
||||
if powerup_manager.current_points >= powerup_manager.MAX_POINTS:
|
||||
result.should_sabotage = true
|
||||
result.reason = "max_powerup"
|
||||
result.target = opponents[randi() % opponents.size()]
|
||||
return result
|
||||
|
||||
# Condition 2: Opponent is close to completing their goal
|
||||
for opponent in opponents:
|
||||
var opponent_progress = _estimate_opponent_progress(opponent)
|
||||
if opponent_progress >= 0.7: # 70% complete
|
||||
result.should_sabotage = true
|
||||
result.reason = "opponent_close_to_winning"
|
||||
result.target = opponent
|
||||
return result
|
||||
|
||||
# Condition 3: Bot is behind in score - need to catch up
|
||||
var goals_cycle_manager = actor.get_tree().get_root().get_node_or_null("Main/GoalsCycleManager")
|
||||
if goals_cycle_manager:
|
||||
var leaderboard = goals_cycle_manager.get_leaderboard()
|
||||
var my_rank = _get_rank_in_leaderboard(leaderboard)
|
||||
if my_rank > 1 and powerup_manager.get_bars() >= 2:
|
||||
result.should_sabotage = true
|
||||
result.reason = "behind_in_score"
|
||||
result.target = opponents[0] if opponents.size() > 0 else null
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
func _get_opponents() -> Array:
|
||||
var all_players = actor.get_tree().get_nodes_in_group("Players")
|
||||
return all_players.filter(func(p): return p != actor)
|
||||
|
||||
func _estimate_opponent_progress(opponent: Node) -> float:
|
||||
"""Estimate opponent's goal progress based on their playerboard."""
|
||||
if not opponent or opponent.goals.size() == 0:
|
||||
return 0.0
|
||||
|
||||
var matches = 0
|
||||
var total = 0
|
||||
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
var goal_idx = i * 3 + j
|
||||
if goal_idx >= opponent.goals.size():
|
||||
continue
|
||||
var goal_value = opponent.goals[goal_idx]
|
||||
if goal_value == -1:
|
||||
continue
|
||||
total += 1
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
if board_idx < opponent.playerboard.size() and opponent.playerboard[board_idx] == goal_value:
|
||||
matches += 1
|
||||
|
||||
return float(matches) / float(max(total, 1))
|
||||
|
||||
func _get_rank_in_leaderboard(leaderboard: Array) -> int:
|
||||
var my_id = actor.get_multiplayer_authority()
|
||||
for i in range(leaderboard.size()):
|
||||
if leaderboard[i].get("peer_id", -1) == my_id:
|
||||
return i + 1
|
||||
return leaderboard.size() + 1
|
||||
@@ -0,0 +1 @@
|
||||
uid://q4lnha3plt3w
|
||||
@@ -5,7 +5,7 @@ extends Node
|
||||
signal game_started()
|
||||
signal game_state_changed()
|
||||
|
||||
@export var enable_bots: bool = false
|
||||
@export var enable_bots: bool = true
|
||||
@export var max_players: int = 4
|
||||
|
||||
var players: Array = []
|
||||
|
||||
@@ -140,7 +140,8 @@ func _initialize_player_scores():
|
||||
"""Initialize scores for all connected players to 0."""
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for player in all_players:
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
# Use name.to_int() for ID because bots share authority 1 but have unique node names
|
||||
var peer_id = player.name.to_int()
|
||||
if not player_scores.has(peer_id):
|
||||
player_scores[peer_id] = 0
|
||||
_update_leaderboard()
|
||||
@@ -186,7 +187,8 @@ func on_goal_completed(player: Node, time_remaining: float):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
# Use name.to_int() for ID because bots share authority 1
|
||||
var peer_id = player.name.to_int()
|
||||
|
||||
# Calculate score: base + time bonus
|
||||
var time_bonus = int(time_remaining * TIME_BONUS_MULTIPLIER)
|
||||
@@ -241,7 +243,8 @@ func _process_cycle_end_for_all_players():
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
|
||||
for player in all_players:
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
# Use name.to_int() for ID because bots share authority 1
|
||||
var peer_id = player.name.to_int()
|
||||
var match_score = _calculate_match_score(player)
|
||||
|
||||
if match_score > 0:
|
||||
@@ -297,7 +300,7 @@ func regenerate_goals_for_player(player: Node):
|
||||
player.goals = int_goals
|
||||
|
||||
# Use main scene's RPC which properly looks up player by ID on each client
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
var peer_id = player.name.to_int()
|
||||
if main_scene:
|
||||
main_scene.rpc("sync_player_goals", peer_id, int_goals)
|
||||
|
||||
|
||||
@@ -47,17 +47,22 @@ func after_action_completed():
|
||||
# Clear the highlights after placing the tiles. (Quickfix for Clientside)
|
||||
clear_highlights()
|
||||
|
||||
# Only update UI if this is the LOCAL HUMAN PLAYER
|
||||
# Bots are owned by the host (authority match) but shouldn't trigger UI updates
|
||||
if multiplayer.get_unique_id() == player.get_multiplayer_authority():
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
main.ui_manager.update_button_states()
|
||||
main.ui_manager.update_playerboard_ui()
|
||||
|
||||
# Add this line to sync all boards
|
||||
main.update_all_players_boards()
|
||||
|
||||
# Add sync for playerboard
|
||||
if player.is_multiplayer_authority():
|
||||
if not player.is_bot and not player.is_in_group("Bots"):
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
main.ui_manager.update_button_states()
|
||||
main.ui_manager.update_playerboard_ui()
|
||||
|
||||
# Add this line to sync all boards
|
||||
main.update_all_players_boards()
|
||||
|
||||
# Sync playerboard (Bots DO need to sync their board logic, just not update local UI)
|
||||
if player.is_multiplayer_authority():
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
main.rpc("sync_playerboard", player.get_multiplayer_authority(), player.playerboard)
|
||||
|
||||
player._is_processing_action = false
|
||||
|
||||
Reference in New Issue
Block a user