162 lines
4.4 KiB
GDScript
162 lines
4.4 KiB
GDScript
class_name NormalAI
|
|
extends AIStrategy
|
|
|
|
## Normal AI - Balanced play using cost/power heuristics
|
|
## Makes generally good decisions but doesn't deeply analyze
|
|
|
|
|
|
func _init(p_player_index: int) -> void:
|
|
super._init(Difficulty.NORMAL, p_player_index)
|
|
|
|
|
|
func decide_main_phase_action() -> Dictionary:
|
|
var playable := get_playable_cards()
|
|
|
|
if playable.is_empty():
|
|
return { "action": "pass" }
|
|
|
|
# Sort by value (best cards first)
|
|
playable.sort_custom(_compare_card_value)
|
|
|
|
# Consider board state - prioritize forwards if we're behind
|
|
var board_eval := evaluate_board_state()
|
|
|
|
for card in playable:
|
|
var card_type := card.card_data.type
|
|
|
|
# If behind on board, prioritize forwards
|
|
if board_eval < -5.0 and card_type == Enums.CardType.FORWARD:
|
|
return { "action": "play", "card": card }
|
|
|
|
# If ahead, might want backups for sustainability
|
|
if board_eval > 5.0 and card_type == Enums.CardType.BACKUP:
|
|
if get_player().field_backups.get_card_count() < 5:
|
|
return { "action": "play", "card": card }
|
|
|
|
# Default: play the highest value card we can afford
|
|
return { "action": "play", "card": playable[0] }
|
|
|
|
|
|
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
|
var player := get_player()
|
|
|
|
# First, dull backups (they refresh next turn)
|
|
for backup in player.field_backups.get_cards():
|
|
if backup.state == Enums.CardState.ACTIVE:
|
|
return { "action": "dull_backup", "card": backup }
|
|
|
|
# Then, discard lowest value card from hand
|
|
var hand_cards := player.hand.get_cards()
|
|
if hand_cards.is_empty():
|
|
return {}
|
|
|
|
# Sort by value (lowest first for discard)
|
|
var sorted_hand := hand_cards.duplicate()
|
|
sorted_hand.sort_custom(_compare_card_value_reverse)
|
|
|
|
return { "action": "discard", "card": sorted_hand[0] }
|
|
|
|
|
|
func decide_attack_action() -> Dictionary:
|
|
var attackers := get_attackable_forwards()
|
|
var opponent := get_opponent()
|
|
|
|
if attackers.is_empty():
|
|
return { "action": "end_attacks" }
|
|
|
|
# Get opponent's potential blockers
|
|
var opponent_blockers := opponent.get_blockable_forwards()
|
|
|
|
# Evaluate each potential attacker
|
|
var best_attacker: CardInstance = null
|
|
var best_score := -999.0
|
|
|
|
for attacker in attackers:
|
|
var score := _evaluate_attack(attacker, opponent_blockers, opponent)
|
|
if score > best_score:
|
|
best_score = score
|
|
best_attacker = attacker
|
|
|
|
# Only attack if the score is positive (favorable)
|
|
if best_score > 0 and best_attacker:
|
|
return { "action": "attack", "card": best_attacker }
|
|
|
|
return { "action": "end_attacks" }
|
|
|
|
|
|
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()
|
|
|
|
# Check if this attack would be lethal
|
|
var current_damage := player.get_damage_count()
|
|
var would_be_lethal := current_damage >= 6 # 7th damage loses
|
|
|
|
# Find best blocker
|
|
var best_blocker: CardInstance = null
|
|
var best_score := -999.0
|
|
|
|
for blocker in blockers:
|
|
var blocker_power := blocker.get_power()
|
|
var score := 0.0
|
|
|
|
# Would we win the trade?
|
|
if blocker_power >= attacker_power:
|
|
score += 5.0 # We kill their forward
|
|
if attacker_power >= blocker_power:
|
|
score -= calculate_card_value(blocker) # We lose our blocker
|
|
|
|
# If lethal, blocking is very important
|
|
if would_be_lethal:
|
|
score += 10.0
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
best_blocker = blocker
|
|
|
|
# Block if favorable or if lethal
|
|
if best_score > 0 or would_be_lethal:
|
|
if best_blocker:
|
|
return { "action": "block", "card": best_blocker }
|
|
|
|
return { "action": "skip" }
|
|
|
|
|
|
func _evaluate_attack(attacker: CardInstance, opponent_blockers: Array[CardInstance], opponent: Player) -> float:
|
|
var score := 0.0
|
|
var attacker_power := attacker.get_power()
|
|
|
|
# Base value: dealing damage is good
|
|
score += 2.0
|
|
|
|
# Check if opponent can block profitably
|
|
var can_be_blocked := false
|
|
for blocker in opponent_blockers:
|
|
if blocker.get_power() >= attacker_power:
|
|
can_be_blocked = true
|
|
score -= 3.0 # Likely to lose our forward
|
|
break
|
|
|
|
# If unblockable damage, more valuable
|
|
if not can_be_blocked:
|
|
score += 3.0
|
|
|
|
# If this would be lethal damage (7th), very valuable
|
|
if opponent.get_damage_count() >= 6:
|
|
score += 10.0
|
|
|
|
return score
|
|
|
|
|
|
func _compare_card_value(a: CardInstance, b: CardInstance) -> bool:
|
|
return calculate_card_value(a) > calculate_card_value(b)
|
|
|
|
|
|
func _compare_card_value_reverse(a: CardInstance, b: CardInstance) -> bool:
|
|
return calculate_card_value(a) < calculate_card_value(b)
|