85 lines
4.0 KiB
C#
85 lines
4.0 KiB
C#
#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));
|
|
|
|
/// <summary>Uses only own hand plus the already-public lead submission.</summary>
|
|
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<ReplacedEffectType>()
|
|
: 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<string>();
|
|
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<string> 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;
|
|
}
|
|
}
|