using Vanguard.Rules.Engine;
using Vanguard.Shared.Protocol;
namespace Vanguard.Shared.Projection;
///
/// Проекция авторитарного состояния на одного игрока.
///
/// Правила видимости:
/// * своя рука — полностью, рука противника — только количество;
/// * колода — только количество у обоих;
/// * урон лицом вниз — только количество у обоих (правила Classic его не открывают);
/// * урон лицом вверх, душа, сброс, зона триггера и круг защиты — полностью;
/// * seed и состояние RNG в проекцию не попадают никогда;
/// * выборы, способности и порядок триггеров видны только тому, кто их разрешает.
///
public static class MatchProjection
{
public static MatchView ForViewer(
GameEngine engine,
int viewer,
string matchId,
long eventSeq,
ClockView clock)
{
if (viewer is not (0 or 1))
throw new ArgumentOutOfRangeException(nameof(viewer), viewer, "Индекс игрока — 0 или 1.");
var state = engine.State;
var isDecisionMaker = state.ActivePlayer == viewer && !state.IsFinished;
return new MatchView
{
MatchId = matchId,
EventSeq = eventSeq,
You = viewer,
ActivePlayer = state.ActivePlayer,
TurnNumber = state.TurnNumber,
Phase = RuleMapping.Map(state.Phase),
Step = RuleMapping.Map(state.Step),
IsFinished = state.IsFinished,
Winner = state.Winner ?? -1,
Players = new List
{
ProjectPlayer(engine, 0, viewer),
ProjectPlayer(engine, 1, viewer),
},
Battle = ProjectBattle(engine),
Trigger = ProjectTrigger(state.PendingTrigger),
Choice = ProjectChoice(state, viewer),
Ability = ProjectAbility(state, viewer),
Order = ProjectOrder(state, viewer),
OpponentChoosing = IsOpponentChoosing(state, viewer),
Clock = clock,
LegalIntents = ProjectLegalIntents(engine, viewer, isDecisionMaker),
};
}
private static PlayerView ProjectPlayer(GameEngine engine, int index, int viewer)
{
var player = engine.State.Players[index];
var isOwn = index == viewer;
return new PlayerView
{
Index = index,
Name = player.Name,
HandCount = player.Hand.Count,
DeckCount = player.Deck.Count,
// Рука противника не покидает сервер: даже пустые слоты не намекают на состав.
Hand = isOwn
? player.Hand.Select(card => ProjectCard(engine, card)).ToList()
: new List(),
Vanguard = player.Vanguard is null ? null : ProjectCard(engine, player.Vanguard),
RearGuards = player.RearGuards
.Select(card => card is null ? null : ProjectCard(engine, card))
.ToArray(),
Guardian = player.Guardian.Select(card => ProjectCard(engine, card)).ToList(),
Soul = player.Soul.Select(card => ProjectCard(engine, card)).ToList(),
FaceDownDamage = player.Damage.Count(card => !card.FaceUp),
FaceUpDamage = player.Damage
.Where(card => card.FaceUp)
.Select(card => ProjectCard(engine, card))
.ToList(),
Drop = player.Drop.Select(card => ProjectCard(engine, card)).ToList(),
Trigger = player.Trigger.Select(card => ProjectCard(engine, card)).ToList(),
IsDefeated = player.IsDefeated,
};
}
private static CardView ProjectCard(GameEngine engine, CardInstance card)
{
var definition = engine.State.Catalog.CardOf(card.PrintingId);
return new CardView
{
InstanceId = card.Id,
PrintingId = card.PrintingId,
Rested = card.Rested,
FaceUp = card.FaceUp,
BasePower = definition.Power,
Power = engine.EffectivePower(card),
BaseCritical = definition.Critical,
Critical = engine.EffectiveCritical(card),
};
}
private static BattleView? ProjectBattle(GameEngine engine)
{
var battle = engine.State.Battle;
if (battle is null)
return null;
var defender = engine.State.Players[1 - battle.AttackerPlayer];
return new BattleView
{
AttackerPlayer = battle.AttackerPlayer,
AttackerSlot = battle.AttackerSlot,
TargetSlot = battle.TargetSlot,
GuardShield = battle.GuardShield,
ChecksRemaining = battle.ChecksRemaining,
Nullified = battle.Nullified,
InterceptorSlot = battle.Interceptor is null
? -1
: Array.IndexOf(defender.RearGuards, battle.Interceptor),
BoosterSlot = battle.BoosterSlot,
GuardCards = battle.GuardCards.Select(card => ProjectCard(engine, card)).ToList(),
};
}
private static TriggerView? ProjectTrigger(PendingTrigger? trigger) => trigger is null
? null
: new TriggerView
{
Player = trigger.Player,
PrintingId = trigger.PrintingId,
Trigger = RuleMapping.Map(trigger.Trigger),
Context = RuleMapping.Map(trigger.Context),
GrantsPower = trigger.GrantsPower,
RemainingDamage = trigger.RemainingDamage,
};
private static ChoiceView? ProjectChoice(GameState state, int viewer)
{
var choice = state.PendingChoice;
if (choice is null || choice.Player != viewer)
return null;
return new ChoiceView
{
Player = choice.Player,
Prompt = choice.Prompt,
Options = choice.Options
.Select(option => new ChoiceOptionView { Slot = option.Slot, Label = option.Label })
.ToList(),
};
}
private static AbilityView? ProjectAbility(GameState state, int viewer)
{
var ability = state.PendingAbility;
if (ability is null || ability.Owner != viewer)
return null;
return new AbilityView
{
Player = ability.Owner,
InstanceId = ability.Context.Source.Id,
PrintingId = ability.Context.Source.PrintingId,
Trigger = RuleMapping.Map(ability.Definition.Trigger),
Optional = ability.Definition.Optional,
};
}
private static OrderView? ProjectOrder(GameState state, int viewer)
{
var order = state.PendingOrder;
if (order is null || order.Count == 0 || state.ActivePlayer != viewer || order[0].Owner != viewer)
return null;
return new OrderView
{
Player = viewer,
Options = order
.Select((ability, index) => new { ability, index })
.Where(item => item.ability.Owner == viewer)
.Select(item => new OrderOptionView
{
Index = item.index,
InstanceId = item.ability.Context.Source.Id,
PrintingId = item.ability.Context.Source.PrintingId,
Trigger = RuleMapping.Map(item.ability.Definition.Trigger),
})
.ToList(),
};
}
private static bool IsOpponentChoosing(GameState state, int viewer)
{
var opponent = 1 - viewer;
return state.PendingChoice?.Player == opponent
|| state.PendingAbility?.Owner == opponent
|| (state.PendingOrder is { Count: > 0 } order && order[0].Owner == opponent);
}
private static List ProjectLegalIntents(GameEngine engine, int viewer, bool isDecisionMaker)
{
var intents = new List();
if (engine.State.IsFinished)
return intents;
// Сдаться можно в любой момент, независимо от очереди хода.
intents.Add(new ConcedeIntent());
if (!isDecisionMaker)
return intents;
// LegalCommands() всегда описывает состояние того, кто сейчас решает,
// поэтому вызывать его для наблюдателя нельзя: там бывают карты чужой руки.
foreach (var command in engine.LegalCommands())
intents.Add(IntentMapping.FromCommand(command));
return intents;
}
}