247 lines
9.2 KiB
C#
247 lines
9.2 KiB
C#
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;
|
||
}
|
||
}
|