Files
Workspace/Assets/Demos/ReplacedPerson/Core/ReplacedAi.cs
T
2026-08-17 18:18:13 +08:00

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;
}
}