feature updates
This commit is contained in:
271
scripts/game/ai/HardAI.gd
Normal file
271
scripts/game/ai/HardAI.gd
Normal file
@@ -0,0 +1,271 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user