using Vanguard.Rules.Cards;
using Vanguard.Rules.Decks;
using Vanguard.Rules.Engine.Abilities;
namespace Vanguard.Rules.Engine;
///
/// Ядро партии. Не знает ни о сети, ни о БД: на вход команды, на выход события.
///
/// Что реализовано и какие есть упрощения:
/// * идеальная защита (Sentinel): щит 0, но при оплате стоимости (сброс карты того же клана)
/// атака полностью отменяется; без оплаты не даёт ничего;
/// * поддержка работает как отдельная способность: карты 0 и 1 ранга могут передать
/// свою силу атакующему Арьергарду из той же колонны, сила действует только в этом бою;
/// * перехват — отдельная способность карт 2 ранга: карта из переднего ряда выходит
/// в круг защиты и при атаке по Авангарду, и при атаке по Арьергарду;
/// атакованный Арьергард не может перехватить атаку на самого себя;
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
/// Арьергард по умолчанию без проверок, но эффект может их выдать;
/// * модификаторы силы и крита хранят источник и срок жизни (бой / ход / постоянно);
/// способности можно выдавать эффектом и снимать по источнику;
/// * эффекты попадания срабатывают после нанесения урона и помещения карты в зону урона;
/// * эффекты не вкладываются друг в друга: триггеры, поднятые во время эффекта,
/// копятся в очереди и разрешаются после него;
/// * непрерывные эффекты пересчитываются при каждом запросе (EffectivePower);
/// * триггер требует выбора целей через ResolveTriggerCommand:
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
/// правило одинаково для drive check и damage check;
/// * порядок проверки: карта → зона триггера → эффекты → рука (drive) или урон (damage);
/// * усиления от триггеров живут до конца хода;
/// * атаковать можно Авангард или любой круг Арьергарда: деления на передний
/// и задний ряд при выборе цели пока нет (поэтому Tejas не выделяется);
/// * задний ряд защищён только перехватом, отдельного правила «атакуют лишь передний ряд» нет.
///
public sealed class GameEngine
{
public const int StartingHand = 5;
public const int MaxDamage = 6;
public const int TriggerPowerBonus = 5000;
private readonly List _events = new();
private readonly IReadOnlyDictionary _scripts;
public GameEngine(
CardCatalog catalog,
ulong seed,
IReadOnlyList deck0,
IReadOnlyList deck1,
IReadOnlyDictionary? scripts = null,
string player0Name = "Player 1",
string player1Name = "Player 2")
{
var violations = DeckValidator.Validate(catalog, deck0.ToList())
.Concat(DeckValidator.Validate(catalog, deck1.ToList()))
.ToList();
if (violations.Count > 0)
throw new InvalidOperationException(
"Некорректная колода: " + string.Join("; ", violations.Select(v => v.Message)));
_scripts = scripts ?? CardScripts.Default;
State = new GameState(catalog, seed, player0Name, player1Name);
FillDeck(0, deck0);
FillDeck(1, deck1);
}
public GameState State { get; }
public IReadOnlyList Events => _events;
public IReadOnlyDictionary Scripts => _scripts;
/// Глубина разрешения эффекта: пока > 0, новые триггеры только ставятся в очередь.
private int _resolvingDepth;
private int _counterBlastDisplayIndex = -1;
///
/// Сколько проверок делает атакующая карта. Эффект может изменить число;
/// Арьергард по умолчанию проверок не делает.
///
public int DriveChecksFor(int slot, CardInstance attacker)
{
if (attacker.DriveChecksOverride is { } overridden)
return Math.Max(0, overridden);
return slot < 0 ? State.Catalog.CardOf(attacker.PrintingId).DefaultDriveChecks : 0;
}
// ---------- способности ----------
private void TriggerFromCard(int owner, CardInstance? source, AbilityTrigger trigger, CardInstance? other)
{
if (source is null)
return;
var context = new AbilityContext
{
Engine = this,
Source = source,
Owner = owner,
Other = other,
Phase = State.Phase,
};
var triggered = AbilitiesOf(source)
.Where(a => a.Trigger == trigger && (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context })
.ToList();
if (triggered.Count == 0)
return;
RaiseBatch(triggered);
}
/// Печатные способности карты плюс выданные эффектами.
private IEnumerable AbilitiesOf(CardInstance unit)
{
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
if (_scripts.TryGetValue(nameId, out var script))
{
foreach (var ability in script.Abilities)
yield return ability;
}
foreach (var granted in unit.GrantedAbilities)
yield return granted.Definition;
}
private void RaiseBatch(List triggered)
{
if (triggered.Count == 0)
return;
// внутри другого эффекта триггеры только копятся
if (_resolvingDepth > 0)
{
State.PendingQueue ??= new List();
State.PendingQueue.AddRange(triggered);
return;
}
if (triggered.Count == 1)
{
OfferAbility(triggered[0]);
return;
}
State.PendingOrder = triggered;
State.Step = Step.Ability;
State.ActivePlayer = triggered[0].Owner;
}
/// Собирает срабатывания со всех карт на поле, активный игрок первым.
private void TriggerAll(AbilityTrigger trigger) {
var triggered = new List();
for (var owner = 0; owner < 2; owner++)
{
foreach (var unit in AllUnits(owner))
{
var context = new AbilityContext
{
Engine = this,
Source = unit,
Owner = owner,
Phase = State.Phase,
};
triggered.AddRange(AbilitiesOf(unit)
.Where(a => a.Trigger == trigger && (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context }));
}
}
if (triggered.Count == 0)
return;
triggered = triggered
.OrderBy(t => t.Owner == State.ActivePlayer ? 0 : 1)
.ToList();
RaiseBatch(triggered);
}
///
/// «Когда Арьергард противника ушёл в сброс»: срабатывания карт на поле
/// противоположной стороны. В руке такие способности пока не ищутся.
///
private void TriggerOpponentRetire(int retiredOwner)
{
var owner = 1 - retiredOwner;
var triggered = new List();
foreach (var unit in AllUnits(owner))
{
var context = new AbilityContext
{
Engine = this,
Source = unit,
Owner = owner,
Phase = State.Phase,
};
triggered.AddRange(AbilitiesOf(unit)
.Where(a => a.Trigger == AbilityTrigger.OnOpponentRetire
&& (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context }));
}
// способности из руки (Yaksha): «покажи и соверши райд»
foreach (var card in State.Players[owner].Hand)
{
var context = new AbilityContext
{
Engine = this,
Source = card,
Owner = owner,
Phase = State.Phase,
};
triggered.AddRange(AbilitiesOf(card)
.Where(a => a.Trigger == AbilityTrigger.OnOpponentRetire
&& (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context }));
}
if (triggered.Count == 0)
return;
RaiseBatch(triggered);
}
private void EnterMainPhase()
{
State.Phase = Phase.Main;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Main));
TriggerAll(AbilityTrigger.OnPhaseStart);
}
private enum OfferResult
{
Skipped,
Paused,
Resolved,
}
private OfferResult OfferAbility(TriggeredAbility ability)
{
var definition = ability.Definition;
// если стоимость оплатить нельзя, способность не предлагается
if (definition.CanPay is not null && !definition.CanPay(ability.Context))
return OfferResult.Skipped;
_events.Add(new AbilityTriggeredEvent(ability.Owner, ability.Context.Source.PrintingId, definition.Trigger));
if (definition.Optional)
{
State.PendingAbility = ability;
State.Step = Step.Ability;
State.ActivePlayer = ability.Owner;
return OfferResult.Paused;
}
ResolveAbility(ability, accepted: true);
return OfferResult.Resolved;
}
private void ResolveAbility(TriggeredAbility ability, bool accepted)
{
if (accepted)
{
_resolvingDepth++;
try
{
ability.Definition.Pay?.Invoke(ability.Context);
ability.Definition.Resolve(ability.Context);
}
finally
{
_resolvingDepth--;
}
}
_events.Add(new AbilityResolvedEvent(ability.Owner, ability.Context.Source.PrintingId, accepted));
// способность попросила выбрать цель — ждём ChooseCommand
if (State.Step == Step.Choice)
return;
ContinueOrder();
}
private void ContinueOrder()
{
// сначала доигрываем триггеры, накопившиеся во время эффекта
if (State.PendingQueue is { Count: > 0 } queued
&& State.PendingOrder is null
&& State.PendingAbility is null)
{
var batch = queued.ToList();
State.PendingQueue = null;
RaiseBatch(batch);
return;
}
if (State.PendingOrder is { Count: > 0 } order)
{
if (order.Count == 1)
{
var next = order[0];
State.PendingOrder = null;
State.ActivePlayer = next.Owner;
if (OfferAbility(next) == OfferResult.Skipped)
ContinueOrder();
return;
}
State.Step = Step.Ability;
State.ActivePlayer = order[0].Owner;
return;
}
State.Step = Step.None;
if (State.Continuation is { } continuation)
{
State.Continuation = null;
continuation();
}
}
private CommandResult Activate(int instanceId, string? discardPrintingId, int damageIndex)
{
if (State.Phase != Phase.Main)
return CommandResult.Reject("Активируемые способности доступны только в Основной фазе.");
if (State.Step != Step.None || State.PendingAbility is not null || State.PendingChoice is not null)
return CommandResult.Reject("Сейчас нельзя активировать способности.");
var player = State.ActivePlayer;
var p = State.Players[player];
var unit = AllUnits(player).FirstOrDefault(u => u.Id == instanceId);
var fromHand = false;
var fromDrop = false;
if (unit is null)
{
unit = p.Hand.FirstOrDefault(c => c.Id == instanceId);
fromHand = unit is not null;
}
if (unit is null)
{
unit = p.Drop.FirstOrDefault(c => c.Id == instanceId);
fromDrop = unit is not null;
}
if (unit is null)
return CommandResult.Reject("Карта не на поле, в руке или в сбросе.");
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
if (!_scripts.TryGetValue(nameId, out var script))
return CommandResult.Reject("У карты нет способностей.");
var context = new AbilityContext
{
Engine = this,
Source = unit,
Owner = player,
Phase = State.Phase,
};
var candidates = script.Activated
.Where(a =>
(fromHand ? a.FromHand : fromDrop ? a.FromDropZone : !a.FromHand && !a.FromDropZone)
&& a.CanActivate(context))
.ToList();
var ability = discardPrintingId is null
? candidates.FirstOrDefault(a => !a.RequiresDiscard)
: candidates.FirstOrDefault(a => a.RequiresDiscard);
if (ability is null)
{
if (discardPrintingId is null && candidates.Any(a => a.RequiresDiscard))
return CommandResult.Reject("Нужно выбрать карту для сброса.");
if (discardPrintingId is not null && candidates.Count > 0)
return CommandResult.Reject("Эта способность не требует сброса.");
return CommandResult.Reject("Нет доступной способности.");
}
if ((fromHand || fromDrop) && (ability.RestsSelf || ability.RetiresSelf))
return CommandResult.Reject("Способность из этой зоны не оплачивается поворотом или уничтожением.");
if (ability.RestsSelf && unit.Rested)
return CommandResult.Reject("Карта уже повёрнута.");
CardInstance? discard = null;
if (ability.RequiresDiscard)
{
if (discardPrintingId is null)
return CommandResult.Reject("Нужно выбрать карту для сброса.");
var index = p.Hand.FindIndex(c => c.PrintingId == discardPrintingId);
if (index < 0)
return CommandResult.Reject("Карты для сброса нет в руке.");
discard = p.Hand[index];
}
if (ability.RestsSelf)
unit.Rested = true;
if (discard is not null)
{
p.Hand.Remove(discard);
p.Drop.Add(discard);
}
if (ability.RetiresSelf)
{
var slot = Array.IndexOf(p.RearGuards, unit);
if (slot < 0)
return CommandResult.Reject("Уничтожить себя можно только из круга Арьергарда.");
p.RearGuards[slot] = null;
p.Drop.Add(unit);
_events.Add(new UnitRetiredEvent(player, slot));
}
_counterBlastDisplayIndex = damageIndex;
try
{
ability.Resolve(context);
}
finally
{
_counterBlastDisplayIndex = -1;
}
_events.Add(new AbilityResolvedEvent(player, unit.PrintingId, true));
return CommandResult.Ok();
}
private CommandResult Choose(int index)
{
var choice = State.PendingChoice;
if (choice is null || State.Step != Step.Choice)
return CommandResult.Reject("Сейчас нет выбора.");
if (State.ActivePlayer != choice.Player)
return CommandResult.Reject("Сейчас выбирает другой игрок.");
if (index < 0 || index >= choice.Options.Count)
return CommandResult.Reject("Некорректный вариант выбора.");
var option = choice.Options[index];
State.PendingChoice = null;
State.Step = Step.None;
_resolvingDepth++;
try
{
choice.Resolve(option.Slot);
}
finally
{
_resolvingDepth--;
}
if (State.Step != Step.Choice)
ContinueOrder();
return CommandResult.Ok();
}
private CommandResult SelectTrigger(int index)
{
var order = State.PendingOrder;
if (order is null || State.Step != Step.Ability)
return CommandResult.Reject("Сейчас нет выбора порядка триггеров.");
if (index < 0 || index >= order.Count)
return CommandResult.Reject("Некорректный индекс.");
if (order[index].Owner != State.ActivePlayer)
return CommandResult.Reject("Сейчас выбирает другой игрок.");
var chosen = order[index];
order.RemoveAt(index);
if (order.Count == 0)
State.PendingOrder = null;
if (OfferAbility(chosen) == OfferResult.Skipped)
ContinueOrder();
return CommandResult.Ok();
}
private CommandResult ResolvePendingAbility(bool accept, int damageIndex)
{
var ability = State.PendingAbility;
if (ability is null || State.Step != Step.Ability)
return CommandResult.Reject("Сейчас нет способности, ожидающей решения.");
State.PendingAbility = null;
_counterBlastDisplayIndex = damageIndex;
try
{
ResolveAbility(ability, accept);
}
finally
{
_counterBlastDisplayIndex = -1;
}
return CommandResult.Ok();
}
/// Сила карты с учётом всех модификаторов и непрерывных эффектов.
public int EffectivePower(CardInstance unit)
{
var total = State.Catalog.CardOf(unit.PrintingId).Power;
total += unit.Modifiers.Sum(m => m.Power);
return total + ContinuousPower(unit);
}
public int EffectiveCritical(CardInstance unit) =>
State.Catalog.CardOf(unit.PrintingId).Critical + unit.Modifiers.Sum(m => m.Critical);
private int ContinuousPower(CardInstance unit)
{
var owner = OwnerOf(unit);
if (owner < 0)
return 0;
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
if (!_scripts.TryGetValue(nameId, out var script))
return 0;
var context = new AbilityContext
{
Engine = this,
Source = unit,
Owner = owner,
Phase = State.Phase,
};
return script.Continuous
.Where(c => c.Applies(context))
.Sum(c => c.PowerBonus?.Invoke(context) ?? 0);
}
public int OwnerOf(CardInstance unit)
{
for (var i = 0; i < 2; i++)
{
if (ReferenceEquals(State.Players[i].Vanguard, unit))
return i;
if (State.Players[i].RearGuards.Any(u => ReferenceEquals(u, unit)))
return i;
}
return -1;
}
public bool IsVanguardOf(CardInstance unit, int player) =>
ReferenceEquals(State.Players[player].Vanguard, unit);
public int CountRearGuards(int player, Clan clan) =>
State.Players[player].RearGuards.Count(u =>
u is not null && State.Catalog.CardOf(u.PrintingId).Clan == clan);
/// Призыв карты из души — используется способностями вроде Barcgal.
public bool CallFromSoul(CardInstance card, int player)
{
var p = State.Players[player];
if (!p.Soul.Contains(card))
return false;
var slot = Array.FindIndex(p.RearGuards, u => u is null);
if (slot < 0)
return false;
p.Soul.Remove(card);
card.Rested = false;
p.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
return true;
}
/// Бонус к силе текущего боя от способности карты.
public void AddBattlePower(CardInstance source, int amount) =>
AddBattlePower(source, amount, source);
public void AddBattlePower(CardInstance target, int amount, CardInstance source) =>
AddPower(target, amount, source, "ability", ModifierDuration.UntilEndOfBattle);
/// Добавить модификатор силы и крита с указанием источника и срока жизни.
public void AddPower(
CardInstance unit,
int power,
CardInstance source,
string effectId,
ModifierDuration duration,
int critical = 0)
{
unit.Modifiers.Add(new StatModifier
{
Power = power,
Critical = critical,
Source = source,
EffectId = effectId,
Duration = duration,
});
}
/// Выдать карте способность, которой нет в её тексте.
public void GrantAbility(
CardInstance unit,
AbilityDefinition definition,
CardInstance source,
string effectId,
ModifierDuration duration)
{
unit.GrantedAbilities.Add(new GrantedAbility
{
Definition = definition,
Source = source,
EffectId = effectId,
Duration = duration,
});
}
/// Снять способности, выданные конкретным эффектом.
public void RemoveGrantedAbilities(Func predicate)
{
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
unit.GrantedAbilities.RemoveAll(g => predicate(g));
}
/// Запретить карте действие с указанием источника и срока жизни.
public void AddRestriction(
CardInstance unit,
RestrictionType type,
CardInstance source,
string effectId,
ModifierDuration duration)
{
unit.Restrictions.Add(new CardRestriction
{
Type = type,
Source = source,
EffectId = effectId,
Duration = duration,
});
}
/// Снять запреты, подходящие под условие.
public void RemoveRestrictions(Func predicate)
{
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
unit.Restrictions.RemoveAll(r => predicate(r));
}
// ---------- стоимости ----------
public int FaceDownDamage(int player) =>
State.Players[player].Damage.Count(c => !c.FaceUp);
public bool CanCounterBlast(int player, int amount) =>
FaceDownDamage(player) >= amount;
public void CounterBlast(int player, int amount)
{
var damage = State.Players[player].Damage;
var flipped = 0;
// Клиент передаёт индекс в том же порядке, в котором рисует урон:
// сначала закрытые, затем открытые карты.
if (_counterBlastDisplayIndex >= 0)
{
var order = DamageDisplayOrder(player);
if (_counterBlastDisplayIndex < order.Count)
{
var chosen = order[_counterBlastDisplayIndex].Card;
if (!chosen.FaceUp)
{
chosen.FaceUp = true;
flipped++;
}
}
_counterBlastDisplayIndex = -1;
}
foreach (var card in damage)
{
if (flipped >= amount)
break;
if (card.FaceUp)
continue;
card.FaceUp = true;
flipped++;
}
}
public bool CanSoulBlast(int player, int amount) =>
State.Players[player].Soul.Count >= amount;
public void SoulCharge(int player, int amount)
{
var p = State.Players[player];
for (var i = 0; i < amount; i++)
{
if (p.Deck.Count == 0)
{
Defeat(player);
return;
}
var card = p.Deck[0];
p.Deck.RemoveAt(0);
p.Soul.Add(card);
}
}
public void SoulBlast(int player, int amount)
{
var soul = State.Players[player].Soul;
for (var i = 0; i < amount && soul.Count > 0; i++)
{
var card = soul[0];
soul.RemoveAt(0);
State.Players[player].Drop.Add(card);
}
}
// ---------- выбор целей ----------
/// Круги Арьергарда игрока, подходящие под фильтр.
public IReadOnlyList RearGuardSlots(int player, Func predicate)
{
var slots = new List();
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
{
var unit = State.Players[player].RearGuards[slot];
if (unit is null)
continue;
if (predicate(State.Catalog.CardOf(unit.PrintingId)))
slots.Add(slot);
}
return slots;
}
/// Останавливает разрешение и просит игрока выбрать карту на поле.
public void AskUnitChoice(
int chooser,
int targetPlayer,
string prompt,
IReadOnlyList slots,
Action onChosen)
{
var options = slots
.Select(slot =>
{
var unit = State.GetUnit(targetPlayer, slot)!;
return new ChoiceOption(slot, State.Catalog.CardOf(unit.PrintingId).NameEn);
})
.ToList();
State.PendingChoice = new PendingChoice
{
Player = chooser,
Prompt = prompt,
Options = options,
Resolve = onChosen,
};
State.Step = Step.Choice;
State.ActivePlayer = chooser;
}
///
/// Выбор от min до max целей на поле. Игрок выбирает по одной; когда минимум
/// набран, появляется вариант «Готово». Выбранные цели приходят одним списком.
///
public void AskUnitMultiChoice(
int chooser,
string prompt,
IReadOnlyList targets,
int min,
int max,
Action> onChosen)
{
var chosen = new List();
void Ask()
{
var remaining = targets.Where(t => !chosen.Contains(t)).ToList();
if (remaining.Count == 0 || chosen.Count >= max)
{
onChosen(chosen.ToList());
return;
}
var options = remaining
.Select((target, index) =>
{
var unit = State.GetUnit(target.Player, target.Slot)!;
return new ChoiceOption(index, State.Catalog.CardOf(unit.PrintingId).NameEn);
})
.ToList();
// «Готово» появляется только когда минимум уже набран
var stopIndex = -1;
if (chosen.Count >= min)
{
stopIndex = options.Count;
options.Add(new ChoiceOption(stopIndex, "Готово"));
}
var currentPrompt = chosen.Count == 0
? prompt
: $"{prompt} (выбрано: {chosen.Count})";
State.PendingChoice = new PendingChoice
{
Player = chooser,
Prompt = currentPrompt,
Options = options,
Resolve = index =>
{
if (index == stopIndex)
{
onChosen(chosen.ToList());
return;
}
chosen.Add(remaining[index]);
Ask();
},
};
State.Step = Step.Choice;
State.ActivePlayer = chooser;
}
Ask();
}
public void RetireRearGuard(int player, int slot)
{
var unit = State.Players[player].RearGuards[slot];
if (unit is null)
return;
State.Players[player].RearGuards[slot] = null;
State.Players[player].Drop.Add(unit);
_events.Add(new UnitRetiredEvent(player, slot));
TriggerFromCard(player, unit, AbilityTrigger.OnRetireFromRC, null);
TriggerOpponentRetire(player);
}
// ---------- эффекты карт ----------
public int HandCount(int player) => State.Players[player].Hand.Count;
public bool HasInSoul(int player, string nameId) =>
State.Players[player].Soul.Any(c => State.Catalog.CardOf(c.PrintingId).NameId == nameId);
public bool HasInDrop(int player, string nameId) =>
State.Players[player].Drop.Any(c => State.Catalog.CardOf(c.PrintingId).NameId == nameId);
/// Сколько карт клана на поле, не считая указанную.
public int CountFieldUnits(int player, Clan clan, CardInstance? except = null) =>
AllUnits(player).Count(u =>
!ReferenceEquals(u, except)
&& State.Catalog.CardOf(u.PrintingId).Clan == clan);
public bool AllUnitsRested(int player) => AllUnits(player).All(u => u.Rested);
/// Перевернуть всю зону урона лицом вверх.
public void TurnAllDamageFaceUp(int player)
{
foreach (var card in State.Players[player].Damage)
card.FaceUp = true;
}
/// Перевернуть одну закрытую карту урона лицом вверх. false — закрытых нет.
public bool TurnOneDamageFaceUp(int player)
{
var card = State.Players[player].Damage.FirstOrDefault(c => !c.FaceUp);
if (card is null)
return false;
card.FaceUp = true;
return true;
}
public void RetireAllRearGuards(int player)
{
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
RetireRearGuard(player, slot);
}
public void StandAll(int player)
{
foreach (var unit in AllUnits(player))
unit.Rested = false;
}
/// Вернуть выбранную карту из руки в колоду и перемешать.
public void ReturnHandCardToDeck(int player, string prompt)
{
if (State.Players[player].Hand.Count == 0)
return;
AskHandCardChoice(player, prompt, null, card =>
{
var p = State.Players[player];
p.Hand.Remove(card);
p.Deck.Add(card);
ShuffleDeck(player);
_events.Add(new CardReturnedToDeckEvent(player, card.PrintingId));
});
}
/// Противник выбирает карту в руке и сбрасывает её.
public void OpponentDiscardsChosen(int player, string prompt)
{
var p = State.Players[player];
if (p.Hand.Count == 0)
return;
AskHandCardChoice(player, prompt, null, card =>
{
p.Hand.Remove(card);
p.Drop.Add(card);
_events.Add(new CardDiscardedEvent(player, card.PrintingId));
});
}
/// Положить верхние карты колоды в сброс.
public void MillTop(int player, int count)
{
var p = State.Players[player];
for (var i = 0; i < count && p.Deck.Count > 0; i++)
{
var card = p.Deck[0];
p.Deck.RemoveAt(0);
p.Drop.Add(card);
}
}
/// Переместить карту из сброса в душу (Rock the Wall, Redshoe Milly).
public bool MoveDropToSoul(int player, CardInstance card)
{
var p = State.Players[player];
if (!p.Drop.Remove(card))
return false;
p.Soul.Add(card);
_events.Add(new CardMovedToSoulEvent(player, card.PrintingId));
return true;
}
/// Вернуть карту с поля в колоду и перемешать. Авангард так вернуть нельзя.
public bool ReturnUnitToDeck(int player, CardInstance unit)
{
var p = State.Players[player];
var slot = Array.IndexOf(p.RearGuards, unit);
if (slot < 0)
return false;
p.RearGuards[slot] = null;
unit.ReturnToDeck = null;
p.Deck.Add(unit);
ShuffleDeck(player);
_events.Add(new UnitReturnedToDeckEvent(player, unit.PrintingId));
return true;
}
// ---------- поиск в колоде ----------
///
/// Серверный поиск: игрок не видит колоду. Найдена одна карта — сразу передаётся
/// в onFound; несколько — предлагается выбор (разные арты считаются разными картами);
/// ни одной — уведомление. После поиска колода всегда перемешивается.
///
public void SearchDeck(
int player,
string prompt,
Func predicate,
Action onFound)
{
var deck = State.Players[player].Deck;
var matches = deck
.Where(c => predicate(State.Catalog.CardOf(c.PrintingId)))
.ToList();
if (matches.Count == 0)
{
_events.Add(new SearchFailedEvent(player, prompt));
ShuffleDeck(player);
return;
}
if (matches.Count == 1)
{
CompleteSearch(player, matches[0], onFound);
return;
}
AskCardChoice(player, prompt, matches, chosen => CompleteSearch(player, chosen, onFound));
}
private void CompleteSearch(int player, CardInstance card, Action onFound)
{
State.Players[player].Deck.Remove(card);
onFound(card);
_events.Add(new CardSearchedEvent(player, card.PrintingId));
ShuffleDeck(player);
}
/// Поиск с добавлением карты в руку.
public void SearchDeckToHand(int player, string prompt, Func predicate) =>
SearchDeck(player, prompt, predicate, card =>
{
_events.Add(new CardRevealedEvent(player, card.PrintingId));
State.Players[player].Hand.Add(card);
});
/// Поиск с призывом в выбранный круг Арьергарда.
public void SearchDeckToCall(int player, string prompt, Func predicate)
{
SearchDeck(player, prompt, predicate, card =>
{
_events.Add(new CardRevealedEvent(player, card.PrintingId));
// призывать некуда — карта возвращается в руку, а не теряется
if (!HasEmptyRearGuard(player))
{
State.Players[player].Hand.Add(card);
return;
}
State.InTransit.Add((player, card));
AskEmptyRearGuardChoice(player, "Выберите круг Арьергарда", slot =>
{
State.InTransit.Remove((player, card));
CallFromDeck(card, player, slot);
});
});
}
/// Поиск с райдом найденной карты (superior ride).
public void SearchDeckToRide(int player, string prompt, Func predicate)
{
SearchDeck(player, prompt, predicate, card =>
{
_events.Add(new CardRevealedEvent(player, card.PrintingId));
RideFromDeck(card, player);
});
}
/// Посмотреть верхнюю карту и оставить её на верху или отправить в низ.
public void LookTopAndReorder(int player, string prompt)
{
var deck = State.Players[player].Deck;
if (deck.Count == 0)
return;
var top = deck[0];
State.PendingChoice = new PendingChoice
{
Player = player,
Prompt = $"{prompt}: {State.Catalog.CardOf(top.PrintingId).NameEn}",
Options = new List
{
new(0, "Оставить на верху"),
new(1, "Положить в низ"),
},
Resolve = index =>
{
if (index == 1)
{
deck.RemoveAt(0);
deck.Add(top);
}
_events.Add(new CardRevealedEvent(player, top.PrintingId));
},
};
State.Step = Step.Choice;
State.ActivePlayer = player;
}
public bool HasEmptyRearGuard(int player) =>
State.Players[player].RearGuards.Any(u => u is null);
/// Выбор карты из руки (стоимость, сброс, цель эффекта).
public void AskHandCardChoice(
int player,
string prompt,
Func? predicate,
Action onChosen)
{
var cards = State.Players[player].Hand
.Where(c => predicate is null || predicate(c))
.ToList();
if (cards.Count == 0)
return;
State.PendingChoice = new PendingChoice
{
Player = player,
Prompt = prompt,
Options = cards
.Select((card, index) => new ChoiceOption(index, State.Catalog.CardOf(card.PrintingId).NameEn))
.ToList(),
Resolve = index => onChosen(cards[index]),
};
State.Step = Step.Choice;
State.ActivePlayer = player;
}
public void AskEmptyRearGuardChoice(int player, string prompt, Action onChosen)
{
var slots = Enumerable.Range(0, PlayerState.RearGuardSlots)
.Where(s => State.Players[player].RearGuards[s] is null)
.ToList();
if (slots.Count == 0)
return;
State.PendingChoice = new PendingChoice
{
Player = player,
Prompt = prompt,
Options = slots.Select(s => new ChoiceOption(s, $"Круг {s + 1}")).ToList(),
Resolve = onChosen,
};
State.Step = Step.Choice;
State.ActivePlayer = player;
}
public void CallFromDeck(CardInstance card, int player, int slot)
{
var p = State.Players[player];
if (p.RearGuards[slot] is not null)
{
p.Hand.Add(card);
return;
}
card.Rested = false;
p.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
TriggerFromCard(player, card, AbilityTrigger.OnPlace, null);
}
/// Райд картой не из руки (superior ride).
public void RideFromDeck(CardInstance card, int player) => RideUnit(card, player);
/// Райд картой из сброса (Monster Frank).
public void RideFromDrop(CardInstance card, int player)
{
if (!State.Players[player].Drop.Remove(card))
return;
RideUnit(card, player);
}
/// Райд картой из руки по эффекту (Demonic Dragon Berserker, Yaksha).
public void RideFromHand(CardInstance card, int player)
{
if (!State.Players[player].Hand.Remove(card))
return;
RideUnit(card, player);
}
public bool IsInHand(int player, CardInstance card) =>
State.Players[player].Hand.Contains(card);
private void RideUnit(CardInstance card, int player)
{
var p = State.Players[player];
var riddenOver = p.Vanguard;
if (riddenOver is not null)
{
riddenOver.Rested = false;
p.Soul.Add(riddenOver);
}
card.Rested = false;
p.Vanguard = card;
_events.Add(new UnitRodeEvent(player, card.PrintingId));
TriggerFromCard(player, card, AbilityTrigger.OnPlace, null);
if (State.Step == Step.None)
TriggerFromCard(player, riddenOver, AbilityTrigger.OnRideOver, card);
}
///
/// Останавливает разрешение и просит выбрать карту из найденных.
/// Варианты различаются номером принта, поэтому два Blaster Blade с разными артами — два варианта.
///
public void AskCardChoice(
int player,
string prompt,
IReadOnlyList cards,
Action onChosen)
{
var options = cards
.Select((card, index) => new ChoiceOption(
index,
$"{State.Catalog.CardOf(card.PrintingId).NameEn} ({card.PrintingId})"))
.ToList();
State.PendingChoice = new PendingChoice
{
Player = player,
Prompt = prompt,
Options = options,
Resolve = index => onChosen(cards[index]),
};
State.Step = Step.Choice;
State.ActivePlayer = player;
}
public Clan VanguardClan(int player)
{
var vanguard = State.Players[player].Vanguard;
return vanguard is null ? Clan.RoyalPaladin : State.Catalog.CardOf(vanguard.PrintingId).Clan;
}
public int VanguardGrade(int player) =>
State.Players[player].Vanguard is { } vanguard
? State.Catalog.CardOf(vanguard.PrintingId).Grade
: 0;
/// Есть ли на своих кругах Арьергарда по одной карте каждого имени.
public bool HasFieldUnits(int player, IReadOnlyList nameIds)
{
var slots = new HashSet();
foreach (var nameId in nameIds)
{
var found = false;
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
{
if (slots.Contains(slot))
continue;
var unit = State.Players[player].RearGuards[slot];
if (unit is null || State.Catalog.CardOf(unit.PrintingId).NameId != nameId)
continue;
slots.Add(slot);
found = true;
break;
}
if (!found)
return false;
}
return true;
}
/// Положить по одной карте каждого имени со своих кругов в душу.
public bool PutFieldUnitsToSoul(int player, IReadOnlyList nameIds)
{
if (!HasFieldUnits(player, nameIds))
return false;
var p = State.Players[player];
var used = new HashSet();
foreach (var nameId in nameIds)
{
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
{
if (used.Contains(slot))
continue;
var unit = p.RearGuards[slot];
if (unit is null || State.Catalog.CardOf(unit.PrintingId).NameId != nameId)
continue;
used.Add(slot);
p.RearGuards[slot] = null;
p.Soul.Add(unit);
_events.Add(new CardMovedToSoulEvent(player, unit.PrintingId));
break;
}
}
return true;
}
// ---------- легальные интенты ----------
///
/// Все команды, которые сейчас имеют смысл. Нужны ботам, тестам и будущему UI.
/// Список может быть неполным там, где команда требует дополнительного выбора
/// (например, сброс карты как стоимость активируемой способности).
///
public IReadOnlyList LegalCommands()
{
// Защита от состояния «бой уже начался, но очередь эффектов оставила
// continuation невыполненным». Иначе клиент получает EndTurn,
// который движок затем отклоняет.
if (State.Step == Step.None && State.Battle is not null
&& State.Continuation is not null)
{
ContinueOrder();
}
var commands = new List();
if (State.IsFinished)
return commands;
if (State.PendingChoice is { } choice)
{
for (var i = 0; i < choice.Options.Count; i++)
commands.Add(new ChooseCommand(i));
return commands;
}
if (State.PendingAbility is not null)
{
commands.Add(new ResolveAbilityCommand(true));
commands.Add(new ResolveAbilityCommand(false));
return commands;
}
if (State.PendingOrder is { Count: > 0 } order)
{
for (var i = 0; i < order.Count; i++)
{
if (order[i].Owner == State.ActivePlayer)
commands.Add(new SelectTriggerCommand(i));
}
return commands;
}
if (State.PendingTrigger is { } pending)
{
var hasRearGuard = State.Players[pending.Player].RearGuards.Any(u => u is not null);
foreach (var powerSlot in UnitSlots(pending.Player))
{
if (pending.Trigger is TriggerType.Critical or TriggerType.Stand)
{
foreach (var effectSlot in UnitSlots(pending.Player))
{
if (pending.Trigger == TriggerType.Stand && effectSlot < 0 && hasRearGuard)
continue;
commands.Add(new ResolveTriggerCommand(powerSlot, effectSlot));
}
}
else
{
commands.Add(new ResolveTriggerCommand(powerSlot, -1));
}
}
return commands;
}
var player = State.ActivePlayer;
var p = State.Players[player];
switch (State.Phase)
{
case Phase.Mulligan:
commands.Add(new FinishMulliganCommand());
break;
case Phase.Ride:
foreach (var printingId in p.Hand.Select(c => c.PrintingId).Distinct())
{
if (State.Catalog.CardOf(printingId).Grade == VanguardGrade(player) + 1)
commands.Add(new RideCommand(printingId));
}
commands.Add(new SkipRideCommand());
break;
case Phase.Main:
AddMainCommands(commands, player);
break;
case Phase.Battle:
AddBattleCommands(commands, player);
break;
}
return commands;
}
private void AddMainCommands(List commands, int player)
{
var p = State.Players[player];
var maxGrade = VanguardGrade(player);
var emptySlots = Enumerable.Range(0, PlayerState.RearGuardSlots)
.Where(s => p.RearGuards[s] is null)
.ToList();
foreach (var printingId in p.Hand.Select(c => c.PrintingId).Distinct())
{
if (State.Catalog.CardOf(printingId).Grade > maxGrade)
continue;
foreach (var slot in emptySlots)
commands.Add(new CallCommand(printingId, slot));
}
for (var from = 0; from < PlayerState.RearGuardSlots; from++)
{
if (p.RearGuards[from] is null)
continue;
for (var to = 0; to < PlayerState.RearGuardSlots; to++)
{
if (to != from && SameColumn(from, to))
commands.Add(new MoveCommand(from, to));
}
}
foreach (var unit in AllUnits(player))
AddActivatedCommands(commands, unit, player, fromHand: false, fromDrop: false);
foreach (var card in p.Hand)
AddActivatedCommands(commands, card, player, fromHand: true, fromDrop: false);
foreach (var card in p.Drop)
AddActivatedCommands(commands, card, player, fromHand: false, fromDrop: true);
commands.Add(new EnterBattleCommand());
commands.Add(new EndTurnCommand());
}
private void AddBattleCommands(List commands, int player)
{
var p = State.Players[player];
if (State.Step == Step.None)
{
foreach (var attackerSlot in UnitSlots(player))
{
// Задний ряд не атакует; -1 — Вэнгард, 3/4 — передний ряд.
if (attackerSlot is 0 or 1 or 2)
continue;
var attacker = State.GetUnit(player, attackerSlot);
if (attacker is null || attacker.Rested)
continue;
if (State.IsRestraint(attacker)
|| State.HasRestriction(attacker, RestrictionType.CannotAttack))
continue;
var targets = new List { -1 };
var canSnipe = State.Catalog.CardOf(attacker.PrintingId).CanSnipeBackRow;
targets.AddRange(UnitSlots(1 - player).Where(slot => slot >= 0
&& (slot is 3 or 4
|| (canSnipe
&& attackerSlot is 3 or 4
&& SameColumn(attackerSlot, slot)))));
foreach (var target in targets)
commands.Add(new AttackCommand(attackerSlot, target));
if (attackerSlot is 3 or 4)
{
var boosterSlot = attackerSlot == 3 ? 0 : 2;
var booster = State.GetUnit(player, boosterSlot);
if (booster is not null && !booster.Rested && State.CanBoost(booster))
{
foreach (var target in targets)
commands.Add(new AttackCommand(attackerSlot, target, boosterSlot));
}
}
}
commands.Add(new EndTurnCommand());
return;
}
if (State.Step != Step.Guard || State.Battle is null)
return;
foreach (var printingId in p.Hand.Select(c => c.PrintingId).Distinct())
{
var def = State.Catalog.CardOf(printingId);
if (def.IsSentinel)
{
commands.Add(new GuardCommand(printingId));
foreach (var discardId in p.Hand
.Where(c => c.PrintingId != printingId
&& State.Catalog.CardOf(c.PrintingId).Clan == def.Clan)
.Select(c => c.PrintingId)
.Distinct())
{
commands.Add(new GuardCommand(printingId, discardId));
}
}
else if (def.Shield > 0)
{
commands.Add(new GuardCommand(printingId));
}
}
foreach (var slot in Enumerable.Range(0, PlayerState.RearGuardSlots))
{
if (State.Battle.Interceptor is not null)
break;
if (slot == State.Battle.TargetSlot)
continue;
var unit = p.RearGuards[slot];
if (unit is null || unit.Rested)
continue;
if (slot is not (3 or 4) && !unit.InterceptFromBackRow)
continue;
if (!State.CanIntercept(unit))
continue;
commands.Add(new InterceptCommand(slot));
}
commands.Add(new PassGuardCommand());
}
private void AddActivatedCommands(
List commands,
CardInstance card,
int player,
bool fromHand,
bool fromDrop)
{
var nameId = State.Catalog.CardOf(card.PrintingId).NameId;
if (!_scripts.TryGetValue(nameId, out var script))
return;
var context = new AbilityContext
{
Engine = this,
Source = card,
Owner = player,
Phase = State.Phase,
};
var isOnRearGuard = !fromHand && !fromDrop
&& Array.IndexOf(State.Players[player].RearGuards, card) >= 0;
var available = script.Activated
.Where(ability =>
(fromHand ? ability.FromHand : fromDrop ? ability.FromDropZone
: !ability.FromHand && !ability.FromDropZone)
&& (!(fromHand || fromDrop) || (!ability.RestsSelf && !ability.RetiresSelf))
&& (!ability.RestsSelf || !card.Rested)
&& (!ability.RetiresSelf || isOnRearGuard)
&& ability.CanActivate(context))
.ToList();
if (available.Count == 0)
return;
if (available.Any(ability => !ability.RequiresDiscard))
commands.Add(new ActivateAbilityCommand(card.Id));
if (!available.Any(ability => ability.RequiresDiscard))
return;
var discardPrintings = State.Players[player].Hand
.Select(handCard => handCard.PrintingId)
.Distinct(StringComparer.Ordinal)
.ToList();
foreach (var printingId in discardPrintings)
{
// карту из руки нельзя предлагать самой себе как стоимость
if (fromHand && printingId == card.PrintingId)
continue;
commands.Add(new ActivateAbilityCommand(card.Id, printingId));
}
}
private IEnumerable UnitSlots(int player)
{
yield return -1;
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
{
if (State.Players[player].RearGuards[slot] is not null)
yield return slot;
}
}
// ---------- старт партии ----------
public void Start()
{
ShuffleDeck(0);
ShuffleDeck(1);
PlaceStartingVanguard(0);
PlaceStartingVanguard(1);
Draw(0, StartingHand);
Draw(1, StartingHand);
State.Phase = Phase.Mulligan;
_events.Add(new GameStartedEvent(0));
}
private void ShuffleDeck(int player)
{
var state = State.RngState;
Rng.Shuffle(ref state, State.Players[player].Deck);
State.RngState = state;
}
private void FillDeck(int player, IReadOnlyList printingIds)
{
foreach (var printingId in printingIds)
{
State.Players[player].Deck.Add(new CardInstance
{
Id = State.NextInstanceId++,
PrintingId = printingId,
});
}
}
private void PlaceStartingVanguard(int player)
{
var p = State.Players[player];
var index = p.Deck.FindIndex(c =>
{
var def = State.Catalog.CardOf(c.PrintingId);
return def.IsStartingVanguardCandidate;
});
if (index < 0)
throw new InvalidOperationException("В колоде нет карты для стартового Авангарда (grade 0 без триггера).");
var card = p.Deck[index];
p.Deck.RemoveAt(index);
p.Vanguard = card;
}
// ---------- команды ----------
public CommandResult Execute(GameCommand command)
{
if (State.IsFinished)
return CommandResult.Reject("Партия завершена.");
// пока идёт разрешение способности или триггера, посторонние команды запрещены
if (State.Step is Step.Trigger or Step.Ability or Step.Choice)
{
var allowed = command switch
{
ResolveTriggerCommand => State.Step == Step.Trigger,
ResolveAbilityCommand or SelectTriggerCommand => State.Step == Step.Ability,
ChooseCommand => State.Step == Step.Choice,
_ => false,
};
if (!allowed)
return CommandResult.Reject("Сначала завершите текущее разрешение.");
}
return command switch
{
MulliganCommand c => Mulligan(c),
FinishMulliganCommand => FinishMulligan(),
RideCommand c => Ride(c.PrintingId),
SkipRideCommand => SkipRide(),
CallCommand c => Call(c.PrintingId, c.Slot),
MoveCommand c => Move(c.FromSlot, c.ToSlot),
EnterBattleCommand => EnterBattle(),
AttackCommand c => Attack(c.AttackerSlot, c.TargetSlot, c.BoosterSlot),
GuardCommand c => Guard(c.PrintingId, c.DiscardPrintingId),
InterceptCommand c => Intercept(c.Slot),
PassGuardCommand => PassGuard(),
ResolveTriggerCommand c => ResolveTrigger(c),
SelectTriggerCommand c => SelectTrigger(c.Index),
ResolveAbilityCommand c => ResolvePendingAbility(c.Accept, c.DamageIndex),
ChooseCommand c => Choose(c.Index),
ActivateAbilityCommand c => Activate(c.InstanceId, c.DiscardPrintingId, c.DamageIndex),
EndTurnCommand => EndTurn(),
_ => CommandResult.Reject("Неизвестная команда."),
};
}
private CommandResult Mulligan(MulliganCommand command)
{
if (State.Phase != Phase.Mulligan)
return CommandResult.Reject("Маллиган доступен только в начале партии.");
var player = State.ActivePlayer;
var hand = State.Players[player].Hand;
var indexes = command.HandIndexes.Distinct().OrderByDescending(i => i).ToList();
if (indexes.Any(i => i < 0 || i >= hand.Count))
return CommandResult.Reject("Некорректный индекс карты в руке.");
foreach (var index in indexes)
{
var card = hand[index];
hand.RemoveAt(index);
State.Players[player].Deck.Add(card);
}
ShuffleDeck(player);
Draw(player, indexes.Count);
_events.Add(new MulliganEvent(player, indexes.Count));
return CommandResult.Ok();
}
private CommandResult FinishMulligan()
{
if (State.Phase != Phase.Mulligan)
return CommandResult.Reject("Маллиган уже завершён.");
State.ActivePlayer = 0;
StartTurn();
return CommandResult.Ok();
}
private CommandResult Ride(string printingId)
{
if (State.Phase != Phase.Ride)
return CommandResult.Reject("Райд доступен только в фазе Смены.");
var player = State.Players[State.ActivePlayer];
var index = player.Hand.FindIndex(c => c.PrintingId == printingId);
if (index < 0)
return CommandResult.Reject("Карты нет в руке.");
var card = player.Hand[index];
var def = State.Catalog.CardOf(card.PrintingId);
var current = player.Vanguard is null ? -1 : State.Catalog.CardOf(player.Vanguard.PrintingId).Grade;
if (def.Grade != current && def.Grade != current + 1)
return CommandResult.Reject($"Нельзя совершить смену с {current}-го ранга на {def.Grade}-й.");
var riddenOver = player.Vanguard;
if (riddenOver is not null)
{
riddenOver.Rested = false;
player.Soul.Add(riddenOver);
}
player.Hand.RemoveAt(index);
card.Rested = false;
player.Vanguard = card;
_events.Add(new UnitRodeEvent(State.ActivePlayer, card.PrintingId));
TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null);
if (State.Step == Step.None)
TriggerFromCard(State.ActivePlayer, riddenOver, AbilityTrigger.OnRideOver, card);
if (State.Step == Step.None)
EnterMainPhase();
else
State.Continuation = EnterMainPhase;
return CommandResult.Ok();
}
private CommandResult SkipRide()
{
if (State.Phase != Phase.Ride)
return CommandResult.Reject("Пропустить смену можно только в фазе Смены.");
EnterMainPhase();
return CommandResult.Ok();
}
private CommandResult Call(string printingId, int slot)
{
if (State.Phase != Phase.Main)
return CommandResult.Reject("Призыв доступен только в Основной фазе.");
if (slot is < 0 or >= PlayerState.RearGuardSlots)
return CommandResult.Reject("Некорректный круг Арьергарда.");
var player = State.Players[State.ActivePlayer];
if (player.RearGuards[slot] is not null)
return CommandResult.Reject("Круг уже занят.");
var index = player.Hand.FindIndex(c => c.PrintingId == printingId);
if (index < 0)
return CommandResult.Reject("Карты нет в руке.");
var card = player.Hand[index];
player.Hand.RemoveAt(index);
card.Rested = false;
player.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null);
return CommandResult.Ok();
}
private CommandResult Move(int fromSlot, int toSlot)
{
if (State.Phase != Phase.Main)
return CommandResult.Reject("Перемещение доступно только в Основной фазе.");
if (fromSlot is < 0 or >= PlayerState.RearGuardSlots || toSlot is < 0 or >= PlayerState.RearGuardSlots)
return CommandResult.Reject("Некорректный круг Арьергарда.");
var player = State.Players[State.ActivePlayer];
var unit = player.RearGuards[fromSlot];
if (unit is null)
return CommandResult.Reject("В исходном кругу нет карты.");
if (!SameColumn(fromSlot, toSlot))
return CommandResult.Reject("Двигать можно только в пределах своей колонки.");
// целевой круг может быть занят — тогда карты меняются местами
var other = player.RearGuards[toSlot];
player.RearGuards[toSlot] = unit;
player.RearGuards[fromSlot] = other;
_events.Add(new UnitMovedEvent(State.ActivePlayer, fromSlot, toSlot));
return CommandResult.Ok();
}
/// Колонки: 0↔3 (левая), 2↔4 (правая). У центрального заднего ряда пары нет.
private static bool SameColumn(int fromSlot, int toSlot) =>
(fromSlot, toSlot) is (0, 3) or (3, 0) or (2, 4) or (4, 2);
private CommandResult EnterBattle()
{
if (State.Phase != Phase.Main)
return CommandResult.Reject("Перейти в Боевую фазу можно только из Основной.");
State.Phase = Phase.Battle;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Battle));
return CommandResult.Ok();
}
private CommandResult Attack(int attackerSlot, int targetSlot, int boosterSlot = -1)
{
if (State.Phase != Phase.Battle)
return CommandResult.Reject("Атака доступна только в Боевой фазе.");
if (State.Battle is not null)
return CommandResult.Reject("Идёт бой: сначала завершите защиту.");
if (attackerSlot is < -1 or >= PlayerState.RearGuardSlots)
return CommandResult.Reject("Некорректный атакующий.");
if (targetSlot is < -1 or >= PlayerState.RearGuardSlots)
return CommandResult.Reject("Некорректная цель.");
var attacker = State.GetUnit(State.ActivePlayer, attackerSlot);
if (attacker is null)
return CommandResult.Reject("Нет атакующего юнита.");
if (attacker.Rested)
return CommandResult.Reject("Юнит уже повёрнут.");
if (State.IsRestraint(attacker))
return CommandResult.Reject("Юнит со Сдерживанием не может атаковать.");
if (State.HasRestriction(attacker, RestrictionType.CannotAttack))
return CommandResult.Reject("Юнит не может атаковать.");
// Атаковать могут только Вэнгард и Арьергард переднего ряда.
// Задний ряд — исключительно поддержка (boost) и эффекты.
if (attackerSlot is 0 or 1 or 2)
return CommandResult.Reject("Атаковать могут только юниты переднего ряда.");
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
if (target is null)
return CommandResult.Reject("Нет цели.");
// Задний ряд защищён: атаковать его можно только способностью
// и только из переднего ряда своей колонки.
if (targetSlot is 0 or 1 or 2)
{
if (!State.Catalog.CardOf(attacker.PrintingId).CanSnipeBackRow)
return CommandResult.Reject("Арьергард в заднем ряду нельзя атаковать без способности.");
if (attackerSlot is not (3 or 4) || !SameColumn(attackerSlot, targetSlot))
return CommandResult.Reject("Атаковать задний ряд можно только из своей колонки.");
}
CardInstance? booster = null;
if (boosterSlot >= 0)
{
if (attackerSlot is not (3 or 4))
return CommandResult.Reject("Поддерживать можно только атаку Арьергарда из переднего ряда.");
var expected = attackerSlot == 3 ? 0 : 2;
if (boosterSlot != expected)
return CommandResult.Reject("Поддержка возможна только из заднего ряда той же колонны.");
booster = State.GetUnit(State.ActivePlayer, boosterSlot);
if (booster is null)
return CommandResult.Reject("Нет карты для поддержки.");
if (booster.Rested)
return CommandResult.Reject("Карта поддержки уже повёрнута.");
if (!State.CanBoost(booster))
return CommandResult.Reject("Эта карта не может поддерживать.");
}
var attackerPlayer = State.ActivePlayer;
attacker.Rested = true;
State.Battle = new BattleState
{
AttackerPlayer = attackerPlayer,
AttackerSlot = attackerSlot,
TargetSlot = targetSlot,
BoosterSlot = boosterSlot,
};
var defenderIndex = 1 - attackerPlayer;
_events.Add(new AttackDeclaredEvent(attackerPlayer, attackerSlot, targetSlot));
void BeginGuard()
{
State.Step = Step.Guard;
State.ActivePlayer = defenderIndex;
}
void AfterAttackDeclared()
{
TriggerFromCard(attackerPlayer, attacker, AbilityTrigger.OnAttackDeclared, null);
if (State.Step is Step.Ability or Step.Choice)
State.Continuation = BeginGuard;
else
BeginGuard();
}
void AfterBoosted()
{
TriggerFromCard(attackerPlayer, attacker, AbilityTrigger.OnBoosted, booster);
if (State.Step is Step.Ability or Step.Choice)
State.Continuation = AfterAttackDeclared;
else
AfterAttackDeclared();
}
if (booster is not null)
{
booster.Rested = true;
AddPower(
attacker,
State.Catalog.CardOf(booster.PrintingId).Power,
booster,
"boost",
ModifierDuration.UntilEndOfBattle);
_events.Add(new BoostedEvent(attackerPlayer, boosterSlot, attackerSlot));
TriggerFromCard(attackerPlayer, booster, AbilityTrigger.OnBoost, attacker);
if (State.Step is Step.Ability or Step.Choice)
State.Continuation = AfterBoosted;
else
AfterBoosted();
}
else
{
AfterAttackDeclared();
}
return CommandResult.Ok();
}
private CommandResult Guard(string printingId, string? discardPrintingId = null)
{
var battle = State.Battle;
if (State.Step != Step.Guard || battle is null)
return CommandResult.Reject("Сейчас нельзя защищаться.");
var defenderIndex = 1 - battle.AttackerPlayer;
if (State.ActivePlayer != defenderIndex)
return CommandResult.Reject("Защищается только обороняющийся.");
var defender = State.Players[defenderIndex];
var index = defender.Hand.FindIndex(c => c.PrintingId == printingId);
if (index < 0)
return CommandResult.Reject("Карты нет в руке.");
var card = defender.Hand[index];
var def = State.Catalog.CardOf(card.PrintingId);
if (!def.IsSentinel && discardPrintingId is not null)
return CommandResult.Reject("Эта карта не требует оплаты стоимости.");
if (def.IsSentinel && discardPrintingId is not null)
{
var discardIndex = defender.Hand.FindIndex(c => c.PrintingId == discardPrintingId);
if (discardIndex < 0)
return CommandResult.Reject("Карты для оплаты нет в руке.");
if (State.Catalog.CardOf(defender.Hand[discardIndex].PrintingId).Clan != def.Clan)
return CommandResult.Reject("Для оплаты нужна карта того же клана.");
}
// Sentinel сам по себе даёт ноль щита: либо оплата и отмена атаки, либо ничего
var shield = def.IsSentinel ? 0 : def.Shield;
defender.Hand.RemoveAt(index);
defender.Guardian.Add(card);
battle.GuardCards.Add(card);
battle.GuardShield += shield;
_events.Add(new GuardAddedEvent(defenderIndex, card.PrintingId, shield));
if (def.IsSentinel && discardPrintingId is not null)
{
var discardIndex = defender.Hand.FindIndex(c => c.PrintingId == discardPrintingId);
var discarded = defender.Hand[discardIndex];
defender.Hand.RemoveAt(discardIndex);
defender.Drop.Add(discarded);
battle.Nullified = true;
_events.Add(new PerfectGuardEvent(defenderIndex, card.PrintingId, discarded.PrintingId));
}
TriggerFromCard(defenderIndex, card, AbilityTrigger.OnGuard, null);
// способность на размещение в (GC) может встать на выбор игрока:
// после её разрешения возвращаемся в шаг защиты
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = () =>
{
State.Step = Step.Guard;
State.ActivePlayer = defenderIndex;
};
}
return CommandResult.Ok();
}
private CommandResult Intercept(int slot)
{
var battle = State.Battle;
if (State.Step != Step.Guard || battle is null)
return CommandResult.Reject("Перехват доступен только на шаге защиты.");
var defenderIndex = 1 - battle.AttackerPlayer;
if (State.ActivePlayer != defenderIndex)
return CommandResult.Reject("Перехватывает только обороняющийся.");
if (battle.Interceptor is not null)
return CommandResult.Reject("Перехват уже объявлен.");
if (slot is < 0 or >= PlayerState.RearGuardSlots || slot == battle.TargetSlot)
return CommandResult.Reject("Некорректный круг для перехвата.");
var defender = State.Players[defenderIndex];
var unit = defender.RearGuards[slot];
if (unit is null)
return CommandResult.Reject("Круг пуст.");
if (unit.Rested)
return CommandResult.Reject("Карта повёрнута и не может перехватывать.");
var isFrontRow = slot is 3 or 4;
if (!isFrontRow && !unit.InterceptFromBackRow)
return CommandResult.Reject("Перехват возможен только из переднего ряда.");
if (!State.CanIntercept(unit))
return CommandResult.Reject("Эта карта не может перехватывать.");
defender.RearGuards[slot] = null;
defender.Guardian.Add(unit);
battle.GuardCards.Add(unit);
battle.Interceptor = unit;
_events.Add(new InterceptedEvent(defenderIndex, slot, unit.PrintingId));
return CommandResult.Ok();
}
private CommandResult PassGuard()
{
var battle = State.Battle;
if (State.Step != Step.Guard || battle is null)
return CommandResult.Reject("Сейчас нет боя.");
if (State.ActivePlayer != 1 - battle.AttackerPlayer)
return CommandResult.Reject("Защищается только обороняющийся.");
BeginResolution(battle);
return CommandResult.Ok();
}
private CommandResult EndTurn()
{
if (State.Phase is not (Phase.Main or Phase.Battle))
return CommandResult.Reject("Завершить ход можно из Основной или Боевой фазы.");
if (State.Battle is not null)
return CommandResult.Reject("Сначала завершите текущий бой.");
State.Phase = Phase.End;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.End));
// «в начале Завершающей фазы вернуть в колоду»
foreach (var player in new[] { 0, 1 })
{
foreach (var unit in AllUnits(player).ToList())
{
if (unit.ReturnToDeck == ReturnToDeckTiming.EndOfTurn)
ReturnUnitToDeck(player, unit);
}
}
ClearTurnModifiers();
if (State.IsFinished)
return CommandResult.Ok();
State.ActivePlayer = 1 - State.ActivePlayer;
StartTurn();
return CommandResult.Ok();
}
// ---------- ход ----------
private void StartTurn()
{
State.TurnNumber++;
foreach (var unit in AllUnits(State.ActivePlayer))
{
if (!State.HasRestriction(unit, RestrictionType.CannotStand))
unit.Rested = false;
}
// Запрет «не вставать» действует до этой фазы Подъёма и снимается после неё
foreach (var unit in AllUnits(State.ActivePlayer))
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilOwnerNextStand);
State.Phase = Phase.Stand;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Stand));
State.Phase = Phase.Draw;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Draw));
Draw(State.ActivePlayer, 1);
if (State.IsFinished)
return;
State.Phase = Phase.Ride;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Ride));
}
private IEnumerable AllUnits(int player)
{
var p = State.Players[player];
if (p.Vanguard is not null)
yield return p.Vanguard;
foreach (var unit in p.RearGuards)
{
if (unit is not null)
yield return unit;
}
}
///
/// Усиления от триггеров живут до конца хода, а не до конца боя.
/// Переопределения Сдерживания и числа проверок тоже сбрасываются здесь:
/// в BT01 они выдаются только до конца хода.
///
private void ClearTurnModifiers()
{
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
{
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfTurn);
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfTurn);
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilEndOfTurn);
unit.RestraintOverride = null;
unit.DriveChecksOverride = null;
}
}
private void BeginResolution(BattleState battle)
{
var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
if (attacker is null)
{
FinishBattle(battle);
return;
}
var checks = DriveChecksFor(battle.AttackerSlot, attacker);
if (checks == 0)
{
FinishBattle(battle);
return;
}
battle.ChecksRemaining = checks;
State.ActivePlayer = battle.AttackerPlayer;
State.Step = Step.Drive;
ContinueDriveChecks(battle);
}
///
/// Идёт по drive-чекам. На триггере останавливается и ждёт ResolveTriggerCommand:
/// игрок сам выбирает, кому дать +5000 и куда применить второй эффект.
///
private void ContinueDriveChecks(BattleState battle)
{
var player = battle.AttackerPlayer;
while (battle.ChecksRemaining > 0 && !State.IsFinished)
{
var card = TakeTop(player);
if (card is null)
{
FinishBattle(battle);
return;
}
var def = State.Catalog.CardOf(card.PrintingId);
_events.Add(new DriveCheckedEvent(player, card.PrintingId, def.Trigger));
if (def.Trigger == TriggerType.None)
{
State.Players[player].Hand.Add(card);
_events.Add(new CardDrawnEvent(player));
battle.ChecksRemaining--;
var checkedAttacker = State.GetUnit(player, battle.AttackerSlot);
TriggerFromCard(player, checkedAttacker, AbilityTrigger.OnDriveCheck, card);
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = () => ContinueDriveChecks(battle);
return;
}
continue;
}
State.Players[player].Trigger.Add(card);
battle.ChecksRemaining--;
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
State.PendingTrigger = new PendingTrigger
{
Player = player,
PrintingId = card.PrintingId,
Card = card,
Trigger = def.Trigger,
Context = TriggerContext.DriveCheck,
GrantsPower = true,
};
State.Step = Step.Trigger;
State.ActivePlayer = player;
return;
}
FinishBattle(battle);
}
private CommandResult ResolveTrigger(ResolveTriggerCommand command)
{
var pending = State.PendingTrigger;
if (pending is null || State.Step != Step.Trigger)
return CommandResult.Reject("Сейчас нет триггера для разрешения.");
if (State.ActivePlayer != pending.Player)
return CommandResult.Reject("Триггер разрешает его владелец.");
var player = pending.Player;
if (pending.GrantsPower)
{
var powerTarget = State.GetUnit(player, command.PowerTarget);
if (powerTarget is null)
return CommandResult.Reject("Нет цели для +5000 силы.");
}
if (pending.Trigger is TriggerType.Critical or TriggerType.Stand)
{
var effectUnit = State.GetUnit(player, command.EffectTarget);
if (effectUnit is null)
return CommandResult.Reject("Нет цели для второго эффекта.");
if (pending.Trigger == TriggerType.Stand && command.EffectTarget < 0)
{
// если Арьергардов нет, поднимать нечего — второй эффект не применяется
if (State.Players[player].RearGuards.Any(u => u is not null))
return CommandResult.Reject("Поднять можно только карту в кругу Арьергарда.");
}
}
if (pending.GrantsPower)
{
var powerTarget = State.GetUnit(player, command.PowerTarget)!;
AddPower(powerTarget, TriggerPowerBonus, pending.Card, "trigger", ModifierDuration.UntilEndOfTurn);
}
switch (pending.Trigger)
{
case TriggerType.Critical:
AddPower(
State.GetUnit(player, command.EffectTarget)!,
0,
pending.Card,
"trigger",
ModifierDuration.UntilEndOfTurn,
critical: 1);
break;
case TriggerType.Stand:
if (command.EffectTarget >= 0)
State.GetUnit(player, command.EffectTarget)!.Rested = false;
break;
case TriggerType.Draw:
Draw(player, 1);
break;
case TriggerType.Heal:
if (TryBeginHeal(player, pending, command))
return CommandResult.Ok();
break;
}
return CompleteTrigger(pending, command);
}
private CommandResult CompleteTrigger(PendingTrigger pending, ResolveTriggerCommand command)
{
var player = pending.Player;
State.PendingTrigger = null;
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
if (pending.Context == TriggerContext.DriveCheck)
{
// эффекты сработали — теперь карта уходит из зоны триггера в руку
var p = State.Players[player];
p.Trigger.Remove(pending.Card);
p.Hand.Add(pending.Card);
if (State.Battle is { } battle)
{
State.Step = Step.Drive;
ContinueDriveChecks(battle);
}
return CommandResult.Ok();
}
FinishDamage(pending);
return CommandResult.Ok();
}
private bool TryBeginHeal(int player, PendingTrigger pending, ResolveTriggerCommand command)
{
var p = State.Players[player];
var opponent = State.Players[1 - player];
if (p.Damage.Count <= opponent.Damage.Count || p.Damage.Count == 0)
return false;
AskDamageChoice(
player,
"Выберите карту урона для лечения",
actualIndex =>
{
var card = p.Damage[actualIndex];
p.Damage.RemoveAt(actualIndex);
p.Drop.Add(card);
_events.Add(new HealedEvent(player));
});
var previousContinuation = State.Continuation;
State.Continuation = () =>
{
State.Continuation = previousContinuation;
CompleteTrigger(pending, command);
};
return true;
}
private List<(CardInstance Card, int Index)> DamageDisplayOrder(int player) =>
State.Players[player].Damage
.Select((card, index) => (Card: card, Index: index))
.OrderBy(item => item.Card.FaceUp)
.ThenBy(item => item.Index)
.ToList();
private void AskDamageChoice(int player, string prompt, Action onChosen)
{
var order = DamageDisplayOrder(player);
var options = order
.Select((item, displayIndex) => new ChoiceOption(
displayIndex,
item.Card.FaceUp
? State.Catalog.CardOf(item.Card.PrintingId).NameEn
: "Закрытая карта"))
.ToList();
State.PendingChoice = new PendingChoice
{
Player = player,
Prompt = prompt,
Options = options,
Kind = ChoiceTargetKind.Damage,
Resolve = displayIndex => onChosen(order[displayIndex].Index),
};
State.Step = Step.Choice;
State.ActivePlayer = player;
}
private void FinishBattle(BattleState battle)
{
if (State.IsFinished)
{
ClearBattle(battle);
return;
}
var defenderIndex = 1 - battle.AttackerPlayer;
var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
var target = State.GetUnit(defenderIndex, battle.TargetSlot);
if (attacker is null || target is null)
{
ClearBattle(battle);
return;
}
var defenseUnit = battle.Interceptor ?? target;
var attackPower = EffectivePower(attacker);
var defensePower = EffectivePower(defenseUnit) + battle.GuardShield;
var hit = !battle.Nullified && attackPower > defensePower;
_events.Add(new BattleResolvedEvent(hit));
if (hit)
{
// попадание: сначала «атака, которую поддержала эта карта, попала»,
// затем «эта карта попала»; каждый шаг может встать на выбор игрока
void AttackerHitTriggers(CardInstance? hitUnit, Action after)
{
TriggerFromCard(battle.AttackerPlayer, attacker, AbilityTrigger.OnAttackHit, hitUnit);
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = after;
return;
}
after();
}
void BoosterHitTriggers(CardInstance? hitUnit, Action after)
{
var booster = battle.BoosterSlot >= 0
? State.GetUnit(battle.AttackerPlayer, battle.BoosterSlot)
: null;
if (booster is null)
{
AttackerHitTriggers(hitUnit, after);
return;
}
TriggerFromCard(battle.AttackerPlayer, booster, AbilityTrigger.OnBoostedHit, hitUnit);
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = () => AttackerHitTriggers(hitUnit, after);
return;
}
AttackerHitTriggers(hitUnit, after);
}
if (battle.Interceptor is not null)
{
// перехватчик уже вышел из круга и уйдёт в сброс при закрытии боя,
// первоначальная цель (Авангард или Арьергард) не страдает
_events.Add(new InterceptorRetiredEvent(defenderIndex, battle.Interceptor.PrintingId));
}
else if (battle.TargetSlot < 0)
{
var damage = EffectiveCritical(attacker);
void AfterDamage() => BoosterHitTriggers(null, () => ClearBattle(battle));
void ResolveDamage()
{
DealDamage(defenderIndex, damage);
// урон ещё разрешается (damage-триггер) — hit-эффекты после него
if (State.PendingTrigger is not null)
{
State.Continuation = AfterDamage;
return;
}
AfterDamage();
}
ResolveDamage();
return;
}
else
{
State.Players[defenderIndex].RearGuards[battle.TargetSlot] = null;
State.Players[defenderIndex].Drop.Add(target);
_events.Add(new UnitRetiredEvent(defenderIndex, battle.TargetSlot));
void AfterRetire()
{
TriggerOpponentRetire(defenderIndex);
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = () => BoosterHitTriggers(target, () => ClearBattle(battle));
return;
}
BoosterHitTriggers(target, () => ClearBattle(battle));
}
TriggerFromCard(defenderIndex, target, AbilityTrigger.OnRetireFromRC, null);
if (State.Step is Step.Ability or Step.Choice)
{
State.Continuation = AfterRetire;
return;
}
AfterRetire();
return;
}
}
if (State.PendingTrigger is not null)
return;
ClearBattle(battle);
}
private void ClearBattle(BattleState battle)
{
var defenderIndex = 1 - battle.AttackerPlayer;
var defender = State.Players[defenderIndex];
var leftGuardian = new List();
foreach (var guard in battle.GuardCards)
{
defender.Guardian.Remove(guard);
defender.Drop.Add(guard);
leftGuardian.Add(guard);
}
// карты, которые должны вернуться в колоду сразу после боя
foreach (var player in new[] { 0, 1 })
{
foreach (var unit in AllUnits(player).ToList())
{
if (unit.ReturnToDeck == ReturnToDeckTiming.AfterBattle)
ReturnUnitToDeck(player, unit);
}
}
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
{
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfBattle);
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfBattle);
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilEndOfBattle);
}
State.Battle = null;
State.PendingTrigger = null;
State.Step = Step.None;
State.ActivePlayer = battle.AttackerPlayer;
// бой уже закрыт: эффекты «попала в сброс из (GC)» срабатывают после
foreach (var guard in leftGuardian)
TriggerFromCard(defenderIndex, guard, AbilityTrigger.OnLeaveGuardian, null);
}
private void DealDamage(int player, int amount)
{
for (var i = 0; i < amount; i++)
{
if (State.IsFinished)
return;
var card = TakeTop(player);
if (card is null)
return;
var def = State.Catalog.CardOf(card.PrintingId);
if (def.Trigger != TriggerType.None)
{
// карта попадает в зону триггера, эффекты срабатывают до урона
State.Players[player].Trigger.Add(card);
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
State.PendingTrigger = new PendingTrigger
{
Player = player,
PrintingId = card.PrintingId,
Card = card,
Trigger = def.Trigger,
Context = TriggerContext.DamageCheck,
GrantsPower = true,
RemainingDamage = amount - i - 1,
};
State.Step = Step.Trigger;
State.ActivePlayer = player;
return;
}
PutIntoDamage(player, card);
if (State.IsFinished)
return;
}
}
private void PutIntoDamage(int player, CardInstance card)
{
card.FaceUp = false;
var p = State.Players[player];
p.Damage.Add(card);
_events.Add(new DamageTakenEvent(player, 1, p.Damage.Count));
if (p.Damage.Count >= MaxDamage)
Defeat(player);
}
/// Продолжает урон после разрешения триггера damage check.
private void FinishDamage(PendingTrigger pending)
{
if (State.IsFinished)
return;
var p = State.Players[pending.Player];
p.Trigger.Remove(pending.Card);
PutIntoDamage(pending.Player, pending.Card);
if (State.IsFinished)
return;
if (pending.RemainingDamage > 0)
DealDamage(pending.Player, pending.RemainingDamage);
if (State.PendingTrigger is not null)
return;
if (State.Continuation is { } continuation)
{
State.Continuation = null;
continuation();
return;
}
State.Step = Step.None;
if (State.Battle is { } battle)
ClearBattle(battle);
}
// ---------- служебное ----------
private CardInstance? TakeTop(int player)
{
var deck = State.Players[player].Deck;
if (deck.Count == 0)
{
Defeat(player);
return null;
}
var card = deck[0];
deck.RemoveAt(0);
return card;
}
public void DrawCards(int player, int count) => Draw(player, count);
private void Draw(int player, int count)
{
for (var i = 0; i < count; i++)
{
if (State.IsFinished)
return;
var card = TakeTop(player);
if (card is null)
return;
State.Players[player].Hand.Add(card);
_events.Add(new CardDrawnEvent(player));
}
}
private void Defeat(int player)
{
if (State.IsFinished)
return;
State.Players[player].IsDefeated = true;
State.IsFinished = true;
State.Winner = 1 - player;
_events.Add(new GameFinishedEvent(State.Winner.Value));
}
///
/// Завершить партию извне: сдача, истечение времени, отсутствие игрока.
/// Победа достаётся , событие пишется в общий журнал.
///
public void Finish(int winner)
{
if (winner is not (0 or 1))
throw new ArgumentOutOfRangeException(nameof(winner), winner, "Победитель — 0 или 1.");
if (State.IsFinished)
return;
Defeat(1 - winner);
// незавершённые выборы больше не имеют смысла: партия закончена
State.PendingChoice = null;
State.PendingTrigger = null;
State.PendingAbility = null;
State.PendingOrder = null;
State.PendingQueue = null;
State.Continuation = null;
State.Step = Step.None;
}
}