Files
FFCardGame/scripts/game/ai/AIStrategy.gd
2026-02-02 16:28:53 -05:00

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