191 lines
5.5 KiB
GDScript
191 lines
5.5 KiB
GDScript
class_name AIStrategy
|
|
extends RefCounted
|
|
|
|
## Base class for AI decision-making strategies
|
|
## Subclasses implement different difficulty levels
|
|
|
|
enum Difficulty { EASY, NORMAL, HARD }
|
|
|
|
var difficulty: Difficulty
|
|
var player_index: int
|
|
var game_state: GameState
|
|
|
|
|
|
func _init(p_difficulty: Difficulty, p_player_index: int) -> void:
|
|
difficulty = p_difficulty
|
|
player_index = p_player_index
|
|
|
|
|
|
func set_game_state(state: GameState) -> void:
|
|
game_state = state
|
|
|
|
|
|
## Returns the player this AI controls
|
|
func get_player() -> Player:
|
|
return game_state.get_player(player_index)
|
|
|
|
|
|
## Returns the opponent player
|
|
func get_opponent() -> Player:
|
|
return game_state.get_player(1 - player_index)
|
|
|
|
|
|
## Called during Main Phase - decide what card to play or pass
|
|
## Returns: { "action": "play", "card": CardInstance } or { "action": "pass" }
|
|
func decide_main_phase_action() -> Dictionary:
|
|
push_error("AIStrategy.decide_main_phase_action() must be overridden")
|
|
return { "action": "pass" }
|
|
|
|
|
|
## Called when CP is needed - decide how to generate CP
|
|
## Returns: { "action": "discard", "card": CardInstance } or { "action": "dull_backup", "card": CardInstance }
|
|
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
|
push_error("AIStrategy.decide_cp_generation() must be overridden")
|
|
return {}
|
|
|
|
|
|
## Called during Attack Phase - decide which forward to attack with
|
|
## Returns: { "action": "attack", "card": CardInstance } or { "action": "end_attacks" }
|
|
func decide_attack_action() -> Dictionary:
|
|
push_error("AIStrategy.decide_attack_action() must be overridden")
|
|
return { "action": "end_attacks" }
|
|
|
|
|
|
## Called during Block Declaration - decide how to block
|
|
## Returns: { "action": "block", "card": CardInstance } or { "action": "skip" }
|
|
func decide_block_action(attacker: CardInstance) -> Dictionary:
|
|
push_error("AIStrategy.decide_block_action() must be overridden")
|
|
return { "action": "skip" }
|
|
|
|
|
|
## Get thinking delay range in seconds based on difficulty
|
|
func get_thinking_delay() -> float:
|
|
match difficulty:
|
|
Difficulty.EASY:
|
|
return randf_range(1.5, 2.5)
|
|
Difficulty.NORMAL:
|
|
return randf_range(1.0, 1.5)
|
|
Difficulty.HARD:
|
|
return randf_range(0.5, 1.0)
|
|
return 1.0
|
|
|
|
|
|
# ============ HELPER METHODS FOR SUBCLASSES ============
|
|
|
|
## Get all cards in hand that can be played (have enough CP or can generate CP)
|
|
func get_playable_cards() -> Array[CardInstance]:
|
|
var player := get_player()
|
|
var playable: Array[CardInstance] = []
|
|
|
|
for card in player.hand.get_cards():
|
|
if _can_afford_card(card):
|
|
playable.append(card)
|
|
|
|
return playable
|
|
|
|
|
|
## Check if a card can be afforded (either have CP or can generate it)
|
|
func _can_afford_card(card: CardInstance) -> bool:
|
|
var player := get_player()
|
|
var cost := card.card_data.cost
|
|
var elements := card.card_data.elements
|
|
|
|
# Check if we already have enough CP
|
|
var current_cp := player.cp_pool.get_total_cp()
|
|
if current_cp >= cost:
|
|
# Check element requirements
|
|
for element in elements:
|
|
if player.cp_pool.get_cp(element) > 0 or player.cp_pool.get_cp(Enums.Element.NONE) > 0:
|
|
return true
|
|
# If no specific element needed (Light/Dark cards), any CP works
|
|
if elements.is_empty():
|
|
return true
|
|
|
|
# Check if we can generate enough CP
|
|
var potential_cp := _calculate_potential_cp()
|
|
return potential_cp >= cost
|
|
|
|
|
|
## Calculate total CP we could generate (hand discards + backup dulls)
|
|
func _calculate_potential_cp() -> int:
|
|
var player := get_player()
|
|
var total := player.cp_pool.get_total_cp()
|
|
|
|
# Each card in hand can be discarded for 2 CP
|
|
total += player.hand.get_card_count() * 2
|
|
|
|
# Each active backup can be dulled for 1 CP
|
|
for backup in player.field_backups.get_cards():
|
|
if backup.state == Enums.CardState.ACTIVE:
|
|
total += 1
|
|
|
|
return total
|
|
|
|
|
|
## Get forwards that can attack
|
|
func get_attackable_forwards() -> Array[CardInstance]:
|
|
return get_player().get_attackable_forwards()
|
|
|
|
|
|
## Get forwards that can block
|
|
func get_blockable_forwards() -> Array[CardInstance]:
|
|
return get_player().get_blockable_forwards()
|
|
|
|
|
|
## Calculate a simple card value score
|
|
func calculate_card_value(card: CardInstance) -> float:
|
|
var data := card.card_data
|
|
var value := 0.0
|
|
|
|
match data.type:
|
|
Enums.CardType.FORWARD:
|
|
# Forwards valued by power/cost ratio + abilities
|
|
value = float(data.power) / float(max(data.cost, 1))
|
|
if data.has_ability("Brave"):
|
|
value *= 1.3
|
|
if data.has_ability("First Strike"):
|
|
value *= 1.2
|
|
if data.has_ability("Haste"):
|
|
value *= 1.4
|
|
Enums.CardType.BACKUP:
|
|
# Backups valued by utility (cost efficiency)
|
|
value = 3.0 / float(max(data.cost, 1))
|
|
Enums.CardType.SUMMON:
|
|
# Summons valued by effect strength (approximated by cost)
|
|
value = float(data.cost) * 0.8
|
|
Enums.CardType.MONSTER:
|
|
# Monsters similar to forwards
|
|
value = float(data.power) / float(max(data.cost, 1))
|
|
|
|
return value
|
|
|
|
|
|
## Evaluate board advantage (positive = we're ahead)
|
|
func evaluate_board_state() -> float:
|
|
var player := get_player()
|
|
var opponent := get_opponent()
|
|
|
|
var score := 0.0
|
|
|
|
# Forward power advantage
|
|
var our_power := 0
|
|
for forward in player.field_forwards.get_cards():
|
|
our_power += forward.get_power()
|
|
|
|
var their_power := 0
|
|
for forward in opponent.field_forwards.get_cards():
|
|
their_power += forward.get_power()
|
|
|
|
score += (our_power - their_power) / 1000.0
|
|
|
|
# Backup count advantage
|
|
score += (player.field_backups.get_card_count() - opponent.field_backups.get_card_count()) * 2.0
|
|
|
|
# Hand size advantage
|
|
score += (player.hand.get_card_count() - opponent.hand.get_card_count()) * 0.5
|
|
|
|
# Damage disadvantage (more damage = worse)
|
|
score -= (player.get_damage_count() - opponent.get_damage_count()) * 3.0
|
|
|
|
return score
|