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

272 lines
7.5 KiB
GDScript

class_name HardAI
extends AIStrategy
## Hard AI - Optimal rule-based decisions with full board analysis
## Considers multiple factors and makes the best available play
func _init(p_player_index: int) -> void:
super._init(Difficulty.HARD, p_player_index)
func decide_main_phase_action() -> Dictionary:
var playable := get_playable_cards()
if playable.is_empty():
return { "action": "pass" }
var board_eval := evaluate_board_state()
var player := get_player()
var opponent := get_opponent()
# Analyze the best play considering multiple factors
var best_card: CardInstance = null
var best_score := -999.0
for card in playable:
var score := _evaluate_play(card, board_eval, player, opponent)
if score > best_score:
best_score = score
best_card = card
# Only play if the score is positive
if best_score > 0 and best_card:
return { "action": "play", "card": best_card }
return { "action": "pass" }
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
var player := get_player()
# Prioritize dulling backups (they refresh next turn, no card loss)
var backups_to_dull: Array[CardInstance] = []
for backup in player.field_backups.get_cards():
if backup.state == Enums.CardState.ACTIVE:
backups_to_dull.append(backup)
if not backups_to_dull.is_empty():
# Dull backups with least useful abilities first
backups_to_dull.sort_custom(_compare_backup_utility)
return { "action": "dull_backup", "card": backups_to_dull[0] }
# Discard cards - choose most expendable
var hand_cards := player.hand.get_cards()
if hand_cards.is_empty():
return {}
# Evaluate each card for discard value
var best_discard: CardInstance = null
var lowest_value := 999.0
for card in hand_cards:
var value := _evaluate_discard_value(card, player)
if value < lowest_value:
lowest_value = value
best_discard = card
if best_discard:
return { "action": "discard", "card": best_discard }
return {}
func decide_attack_action() -> Dictionary:
var attackers := get_attackable_forwards()
var opponent := get_opponent()
if attackers.is_empty():
return { "action": "end_attacks" }
var opponent_blockers := opponent.get_blockable_forwards()
# Calculate optimal attack order
var attack_order := _calculate_attack_order(attackers, opponent_blockers, opponent)
if attack_order.is_empty():
return { "action": "end_attacks" }
# Return the best attack
return { "action": "attack", "card": attack_order[0] }
func decide_block_action(attacker: CardInstance) -> Dictionary:
var blockers := get_blockable_forwards()
var player := get_player()
if blockers.is_empty():
return { "action": "skip" }
var attacker_power := attacker.get_power()
var current_damage := player.get_damage_count()
var would_be_lethal := current_damage >= 6
# Evaluate all blocking options
var best_blocker: CardInstance = null
var best_score := 0.0 # Baseline: skip blocking (score 0)
for blocker in blockers:
var score := _evaluate_block(blocker, attacker, would_be_lethal)
if score > best_score:
best_score = score
best_blocker = blocker
if best_blocker:
return { "action": "block", "card": best_blocker }
return { "action": "skip" }
func _evaluate_play(card: CardInstance, board_eval: float, player: Player, opponent: Player) -> float:
var data := card.card_data
var score := calculate_card_value(card)
match data.type:
Enums.CardType.FORWARD:
# Forwards more valuable when behind on board
if board_eval < 0:
score *= 1.5
# Extra value if opponent has no blockers
if opponent.get_blockable_forwards().is_empty():
score *= 1.3
# Consider if we already have 5 forwards (max)
if player.field_forwards.get_card_count() >= 5:
score *= 0.3
Enums.CardType.BACKUP:
# Backups valuable for long game
var backup_count := player.field_backups.get_card_count()
if backup_count >= 5:
score = -10.0 # Can't play more
elif backup_count < 3:
score *= 1.5 # Need more backups
elif board_eval > 5:
score *= 1.3 # Ahead, build infrastructure
Enums.CardType.SUMMON:
# Summons are situational - evaluate based on current needs
# This is simplified; real evaluation would check summon effects
if board_eval < -3:
score *= 1.4 # Need removal/utility when behind
Enums.CardType.MONSTER:
# Similar to forwards but usually less efficient
score *= 0.9
# Penalize expensive plays when low on cards
if player.hand.get_card_count() <= 2 and data.cost >= 4:
score *= 0.5
return score
func _evaluate_discard_value(card: CardInstance, player: Player) -> float:
var value := calculate_card_value(card)
# Duplicates in hand are less valuable
var same_name_count := 0
for hand_card in player.hand.get_cards():
if hand_card.card_data.name == card.card_data.name:
same_name_count += 1
if same_name_count > 1:
value *= 0.5
# High cost cards we can't afford soon are less valuable
var potential_cp := _calculate_potential_cp()
if card.card_data.cost > potential_cp:
value *= 0.7
# Cards matching elements we don't have CP for are less valuable
var has_element_match := false
for element in card.card_data.elements:
if player.cp_pool.get_cp(element) > 0:
has_element_match = true
break
if not has_element_match and not card.card_data.elements.is_empty():
value *= 0.8
return value
func _calculate_attack_order(attackers: Array[CardInstance], blockers: Array[CardInstance], opponent: Player) -> Array[CardInstance]:
var order: Array[CardInstance] = []
var scores: Array[Dictionary] = []
for attacker in attackers:
var score := _evaluate_attack_value(attacker, blockers, opponent)
if score > 0:
scores.append({ "card": attacker, "score": score })
# Sort by score descending
scores.sort_custom(func(a, b): return a.score > b.score)
for entry in scores:
order.append(entry.card)
return order
func _evaluate_attack_value(attacker: CardInstance, blockers: Array[CardInstance], opponent: Player) -> float:
var score := 0.0
var attacker_power := attacker.get_power()
# Base value for dealing damage
score += 3.0
# Lethal damage is extremely valuable
if opponent.get_damage_count() >= 6:
score += 20.0
# Evaluate blocking scenarios
var profitable_blocks := 0
for blocker in blockers:
var blocker_power := blocker.get_power()
if blocker_power >= attacker_power:
profitable_blocks += 1
if profitable_blocks == 0:
# No profitable blocks - guaranteed damage
score += 5.0
else:
# Risk of losing our forward
score -= calculate_card_value(attacker) * 0.5
# Brave forwards can attack safely (don't dull)
if attacker.card_data.has_ability("Brave"):
score += 2.0
return score
func _evaluate_block(blocker: CardInstance, attacker: CardInstance, would_be_lethal: bool) -> float:
var blocker_power := blocker.get_power()
var attacker_power := attacker.get_power()
var score := 0.0
# If lethal, blocking is almost always correct
if would_be_lethal:
score += 15.0
# Do we kill the attacker?
if blocker_power >= attacker_power:
score += calculate_card_value(attacker)
# Do we lose our blocker?
if attacker_power >= blocker_power:
score -= calculate_card_value(blocker)
# First strike changes the calculation
if blocker.card_data.has_ability("First Strike") and blocker_power >= attacker_power:
# We kill them before they hit us
score += calculate_card_value(blocker) * 0.5
return score
func _compare_backup_utility(a: CardInstance, b: CardInstance) -> bool:
# Lower utility = dull first
# This is simplified; could check specific backup abilities
return calculate_card_value(a) < calculate_card_value(b)