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)