overhaul bot
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user