VanguardReMaster/server/src/Vanguard.Shared/Projection/MatchProjection.cs

247 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Vanguard.Rules.Engine;
using Vanguard.Shared.Protocol;
namespace Vanguard.Shared.Projection;
/// <summary>
/// Проекция авторитарного состояния на одного игрока.
///
/// Правила видимости:
/// * своя рука — полностью, рука противника — только количество;
/// * колода — только количество у обоих;
/// * урон лицом вниз — только количество у обоих (правила Classic его не открывают);
/// * урон лицом вверх, душа, сброс, зона триггера и круг защиты — полностью;
/// * seed и состояние RNG в проекцию не попадают никогда;
/// * выборы, способности и порядок триггеров видны только тому, кто их разрешает.
/// </summary>
public static class MatchProjection
{
public static MatchView ForViewer(
GameEngine engine,
int viewer,
string matchId,
long eventSeq,
ClockView clock,
bool includeLegalIntents = true)
{
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<PlayerView>
{
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 = includeLegalIntents
? ProjectLegalIntents(engine, viewer, isDecisionMaker)
: new List<IntentPayload>(),
};
}
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<CardView>(),
Vanguard = player.Vanguard is null
? null
: ProjectCard(engine, player.Vanguard, forceFaceUp: true),
RearGuards = player.RearGuards
.Select(card => card is null
? null
: ProjectCard(engine, card, forceFaceUp: true))
.ToArray(),
Guardian = player.Guardian
.Select(card => ProjectCard(engine, card, forceFaceUp: true))
.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,
bool forceFaceUp = false)
{
var definition = engine.State.Catalog.CardOf(card.PrintingId);
return new CardView
{
InstanceId = card.Id,
PrintingId = card.PrintingId,
Rested = card.Rested,
FaceUp = forceFaceUp || 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, forceFaceUp: true))
.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<IntentPayload> ProjectLegalIntents(GameEngine engine, int viewer, bool isDecisionMaker)
{
var intents = new List<IntentPayload>();
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;
}
}