611 lines
26 KiB
C#
611 lines
26 KiB
C#
#nullable enable
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
namespace ReplacedPerson.Core
|
||
{
|
||
public sealed class ReplacedMatchEngine
|
||
{
|
||
private sealed class PreparedSlot
|
||
{
|
||
public ReplacedEffectType Type;
|
||
public int BaseValue;
|
||
public int Factor;
|
||
public bool DrawAndRecover;
|
||
public bool BonusDamage;
|
||
public bool Reflect;
|
||
public bool AddDefense;
|
||
public bool GuaranteedDodge;
|
||
public readonly List<string> SpecialEffects = new();
|
||
}
|
||
|
||
private readonly HashSet<string> _acceptedTokens = new(StringComparer.Ordinal);
|
||
private readonly List<ReplacedMatchCommand> _replay = new();
|
||
|
||
public ReplacedContentCatalog Content { get; }
|
||
public ReplacedMatchState State { get; }
|
||
public Pcg32 Random { get; }
|
||
public IReadOnlyList<ReplacedMatchCommand> Replay => _replay;
|
||
|
||
public ReplacedMatchEngine(ReplacedContentCatalog content, ReplacedDeck playerOneDeck,
|
||
ReplacedDeck playerTwoDeck, ulong seed, string playerOneName = "玩家", string playerTwoName = "贪婪")
|
||
{
|
||
Content = (content ?? throw new ArgumentNullException(nameof(content))).Clone();
|
||
ValidateDeck(playerOneDeck, nameof(playerOneDeck));
|
||
ValidateDeck(playerTwoDeck, nameof(playerTwoDeck));
|
||
Random = new Pcg32(seed);
|
||
State = new ReplacedMatchState
|
||
{
|
||
Round = 0,
|
||
Players = new[]
|
||
{
|
||
CreatePlayer("p1", playerOneName, playerOneDeck),
|
||
CreatePlayer("p2", playerTwoName, playerTwoDeck)
|
||
},
|
||
ContentHash = Content.ComputeHash()
|
||
};
|
||
Random.Shuffle(State.Players[0].NormalDrawPile);
|
||
Random.Shuffle(State.Players[0].EndDrawPile);
|
||
Random.Shuffle(State.Players[1].NormalDrawPile);
|
||
Random.Shuffle(State.Players[1].EndDrawPile);
|
||
BeginRound(initial: true);
|
||
}
|
||
|
||
public ReplacedCommandResult Apply(ReplacedMatchCommand command)
|
||
{
|
||
if (command == null)
|
||
return Reject("command.null", "指令不能为空。");
|
||
if (State.Phase == ReplacedMatchPhase.Completed)
|
||
return Reject("match.completed", "对局已经结束。");
|
||
if (command.PlayerIndex is < 0 or > 1)
|
||
return Reject("player.invalid", "玩家索引无效。");
|
||
if (string.IsNullOrWhiteSpace(command.IdempotencyToken))
|
||
return Reject("token.empty", "幂等令牌不能为空。");
|
||
if (_acceptedTokens.Contains(command.IdempotencyToken))
|
||
return new ReplacedCommandResult { Accepted = true, Duplicate = true, Message = "duplicate", StateHash = ComputeStateHash() };
|
||
if (command.Sequence <= State.LastAcceptedSequence[command.PlayerIndex])
|
||
return Reject("sequence.stale", "指令序号必须严格递增。");
|
||
|
||
var expectedPlayer = State.Phase == ReplacedMatchPhase.AwaitingLead
|
||
? State.LeadPlayer
|
||
: 1 - State.LeadPlayer;
|
||
if (command.PlayerIndex != expectedPlayer)
|
||
return Reject("turn.not-yours", "尚未轮到该玩家提交。");
|
||
|
||
var validation = ValidateSubmission(command.PlayerIndex, command.Submission);
|
||
if (validation != null)
|
||
return Reject(validation.Value.code, validation.Value.message);
|
||
|
||
LockSubmission(command.PlayerIndex, command.Submission);
|
||
State.LastAcceptedSequence[command.PlayerIndex] = command.Sequence;
|
||
_acceptedTokens.Add(command.IdempotencyToken);
|
||
_replay.Add(command.Clone());
|
||
State.Log.Add($"R{State.Round} {State.Players[command.PlayerIndex].DisplayName} 锁定出牌");
|
||
|
||
if (State.Phase == ReplacedMatchPhase.AwaitingLead)
|
||
{
|
||
State.Phase = ReplacedMatchPhase.AwaitingResponse;
|
||
}
|
||
else
|
||
{
|
||
State.Phase = ReplacedMatchPhase.Resolving;
|
||
ResolveRound();
|
||
}
|
||
|
||
return new ReplacedCommandResult
|
||
{
|
||
Accepted = true,
|
||
Message = "accepted",
|
||
StateHash = ComputeStateHash()
|
||
};
|
||
}
|
||
|
||
public string ComputeStateHash() => ReplacedStateHasher.Compute(State, Random);
|
||
|
||
private void BeginRound(bool initial)
|
||
{
|
||
State.Round++;
|
||
State.Submissions = new ReplacedTurnSubmission?[2];
|
||
for (var i = 0; i < 2; i++)
|
||
{
|
||
var player = State.Players[i];
|
||
player.Defense = 0;
|
||
player.SpentThisRound = 0;
|
||
player.EffectiveEffectsThisRound = 0;
|
||
player.DamageThisRound = 0;
|
||
if (State.IsLastStand)
|
||
{
|
||
DiscardHands(player);
|
||
DrawNormal(player, 4);
|
||
DrawEnd(player, 2);
|
||
}
|
||
else
|
||
{
|
||
DrawNormal(player, initial ? 4 : 2 + player.BonusNormalDraw);
|
||
player.BonusNormalDraw = 0;
|
||
if (initial || !player.SkipNextEndDraw)
|
||
DrawEnd(player, 1);
|
||
player.SkipNextEndDraw = false;
|
||
}
|
||
}
|
||
|
||
State.InitiativeRoll = Random.RollD6();
|
||
State.LeadPlayer = State.InitiativeRoll % 2 == 1 ? 0 : 1;
|
||
State.Phase = ReplacedMatchPhase.AwaitingLead;
|
||
State.Log.Add($"R{State.Round} 先手骰={State.InitiativeRoll},{State.Players[State.LeadPlayer].DisplayName}先手");
|
||
}
|
||
|
||
private (string code, string message)? ValidateSubmission(int playerIndex, ReplacedTurnSubmission? submission)
|
||
{
|
||
if (submission == null)
|
||
return ("submission.null", "提交内容不能为空。");
|
||
if (submission.RerollSlot is < -1 or > 2)
|
||
return ("reroll.invalid", "重掷槽必须为 -1 到 2。");
|
||
|
||
var player = State.Players[playerIndex];
|
||
if (submission.Pass)
|
||
{
|
||
if (submission.NormalCardIds.Count != 0 || submission.EndCardIds.Count != 0)
|
||
return ("pass.with-cards", "跳过时不能同时提交卡牌。");
|
||
return null;
|
||
}
|
||
|
||
var requiredNormal = State.IsLastStand ? 4 : 1;
|
||
var requiredEnd = State.IsLastStand ? 2 : 1;
|
||
if (submission.NormalCardIds.Count < requiredNormal || submission.NormalCardIds.Count > 4)
|
||
return ("normal.count", State.IsLastStand ? "孤注一掷必须提交 4 张普通牌。" : "必须提交 1 到 4 张普通牌。");
|
||
if (submission.EndCardIds.Count != requiredEnd)
|
||
return ("end.count", State.IsLastStand ? "孤注一掷必须提交主、副两张结束牌。" : "必须提交 1 张结束牌。");
|
||
if (!ContainsAll(player.NormalHand, submission.NormalCardIds))
|
||
return ("normal.not-in-hand", "普通牌不在手牌中。");
|
||
if (!ContainsAll(player.EndHand, submission.EndCardIds))
|
||
return ("end.not-in-hand", "结束牌不在手牌中。");
|
||
|
||
var cost = 0;
|
||
foreach (var id in submission.NormalCardIds)
|
||
{
|
||
if (!Content.Cards.TryGetValue(id, out var card))
|
||
return ("normal.unknown", "未知普通牌:" + id);
|
||
cost += card.Cost;
|
||
}
|
||
foreach (var id in submission.EndCardIds)
|
||
{
|
||
if (!Content.EndCards.TryGetValue(id, out var card))
|
||
return ("end.unknown", "未知结束牌:" + id);
|
||
cost += card.Cost;
|
||
}
|
||
if (cost > player.Cost)
|
||
return ("cost.insufficient", $"COST 不足:需要 {cost},当前 {player.Cost}。");
|
||
return null;
|
||
}
|
||
|
||
private void LockSubmission(int playerIndex, ReplacedTurnSubmission submission)
|
||
{
|
||
var player = State.Players[playerIndex];
|
||
var copy = submission.Clone();
|
||
State.Submissions[playerIndex] = copy;
|
||
if (copy.Pass)
|
||
{
|
||
player.SkipNextEndDraw = true;
|
||
if (copy.DiscardEndOnPass)
|
||
{
|
||
player.EndDiscard.AddRange(player.EndHand);
|
||
player.EndHand.Clear();
|
||
}
|
||
return;
|
||
}
|
||
|
||
var spent = 0;
|
||
foreach (var id in copy.NormalCardIds)
|
||
{
|
||
player.NormalHand.Remove(id);
|
||
spent += Content.Cards[id].Cost;
|
||
}
|
||
foreach (var id in copy.EndCardIds)
|
||
{
|
||
player.EndHand.Remove(id);
|
||
spent += Content.EndCards[id].Cost;
|
||
}
|
||
player.Cost -= spent;
|
||
player.SpentThisRound = spent;
|
||
}
|
||
|
||
private void ResolveRound()
|
||
{
|
||
State.LastResolution.Clear();
|
||
var prepared = new[]
|
||
{
|
||
PrepareSlots(State.Submissions[0]),
|
||
PrepareSlots(State.Submissions[1])
|
||
};
|
||
|
||
for (var slotIndex = 0; slotIndex < 3; slotIndex++)
|
||
ResolveSlot(slotIndex, prepared[0][slotIndex], prepared[1][slotIndex]);
|
||
|
||
ApplyRoundRecoveryAndDiscard();
|
||
if (State.IsLastStand)
|
||
{
|
||
CompleteLastStand();
|
||
return;
|
||
}
|
||
|
||
var oneDead = State.Players[0].Health <= 0;
|
||
var twoDead = State.Players[1].Health <= 0;
|
||
if (oneDead && twoDead && !State.LastStandUsed)
|
||
{
|
||
State.LastStandUsed = true;
|
||
State.IsLastStand = true;
|
||
State.Players[0].Health = 1;
|
||
State.Players[1].Health = 1;
|
||
State.Log.Add("双方同时归零,进入孤注一掷");
|
||
BeginRound(initial: false);
|
||
return;
|
||
}
|
||
if (oneDead || twoDead)
|
||
{
|
||
Complete(oneDead ? ReplacedWinner.PlayerTwo : ReplacedWinner.PlayerOne);
|
||
return;
|
||
}
|
||
BeginRound(initial: false);
|
||
}
|
||
|
||
private PreparedSlot?[] PrepareSlots(ReplacedTurnSubmission? submission)
|
||
{
|
||
var result = new PreparedSlot?[3];
|
||
if (submission == null || submission.Pass)
|
||
return result;
|
||
var primaryEnd = Content.EndCards[submission.EndCardIds[0]];
|
||
for (var i = 0; i < submission.NormalCardIds.Count; i++)
|
||
{
|
||
var card = Content.Cards[submission.NormalCardIds[i]];
|
||
var slot = Math.Min(i, 2);
|
||
var next = new PreparedSlot
|
||
{
|
||
Type = card.EffectType,
|
||
BaseValue = card.BaseValue,
|
||
Factor = card.Factor
|
||
};
|
||
if (!string.IsNullOrWhiteSpace(card.SpecialEffectId))
|
||
next.SpecialEffects.Add(card.SpecialEffectId);
|
||
result[slot] = result[slot] == null ? next : Fuse(result[slot]!, next);
|
||
}
|
||
|
||
for (var i = 0; i < result.Length; i++)
|
||
{
|
||
if (result[i] == null)
|
||
continue;
|
||
result[i]!.BaseValue += primaryEnd.SlotBase[i];
|
||
result[i]!.Factor += primaryEnd.SlotFactor[i];
|
||
}
|
||
|
||
if (State.IsLastStand && submission.EndCardIds.Count > 1 && result[0] != null)
|
||
{
|
||
var secondary = Content.EndCards[submission.EndCardIds[1]];
|
||
result[0]!.BaseValue += secondary.SlotBase[0];
|
||
result[0]!.Factor += secondary.SlotFactor[0];
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private static PreparedSlot Fuse(PreparedSlot left, PreparedSlot right)
|
||
{
|
||
var result = new PreparedSlot
|
||
{
|
||
Type = left.Type == ReplacedEffectType.Attack || right.Type == ReplacedEffectType.Attack
|
||
? ReplacedEffectType.Attack
|
||
: left.Type == ReplacedEffectType.Counter || right.Type == ReplacedEffectType.Counter
|
||
? ReplacedEffectType.Counter
|
||
: ReplacedEffectType.Dodge,
|
||
BaseValue = left.BaseValue + right.BaseValue,
|
||
Factor = left.Factor + right.Factor,
|
||
DrawAndRecover = left.DrawAndRecover || right.DrawAndRecover,
|
||
BonusDamage = left.BonusDamage || right.BonusDamage,
|
||
Reflect = left.Reflect || right.Reflect,
|
||
AddDefense = left.AddDefense || right.AddDefense,
|
||
GuaranteedDodge = left.GuaranteedDodge || right.GuaranteedDodge
|
||
};
|
||
result.SpecialEffects.AddRange(left.SpecialEffects);
|
||
result.SpecialEffects.AddRange(right.SpecialEffects);
|
||
|
||
var a = left.Type;
|
||
var b = right.Type;
|
||
if (a == ReplacedEffectType.Attack && b == ReplacedEffectType.Attack)
|
||
{
|
||
result.BaseValue = (int)Math.Round(result.BaseValue * 1.5, MidpointRounding.AwayFromZero);
|
||
result.Factor = (int)Math.Round(result.Factor * 1.5, MidpointRounding.AwayFromZero);
|
||
}
|
||
else if (IsPair(a, b, ReplacedEffectType.Attack, ReplacedEffectType.Dodge)) result.DrawAndRecover = true;
|
||
else if (IsPair(a, b, ReplacedEffectType.Attack, ReplacedEffectType.Counter)) result.BonusDamage = true;
|
||
else if (a == ReplacedEffectType.Counter && b == ReplacedEffectType.Counter) result.Reflect = true;
|
||
else if (IsPair(a, b, ReplacedEffectType.Dodge, ReplacedEffectType.Counter)) result.AddDefense = true;
|
||
else if (a == ReplacedEffectType.Dodge && b == ReplacedEffectType.Dodge) result.GuaranteedDodge = true;
|
||
return result;
|
||
}
|
||
|
||
private void ResolveSlot(int slotIndex, PreparedSlot? one, PreparedSlot? two)
|
||
{
|
||
if (one == null && two == null)
|
||
return;
|
||
var submissionOne = State.Submissions[0]!;
|
||
var submissionTwo = State.Submissions[1]!;
|
||
var oneRerolled = false;
|
||
var twoRerolled = false;
|
||
var oneRoll = one == null ? 0 : RollWithMarkedReroll(submissionOne, slotIndex, out oneRerolled);
|
||
var twoRoll = two == null ? 0 : RollWithMarkedReroll(submissionTwo, slotIndex, out twoRerolled);
|
||
var oneValue = one == null ? 0 : one.BaseValue + one.Factor * oneRoll;
|
||
var twoValue = two == null ? 0 : two.BaseValue + two.Factor * twoRoll;
|
||
var tieGuard = 0;
|
||
while (one != null && two != null && oneValue == twoValue && tieGuard++ < 32)
|
||
{
|
||
oneRoll = Random.RollD6();
|
||
twoRoll = Random.RollD6();
|
||
oneValue = one.BaseValue + one.Factor * oneRoll;
|
||
twoValue = two.BaseValue + two.Factor * twoRoll;
|
||
}
|
||
|
||
var resolution = new ReplacedSlotResolution
|
||
{
|
||
SlotIndex = slotIndex,
|
||
PlayerOneType = one?.Type ?? ReplacedEffectType.Dodge,
|
||
PlayerTwoType = two?.Type ?? ReplacedEffectType.Dodge,
|
||
PlayerOneRoll = oneRoll,
|
||
PlayerTwoRoll = twoRoll,
|
||
PlayerOneValue = oneValue,
|
||
PlayerTwoValue = twoValue,
|
||
PlayerOneRerolled = oneRerolled,
|
||
PlayerTwoRerolled = twoRerolled
|
||
};
|
||
|
||
if (one == null)
|
||
ApplyUnopposed(1, two!, twoValue, resolution);
|
||
else if (two == null)
|
||
ApplyUnopposed(0, one, oneValue, resolution);
|
||
else if (oneValue == twoValue)
|
||
resolution.Summary = "连续平点,槽位无效";
|
||
else
|
||
ApplyOpposed(oneValue > twoValue ? 0 : 1, one, two, oneValue, twoValue, resolution);
|
||
|
||
State.LastResolution.Add(resolution);
|
||
State.Log.Add($"槽{slotIndex + 1}: {resolution.Summary}");
|
||
}
|
||
|
||
private void ApplyUnopposed(int owner, PreparedSlot slot, int value, ReplacedSlotResolution resolution)
|
||
{
|
||
if (slot.Type == ReplacedEffectType.Attack)
|
||
{
|
||
var damage = Math.Min(8, value);
|
||
DealDamage(owner, 1 - owner, damage, resolution);
|
||
ApplySuccess(owner, slot, resolution);
|
||
resolution.Summary = $"{State.Players[owner].DisplayName} 无对位进攻 {damage}";
|
||
}
|
||
else
|
||
{
|
||
ApplySuccess(owner, slot, resolution);
|
||
resolution.Summary = $"{State.Players[owner].DisplayName} 无对位效果发动";
|
||
}
|
||
}
|
||
|
||
private void ApplyOpposed(int winner, PreparedSlot one, PreparedSlot two, int oneValue, int twoValue,
|
||
ReplacedSlotResolution resolution)
|
||
{
|
||
var loser = 1 - winner;
|
||
var winSlot = winner == 0 ? one : two;
|
||
var loseSlot = winner == 0 ? two : one;
|
||
var difference = Math.Abs(oneValue - twoValue);
|
||
|
||
if (winSlot.Type == ReplacedEffectType.Attack)
|
||
{
|
||
if (loseSlot.Type == ReplacedEffectType.Dodge && State.Players[loser].GuaranteedDodge)
|
||
{
|
||
State.Players[loser].GuaranteedDodge = false;
|
||
resolution.Summary = $"{State.Players[loser].DisplayName} 消耗必定闪避";
|
||
return;
|
||
}
|
||
var damage = Math.Min(8, difference);
|
||
DealDamage(winner, loser, damage, resolution);
|
||
ApplySuccess(winner, winSlot, resolution);
|
||
resolution.Summary = $"{State.Players[winner].DisplayName} 进攻胜出 {damage}";
|
||
return;
|
||
}
|
||
|
||
if (winSlot.Type == ReplacedEffectType.Counter && loseSlot.Type == ReplacedEffectType.Attack)
|
||
{
|
||
var damage = Math.Min(6, difference);
|
||
DealDamage(winner, loser, damage, resolution);
|
||
ApplySuccess(winner, winSlot, resolution);
|
||
resolution.Summary = $"{State.Players[winner].DisplayName} 反击 {damage}";
|
||
return;
|
||
}
|
||
|
||
if (winSlot.Type == ReplacedEffectType.Dodge && loseSlot.Type == ReplacedEffectType.Attack)
|
||
{
|
||
State.Players[winner].BonusNormalDraw++;
|
||
ApplySuccess(winner, winSlot, resolution);
|
||
resolution.Summary = $"{State.Players[winner].DisplayName} 闪避并获得额外抽牌";
|
||
return;
|
||
}
|
||
|
||
ApplySuccess(winner, winSlot, resolution);
|
||
resolution.Summary = $"{State.Players[winner].DisplayName} 对位胜出";
|
||
}
|
||
|
||
private void ApplySuccess(int owner, PreparedSlot slot, ReplacedSlotResolution resolution)
|
||
{
|
||
var player = State.Players[owner];
|
||
player.EffectiveEffectsThisRound++;
|
||
if (slot.DrawAndRecover)
|
||
{
|
||
DrawNormal(player, 1);
|
||
player.Cost = Math.Min(10, player.Cost + 1);
|
||
}
|
||
if (slot.BonusDamage)
|
||
DealDamage(owner, 1 - owner, 3, resolution);
|
||
if (slot.AddDefense)
|
||
player.Defense += 2;
|
||
if (slot.GuaranteedDodge)
|
||
player.GuaranteedDodge = true;
|
||
if (slot.Reflect && player.DamageThisRound > 0)
|
||
DealDamage(owner, 1 - owner, Math.Min(5, (int)Math.Ceiling(player.DamageThisRound * 0.5)), resolution);
|
||
|
||
foreach (var special in slot.SpecialEffects)
|
||
{
|
||
switch (special)
|
||
{
|
||
case "greed.draw": DrawNormal(player, 1); break;
|
||
case "greed.refund": player.Cost = Math.Min(10, player.Cost + 1); break;
|
||
case "greed.wage-cut": State.Players[1 - owner].Cost = Math.Max(0, State.Players[1 - owner].Cost - 2); break;
|
||
default:
|
||
if (Content.EffectResolvers.TryGetValue(special, out var resolver))
|
||
{
|
||
var effectResult = resolver.Resolve(new ReplacedEffectContext
|
||
{
|
||
EffectId = special,
|
||
OwnerIndex = owner,
|
||
State = State,
|
||
Value = 0
|
||
});
|
||
player.Cost = Math.Min(10, Math.Max(0, player.Cost + effectResult.CostDelta));
|
||
DrawNormal(player, Math.Max(0, effectResult.DrawDelta));
|
||
if (effectResult.BonusDamage > 0) DealDamage(owner, 1 - owner, effectResult.BonusDamage, resolution);
|
||
if (!string.IsNullOrWhiteSpace(effectResult.Log)) State.Log.Add(effectResult.Log);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DealDamage(int source, int target, int amount, ReplacedSlotResolution resolution)
|
||
{
|
||
if (amount <= 0) return;
|
||
var targetState = State.Players[target];
|
||
var afterDefense = Math.Max(0, amount - targetState.Defense);
|
||
if (State.IsLastStand)
|
||
{
|
||
State.Players[source].DamageThisRound += afterDefense;
|
||
}
|
||
else
|
||
{
|
||
targetState.Health -= afterDefense;
|
||
State.Players[source].DamageThisRound += afterDefense;
|
||
}
|
||
if (target == 0) resolution.DamageToPlayerOne += afterDefense;
|
||
else resolution.DamageToPlayerTwo += afterDefense;
|
||
}
|
||
|
||
private int RollWithMarkedReroll(ReplacedTurnSubmission submission, int slotIndex, out bool rerolled)
|
||
{
|
||
var roll = Random.RollD6();
|
||
rerolled = submission.RerollSlot == slotIndex && roll <= 2;
|
||
return rerolled ? Random.RollD6() : roll;
|
||
}
|
||
|
||
private void ApplyRoundRecoveryAndDiscard()
|
||
{
|
||
for (var i = 0; i < 2; i++)
|
||
{
|
||
var player = State.Players[i];
|
||
player.Cost = Math.Min(10, player.Cost + 1 + player.SpentThisRound / 2);
|
||
var submission = State.Submissions[i];
|
||
if (submission == null || submission.Pass) continue;
|
||
player.NormalDiscard.AddRange(submission.NormalCardIds);
|
||
player.EndDiscard.AddRange(submission.EndCardIds);
|
||
}
|
||
}
|
||
|
||
private void CompleteLastStand()
|
||
{
|
||
var first = State.Players[0];
|
||
var second = State.Players[1];
|
||
if (first.DamageThisRound != second.DamageThisRound)
|
||
Complete(first.DamageThisRound > second.DamageThisRound ? ReplacedWinner.PlayerOne : ReplacedWinner.PlayerTwo);
|
||
else if (first.EffectiveEffectsThisRound != second.EffectiveEffectsThisRound)
|
||
Complete(first.EffectiveEffectsThisRound > second.EffectiveEffectsThisRound ? ReplacedWinner.PlayerOne : ReplacedWinner.PlayerTwo);
|
||
else
|
||
Complete(Random.RollD6() % 2 == 1 ? ReplacedWinner.PlayerOne : ReplacedWinner.PlayerTwo);
|
||
}
|
||
|
||
private void Complete(ReplacedWinner winner)
|
||
{
|
||
State.Winner = winner;
|
||
State.Phase = ReplacedMatchPhase.Completed;
|
||
State.Log.Add("对局结束:" + winner);
|
||
}
|
||
|
||
private void DrawNormal(ReplacedPlayerState player, int count)
|
||
{
|
||
for (var i = 0; i < count && player.NormalHand.Count < 9; i++)
|
||
{
|
||
RecycleIfNeeded(player.NormalDrawPile, player.NormalDiscard);
|
||
if (player.NormalDrawPile.Count == 0) return;
|
||
var last = player.NormalDrawPile.Count - 1;
|
||
player.NormalHand.Add(player.NormalDrawPile[last]);
|
||
player.NormalDrawPile.RemoveAt(last);
|
||
}
|
||
}
|
||
|
||
private void DrawEnd(ReplacedPlayerState player, int count)
|
||
{
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
RecycleIfNeeded(player.EndDrawPile, player.EndDiscard);
|
||
if (player.EndDrawPile.Count == 0) return;
|
||
var last = player.EndDrawPile.Count - 1;
|
||
player.EndHand.Add(player.EndDrawPile[last]);
|
||
player.EndDrawPile.RemoveAt(last);
|
||
}
|
||
}
|
||
|
||
private void RecycleIfNeeded(List<string> draw, List<string> discard)
|
||
{
|
||
if (draw.Count > 0 || discard.Count == 0) return;
|
||
draw.AddRange(discard);
|
||
discard.Clear();
|
||
Random.Shuffle(draw);
|
||
}
|
||
|
||
private static bool ContainsAll(List<string> hand, List<string> requested)
|
||
{
|
||
var available = new List<string>(hand);
|
||
foreach (var id in requested)
|
||
if (!available.Remove(id)) return false;
|
||
return true;
|
||
}
|
||
|
||
private static bool IsPair(ReplacedEffectType a, ReplacedEffectType b, ReplacedEffectType x, ReplacedEffectType y) =>
|
||
(a == x && b == y) || (a == y && b == x);
|
||
|
||
private static ReplacedPlayerState CreatePlayer(string id, string name, ReplacedDeck deck) => new()
|
||
{
|
||
PlayerId = id,
|
||
DisplayName = name,
|
||
NormalDrawPile = new List<string>(deck.NormalCards),
|
||
EndDrawPile = new List<string>(deck.EndCards)
|
||
};
|
||
|
||
private void ValidateDeck(ReplacedDeck deck, string parameter)
|
||
{
|
||
var errors = ReplacedDeckValidator.Validate(deck, Content);
|
||
if (errors.Count > 0)
|
||
throw new ArgumentException(string.Join(", ", errors), parameter);
|
||
}
|
||
|
||
private static void DiscardHands(ReplacedPlayerState player)
|
||
{
|
||
player.NormalDiscard.AddRange(player.NormalHand);
|
||
player.NormalHand.Clear();
|
||
player.EndDiscard.AddRange(player.EndHand);
|
||
player.EndHand.Clear();
|
||
}
|
||
|
||
private ReplacedCommandResult Reject(string code, string message) => new()
|
||
{
|
||
Accepted = false,
|
||
ErrorCode = code,
|
||
Message = message,
|
||
StateHash = ComputeStateHash()
|
||
};
|
||
}
|
||
}
|