#nullable enable using System; using System.Collections.Generic; using System.Linq; namespace ReplacedPerson.Core { public sealed class ReplacedAi { private readonly ReplacedContentCatalog _content; public ReplacedAi(ReplacedContentCatalog content) => _content = content ?? throw new ArgumentNullException(nameof(content)); /// Uses only own hand plus the already-public lead submission. public ReplacedTurnSubmission Choose(ReplacedMatchState visibleState, int playerIndex) { var self = visibleState.Players[playerIndex]; var targetCount = visibleState.IsLastStand ? 4 : Math.Min(3, self.NormalHand.Count); var publicOpponent = visibleState.Submissions[1 - playerIndex]; var opponentTypes = publicOpponent == null ? Array.Empty() : publicOpponent.NormalCardIds.Where(_content.Cards.ContainsKey).Select(id => _content.Cards[id].EffectType).ToArray(); var scored = new List<(string id, double score)>(); foreach (var id in self.NormalHand.Distinct(StringComparer.Ordinal)) { var card = _content.Cards[id]; var expected = card.BaseValue + card.Factor * 3.5; var efficiency = expected / Math.Max(1, card.Cost); var survival = self.Health <= 10 && card.EffectType != ReplacedEffectType.Attack ? 4.0 : 0.0; var counterScore = opponentTypes.Sum(type => MatchupScore(card.EffectType, type)); scored.Add((id, expected + efficiency * 2.0 + survival + counterScore)); } var selected = new List(); var remainingCost = self.Cost; foreach (var candidate in scored.OrderByDescending(value => value.score).ThenBy(value => value.id, StringComparer.Ordinal)) { var available = self.NormalHand.Count(id => id == candidate.id); for (var copy = 0; copy < available && selected.Count < targetCount; copy++) { var cost = _content.Cards[candidate.id].Cost; if (cost > remainingCost) continue; selected.Add(candidate.id); remainingCost -= cost; } } var endCount = visibleState.IsLastStand ? 2 : 1; var ends = self.EndHand.Where(id => _content.EndCards[id].Cost <= remainingCost) .OrderByDescending(id => _content.EndCards[id].SlotBase.Sum() + _content.EndCards[id].SlotFactor.Sum() * 3.5) .ThenBy(id => id, StringComparer.Ordinal).Take(endCount).ToList(); if (selected.Count < (visibleState.IsLastStand ? 4 : 1) || ends.Count < endCount) return new ReplacedTurnSubmission { Pass = true, DiscardEndOnPass = false }; return new ReplacedTurnSubmission { NormalCardIds = selected, EndCardIds = ends, RerollSlot = ChooseRerollSlot(selected) }; } private int ChooseRerollSlot(IReadOnlyList selected) { if (selected.Count == 0) return -1; var weakest = double.MaxValue; var result = -1; for (var i = 0; i < Math.Min(3, selected.Count); i++) { var card = _content.Cards[selected[i]]; var expected = card.BaseValue + card.Factor * 3.5; if (expected < weakest) { weakest = expected; result = i; } } return result; } private static double MatchupScore(ReplacedEffectType own, ReplacedEffectType opponent) => own == ReplacedEffectType.Counter && opponent == ReplacedEffectType.Attack ? 4.0 : own == ReplacedEffectType.Dodge && opponent == ReplacedEffectType.Attack ? 3.0 : own == ReplacedEffectType.Attack && opponent == ReplacedEffectType.Dodge ? 2.0 : 0.0; } }