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