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