using DominionBase.Cards; using DominionBase.Enums; using DominionBase.Piles; using DominionBase.Players.PlayerMessages; using DominionBase.Properties; using DominionBase.Utilities; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using System.Threading; using System.Xml; namespace DominionBase.Players { #region Event delegates public delegate void AttackedEventHandler(object sender, AttackedEventArgs e); public delegate void CardsAddedToDeckEventHandler(object sender, CardsAddedToDeckEventArgs e); public delegate void CardsAddedToHandEventHandler(object sender, CardsAddedToHandEventArgs e); public delegate void CardsDiscardEventHandler(object sender, CardsDiscardEventArgs e); public delegate void CardsDiscardingEventHandler(object sender, CardsDiscardEventArgs e); public delegate void BenefitReceivingInitiatedEventHandler(object sender, BenefitReceiveVisualEventArgs e); public delegate void BenefitReceivingEventHandler(object sender, BenefitReceiveEventArgs e); public delegate void BenefitsChangedEventHandler(object sender, BenefitsChangedEventArgs e); public delegate void BoonsChangedEventHandler(object sender, BoonEventArgs e); public delegate void CalledEventHandler(object sender, CallEventArgs e); public delegate void CalledFinishedEventHandler(object sender, CallEventArgs e); public delegate void CallingEventHandler(object sender, CallEventArgs e); public delegate void CardBoughtEventHandler(object sender, CardBuyEventArgs e); public delegate void CardBuyFinishedEventHandler(object sender, CardBuyEventArgs e); public delegate void CardBuyingEventHandler(object sender, CardBuyEventArgs e); public delegate void CardFlippedOverEventHandler(object sender, CardFlippedOverEventArgs e); public delegate void CardFollowingInstructionsEventHandler(object sender, CardFollowingInstructionsEventArgs e); public delegate void CardGainedEventHandler(object sender, CardGainEventArgs e); public delegate void CardGainedIntoEventHandler(object sender, CardGainEventArgs e); public delegate void CardGainFinishedEventHandler(object sender, CardGainEventArgs e); public delegate void CardGainingEventHandler(object sender, CardGainEventArgs e); public delegate void CardPayingEventHandler(object sender, CardPayEventArgs e); public delegate void CardPlayedEventHandler(object sender, CardPlayedEventArgs e); public delegate void CardPlayFinishedEventHandler(object sender, CardPlayedEventArgs e); public delegate void CardPlayingEventHandler(object sender, CardPlayingEventArgs e); public delegate void CardPutIntoPlayEventHandler(object sender, CardPutIntoPlayEventArgs e); public delegate void CardPuttingIntoPlayEventHandler(object sender, CardPutIntoPlayEventArgs e); public delegate void CardReceivedEventHandler(object sender, CardReceivedEventArgs e); public delegate void CardResolvingEventHandler(object sender, CardResolvingEventArgs e); public delegate void CardsDiscardedEventHandler(object sender, CardsDiscardEventArgs e); public delegate void CardsDrawnEventHandler(object sender, CardsDrawnEventArgs e); public delegate void CardsLostEventHandler(object sender, CardsLostEventArgs e); public delegate void CardUndoPlayedEventHandler(object sender, CardUndoPlayedEventArgs e); public delegate void CardUndoPlayingEventHandler(object sender, CardUndoPlayingEventArgs e); public delegate void CleanedUpEventHandler(object sender, CleanedUpEventArgs e); public delegate void CleaningUpEventHandler(object sender, CleaningUpEventArgs e); public delegate void DrawingCardsEventHandler(object sender, DrawCardsEventArgs e); public delegate void DrawNewHandEventHandler(object sender, DrawNewHandEventArgs e); public delegate void PhaseChangedEventHandler(object sender, PhaseChangedEventArgs e); public delegate void PhaseChangedFinishedEventHandler(object sender, PhaseChangedEventArgs e); public delegate void PhaseChangingEventHandler(object sender, PhaseChangingEventArgs e); public delegate void PlayerModeChangedEventHandler(object sender, PlayerModeChangedEventArgs e); public delegate void ShuffledEventHandler(object sender, ShuffleEventArgs e); public delegate void ShufflingEventHandler(object sender, ShuffleEventArgs e); public delegate void ShufflingStartEventHandler(object sender, ShuffleEventArgs e); public delegate void TakeablesChangedEventHandler(object sender, TakeableEventArgs e); public delegate void TakeTurn(IGame game, IPlayer player); public delegate void TokenPlayedEventHandler(object sender, TokenPlayedEventArgs e); public delegate void TokenPlayingEventHandler(object sender, TokenPlayingEventArgs e); public delegate void TrashedEventHandler(object sender, TrashEventArgs e); public delegate void TrashedFinishedEventHandler(object sender, TrashEventArgs e); public delegate void TrashingEventHandler(object sender, TrashEventArgs e); public delegate void TurnEndedEventHandler(object sender, TurnEndedEventArgs e); public delegate void TurnStartedEventHandler(object sender, TurnStartedEventArgs e); public delegate void TurnStartingEventHandler(object sender, TurnStartingEventArgs e); #endregion public abstract class Player : IDisposable, IPlayer { #region Events public virtual event AttackedEventHandler Attacked; public virtual event CardsDrawnEventHandler CardsDrawn; public virtual event CardsAddedToDeckEventHandler CardsAddedToDeck; public virtual event CardsAddedToHandEventHandler CardsAddedToHand; public virtual event CardsDiscardingEventHandler CardsDiscarding; public virtual event CardsDiscardEventHandler CardsDiscard; public virtual event CardsDiscardedEventHandler CardsDiscarded; public virtual event CardBuyingEventHandler CardBuying; public virtual event CardPayingEventHandler CardPaying; public virtual event CardBoughtEventHandler CardBought; public virtual event CardBuyFinishedEventHandler CardBuyFinished; public virtual event CardGainingEventHandler CardGaining; public virtual event CardGainedEventHandler CardGained; public virtual event CardGainedIntoEventHandler CardGainedInto; public virtual event CardGainFinishedEventHandler CardGainFinished; public virtual event CardReceivedEventHandler CardReceived; public virtual event CardsLostEventHandler CardsLost; public virtual event CardPlayingEventHandler CardPlaying; public virtual event CardPuttingIntoPlayEventHandler CardPuttingIntoPlay; public virtual event CardPutIntoPlayEventHandler CardPutIntoPlay; public virtual event CardResolvingEventHandler CardResolving; public virtual event CardFollowingInstructionsEventHandler CardFollowingInstructions; public virtual event CardPlayedEventHandler CardPlayed; public virtual event CardPlayFinishedEventHandler CardPlayFinished; public virtual event CardUndoPlayingEventHandler CardUndoPlaying; public virtual event CardUndoPlayedEventHandler CardUndoPlayed; public virtual event PhaseChangingEventHandler PhaseChanging; public virtual event PhaseChangedEventHandler PhaseChanged; public virtual event PhaseChangedFinishedEventHandler PhaseChangedFinished; public virtual event PlayerModeChangedEventHandler PlayerModeChanged; public virtual event TokenPlayingEventHandler TokenPlaying; public virtual event TokenPlayedEventHandler TokenPlayed; public virtual event TrashingEventHandler Trashing; public virtual event TrashedEventHandler Trashed; public virtual event TrashedFinishedEventHandler TrashedFinished; public virtual event CallingEventHandler Calling; public virtual event CalledEventHandler Called; public virtual event CalledFinishedEventHandler CalledFinished; public virtual event TurnStartingEventHandler TurnStarting; public virtual event TurnStartedEventHandler TurnStarted; public virtual event DrawingCardsEventHandler DrawingCards; public virtual event ShufflingStartEventHandler ShufflingStart; public virtual event ShufflingEventHandler Shuffling; public virtual event ShuffledEventHandler Shuffled; public virtual event CleaningUpEventHandler CleaningUp; public virtual event DrawNewHandEventHandler DrawNewHand; public virtual event CleanedUpEventHandler CleanedUp; public virtual event TurnEndedEventHandler TurnEnded; public virtual event BenefitReceivingInitiatedEventHandler BenefitReceivingInitiated; public virtual event BenefitReceivingEventHandler BenefitReceiving; public virtual event BenefitsChangedEventHandler BenefitsChanged; public virtual event Token.TokenActionEventHandler TokenActedOn; public virtual event CardFlippedOverEventHandler CardFlippedOver; public virtual event BoonsChangedEventHandler BoonsChanged; public virtual event TakeablesChangedEventHandler TakeablesChanged; #endregion protected PlayerType _PlayerType { get; set; } private PhaseEnum _PhaseEnum = PhaseEnum.Setup; private PlayerMode _PlayerMode = PlayerMode.Waiting; private int _Actions = 1; private int _Buys = 1; private Currency _Currency { get; set; } = new Currency(); public Choose Choose { get; set; } = null; public TakeTurn TakeTurn { get; } = null; protected bool _AsynchronousDrawing { get; private set; } protected CardsDrawnEventArgs _AsynchronousCardsDrawnEventArgs { get; private set; } public ConcurrentQueue MessageRequestQueue { get; } = new ConcurrentQueue(); public ConcurrentQueue MessageResponseQueue { get; } = new ConcurrentQueue(); public AutoResetEvent WaitEvent { get; } = new AutoResetEvent(false); private readonly Dictionary _LostTrackStack = new Dictionary(); protected Player(IGame game, string name) { _Game = game; Name = name; } #region IDisposable variables, properties, & methods // Track whether Dispose has been called. private bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { // Check to see if Dispose has already been called. if (!_disposed) { // If disposing equals true, dispose all managed // and unmanaged resources. if (disposing) { // Dispose managed resources. _Currency = null; CurrentTurn = null; DiscardPile = null; DrawPile = null; _Game = null; Hand = null; while (MessageRequestQueue.TryDequeue(out _)) ; while (MessageResponseQueue.TryDequeue(out _)) ; PlayerMats = null; SetAside = null; Private = null; Revealed = null; InPlay = null; EndgamePile = null; TokenPiles = null; } // Call the appropriate methods to clean up // unmanaged resources here. // If disposing is false, // only the following code is executed. // Note disposing has been done. _disposed = true; } } ~Player() { Dispose(false); } #endregion public void FinalizeDeck() { DrawHand(5); PlayerMode = PlayerMode.Waiting; } public Guid UniqueId { get; private set; } = Guid.NewGuid(); public IGame _Game { get; private set; } /// /// Fires off the Attack event, so any listeners can pick it up. /// /// The attacking player /// The card that triggered the Attack event /// "true" if the Attack can proceed (wasn't blocked). "false" if the Attack was blocked. /// public bool AttackedByPlayerAllowed(IPlayer attacker, Card attackingCard) { if (Attacked != null) { AttackedEventArgs aea; var cancelled = false; do { aea = new AttackedEventArgs(attacker, attackingCard); aea.Cancelled |= cancelled; Attacked(this, aea); var cards = new ItemCollection(aea.Revealable.Values.Select(s => s.Card)); if (cards.Any()) { cards.Sort(); var choice = new Choice(Resource.ShouldReveal, null, attackingCard, ChoiceOutcome.Select, cards, this, true, aea); var result = MakeChoice(choice); if (result.Cards.Any()) aea.Revealable[result.Cards[0].Type].Method(this, ref aea); else break; } cancelled |= aea.Cancelled; } while (Attacked != null && aea.HandledBy.Any()); cancelled |= aea.Cancelled; return !cancelled; } return true; } /// /// Fires off the TokenAction event, so any listeners can pick it up. /// /// The acting player (the one playing the card) /// The card that triggered the TokenAction event /// "true" if the TokenAction can proceed (wasn't blocked). "false" if the TokenAction was blocked. public bool TokenActOn(IPlayer actor, Card actingCard) { if (TokenActedOn != null) { var taea = new TokenActionEventArgs(actor, this, actingCard); TokenActedOn(this, taea); return !taea.Cancelled; } return true; } public PlayerType PlayerType => _PlayerType; public string Name { get; } public Deck Hand { get; private set; } = new Deck(DeckLocation.Hand, Visibility.All, VisibilityTo.All, new Cards.Sorting.ByTypeName(Cards.Sorting.SortDirection.Descending), true); public Deck DrawPile { get; private set; } = new Deck(DeckLocation.Deck, Visibility.None, VisibilityTo.All); public Deck DiscardPile { get; private set; } = new Deck(DeckLocation.Discard, Visibility.Top, VisibilityTo.All); public Deck InPlay { get; private set; } = new Deck(DeckLocation.InPlay, Visibility.All, VisibilityTo.All, null, true); public Deck SetAside { get; private set; } = new Deck(DeckLocation.SetAside, Visibility.All, VisibilityTo.All, null, true); public Deck Revealed { get; private set; } = new Deck(DeckLocation.Revealed, Visibility.All, VisibilityTo.All, null, true); public Deck Private { get; private set; } = new Deck(DeckLocation.Private, Visibility.All, VisibilityTo.Owner, null, true); public DeckCollection InPlayAndSetAside => new DeckCollection(InPlay, SetAside); public PointsCollection EndgamePile { get; private set; } = new PointsCollection(); public CardMats PlayerMats { get; private set; } = new CardMats(); public TokenCollections TokenPiles { get; private set; } = new TokenCollections(); public ItemCollection TakenBoons { get; private set; } = new ItemCollection(); public ItemCollection Takeables { get; private set; } = new ItemCollection(); public Turn CurrentTurn { get; private set; } public virtual void StartAsync() { } public ChoiceResult MakeChoice(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var previous = PlayerMode; PlayerMode = PlayerMode.Choosing; ItemCollection cards = null; ChoiceResult result = null; switch (choice.ChoiceType) { case ChoiceType.Options: if (!choice.IsOrdered && choice.Minimum >= choice.Options.Count) result = new ChoiceResult(choice.Options); break; case ChoiceType.Cards: //if (choice.IsSpecific && choice.Minimum == 1 && choice.Cards.Count() == 1) // result = new ChoiceResult(new ItemCollection(choice.Cards)); //else var nonBlankCards = choice.Cards.Where(c => c.Type != Cards.Universal.TypeClass.Blank).ToList(); if (!nonBlankCards.Any()) cards = new ItemCollection(); else if (!choice.IsOrdered && choice.Minimum >= nonBlankCards.Count) result = new ChoiceResult(new ItemCollection(nonBlankCards)); else if (choice.Maximum == 0) cards = new ItemCollection(); else if (choice.Maximum == choice.Minimum && (!choice.IsOrdered || nonBlankCards.Count(card => card.Type == nonBlankCards.ElementAt(0).Type) == nonBlankCards.Count)) cards = new ItemCollection(nonBlankCards); break; case ChoiceType.Supplies: if (choice.Supplies.Count == 1 && choice.Minimum > 0) result = new ChoiceResult(choice.Supplies.Values.OfType().First()); else if (choice.Supplies.Count == 0) result = new ChoiceResult(); break; case ChoiceType.SuppliesAndCards: if (choice.Supplies.Count == 1 && !choice.Cards.Any()) result = new ChoiceResult(choice.Supplies.Values.OfType().First()); else if (choice.Supplies.Count == 0 && choice.Cards.Count() == 1) result = new ChoiceResult(new ItemCollection { choice.Cards.First() }); else if (choice.Supplies.Count == 0) result = new ChoiceResult(); break; case ChoiceType.Boons: if (choice.Boons.Count() == 1 && choice.Minimum > 0) result = new ChoiceResult(new ItemCollection { choice.Boons.First() }); else if (!choice.Boons.Any()) result = new ChoiceResult(); break; default: throw new Exception(Resource.ExceptionUnusableChoiceType); } if (result == null && cards != null) { if (cards.All(c => c.Type == cards[0].Type)) result = new ChoiceResult(new ItemCollection(cards.Take(choice.Minimum))); } if (result == null) { var choiceThread = new Thread(delegate () { Choose?.Invoke(this, choice); }); choiceThread.Start(); WaitEvent.WaitOne(); if (MessageRequestQueue.TryDequeue(out PlayerMessage message)) { //System.Diagnostics.Trace.WriteLine(String.Format("Message: {0}", message.Message)); // Maybe change Message to an enum instead #pragma warning disable CA1303 // Do not pass literals as localized parameters PlayerMessage response = new PlayerResponseMessage { Message = "ACK" }; #pragma warning restore CA1303 // Do not pass literals as localized parameters if (message is PlayerChoiceMessage choiceMessage) result = choiceMessage.ChoiceResult; MessageResponseQueue.Enqueue(response); message.ReturnEvent?.Set(); } } if (PlayerMode == PlayerMode.Choosing) PlayerMode = previous; return result; } public PhaseEnum Phase { get { return _PhaseEnum; } private set { try { var newValue = value; var oldValue = _PhaseEnum; if (_PhaseEnum != newValue) { var cancelled = false; var copyPhaseChanging = PhaseChanging; if (copyPhaseChanging != null) { PhaseChangingEventArgs pcea; var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; pcea = new PhaseChangingEventArgs(this, newValue) { Cancelled = cancelled }; pcea.HandledBy.AddRange(handledBy); copyPhaseChanging(this, pcea); var options = new OptionCollection(); IEnumerable types = pcea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(pcea.Resolvers[key].Text, pcea.Resolvers[key].IsRequired, pcea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.EndOfPhase.Replace("{phase}", ResourcesHelper.Get(oldValue)), options, this, pcea); var result = MakeChoice(choice); if (result.Options.Any()) { pcea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref pcea); actionPerformed = true; } } handledBy = pcea.HandledBy; cancelled |= pcea.Cancelled; copyPhaseChanging = PhaseChanging; } while (copyPhaseChanging != null && actionPerformed); if (pcea.Cancelled) return; } _PhaseEnum = newValue; var copyPhaseChanged = PhaseChanged; if (copyPhaseChanged != null) { PhaseChangedEventArgs pcea; var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; pcea = new PhaseChangedEventArgs(this, oldValue); pcea.HandledBy.AddRange(handledBy); copyPhaseChanged(this, pcea); var options = new OptionCollection(); IEnumerable types = pcea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(pcea.Resolvers[key].Text, pcea.Resolvers[key].IsRequired, pcea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.EndOfPhase.Replace("{phase}", ResourcesHelper.Get(oldValue)), options, this, pcea); var result = MakeChoice(choice); if (result.Options.Any()) { pcea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref pcea); actionPerformed = true; } } handledBy = pcea.HandledBy; copyPhaseChanged = PhaseChanged; } while (copyPhaseChanged != null && actionPerformed); } var copyPhaseChangedFinished = PhaseChangedFinished; copyPhaseChangedFinished?.Invoke(this, new PhaseChangedEventArgs(this, oldValue)); //if (newValue == PhaseEnum.Waiting || newValue == PhaseEnum.Starting || newValue == PhaseEnum.Endgame) // this.PlayerMode = Players.PlayerMode.Waiting; //else // this.PlayerMode = Players.PlayerMode.Normal; } } catch (Exception ex) { Utilities.Logging.LogError(ex); throw; } } } public PlayerMode PlayerMode { get { return _PlayerMode; } set { var oldValue = _PlayerMode; _PlayerMode = value; var copy = PlayerModeChanged; copy?.Invoke(this, new PlayerModeChangedEventArgs(this, oldValue)); } } public int Actions { get { return _Actions; } set { var oldActions = _Actions; _Actions = Math.Max(0, value); var copy = BenefitsChanged; copy?.Invoke(this, new BenefitsChangedEventArgs(this, oldActions)); } } public int Buys { get { return _Buys; } protected set { var oldBuys = _Buys; _Buys = value; var copy = BenefitsChanged; copy?.Invoke(this, new BenefitsChangedEventArgs(this, oldBuys: oldBuys)); } } public int Coffers { get { if (!TokenPiles.ContainsKey(Cards.Guilds.TypeClass.Coffer)) return 0; return TokenPiles[Cards.Guilds.TypeClass.Coffer].Count; } protected set { var oldCoffers = Coffers; if (oldCoffers == 0) TokenPiles[Cards.Guilds.TypeClass.Coffer] = new TokenCollection(); var difference = value - oldCoffers; // Removing Coffers if (difference < 0) TokenPiles.Remove(TokenPiles[Cards.Guilds.TypeClass.Coffer].Take(-difference), this); // Adding Coffers else if (difference > 0) { for (int i = 0; i < difference; i++) TokenPiles.Add(new Cards.Guilds.Coffer(), this); } var copy = BenefitsChanged; copy?.Invoke(this, new BenefitsChangedEventArgs(this, oldCoffers: oldCoffers)); } } public int Villagers { get { if (!TokenPiles.ContainsKey(Cards.Renaissance.TypeClass.Villager)) return 0; return TokenPiles[Cards.Renaissance.TypeClass.Villager].Count; } protected set { var oldVillagers = Villagers; if (oldVillagers == 0) TokenPiles[Cards.Renaissance.TypeClass.Villager] = new TokenCollection(); var difference = value - oldVillagers; // Removing Villagers if (difference < 0) TokenPiles.Remove(TokenPiles[Cards.Renaissance.TypeClass.Villager].Take(-difference), this); // Adding Villagers else if (difference > 0) { for (int i = 0; i < difference; i++) TokenPiles.Add(new Cards.Renaissance.Villager(), this); } var copy = BenefitsChanged; copy?.Invoke(this, new BenefitsChangedEventArgs(this, oldVillagers: oldVillagers)); } } public Currency Currency { get { if (_PhaseEnum == PhaseEnum.Starting || _PhaseEnum == PhaseEnum.Action || _PhaseEnum == PhaseEnum.ActionTreasure || _PhaseEnum == PhaseEnum.BuyTreasure || _PhaseEnum == PhaseEnum.Buy || _PhaseEnum == PhaseEnum.Night || _PhaseEnum == PhaseEnum.Cleanup || _PlayerMode == PlayerMode.Playing || _PlayerMode == PlayerMode.Choosing) return _Currency; return new Currency(); } protected set { var oldCurrency = _Currency; _Currency = value; var copy = BenefitsChanged; copy?.Invoke(this, new BenefitsChangedEventArgs(this, oldCurrency: oldCurrency)); } } public uint VictoryChits { get { if (TokenPiles.ContainsKey(Cards.Prosperity.TypeClass.VictoryToken)) return (uint)TokenPiles[Cards.Prosperity.TypeClass.VictoryToken].Count; return 0; } } public uint ActionsPlayed { get; private set; } public void ActionPlayed() { Actions--; ActionsPlayed++; } public void UndoActionPlayed() { ActionsPlayed--; Actions++; } public void UndoPhaseChange(PhaseEnum newPhase) { if (Phase == PhaseEnum.Buy && newPhase == PhaseEnum.BuyTreasure && CurrentTurn.CardsBought.Count == 0) { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } } public int VictoryPoints => (int)VictoryChits + EndgamePile.GetVictoryPoints(this); public virtual void Setup(IGame game) { } public virtual void Clear() { _Game = null; Hand.Clear(); DrawPile.Clear(); DiscardPile.Clear(); InPlay.Clear(); SetAside.Clear(); Revealed.Clear(); Private.Clear(); EndgamePile.Clear(); PlayerMats.Clear(); TokenPiles.Clear(); CurrentTurn?.Clear(); while (MessageRequestQueue.TryDequeue(out _)) ; while (MessageResponseQueue.TryDequeue(out _)) ; } public virtual void TearDown(IGame game) { Hand.TearDown(game); DrawPile.TearDown(game); DiscardPile.TearDown(game); InPlay.TearDown(game); SetAside.TearDown(game); Revealed.TearDown(game); Private.TearDown(game); EndgamePile.TearDown(game); PlayerMats.TearDown(game); TokenPiles.TearDown(game); } public void TestFireAllEvents() { Attacked?.Invoke(this, new AttackedEventArgs(null, null)); BenefitReceivingInitiated?.Invoke(this, new BenefitReceiveVisualEventArgs(null, null)); BenefitReceiving?.Invoke(this, new BenefitReceiveEventArgs(null, (ICardBase)null, null)); BenefitsChanged?.Invoke(this, new BenefitsChangedEventArgs(null)); var cbea = new CardBuyEventArgs(null, null, null); CardBought?.Invoke(this, cbea); CardBuying?.Invoke(this, cbea); CardBuyFinished?.Invoke(this, cbea); var cgea = new CardGainEventArgs(null, null, null, DeckLocation.Discard, DeckPosition.Automatic, false); CardGained?.Invoke(this, cgea); CardGainedInto?.Invoke(this, cgea); CardGainFinished?.Invoke(this, cgea); CardGaining?.Invoke(this, cgea); CardPlaying?.Invoke(this, new CardPlayingEventArgs(null, new Cards.Universal.Copper(), null)); CardPuttingIntoPlay?.Invoke(this, new CardPutIntoPlayEventArgs(null, new Cards.Universal.Copper())); CardResolving?.Invoke(this, new CardResolvingEventArgs(null, new Cards.Universal.Copper())); CardFollowingInstructions?.Invoke(this, new CardFollowingInstructionsEventArgs(null, new Cards.Universal.Copper())); CardPlayed?.Invoke(this, new CardPlayedEventArgs(null, new Cards.Universal.Copper())); CardPlayFinished?.Invoke(this, new CardPlayedEventArgs(null, new Cards.Universal.Copper())); CardReceived?.Invoke(this, new CardReceivedEventArgs(null, null, DeckLocation.Discard, DeckPosition.Automatic)); CardsAddedToDeck?.Invoke(this, new CardsAddedToDeckEventArgs(new Cards.Universal.Copper(), DeckPosition.Automatic)); CardsAddedToHand?.Invoke(this, new CardsAddedToHandEventArgs(new Cards.Universal.Copper())); CardsDiscarded?.Invoke(this, new CardsDiscardEventArgs(DeckLocation.Discard, new Cards.Universal.Copper())); CardsDiscarding?.Invoke(this, new CardsDiscardEventArgs(DeckLocation.Discard, new Cards.Universal.Copper())); CardsDrawn?.Invoke(this, new CardsDrawnEventArgs(null, DeckPosition.Automatic, 0)); CardsLost?.Invoke(this, new CardsLostEventArgs(null)); CleanedUp?.Invoke(this, new CleanedUpEventArgs(null)); if (CleaningUp != null) { var cmc = new CardMovementCollection(); CleaningUp(this, new CleaningUpEventArgs(null, ref cmc)); } DrawNewHand?.Invoke(this, new DrawNewHandEventArgs(null, 0)); PhaseChanging?.Invoke(this, new PhaseChangingEventArgs(null, PhaseEnum.Endgame)); PhaseChanged?.Invoke(this, new PhaseChangedEventArgs(null, PhaseEnum.Endgame)); PhaseChangedFinished?.Invoke(this, new PhaseChangedEventArgs(null, PhaseEnum.Endgame)); ShufflingStart?.Invoke(this, new ShuffleEventArgs(null, null)); Shuffling?.Invoke(this, new ShuffleEventArgs(null, null)); Shuffled?.Invoke(this, new ShuffleEventArgs(null, null)); Trashing?.Invoke(this, new TrashEventArgs(null, null, null)); Trashed?.Invoke(this, new TrashEventArgs(null, null, null)); TrashedFinished?.Invoke(this, new TrashEventArgs(null, null, null)); TurnEnded?.Invoke(this, new TurnEndedEventArgs(null)); TurnStarted?.Invoke(this, new TurnStartedEventArgs(null)); TurnStarting?.Invoke(this, new TurnStartingEventArgs(null)); } public IPile ResolveDeck(DeckLocation location) { switch (location) { case DeckLocation.Hand: return Hand; case DeckLocation.Revealed: return Revealed; case DeckLocation.Discard: return DiscardPile; case DeckLocation.Deck: return DrawPile; case DeckLocation.InPlay: return InPlay; case DeckLocation.SetAside: return SetAside; case DeckLocation.Private: return Private; case DeckLocation.InPlayAndSetAside: return InPlayAndSetAside; } return null; } public DeckPosition ResolveDeckPosition(DeckLocation location, DeckPosition position) { switch (location) { case DeckLocation.Hand: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.Revealed: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.Discard: return position == DeckPosition.Automatic ? DeckPosition.Top : position; case DeckLocation.Deck: return position == DeckPosition.Automatic ? DeckPosition.Top : position; case DeckLocation.InPlay: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.SetAside: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.PlayerMat: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.Private: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; case DeckLocation.InPlayAndSetAside: return position == DeckPosition.Automatic ? DeckPosition.Bottom : position; } return position; } public virtual void BeginDrawing() { Revealed.BeginChanges(); _AsynchronousDrawing = true; _AsynchronousCardsDrawnEventArgs = null; } public virtual void EndDrawing() { _AsynchronousDrawing = false; if (_AsynchronousCardsDrawnEventArgs != null) { CardsDrawn?.Invoke(this, _AsynchronousCardsDrawnEventArgs); } _AsynchronousCardsDrawnEventArgs = null; Revealed.EndChanges(); } public bool CanDraw => DrawPile.Count > 0 || DiscardPile.Count > 0; public ItemCollection DrawHand(int number) { return Draw(number, DeckLocation.Hand); } public Card Draw(DeckLocation destination) { return Draw(1, destination).FirstOrDefault(); } public ItemCollection Draw(int number, DeckLocation destination) { var cardsDrawn = DrawFrom(DeckPosition.Top, number, destination); return cardsDrawn; } public Card Draw(Type deckType) { return Draw(1, deckType).FirstOrDefault(); } public ItemCollection Draw(int number, Type deckType) { var cardsDrawn = DrawFrom(DeckPosition.Top, number, deckType); return cardsDrawn; } public ItemCollection DrawFrom(DeckPosition deckPosition, int number, object destination) { Contract.Requires(destination != null, "destination cannot be null"); if (!(destination is Type) && !(destination is DeckLocation)) throw new Exception($"Destination of {destination} ({destination.GetType()}) is not supported"); if (DrawingCards != null) { DrawCardsEventArgs dcea; if (destination is DeckLocation dl) dcea = new DrawCardsEventArgs(this, deckPosition, number, dl); else dcea = new DrawCardsEventArgs(this, deckPosition, number, (Type)destination); DrawingCards(this, dcea); number = dcea.Count; destination = dcea.DestinationDeck == null ? dcea.DestinationType : (object)dcea.DestinationDeck; deckPosition = dcea.SourceDeckPosition; } var cards = new ItemCollection(); if (number <= 0) return cards; // 2nd Edition shuffling rules changed to shuffling the discard pile and putting it under the draw pile // if you need to draw more cards than the size of the draw pile (rather than drawing the draw pile empty // first). if (DrawPile.Count < number && DiscardPile.Any()) ShuffleForDrawing(); var cardsFirst = DrawPile.Retrieve(this, deckPosition, c => true, number); cards.AddRange(cardsFirst); cards.RemovedFrom(DeckLocation.Deck, this); if (_AsynchronousDrawing) { if (_AsynchronousCardsDrawnEventArgs == null) _AsynchronousCardsDrawnEventArgs = new CardsDrawnEventArgs(cardsFirst, deckPosition, number); else _AsynchronousCardsDrawnEventArgs.Cards.AddRange(cardsFirst); } else if (CardsDrawn != null) { var cdea = new CardsDrawnEventArgs(cardsFirst, deckPosition, number); CardsDrawn(this, cdea); } if (destination is Type) AddCardsInto((Type)destination, cardsFirst); else AddCardsInto((DeckLocation)destination, cardsFirst); // With 2nd Edition shuffling logic, this should never happen if (cardsFirst.Count < number && DrawPile.Count == 0 && DiscardPile.Any()) { ShuffleForDrawing(); var cardsSecond = DrawPile.Retrieve(this, deckPosition, c => true, number < 0 ? number : number - cards.Count); cards.AddRange(cardsSecond); cardsSecond.RemovedFrom(DeckLocation.Deck, this); if (_AsynchronousDrawing) { if (_AsynchronousCardsDrawnEventArgs == null) _AsynchronousCardsDrawnEventArgs = new CardsDrawnEventArgs(cardsSecond, deckPosition, number); else _AsynchronousCardsDrawnEventArgs.Cards.AddRange(cardsSecond); } else if (CardsDrawn != null) { var cdea = new CardsDrawnEventArgs(cardsSecond, deckPosition, number); CardsDrawn(this, cdea); } var type = destination as Type; if (type != null) AddCardsInto(type, cardsSecond); else AddCardsInto((DeckLocation)destination, cardsSecond); } return cards; } protected void ShuffleForDrawing() { Shuffle(RetrieveCardsFrom(DeckLocation.Discard)); } protected void ShuffleForDiscarding() { AddCardsInto(DeckLocation.Deck, RetrieveCardsFrom(DeckLocation.Discard), DeckPosition.Bottom); ShuffleDrawPile(); } public void ShuffleDrawPile() { Shuffle(RetrieveCardsFrom(DeckLocation.Deck)); } private void Shuffle(ItemCollection cardsToShuffle) { ShufflingStart?.Invoke(this, new ShuffleEventArgs(this, new ItemCollection(cardsToShuffle))); if (Shuffling != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; var sea = new ShuffleEventArgs(this, new ItemCollection(cardsToShuffle)); sea.HandledBy.AddRange(handledBy); Shuffling?.Invoke(this, sea); handledBy = sea.HandledBy; var options = new OptionCollection(); IEnumerable types = sea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(sea.Resolvers[key].Text, sea.Resolvers[key].IsRequired, sea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.Shuffling, options, this, sea); var result = MakeChoice(choice); if (result.Options.Any()) { sea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref sea); cardsToShuffle = new ItemCollection(sea.ShuffleCards.OfType()); actionPerformed = true; } } } while (Shuffling != null && actionPerformed); } cardsToShuffle.Shuffle(); if (Shuffled != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; var sea = new ShuffleEventArgs(this, new ItemCollection(cardsToShuffle)); sea.HandledBy.AddRange(handledBy); Shuffled?.Invoke(this, sea); handledBy = sea.HandledBy; var options = new OptionCollection(); IEnumerable types = sea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(sea.Resolvers[key].Text, sea.Resolvers[key].IsRequired, sea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.Shuffling, options, this, sea); var result = MakeChoice(choice); if (result.Options.Any()) { sea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref sea); cardsToShuffle = new ItemCollection(sea.ShuffleCards.OfType()); actionPerformed = true; } } } while (Shuffled != null && actionPerformed); } AddCardsInto(DeckLocation.Deck, cardsToShuffle, DeckPosition.Bottom); } public void AddCardToDeck(Card card, DeckPosition deckPosition) { AddCardInto(DeckLocation.Deck, card, deckPosition); if (CardsAddedToDeck != null) { var catdea = new CardsAddedToDeckEventArgs(card, deckPosition); CardsAddedToDeck(this, catdea); } } public void AddCardsToDeck(IEnumerable cards, DeckPosition deckPosition) { var cardsList = cards as IList ?? cards.ToList(); AddCardsInto(DeckLocation.Deck, cardsList, deckPosition); if (CardsAddedToDeck != null) { var catdea = new CardsAddedToDeckEventArgs(cardsList, deckPosition); CardsAddedToDeck(this, catdea); } } public void AddCardToHand(Card card) { AddCardInto(DeckLocation.Hand, card, DeckPosition.Bottom); if (CardsAddedToHand != null) { var cathea = new CardsAddedToHandEventArgs(card); CardsAddedToHand(this, cathea); } } public void AddCardsToHand(IEnumerable cards) { var cardsList = cards as IList ?? cards.ToList(); AddCardsInto(DeckLocation.Hand, cardsList, DeckPosition.Bottom); if (CardsAddedToHand != null) { var cathea = new CardsAddedToHandEventArgs(cardsList); CardsAddedToHand(this, cathea); } } public void AddCardsToHand(DeckLocation location) { AddCardsToHand(RetrieveCardsFrom(location)); } public Card Discard(DeckLocation fromLocation) { return Discard(fromLocation, -1).FirstOrDefault(); } public Card Discard(DeckLocation fromLocation, Card card) { return Discard(fromLocation, c => c == card).FirstOrDefault(); } public ItemCollection Discard(DeckLocation fromLocation, int count) { return Discard(fromLocation, null, c => true, count); } public ItemCollection Discard(DeckLocation fromLocation, IEnumerable cards, CardsDiscardResolver discardAction = null) { return Discard(fromLocation, cards.Contains, discardAction); } public ItemCollection Discard(DeckLocation fromLocation, Type type, int count) { return Discard(fromLocation, null, c => c.Type == type, count); } public ItemCollection Discard(DeckLocation fromLocation, Predicate match, CardsDiscardResolver discardAction = null) { return Discard(fromLocation, null, match, discardAction: discardAction); } public ItemCollection Discard(DeckLocation fromLocation, Type matType, Predicate match, int count = -1, CardsDiscardResolver discardAction = null) { ItemCollection matchingCards; var retrieveLocation = fromLocation; if (fromLocation == DeckLocation.Deck) { matchingCards = RetrieveCardsFrom(fromLocation, DeckPosition.Automatic, match, count); AddCardsInto(DeckLocation.Revealed, matchingCards); retrieveLocation = DeckLocation.Revealed; } else if (fromLocation == DeckLocation.PlayerMat) { if (!PlayerMats.ContainsKey(matType)) return new ItemCollection(); matchingCards = PlayerMats[matType][match]; } else { matchingCards = ResolveDeck(fromLocation)[match]; } // Special case for when we're discard from our draw pile and we'd need to shuffle if (fromLocation == DeckLocation.Deck && count > matchingCards.Count && CanDraw) { ShuffleForDiscarding(); matchingCards.AddRange(ResolveDeck(fromLocation)[match]); } if (count > 0 && count < matchingCards.Count) { matchingCards.RemoveRange(count, matchingCards.Count - count); if (matchingCards.Count != count) throw new Exception(Resource.ExceptionIncorrectNumberFound); } if (matchingCards.Count == 0) return matchingCards; if (CardsDiscarding != null) { CardsDiscardEventArgs cdea; var handledBy = new List(); bool actionPerformed; var cancelled = false; do { actionPerformed = false; cdea = new CardsDiscardEventArgs(fromLocation, matType, matchingCards) { Cancelled = cancelled }; cdea.HandledBy.AddRange(handledBy); CardsDiscarding(this, cdea); handledBy = cdea.HandledBy; matchingCards = cdea.Cards; cancelled |= cdea.Cancelled; var options = new OptionCollection(); var types = cdea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cdea.Resolvers[key].Text, cdea.Resolvers[key].IsRequired, cdea.Resolvers[key].Source))); if (options.Any()) { if (discardAction != null && !cdea.HandledBy.Contains(this)) { cdea.AddResolver(GetType(), discardAction); options.Add(new Option(discardAction.Text, discardAction.IsRequired, discardAction.Source)); } options.Sort(); var discardingText = ResourcesHelper.Get($"Discarding{matchingCards.Count}Cards"); if (string.IsNullOrEmpty(discardingText)) discardingText = Resource.DiscardingNCards.Replace("{count}", matchingCards.Count.ToString(CultureInfo.CurrentCulture)); var choice = new Choice(discardingText, options, this, cdea); var result = MakeChoice(choice); if (result.Options.Any()) { var action = cdea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value; cdea.Data = action.Data; action.Method(this, ref cdea); actionPerformed = true; handledBy = cdea.HandledBy; matchingCards = cdea.Cards; cancelled |= cdea.Cancelled; } } } while (CardsDiscarding != null && actionPerformed); if (cancelled) return new ItemCollection(); } if (fromLocation == DeckLocation.PlayerMat) RetrieveCardsFrom(matType, matchingCards); else RetrieveCardsFrom(retrieveLocation, matchingCards); if (CardsDiscard != null) { var handledBy = new List(); var cdea = new CardsDiscardEventArgs(fromLocation, matchingCards); cdea.HandledBy.AddRange(handledBy); CardsDiscard(this, cdea); matchingCards = cdea.Cards; } AddCardsInto(DeckLocation.Discard, matchingCards); if (CardsDiscarded != null) { CardsDiscardEventArgs cdea; var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; cdea = new CardsDiscardEventArgs(fromLocation, matType, matchingCards); cdea.HandledBy.AddRange(handledBy); CardsDiscarded(this, cdea); var options = new OptionCollection(); var types = cdea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cdea.Resolvers[key].Text, cdea.Resolvers[key].IsRequired, cdea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var discardedText = ResourcesHelper.Get($"Discarded{matchingCards.Count}Cards"); if (string.IsNullOrEmpty(discardedText)) discardedText = Resource.DiscardedNCards.Replace("{count}", matchingCards.Count.ToString(CultureInfo.CurrentCulture)); var choice = new Choice(discardedText, options, this, cdea); var result = MakeChoice(choice); if (result.Options.Any()) { cdea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cdea); actionPerformed = true; } } handledBy = cdea.HandledBy; } while (CardsDiscarded != null && actionPerformed); } matchingCards.RemovedFrom(fromLocation, this); return matchingCards; } public void AddCardInto(DeckLocation location, Card card, DeckPosition position = DeckPosition.Automatic) { AddCardsInto(location, new ItemCollection { card }, position); } public void AddCardsInto(DeckLocation location, IEnumerable cards, DeckPosition position = DeckPosition.Automatic) { var cardList = cards as IList ?? cards.ToList(); if (Phase != PhaseEnum.Endgame) { foreach (var card in cardList) card.AddedTo(location, this); } var realPosition = ResolveDeckPosition(location, position); switch (location) { case DeckLocation.Hand: foreach (var card in cardList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.CanCleanUpPlayed.Clear(); } Hand.AddRange(this, cardList, realPosition); break; case DeckLocation.Revealed: foreach (var card in cardList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.CanCleanUpPlayed.Clear(); } Revealed.AddRange(this, cardList, realPosition); break; case DeckLocation.Discard: foreach (var card in cardList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.CanCleanUpPlayed.Clear(); } DiscardPile.AddRange(this, cardList, realPosition); break; case DeckLocation.Deck: foreach (var card in cardList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.CanCleanUpPlayed.Clear(); } DrawPile.AddRange(this, cardList, realPosition); break; case DeckLocation.InPlay: InPlay.AddRange(this, cardList, realPosition); break; case DeckLocation.SetAside: SetAside.AddRange(this, cardList, realPosition); break; case DeckLocation.Private: foreach (var card in cardList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.CanCleanUpPlayed.Clear(); } Private.AddRange(this, cardList, realPosition); break; } } public void AddCardInto(Type deckType, Card card, DeckPosition position = DeckPosition.Automatic) { AddCardsInto(deckType, new ItemCollection { card }, position); } public void AddCardsInto(Type deckType, IEnumerable cards, DeckPosition position = DeckPosition.Automatic) { var cardsList = cards as IList ?? cards.ToList(); if (!cardsList.Any()) return; foreach (var card in cardsList) { card.ModifiedBy = null; card.ChainedInto.Clear(); card.AddedTo(deckType, this); } PlayerMats.Add(this, deckType, cardsList); } public Card RetrieveCardFrom(DeckLocation location, Card card) { var cc = RetrieveCardsFrom(location, DeckPosition.Automatic, c => c == card); if (cc.Count == 1) return cc[0]; throw new Exception(Resource.IncorrectNumberFound); } public Card RetrieveCardFrom(DeckLocation location, Type type) { var cc = RetrieveCardsFrom(location, type, 1); if (cc.Count == 1) return cc[0]; throw new Exception(Resource.IncorrectNumberFound); } public ItemCollection RetrieveCardsFrom(DeckLocation location) { return RetrieveCardsFrom(location, DeckPosition.Automatic, c => true); } public ItemCollection RetrieveCardsFrom(DeckLocation location, ItemCollection cards) { Contract.Requires(cards != null, "cards cannot be null"); return new ItemCollection(RetrieveCardsFrom(location, DeckPosition.Automatic, cards.Contains).OrderBy(cards.IndexOf)); } public ItemCollection RetrieveCardsFrom(DeckLocation location, Categories category, int count = -1) { return RetrieveCardsFrom(location, DeckPosition.Automatic, c => c.Category.HasFlag(category), count); } public ItemCollection RetrieveCardsFrom(DeckLocation location, Type type, int count) { return RetrieveCardsFrom(location, DeckPosition.Automatic, c => c.Type == type, count); } public ItemCollection RetrieveCardsFrom(DeckLocation location, Predicate match, int count = -1) { return RetrieveCardsFrom(location, DeckPosition.Automatic, match, count); } internal ItemCollection RetrieveCardsFrom(DeckLocation location, DeckPosition position, Predicate match, int count = -1) { ItemCollection cards; switch (location) { case DeckLocation.Hand: cards = Hand.Retrieve(this, position, match, count); break; case DeckLocation.Revealed: cards = Revealed.Retrieve(this, position, match, count); break; case DeckLocation.Discard: cards = DiscardPile.Retrieve(this, position, match, count); break; case DeckLocation.Deck: cards = DrawPile.Retrieve(this, position, match, count); if (cards.Count < count && DrawPile.Count == 0 && DiscardPile.Any()) ShuffleForDiscarding(); cards.AddRange(DrawPile.Retrieve(this, position, match, count < 0 ? count : count - cards.Count)); break; case DeckLocation.InPlay: cards = InPlay.Retrieve(this, position, match, count); break; case DeckLocation.SetAside: cards = SetAside.Retrieve(this, position, match, count); break; case DeckLocation.Private: cards = Private.Retrieve(this, position, match, count); break; case DeckLocation.InPlayAndSetAside: cards = InPlay.Retrieve(this, position, match, count); cards.AddRange(SetAside.Retrieve(this, position, match, count < 0 ? count : count - cards.Count)); break; default: cards = new ItemCollection(); break; } cards.RemovedFrom(location, this); return cards; } public Card RetrieveCardFrom(Type deckType, Card card) { var cc = RetrieveCardsFrom(deckType, c => c == card); if (cc.Count == 1) return cc[0]; throw new Exception(Resource.IncorrectNumberFound); } public ItemCollection RetrieveCardsFrom(Type deckType) { return RetrieveCardsFrom(deckType, c => true); } internal ItemCollection RetrieveCardsFrom(Type deckType, ItemCollection cards) { return new ItemCollection(RetrieveCardsFrom(deckType, cards.Contains).OrderBy(cards.IndexOf)); } internal ItemCollection RetrieveCardsFrom(Type deckType, Predicate match, int count = -1) { var cards = PlayerMats.Retrieve(this, deckType, match, count); cards.RemovedFrom(deckType, this); return cards; } public void MoveInPlayToSetAside(Predicate match) { var cardsToMove = InPlay.Retrieve(this, match); // Can't do this because it will trigger Band of Misfit's action // Need to do this to trigger removing of event from cards like Lighthouse // Need to resolve this issue somehow. //cardsToMove.RemovedFrom(DeckLocation.InPlay, this); AddCardsInto(DeckLocation.SetAside, cardsToMove); } private ItemCollection MoveToTrashStart(DeckLocation location, ItemCollection cards) { ItemCollection cardsMoved; switch (location) { case DeckLocation.Hand: cardsMoved = Hand.Retrieve(this, cards.Contains); break; case DeckLocation.Revealed: cardsMoved = Revealed.Retrieve(this, cards.Contains); break; case DeckLocation.Discard: cardsMoved = DiscardPile.Retrieve(this, cards.Contains); break; case DeckLocation.InPlay: cardsMoved = InPlay.Retrieve(this, cards.Contains); break; case DeckLocation.SetAside: cardsMoved = SetAside.Retrieve(this, cards.Contains); break; case DeckLocation.Private: cardsMoved = Private.Retrieve(this, cards.Contains); break; default: throw new Exception(Resource.ExceptionCannotMoveCardToTrash); } _Game.Table.Trash.AddRange(cardsMoved); return cardsMoved; } /// /// Short-hand for discarding all cards in Hand /// public void DiscardHand(bool visible) { if (visible) Discard(DeckLocation.Hand); else AddCardsInto(DeckLocation.Discard, RetrieveCardsFrom(DeckLocation.Hand)); } /// /// Short-hand for discarding all Revealed cards /// public void DiscardRevealed() { Discard(DeckLocation.Revealed); } /// /// Short-hand for discarding all Revealed cards matching the Predicate /// public void DiscardRevealed(Predicate match) { Discard(DeckLocation.Revealed, match); } /// /// Short-hand for revealing Hand /// public ItemCollection RevealHand() { var hand = RetrieveCardsFrom(DeckLocation.Hand); AddCardsInto(DeckLocation.Revealed, hand); return hand; } /// /// Short-hand for returning Revealed to Hand. This version also doesn't trigger the CardsAddedToHandEventArgs event /// public void ReturnHand(ItemCollection hand) { AddCardsInto(DeckLocation.Hand, RetrieveCardsFrom(DeckLocation.Revealed, hand), DeckPosition.Bottom); } private CardGainEventArgs CardGainCheckAllowed(Card card, IGameObject source, DeckLocation location, DeckPosition position, bool isBought) { var cancelled = false; var cgea = new CardGainEventArgs(_Game, card, source, location, position, isBought); if (CardGaining != null) { do { cgea = new CardGainEventArgs(_Game, card, source, location, position, isBought) { Cancelled = cancelled }; CardGaining(this, cgea); var options = new OptionCollection(); IEnumerable types = cgea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cgea.Resolvers[key].Text, cgea.Resolvers[key].IsRequired, cgea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.GainingCard.Replace("{card}", card.Name), options, this, cgea, new ICardBaseCollection { card }); var result = MakeChoice(choice); if (result.Options.Any()) cgea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cgea); } cancelled = cgea.Cancelled; location = cgea.Location; position = cgea.Position; } while (CardGaining != null && cgea.HandledBy.Any()); } return cgea; } /// /// Tries to gain the specified card from the trash specified into the location and position of the deck specified /// /// The trash pile to look through /// The card to gain from the trash /// The deck the card should go into /// The position into the deck the card should go /// True if the card was actually gained, False otherwise public bool Gain(Trash trash, Card card, IGameObject source, DeckLocation location = DeckLocation.Discard, DeckPosition position = DeckPosition.Automatic) { Contract.Requires(trash != null, "trash cannot be null"); if (trash.Contains(card)) { var cgea = CardGainCheckAllowed(card, source, location, position, false); if (!cgea.Cancelled) return Gain(trash.Retrieve(this, card), source, cgea.Location, cgea.Position, cgea.Bought); CardGainFinish(card, source, cgea.Location, cgea.Position, cgea.Bought, cgea.Cancelled, cgea.IsLostTrackOf); return true; } return false; } /// /// Tries to gain the number of cards specified from the specified supply /// /// The supply pile to gain from /// How many cards to gain /// Indicating whether or not the card was bought /// True if the card was actually gained, False otherwise public bool Gain(ISupply supply, IGameObject source, int count = 1, bool isBought = false) { Contract.Requires(supply != null, "supply cannot be null"); var success = true; for (var i = 0; i < count; i++) success &= Gain(supply, supply.TopCard?.Type, source, DeckLocation.Discard, isBought: isBought); return success; } /// /// Tries to gain the number of cards specified from the specified supply into the location and position of the deck specified /// /// The supply pile to gain from /// The deck the card should go into /// The position into the deck the card should go /// How many cards to gain /// Indicating whether or not the card was bought /// True if the card was actually gained, False otherwise public bool Gain(ISupply supply, IGameObject source, DeckLocation location, DeckPosition position = DeckPosition.Automatic, int count = 1, bool isBought = false) { Contract.Requires(supply != null, "supply cannot be null"); var success = true; for (var i = 0; i < count; i++) success &= Gain(supply, supply.TopCard?.Type, source, location, position, isBought); return success; } /// /// Tries to gain the specified type from the specified supply into the location and position of the deck specified /// /// The supply pile to gain from /// The card type we're trying to gain /// The deck the card should go into /// The position into the deck the card should go /// Indicating whether or not the card was bought /// True if the card was actually gained, False otherwise public bool Gain(ISupply supply, Type type, IGameObject source, DeckLocation location, DeckPosition position = DeckPosition.Automatic, bool isBought = false) { Contract.Requires(supply != null, "supply cannot be null"); if (supply.CanGain(type)) { var supplyCard = supply[type].First(); var cgea = CardGainCheckAllowed(supplyCard, source, location, position, isBought); if (!cgea.Cancelled) return Gain(supply.Take(type), source, cgea.Location, cgea.Position, cgea.Bought); else { CardGainFinish(supplyCard, source, cgea.Location, cgea.Position, cgea.Bought, cgea.Cancelled, cgea.IsLostTrackOf); return false; } } return false; } /// /// Tries to gain the specified card into the location and position of the deck specified /// /// The card to gain from the supply /// The deck the card should go into /// The position into the deck the card should go /// Indicating whether or not the card was bought /// True if the card was actually gained, False otherwise private bool Gain(Card card, IGameObject source, DeckLocation location, DeckPosition position, bool isBought) { if (card == null) return false; var cancelled = false; var lostTrackOf = false; CurrentTurn?.Gained(card); CardGainInto(card, source, location, position, isBought, false, false); if (CardGained != null) { // This is a little bit wacky, but we're going to set up an event listener INSIDE this method that listens to both // the Discard Pile and the Draw Pile for changes. We need to do this in order to capture any "Lost Track" updates // that might happen from one card covering up another card (e.g. this card being gained) and causing the game state // to "Lose Track" of the card being gained in this method. _LostTrackStack[card] = false; var pceh = new PileChangedEventHandler(DiscardPile_PileChanged_CaptureLostTrack); DiscardPile.PileChanged += pceh; var handledBy = new List(); CardGainEventArgs cgea; bool actionPerformed; do { actionPerformed = false; cgea = new CardGainEventArgs(_Game, card, source, location, position, isBought); cgea.HandledBy.AddRange(handledBy); cgea.Cancelled = cancelled; cgea.IsLostTrackOf = lostTrackOf; CardGained(this, cgea); cancelled |= cgea.Cancelled; lostTrackOf |= cgea.IsLostTrackOf || _LostTrackStack[card]; location = cgea.Location; position = cgea.Position; var enumerator = _Game.GetPlayersStartingWithEnumerator(this); while (enumerator.MoveNext()) { var options = new OptionCollection(); IEnumerable types = cgea.Resolvers.Keys; options.AddRange( types.Where(key => cgea.Resolvers[key].Player == enumerator.Current) .Select( key => new Option(cgea.Resolvers[key].Text, cgea.Resolvers[key].IsRequired, cgea.Resolvers[key].Source) { Value = cgea.Resolvers[key].Operation })); if (options.Any()) { var choiceText = Resource.PlayerGainedCard.Replace("{player}", this == enumerator.Current ? Resource.You : ToString()).Replace("{card}", card.Name); var choice = new Choice(choiceText, options, this, cgea, new ICardBaseCollection { card }); var result = enumerator.Current.MakeChoice(choice); if (result.Options.Any()) { options.Sort(); cgea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(enumerator.Current, ref cgea); actionPerformed = true; } if (enumerator.Current == this && (cgea.Location != location || cgea.Position != position)) CardGainInto(card, cgea.Source, cgea.Location, cgea.Position, cgea.Bought, cgea.Cancelled, cgea.IsLostTrackOf); handledBy = cgea.HandledBy; cancelled |= cgea.Cancelled; lostTrackOf |= cgea.IsLostTrackOf || _LostTrackStack[card]; location = cgea.Location; position = cgea.Position; } } } while (CardGained != null && actionPerformed); DiscardPile.PileChanged -= pceh; _LostTrackStack.Remove(card); } CardGainFinish(card, source, location, position, isBought, cancelled, lostTrackOf); return true; } public IBoon TakeBoon(IBoonSupply boonSupply) { Contract.Requires(boonSupply != null, "boonSupply cannot be null"); var boon = boonSupply.TakeBoon(this); TakenBoons.Add(boon); BoonsChanged?.Invoke(this, new BoonEventArgs(this, BoonAction.Taken, boon)); return boon; } public ItemCollection TakeBoon(IBoonSupply boonSupply, int count) { Contract.Requires(boonSupply != null, "boonSupply cannot be null"); ItemCollection boons = new ItemCollection(); for (var i = 0; i < count; i++) boons.Add(boonSupply.TakeBoon(this)); TakenBoons.AddRange(boons); BoonsChanged?.Invoke(this, new BoonEventArgs(this, BoonAction.Taken, boons)); return boons; } public bool KeepBoon(IBoonSupply boonSupply, IBoon targetBoon) { Contract.Requires(boonSupply != null, "boonSupply cannot be null"); Contract.Requires(targetBoon != null, "targetBoon cannot be null"); var kept = boonSupply.KeepBoon(this, targetBoon); if (kept) { TakenBoons.Add(targetBoon); BoonsChanged?.Invoke(this, new BoonEventArgs(this, BoonAction.Kept, targetBoon)); } return kept; } public void ReturnBoon(IBoonSupply boonSupply, IBoon boon) { Contract.Requires(boonSupply != null, "boonSupply cannot be null"); Contract.Requires(boon != null, "boon cannot be null"); _Game.SendMessage(this, boon, "Return"); if (TakenBoons.Remove(boon)) { boonSupply.ReturnBoon(boon); BoonsChanged?.Invoke(this, new BoonEventArgs(this, BoonAction.Returned, boon)); } } public void ReturnBoon(IBoonSupply boonSupply, ItemCollection boons) { Contract.Requires(boonSupply != null, "boonSupply cannot be null"); Contract.Requires(boons != null, "boons cannot be null"); foreach (var boon in boons) ReturnBoon(boonSupply, boon); } public void Take(Type takeableType) { Contract.Requires(takeableType != null, "takeableType cannot be null"); //var takeable = _Game.Table.SpecialPiles.Select(sp => sp.Value).OfType().FirstOrDefault(t => t.Type == takeableType); if (!(_Game.Table.GetOrAdd(_Game, takeableType) is ITakeable takeable)) throw new KeyNotFoundException(); if (Takeables.Any(t => t.Type == takeableType)) return; if (!takeable.CanBeMultiples) { // Find where the Takeable item exists (if at all) and Return it from all of the players first var ePlayers = _Game.GetPlayersStartingWithEnumerator(this); while (ePlayers.MoveNext()) { if (ePlayers.Current == this) continue; var otherPlayerTakeables = ePlayers.Current.Takeables.Where(s => s.Type == takeableType).ToList(); foreach (var st in otherPlayerTakeables) ePlayers.Current.Return(st.Type); } } _Game.Table.SpecialPiles.Remove(takeableType); Takeables.Add(takeable); takeable.TakenBy(this); TakeablesChanged?.Invoke(this, new TakeableEventArgs(this, TakeableAction.Taken, takeable)); } public void Return(Type takeableType) { Contract.Requires(takeableType != null, "takeableType cannot be null"); var takeable = Takeables.First(t => t.Type == takeableType); Takeables.RemoveAll(t => t == takeable); takeable.ReturnedBy(this); // Only add it back to the table if there can't be multiples of the takeable item if (!takeable.CanBeMultiples) _Game.Table.SpecialPiles[takeableType] = takeable; TakeablesChanged?.Invoke(this, new TakeableEventArgs(this, TakeableAction.Returned, takeable)); } private void DiscardPile_PileChanged_CaptureLostTrack(object sender, PileChangedEventArgs e) { if (e.OperationPerformed == Operation.Refresh) return; var cards = new List(_LostTrackStack.Keys); foreach (var card in cards) { // Check to see if the card we're tracking is available (we can only look at the top card) // If it isn't the top card, then we've lost track of it. if (!_LostTrackStack[card] && DiscardPile[c => c == card].Count == 0) _LostTrackStack[card] = true; } } private void CardGainInto(Card card, IGameObject source, DeckLocation location, DeckPosition position, bool isBought, bool isCancelled, bool isLostTrackOf) { if (DiscardPile.Contains(card)) RetrieveCardFrom(DeckLocation.Discard, card); card.Gaining(this, ref location, ref position); AddCardInto(location, card, position); card.Gained(this); CardGainedInto?.Invoke(this, new CardGainEventArgs(_Game, card, source, location, position, isBought) { Cancelled = isCancelled, IsLostTrackOf = isLostTrackOf }); } private void CardGainFinish(Card card, IGameObject source, DeckLocation location, DeckPosition position, bool isBought, bool isCancelled, bool isLostTrackOf) { CardGainFinished?.Invoke(this, new CardGainEventArgs(_Game, card, source, location, position, isBought) { Cancelled = isCancelled, IsLostTrackOf = isLostTrackOf }); } public bool Buy(IBuyable buyable) { Contract.Requires(buyable != null, "buyable cannot be null"); var buyableCard = buyable.TopCard; var previousMode = PlayerMode; PlayerMode = PlayerMode.Buying; var cancelled = false; if (CardBuying != null) { var cbea = new CardBuyEventArgs(_Game, buyable, buyableCard); CardBuying(this, cbea); cancelled = cbea.Cancelled; } if (!cancelled) { PayForCard(buyable, buyableCard); if (CardBought != null) { CardBuyEventArgs cbea; var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; cbea = new CardBuyEventArgs(_Game, buyable, buyableCard); cbea.HandledBy.AddRange(handledBy); CardBought(this, cbea); var options = new OptionCollection(); IEnumerable types = cbea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cbea.Resolvers[key].Text, cbea.Resolvers[key].IsRequired, cbea.Resolvers[key].Trigger))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.BoughtCard.Replace("{card}", buyableCard.ToString()), options, this, cbea, new ICardBaseCollection { buyableCard }); var result = MakeChoice(choice); if (result.Options.Any()) { cbea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cbea); actionPerformed = true; } } handledBy = cbea.HandledBy; } while (CardBought != null && actionPerformed); } } if (CardBuyFinished != null) { var cbea = new CardBuyEventArgs(_Game, buyable, buyableCard) { Cancelled = cancelled }; CardBuyFinished(this, cbea); } // TODO -- this casting is a hack and needs to be abstracted out if (!cancelled && buyable is ISupply bSupply && bSupply.CanGain() && buyable.TopCard.Name == buyableCard.Name) Gain(bSupply, null, isBought: true); if (PlayerMode == PlayerMode.Buying) PlayerMode = previousMode; return cancelled; } private void PayForCard(IBuyable buyable, ICost buyableCard) { var cost = new Currency(_Game.ComputeCost(buyableCard)); // CardPaying event can alter the cost that the player pays if (CardPaying != null) { CardPayEventArgs cpea; var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; cpea = new CardPayEventArgs(buyableCard, Currency, cost, handledBy); CardPaying(this, cpea); var options = new OptionCollection(); IEnumerable types = cpea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cpea.Resolvers[key].Text, cpea.Resolvers[key].IsRequired, cpea.Resolvers[key].Trigger))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.PayingForCard.Replace("{card}", buyableCard.ToString()), options, this, cpea, new ICardBaseCollection { buyableCard }); var result = MakeChoice(choice); if (result.Options.Any()) { cpea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cpea); actionPerformed = true; cost = cpea.Cost; } } handledBy = cpea.HandledBy; } while (CardPaying != null && actionPerformed); } CurrentTurn.Bought(buyableCard); buyable.Bought(this); SpendCurrency(cost); Buys--; cost.Dispose(); } public void SpendCurrency(Currency currency) { Contract.Requires(currency != default(Currency), "currency cannot be null"); if (currency.Debt.Value > 0) { var tokens = new TokenCollection(); for (var i = 0; i < currency.Debt.Value; i++) tokens.Add(new Cards.Empires.DebtToken()); TokenPiles.AddCollection(tokens, this); } Currency = new Currency(Currency.Coin - currency.Coin, Currency.Potion - currency.Potion); } public void Receive(IPlayer fromPlayer, Card card, DeckLocation location, DeckPosition position) { Contract.Requires(card != null, "card cannot be null"); CurrentTurn?.Received(card); AddCardInto(location, card, position); card.ReceivedBy(this); if (CardReceived != null) { var crea = new CardReceivedEventArgs(fromPlayer, card, location, position); CardReceived(this, crea); } } public void Lose(ItemCollection cards) { Contract.Requires(cards != null, "cards cannot be null"); cards.LostBy(this); if (CardsLost != null) { var clea = new CardsLostEventArgs(cards); CardsLost(this, clea); } } public void Lose(Card card) { Lose(new ItemCollection { card }); } public virtual void Cleanup() { if (Phase != PhaseEnum.Waiting) { // Go through all of the phases, in order to trigger things properly if (Phase == PhaseEnum.Action || Phase == PhaseEnum.ActionTreasure) Phase = PhaseEnum.BuyTreasure; if (Phase == PhaseEnum.BuyTreasure) Phase = PhaseEnum.Buy; if (Phase == PhaseEnum.Buy) Phase = PhaseEnum.Night; Phase = PhaseEnum.Cleanup; } PerformCleanup(); } public virtual void EndTurn() { Phase = PhaseEnum.Waiting; PlayerMode = PlayerMode.Waiting; CurrentTurn?.FinishTurn(); } private void PerformCleanup() { var cardsToMove = new CardMovementCollection(); // Sets up card movements to indicate where each card should go. if (_Game.ActivePlayer == this) { // Only discard and and clean-up Set Aside if it's our turn cardsToMove.AddRange(SetAside, DeckLocation.SetAside, DeckLocation.Discard); cardsToMove.AddRange(Hand, DeckLocation.Hand, DeckLocation.Discard); } cardsToMove.AddRange(InPlay, DeckLocation.InPlay, DeckLocation.Discard); foreach (var durationCard in InPlayAndSetAside.Where(c => !c.CanCleanUp || c.CanCleanUpPlayed.Any(cup => !cup))) { if (!cardsToMove.Contains(durationCard)) continue; cardsToMove[durationCard].Destination = DeckLocation.SetAside; // I don't like using the .OfType() here, but that will be for another day. // Citadel kinda messes with the tracking. foreach (var durationChain in durationCard.ChainedInto.OfType()) cardsToMove[durationChain].Destination = DeckLocation.SetAside; } foreach (var chainedCard in InPlayAndSetAside.Where(c => c.ChainedInto.Any())) { if (!cardsToMove.Contains(chainedCard)) continue; if (chainedCard.ChainedInto.OfType().Any(cc => !cc.CanCleanUp || cc.CanCleanUpPlayed.Any(cup => !cup))) cardsToMove[chainedCard].Destination = DeckLocation.SetAside; } //cardsToMove // .Where(cm => // (cm.CurrentLocation == DeckLocation.InPlay || cm.CurrentLocation == DeckLocation.SetAside) && // cm.Destination == DeckLocation.SetAside && // cm.Card.ModifiedBy != null && // cardsToMove.Contains(cm.Card.ModifiedBy.PhysicalCard) && // cardsToMove[cm.Card.ModifiedBy.PhysicalCard].Destination == DeckLocation.Discard) // .AsParallel().ForAll(cm => // { // cardsToMove[cm.Card.ModifiedBy.PhysicalCard].Destination = DeckLocation.SetAside; // }); if (CleaningUp != null) { var cancelled = false; // Possibly changing events that can happen in the game CleaningUpEventArgs cuea; do { cuea = new CleaningUpEventArgs(this, ref cardsToMove); cuea.Cancelled |= cancelled; CleaningUp(this, cuea); var options = new OptionCollection(); IEnumerable types = cuea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cuea.Resolvers[key].Text, cuea.Resolvers[key].IsRequired, cuea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var choice = new Choice(Resource.PerformingCleanup, options, this, cuea); var result = MakeChoice(choice); if (result.Options.Any()) cuea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cuea); else break; } else break; cancelled |= cuea.Cancelled; } while (CleaningUp != null); cancelled |= cuea.Cancelled; if (cancelled) return; } // Discard any Revealed cards (should be none?) DiscardRevealed(); CardsDiscardResolver cdaHand = null; if (cardsToMove.Any(c => c.CurrentLocation == DeckLocation.Hand)) cdaHand = new CardsDiscardResolver(this, null, Resource.DiscardHand, Player_DiscardHand, true) { Data = cardsToMove }; // Discard non-Duration (or Duration-modifying) cards in In Play & Set Aside at the same time Discard(DeckLocation.InPlayAndSetAside, cardsToMove.Where(cm => (cm.CurrentLocation == DeckLocation.InPlay || cm.CurrentLocation == DeckLocation.SetAside) && cm.Destination == DeckLocation.Discard).Select(cm => cm.Card), cdaHand); // Discard Hand AddCardsInto(DeckLocation.Discard, RetrieveCardsFrom(DeckLocation.Hand, c => cardsToMove.Contains(c) && cardsToMove[c].CurrentLocation == DeckLocation.Hand && cardsToMove[c].Destination == DeckLocation.Discard)); // Move Duration (and Duration-modifying) cards from In Play into Set Aside MoveInPlayToSetAside(c => cardsToMove.Contains(c) && cardsToMove[c].CurrentLocation == DeckLocation.InPlay && cardsToMove[c].Destination == DeckLocation.SetAside); // Move any cards that have had their Destination changed to their appropriate locations var replaceCards = cardsToMove.Where(cm => cm.Destination == DeckLocation.Deck).Select(cm => cm.Card).ToList(); if (replaceCards.Any()) { var replaceChoice = new Choice(Resource.ChooseOrderToTopdeck, null, replaceCards, ChoiceOutcome.Select, this, true, minimum: replaceCards.Count, maximum: replaceCards.Count); var replaceResult = MakeChoice(replaceChoice); RetrieveCardsFrom(DeckLocation.InPlay, c => cardsToMove[c].CurrentLocation == DeckLocation.InPlay && replaceResult.Cards.Contains(c)); RetrieveCardsFrom(DeckLocation.SetAside, c => cardsToMove[c].CurrentLocation == DeckLocation.SetAside && replaceResult.Cards.Contains(c)); AddCardsToDeck(replaceResult.Cards, DeckPosition.Top); } #if DEBUG if (InPlay.Any()) throw new Exception(Resource.ExceptionCardsInInPlayArea); #endif _Actions = _Buys = 0; _Currency.Coin.Value = 0; _Currency.Potion.Value = 0; ActionsPlayed = 0; #if DEBUG // Check to see that there are no duplicate cards anywhere var allCards = new ItemCollection(); allCards.AddRange(Hand); allCards.AddRange(Revealed); allCards.AddRange(Private); allCards.AddRange(InPlay); allCards.AddRange(SetAside); allCards.AddRange(DrawPile.LookThrough(c => true)); allCards.AddRange(DiscardPile.LookThrough(c => true)); foreach (var mat in PlayerMats.Values) allCards.AddRange(mat); var duplicateCards = allCards.AsParallel().Where(c => allCards.Count(ct => ct == c) > 1); if (duplicateCards.Any()) { duplicateCards.ForAll(c => Console.WriteLine($"Duplicate: {c.Name} - {c.UniqueId}")); // Ruh Roh throw new Exception(Resource.ExceptionDuplicateCards); } #endif if (_Game.ActivePlayer == this) { var drawSize = 5; var copyDrawNewHand = DrawNewHand; if (copyDrawNewHand != null) { var handledBy = new List(); // Possibly changing events that can happen in the game do { var dnhea = new DrawNewHandEventArgs(this, drawSize); dnhea.HandledBy.AddRange(handledBy); copyDrawNewHand(this, dnhea); var options = new OptionCollection(); IEnumerable types = dnhea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(dnhea.Resolvers[key].Text, dnhea.Resolvers[key].IsRequired, dnhea.Resolvers[key].Card))); if (options.Count == 0) break; options.Sort(); var choice = new Choice(Resource.DrawingNewHand, options, this, dnhea); var result = MakeChoice(choice); if (result.Options.Count == 0) break; dnhea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref dnhea); drawSize = dnhea.DrawSize; handledBy = dnhea.HandledBy; } while (true); } DrawHand(drawSize); PlayerMode = PlayerMode.Waiting; var cleanedUpCopy = CleanedUp; if (cleanedUpCopy != null) { var handledBy = new List(); // Possibly changing events that can happen in the game do { var cuea = new CleanedUpEventArgs(this); cuea.HandledBy.AddRange(handledBy); cleanedUpCopy(this, cuea); var options = new OptionCollection(); IEnumerable types = cuea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cuea.Resolvers[key].Text, cuea.Resolvers[key].IsRequired, cuea.Resolvers[key].Card))); if (options.Count == 0) break; options.Sort(); var choice = new Choice(Resource.CleanedUp, options, this, cuea); var result = MakeChoice(choice); if (result.Options.Count == 0) break; cuea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cuea); handledBy = cuea.HandledBy; } while (true); } var turnEndedCopy = TurnEnded; if (turnEndedCopy != null) { var teea = new TurnEndedEventArgs(this); turnEndedCopy(this, teea); if (teea.NextPlayer != null) CurrentTurn.NextPlayer = teea.NextPlayer; CurrentTurn.NextGrantedBy = teea.NextGrantedBy; } } } internal void Player_DiscardHand(IPlayer player, ref CardsDiscardEventArgs e) { var cardsToMove = (CardMovementCollection)e.Data; // Discard Hand AddCardsInto(DeckLocation.Discard, RetrieveCardsFrom(DeckLocation.Hand, c => cardsToMove[c].CurrentLocation == DeckLocation.Hand && cardsToMove[c].Destination == DeckLocation.Discard)); e.HandledBy.Add(this); } public void PlayTreasures(IGame game) { if (Phase == PhaseEnum.Buy || Phase == PhaseEnum.Cleanup || Phase == PhaseEnum.Endgame || PlayerMode == PlayerMode.Waiting || PlayerMode == PlayerMode.Choosing || PlayerMode == PlayerMode.Playing) throw new Exception(Resource.ExceptionCantPlayTreasures); if (Phase == PhaseEnum.Action) { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } // Play all Treasure cards that have no special Play method defined PlayCards(Hand[c => c.Category.HasFlag(Categories.Treasure) && (c.Location == Location.General || c.Type.GetMethod("FollowInstructions", new[] { typeof(Player) }).DeclaringType == typeof(Card))]); } public void PlayTokens(IGame game, Type token, int count) { if (!TokenPiles.ContainsKey(token) || count <= 0) return; var finalCount = count > TokenPiles[token].Count ? TokenPiles[token].Count : count; var tokens = new TokenCollection(TokenPiles[token].Take(finalCount)); if (!tokens.First().PlayablePhases.Contains(Phase)) throw new Exception(Resource.ExceptionCantPlayTokens); if (TokenPlaying != null) { var tpgea = new TokenPlayingEventArgs(this, tokens); TokenPlaying(this, tpgea); } TokenPiles[token].First().Play(this, finalCount); RemoveTokens(tokens); if (TokenPlayed != null) { var tpgea = new TokenPlayedEventArgs(this, tokens); TokenPlayed(this, tpgea); } if (Phase == PhaseEnum.BuyTreasure && Hand[Categories.Treasure].Count == 0 && !TokenPiles.IsAnyPlayable) { Phase = PhaseEnum.Buy; PlayerMode = PlayerMode.Normal; } } public void GoToActionPhase() { Phase = PhaseEnum.Action; PlayerMode = PlayerMode.Normal; } public void GoToActionTreasurePhase() { if (Phase == PhaseEnum.Action) { Phase = PhaseEnum.ActionTreasure; //PlayerMode = PlayerMode.Normal; } } public void DoneWithActionTreasurePhase() { if (Phase == PhaseEnum.ActionTreasure) { Phase = PhaseEnum.Action; //PlayerMode = PlayerMode.Playing; } } public void GoToBuyTreasurePhase() { if (Phase == PhaseEnum.Action || Phase == PhaseEnum.ActionTreasure) { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } } public void GoToBuyPhase() { if (Phase == PhaseEnum.Action || Phase == PhaseEnum.ActionTreasure || Phase == PhaseEnum.BuyTreasure) { Phase = PhaseEnum.Buy; PlayerMode = PlayerMode.Normal; } } public void GoToNightPhase() { if (Phase == PhaseEnum.Action || Phase == PhaseEnum.ActionTreasure || Phase == PhaseEnum.Buy || Phase == PhaseEnum.BuyTreasure) { Phase = PhaseEnum.Night; PlayerMode = PlayerMode.Normal; } } public void GoToNextPhase() { switch (Phase) { case PhaseEnum.Action: case PhaseEnum.ActionTreasure: Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; break; case PhaseEnum.BuyTreasure: Phase = PhaseEnum.Buy; PlayerMode = PlayerMode.Normal; break; case PhaseEnum.Buy: Phase = PhaseEnum.Night; PlayerMode = PlayerMode.Normal; break; case PhaseEnum.Night: Phase = PhaseEnum.Cleanup; PlayerMode = PlayerMode.Normal; break; } } public Facing FlipFaceDown(ICard card) { Contract.Requires(card != null, "card cannot be null"); var oldFacing = card.Facing; var newFacing = _Game.Table.Trash.FlipFaceDown(this, card); if (oldFacing != newFacing) CardFlippedOver?.Invoke(this, new CardFlippedOverEventArgs(card, oldFacing, newFacing)); return newFacing; } public Facing FlipFaceUp(ICard card) { Contract.Requires(card != null, "card cannot be null"); var oldFacing = card.Facing; var newFacing = _Game.Table.Trash.FlipFaceUp(this, card); if (oldFacing != newFacing) CardFlippedOver?.Invoke(this, new CardFlippedOverEventArgs(card, oldFacing, newFacing)); return newFacing; } public Facing FlipOver(ICard card) { Contract.Requires(card != null, "card cannot be null"); var oldFacing = card.Facing; var newFacing = _Game.Table.Trash.FlipOver(this, card); CardFlippedOver?.Invoke(this, new CardFlippedOverEventArgs(card, oldFacing, newFacing)); return newFacing; } public void PlayNothing(string modifier = "") { if (CardPlaying != null) { var cpgea = new CardPlayingEventArgs(this, new ItemCollection(), modifier); CardPlaying(this, cpgea); } //CardPlayedEventArgs cpdea; //if (CardPlayed != null) //{ // var handledBy = new List(); // bool actionPerformed; // do // { // actionPerformed = false; // cpdea = new CardPlayedEventArgs(this, new ItemCollection()); // cpdea.HandledBy.AddRange(handledBy); // CardPlayed(this, cpdea); // handledBy = cpdea.HandledBy; // var options = new OptionCollection(); // IEnumerable types = cpdea.Actions.Keys; // options.AddRange(types.Select(key => new Option(cpdea.Actions[key].Text, cpdea.Actions[key].IsRequired, cpdea.Actions[key].Source))); // if (options.Any()) // { // options.Sort(); // var choice = new Choice("You played nothing", options, this, cpdea); // var result = MakeChoice(choice); // if (result.Options.Any()) // { // cpdea.Actions.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cpdea); // actionPerformed = true; // } // } // } while (CardPlayed != null && actionPerformed); //} if (CardPlayFinished != null) { var cpea = new CardPlayedEventArgs(this, new ItemCollection()); CardPlayFinished(this, cpea); } } public void PlayCard(Card card, DeckLocation destination = DeckLocation.InPlay) { PlayCards(new ItemCollection { card }, destination); } public void PlayCards(ItemCollection cards, DeckLocation destination = DeckLocation.InPlay) { // if (Phase != PhaseEnum.Waiting //#if DEBUG // && (Actions == 0 && cards.Any(c => c.Category.HasFlag(Category.Action) && !c.Category.HasFlag(Category.Treasure))) //#endif // ) // throw new Exception("You cannot play any Action cards right now!"); PlayCardsInternal(cards, destination); // Check Phase after playing -- we may need to switch phases // Test out removing this code if (Phase != PhaseEnum.Waiting) { // If we don't have any Actions left or any Action cards in hand, automatically move to the Buy[Treasure] phase if (Phase == PhaseEnum.Action && (Actions == 0 || !Hand[Categories.Action].Any()) && !TokenPiles.IsAnyPlayable) { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } // If we don't have any Treasure cards in hand or any playable tokens, automatically move to the Buy phase else if (Phase == PhaseEnum.BuyTreasure && !Hand[Categories.Treasure].Any() && !TokenPiles.IsAnyPlayable) { Phase = PhaseEnum.Buy; PlayerMode = PlayerMode.Normal; } // If we don't have any Buys left or playable tokens and have any Night cards in hand, automatically move to the Night phase else if (Phase == PhaseEnum.Buy && Buys == 0 && Hand[Categories.Night].Any() && !TokenPiles.IsAnyPlayable) { Phase = PhaseEnum.Night; PlayerMode = PlayerMode.Normal; } } } public void PlayCardInternal(Card card, DeckLocation destination = DeckLocation.InPlay, string modifier = "") { PlayCardsInternal(new ItemCollection { card }, destination, modifier); } public void PlayCardsInternal(ItemCollection cards, DeckLocation destination = DeckLocation.InPlay, string modifier = "") { Contract.Requires(cards != null, "cards cannot be null"); // Don't even bother; just return straight away if (cards.Count == 0) return; if (destination != DeckLocation.InPlay && destination != DeckLocation.SetAside) throw new ArgumentException(Resource.ExceptionInvalidDestination); // So the AI doesn't blow things up, just return immediately if the Phase is Endgame if (Phase == PhaseEnum.Endgame) return; foreach (var card in cards) { if (Phase == PhaseEnum.Action && !card.Category.HasFlag(Categories.Action)) { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } } var currentPlayerMode = PlayerMode; PlayerMode = PlayerMode.Playing; // Retrieve the actual card instead of the one we're passed. It might not exist // Also, we need to remove them from the Hand, Revealed, Private, or Discard // (these are the "standard" places cards can be played from) var actualCards = RetrieveCardsFrom(DeckLocation.Hand, cards); actualCards.AddRange(RetrieveCardsFrom(DeckLocation.Revealed, cards)); actualCards.AddRange(RetrieveCardsFrom(DeckLocation.Private, cards)); actualCards.AddRange(RetrieveCardsFrom(DeckLocation.Discard, cards)); if (CardPlaying != null) { var cpgea = new CardPlayingEventArgs(this, cards, modifier); CardPlaying(this, cpgea); } // Add them to In Play, add them to the Played list for the turn, and Play them (individually) foreach (var card in cards) { if (actualCards.Contains(card)) { if (CardPuttingIntoPlay != null) { var cpipea = new CardPutIntoPlayEventArgs(this, card); CardPuttingIntoPlay(this, cpipea); } AddCardInto(destination, card); if (CardPutIntoPlay != null) { var cpipea = new CardPutIntoPlayEventArgs(this, card); CardPutIntoPlay(this, cpipea); } } ExecuteCardAction(card); } InPlay.Refresh(this); if (CardPlayed != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; var cpdea = new CardPlayedEventArgs(this, cards); cpdea.HandledBy.AddRange(handledBy); CardPlayed(this, cpdea); handledBy = cpdea.HandledBy; var options = new OptionCollection(); IEnumerable types = cpdea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cpdea.Resolvers[key].Text, cpdea.Resolvers[key].IsRequired, cpdea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choiceText = Resource.PlayedCards.Replace("{cards}", cards.ToString()); var choice = new Choice(choiceText, options, this, cpdea, new ICardBaseCollection(cards)); var result = MakeChoice(choice); if (result.Options.Any()) { cpdea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cpdea); actionPerformed = true; } } } while (CardPlayed != null && actionPerformed); } if (CardPlayFinished != null) { var cpea = new CardPlayedEventArgs(this, cards); CardPlayFinished(this, cpea); } PlayerMode = currentPlayerMode; } public PlayerMode PutCardIntoPlay(Card card, string modifier = "") { var currentPlayerMode = PlayerMode; PlayerMode = PlayerMode.Playing; if (CardPlaying != null) { var cpgea = new CardPlayingEventArgs(this, card, modifier); CardPlaying(this, cpgea); } // Retrieve the actual card instead of the one we're passed. It might not exist // Also, we need to remove it from the Hand, Revealed, Private, Discard Pile, or Set Aside // (these are the places cards can be played from) Card actualCard = null; if (Hand.Contains(card)) actualCard = RetrieveCardFrom(DeckLocation.Hand, card); if (actualCard == null && Revealed.Contains(card)) actualCard = RetrieveCardFrom(DeckLocation.Revealed, card); if (actualCard == null && Private.Contains(card)) actualCard = RetrieveCardFrom(DeckLocation.Private, card); if (actualCard == null && DiscardPile.Contains(card)) actualCard = RetrieveCardFrom(DeckLocation.Discard, card); if (actualCard == null && SetAside.Contains(card)) actualCard = RetrieveCardFrom(DeckLocation.SetAside, card); if (actualCard == null) { var matchingMat = PlayerMats.FirstOrDefault(m => m.Value.Contains(card)); if (matchingMat.Value != null) actualCard = RetrieveCardFrom(matchingMat.Key, card); } // Add it to In Play, add it to the Played list for the turn, and Play it if (actualCard == card) { CardPuttingIntoPlay?.Invoke(this, new CardPutIntoPlayEventArgs(this, card)); AddCardInto(DeckLocation.InPlay, card); CardPutIntoPlay?.Invoke(this, new CardPutIntoPlayEventArgs(this, card)); } return currentPlayerMode; } public void PlayCard(Card card, PlayerMode previousPlayerModeToRestore) { Contract.Requires(card != null, "card cannot be null"); ExecuteCardAction(card); InPlay.Refresh(this); CardPlayedEventArgs cpdea; if (CardPlayed != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; cpdea = new CardPlayedEventArgs(this, card); cpdea.HandledBy.AddRange(handledBy); CardPlayed(this, cpdea); handledBy = cpdea.HandledBy; var options = new OptionCollection(); IEnumerable types = cpdea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cpdea.Resolvers[key].Text, cpdea.Resolvers[key].IsRequired, cpdea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choiceText = Resource.PlayedCard.Replace("{card}", card.ToString()); var choice = new Choice(choiceText, options, this, cpdea, new ICardBaseCollection { card }); var result = MakeChoice(choice); if (result.Options.Any()) { cpdea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cpdea); actionPerformed = true; } } } while (CardPlayed != null && actionPerformed); } CardPlayFinished?.Invoke(this, new CardPlayedEventArgs(this, card)); PlayerMode = previousPlayerModeToRestore; } private void ExecuteCardAction(Card card) { CurrentTurn.Played(card); card.PlaySetup(this); ResolvingCard(card); CardFollowingInstructionsEventArgs cfiea = null; if (CardFollowingInstructions != null) { var handledBy = new List(); var cancelled = false; bool actionPerformed; do { actionPerformed = false; cfiea = new CardFollowingInstructionsEventArgs(this, card); cfiea.Cancelled |= cancelled; cfiea.HandledBy.AddRange(handledBy); CardFollowingInstructions(this, cfiea); handledBy = cfiea.HandledBy; var options = new OptionCollection(); IEnumerable types = cfiea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(cfiea.Resolvers[key].Text, cfiea.Resolvers[key].IsRequired, cfiea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choiceText = Resource.PlayingCard.Replace("{card}", card.ToString()); var choice = new Choice(choiceText, options, this, cfiea, new ICardBaseCollection { card }); var result = MakeChoice(choice); if (result.Options.Any()) { cfiea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref cfiea); actionPerformed = true; cancelled |= cfiea.Cancelled; } } } while (CardFollowingInstructions != null && actionPerformed); cancelled |= cfiea.Cancelled; } if (cfiea == null || !cfiea.Cancelled) card.FollowInstructions(this); card.PlayFinished(this); } internal void ResolvingCard(Card card) { CardResolvingEventArgs crea; if (CardResolving != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; crea = new CardResolvingEventArgs(this, card); crea.HandledBy.AddRange(handledBy); CardResolving(this, crea); handledBy = crea.HandledBy; var options = new OptionCollection(); IEnumerable types = crea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(crea.Resolvers[key].Text, crea.Resolvers[key].IsRequired, crea.Resolvers[key].Source))); if (options.Any()) { options.Sort(); var choiceText = Resource.PlayedCard.Replace("{card}", card.ToString()); var choice = new Choice(choiceText, options, this, crea, new ICardBaseCollection { card }); var result = MakeChoice(choice); if (result.Options.Any()) { crea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref crea); actionPerformed = true; } } } while (CardResolving != null && actionPerformed); } } public void UndoPlayCard(Card card) { UndoPlayCards(new ItemCollection { card }); } public void UndoPlayCards(ItemCollection cards) { Contract.Requires(cards != null, "cards cannot be null"); if (Actions == 0 && Phase != PhaseEnum.BuyTreasure && cards.Any(c => c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure))) throw new Exception(Resource.ExceptionCantPlayActions); UndoPlayCardsInternal(cards, string.Empty); if (cards.Any(c => c.Category.HasFlag(Categories.Action))) { Phase = PhaseEnum.Action; PlayerMode = PlayerMode.Normal; } else { Phase = PhaseEnum.BuyTreasure; PlayerMode = PlayerMode.Normal; } } internal void UndoPlayCardsInternal(ItemCollection cards, string modifier) { // Don't even bother; just return straight away if (cards.Count == 0) return; // So the AI doesn't blow things up, just return immediately if the Phase is Endgame if (Phase == PhaseEnum.Endgame) return; var currentPlayerMode = PlayerMode; PlayerMode = PlayerMode.Playing; if (CardUndoPlaying != null) { var cupea = new CardUndoPlayingEventArgs(this, cards, modifier); CardUndoPlaying(this, cupea); } // Retrieve the actual card instead of the one we're passed. It might not exist var actualCards = RetrieveCardsFrom(DeckLocation.InPlay, cards); // Add them to the Hand, remove them from the Played list for the turn, and un-Play them (individually) foreach (var card in cards) { //if (actualCards.Any(c => c.Type == card.Type)) //{ // this.AddCardInto(DeckLocation.Hand, card); // actualCards.Remove(actualCards.First(c => c.Type == card.Type)); //} if (actualCards.Contains(card)) AddCardInto(DeckLocation.Hand, card); CurrentTurn.UndoPlayed(card); card.UndoPlay(this); card.PlayFinished(this); } InPlay.Refresh(this); if (CardUndoPlayed != null) { var cupea = new CardUndoPlayedEventArgs(this, cards); CardUndoPlayed(this, cupea); } if (PlayerMode == PlayerMode.Playing) PlayerMode = currentPlayerMode; } public bool AnyActionsInHand => Hand[Categories.Action].Count != 0; public void Reset() { Hand.Reset(); DrawPile.Reset(); DiscardPile.Reset(); Revealed.Reset(); SetAside.Reset(); InPlay.Reset(); Private.Reset(); foreach (var mat in PlayerMats.Values) mat.Reset(); } public override string ToString() { return Name; } public void End() { Phase = PhaseEnum.Endgame; PlayerMode = PlayerMode.Waiting; EndgamePile.BeginChanges(); lock (EndgamePile) { EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.Hand)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.InPlay)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.Revealed)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.SetAside)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.Discard)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.Deck)); EndgamePile.AddRange(RetrieveCardsFrom(DeckLocation.Private)); foreach (var deckType in PlayerMats.Keys) EndgamePile.AddRange(RetrieveCardsFrom(deckType)); var takeablesToMove = Takeables.Where(s => s.VictoryPoints != 0).ToList(); takeablesToMove.ForEach(s => Return(s.Type)); EndgamePile.AddRange(takeablesToMove); //foreach (var d in EndgamePile) for (int i = 0; i < EndgamePile.Count; i++) EndgamePile[i].PerformEndgameCalculations(this, EndgamePile); EndgamePile.Sort(new Cards.Sorting.ForEndgame()); } EndgamePile.EndChanges(); } public void Trash(IGameObject source, Card card) { Trash(source, new ItemCollection { card }); } public void Trash(IGameObject source, DeckLocation location, Card card) { Trash(source, location, new ItemCollection { card }); } public void Trash(IGameObject source, ItemCollection cards) { Contract.Requires(cards != null, "cards cannot be null"); void postTrashing(ItemCollection c) => _Game.Table.Trash.AddRange(c); void postTrashedFinished(ItemCollection c) { } Trash(source, cards, postTrashing, Lose, postTrashedFinished); } internal void Trash(IGameObject source, DeckLocation location, ItemCollection cards) { void postTrashing(ItemCollection c) => MoveToTrashStart(location, c); void postTrashedFinished(ItemCollection c) => c.RemovedFrom(location, this); Trash(source, cards, postTrashing, Lose, postTrashedFinished); } public void Trash(IGameObject source, ISupply supply) { Contract.Requires(supply != null, "supply cannot be null"); // Trash the top card of the Supply pile if (supply.TopCard == null) return; var card = (Card)supply.TopCard; var cards = new ItemCollection { card }; void postTrashing(ItemCollection c) { card = supply.Take(); cards = new ItemCollection { card }; _Game.Table.Trash.Add(card); } void postTrashed(ItemCollection c) { } void postTrashedFinished(ItemCollection c) { } Trash(source, cards, postTrashing, postTrashed, postTrashedFinished); } private void Trash(IGameObject source, ItemCollection cards, Action> postTrashing, Action> postTrashed, Action> postTrashedFinished) { if (cards.Count == 0) return; TrashEventArgs tea; if (Trashing != null) { do { tea = new TrashEventArgs(this, source, cards); Trashing(this, tea); var options = new OptionCollection(); IEnumerable types = tea.Resolvers.Keys; options.AddRange(types.Select(key => new Option(tea.Resolvers[key].Text, tea.Resolvers[key].IsRequired, tea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var trashingText = ResourcesHelper.Get($"Trashing{cards.Count}Cards"); if (string.IsNullOrEmpty(trashingText)) trashingText = Resource.TrashingNCards.Replace("{count}", cards.Count.ToString(CultureInfo.CurrentCulture)); var choice = new Choice($"You are trashing {cards.Count} cards", options, this, tea, new ICardBaseCollection(cards)); var result = MakeChoice(choice); if (result.Options.Any()) tea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(this, ref tea); } } while (Trashing != null && tea.HandledBy.Any()); } postTrashing(cards); CurrentTurn?.Trashed(cards); if (Trashed != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; tea = new TrashEventArgs(this, source, cards); tea.HandledBy.AddRange(handledBy); Trashed(this, tea); handledBy = tea.HandledBy; var enumerator = _Game.GetPlayersStartingWithEnumerator(this); while (enumerator.MoveNext()) { var options = new OptionCollection(); IEnumerable types = tea.Resolvers.Keys; options.AddRange( types.Where(key => tea.Resolvers[key].Player == enumerator.Current) .Select(key => new Option(tea.Resolvers[key].Text, tea.Resolvers[key].IsRequired, tea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var trashedText = ResourcesHelper.Get($"PlayerTrashed{cards.Count}Cards"); if (string.IsNullOrEmpty(trashedText)) trashedText = Resource.PlayerTrashedNCards.Replace("{count}", cards.Count.ToString(CultureInfo.CurrentCulture)); trashedText = trashedText.Replace("{player}", this == enumerator.Current ? Resource.You : ToString()); var choice = new Choice(trashedText, options, this, tea, new ICardBaseCollection(cards)); var result = enumerator.Current.MakeChoice(choice); if (result.Options.Any()) { tea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(enumerator.Current, ref tea); actionPerformed = true; } } } } while (Trashed != null && actionPerformed); } cards.TrashedBy(this); postTrashed(cards); if (TrashedFinished != null) { tea = new TrashEventArgs(this, source, cards); TrashedFinished(this, tea); } postTrashedFinished(cards); } public void Call(Card card) { if (card == null) return; if (Calling != null) { var cea = new CallEventArgs(this, card); Calling(this, cea); } card.Call(this); if (Called != null) { var cea = new CallEventArgs(this, card); Called(this, cea); } if (CalledFinished != null) { var cea = new CallEventArgs(this, card); CalledFinished(this, cea); } } public void Start(Turn turn) { Contract.Requires(turn != null, "turn cannot be null"); if (TurnStarting != null) { var tsea = new TurnStartingEventArgs(this, turn.GrantedBy); TurnStarting(this, tsea); if (tsea.Cancelled) return; } CurrentTurn = turn; _Actions = _Buys = 1; ActionsPlayed = 0; Phase = PhaseEnum.Starting; PlayerMode = PlayerMode.Waiting; if (BenefitsChanged != null) { var bcea = new BenefitsChangedEventArgs(this); BenefitsChanged(this, bcea); } if (TurnStarted != null) { var handledBy = new List(); bool actionPerformed; do { actionPerformed = false; var tsea = new TurnStartedEventArgs(this); tsea.HandledBy.AddRange(handledBy); TurnStarted(this, tsea); handledBy = tsea.HandledBy; var enumerator = _Game.GetPlayersStartingWithEnumerator(this); while (enumerator.MoveNext()) { var options = new OptionCollection(); var types = tsea.Resolvers.Keys; options.AddRange( types.Where(key => tsea.Resolvers[key].Player == enumerator.Current) .Select(key => new Option(tsea.Resolvers[key].Text, tsea.Resolvers[key].IsRequired, tsea.Resolvers[key].Card))); if (options.Any()) { options.Sort(); var choiceText = Resource.PlayerTurnStarted.Replace("{player}", this == enumerator.Current ? Resource.You : ToString()); var choice = new Choice(choiceText, options, this, tsea); var result = enumerator.Current.MakeChoice(choice); if (result.Options.Any()) { tsea.Resolvers.First(kvp => kvp.Value.Text == result.Options[0]).Value.Method(enumerator.Current, ref tsea); actionPerformed = true; } } } } while (TurnStarted != null && actionPerformed); } SetAside.Refresh(this); Phase = PhaseEnum.Action; PlayerMode = PlayerMode.Normal; //TakeTurn(this._Game, this); } public void SetCurrentTurn(Turn turn) { CurrentTurn = turn; } public void ReceiveBenefit(ICardBase sourceOfBenefit, CardBenefit benefit, bool isInternal = false) { Contract.Requires(sourceOfBenefit != null, "sourceOfBenefit cannot be null"); Contract.Requires(benefit != null, "benefit cannot be null"); if (!benefit.Any) return; if (BenefitReceivingInitiated != null && (sourceOfBenefit.Category.HasFlag(Categories.Action) || sourceOfBenefit.Category.HasFlag(Categories.Victory) || sourceOfBenefit.Category.HasFlag(Categories.Night) || sourceOfBenefit.Category.HasFlag(Categories.Event) || sourceOfBenefit.Category.HasFlag(Categories.Landmark) || sourceOfBenefit.Category.HasFlag(Categories.Boon) || sourceOfBenefit.Category.HasFlag(Categories.Hex) || sourceOfBenefit.Category.HasFlag(Categories.State) || sourceOfBenefit.Category.HasFlag(Categories.Artifact) || sourceOfBenefit.Category.HasFlag(Categories.Project) || sourceOfBenefit.Category.HasFlag(Categories.Way) || (sourceOfBenefit.Category.HasFlag(Categories.Treasure) && (!isInternal || benefit.Actions > 0 || benefit.Buys > 0 || benefit.Cards > 0 || benefit.VictoryPoints > 0)) )) { var brea = new BenefitReceiveVisualEventArgs(this, benefit); BenefitReceivingInitiated(sourceOfBenefit, brea); } if (benefit.Any) { var brea = new BenefitReceiveEventArgs(this, sourceOfBenefit, benefit); BenefitReceiving?.Invoke(sourceOfBenefit, brea); benefit = brea.Benefit; } if (benefit.Cards > 0) Draw(benefit.Cards, DeckLocation.Hand); //if (this.Phase == PhaseEnum.Starting || this.Phase == PhaseEnum.Action || this.Phase == PhaseEnum.ActionTreasure) Actions += benefit.Actions; Buys += benefit.Buys; Coffers += benefit.Coffers; Villagers += benefit.Villagers; Currency += benefit.Currency; if (benefit.VictoryPoints > 0) { if (!TokenPiles.ContainsKey(Cards.Prosperity.TypeClass.VictoryToken)) TokenPiles[Cards.Prosperity.TypeClass.VictoryToken] = new TokenCollection(); for (var count = 0; count < benefit.VictoryPoints; count++) TokenPiles.Add(new Cards.Prosperity.VictoryToken(), this); } if (benefit.DiscardCards > 0) { var discardText = ResourcesHelper.Get($"Discard{benefit.DiscardCards}Cards"); var choice = new Choice(discardText, sourceOfBenefit, Hand, ChoiceOutcome.Discard, this, minimum: benefit.DiscardCards, maximum: benefit.DiscardCards); var result = MakeChoice(choice); Discard(DeckLocation.Hand, result.Cards); } } public void ReceiveBenefit(Token sourceOfBenefit, CardBenefit benefit) { Contract.Requires(benefit != null, "benefit cannot be null"); if (benefit.Any && BenefitReceivingInitiated != null) { var brea = new BenefitReceiveVisualEventArgs(this, benefit); BenefitReceivingInitiated(sourceOfBenefit, brea); } if (benefit.Any) { var brea = new BenefitReceiveEventArgs(this, sourceOfBenefit, benefit); BenefitReceiving?.Invoke(sourceOfBenefit, brea); benefit = brea.Benefit; } if (benefit.Cards > 0) Draw(benefit.Cards, DeckLocation.Hand); Actions += benefit.Actions; Buys += benefit.Buys; Currency += benefit.Currency; for (var count = 0; count < benefit.VictoryPoints; count++) TokenPiles.Add(new Cards.Prosperity.VictoryToken(), this); } public void RemoveBenefit(Card sourceOfBenefit, CardBenefit benefit, bool isInternal) { Contract.Requires(benefit != null, "benefit cannot be null"); ReceiveBenefit(sourceOfBenefit, new CardBenefit { Actions = -benefit.Actions, Buys = -benefit.Buys, Cards = -benefit.Cards, DiscardCards = -benefit.DiscardCards, Currency = -benefit.Currency, FlavorText = benefit.FlavorText, VictoryPoints = -benefit.VictoryPoints }, isInternal); } public IEnumerable GetEnumeratorIPoints(bool onlyObtainable = false) { var allCards = new List(); allCards.AddRange(DrawPile.LookThrough(c => true)); allCards.AddRange(DiscardPile.LookThrough(c => true)); if (!onlyObtainable) { allCards.AddRange(Hand.LookThrough(c => true)); allCards.AddRange(Revealed.LookThrough(c => true)); allCards.AddRange(Private.LookThrough(c => true)); allCards.AddRange(InPlay.LookThrough(c => true)); allCards.AddRange(InPlay.SelectMany(ipc => ipc.LookThrough(c => true))); allCards.AddRange(SetAside.LookThrough(c => true)); allCards.AddRange(SetAside.SelectMany(ipc => ipc.LookThrough(c => true))); allCards.AddRange(EndgamePile); foreach (var mat in PlayerMats) allCards.AddRange(mat.Value); } foreach (var i in allCards) yield return i; } public void IterateAll(Action action, Predicate predicate = null) { if (predicate == null) predicate = c => true; DiscardPile.LookThrough(predicate).ForEach(action); DrawPile.LookThrough(predicate).ForEach(action); Hand[predicate].ForEach(action); Private[predicate].ForEach(action); Revealed[predicate].ForEach(action); InPlay[predicate].ForEach(action); SetAside[predicate].ForEach(action); foreach (var deckType in PlayerMats.Keys) PlayerMats[deckType].LookThrough(predicate).ForEach(action); } public int CountAll(IPlayer fromPlayer = null, Predicate predicate = null, bool onlyObtainable = true, bool onlyCurrentlyDrawable = false) { if (fromPlayer == null) fromPlayer = this; if (predicate == null) predicate = c => true; var count = 0; count += DiscardPile.LookThrough(predicate).Count; count += DrawPile.LookThrough(predicate).Count; if (!onlyCurrentlyDrawable) { count += Hand[predicate].Count; count += Private[predicate].Count; count += Revealed[predicate].Count; // Champion & Hireling aren't drawable or obtainable // Hardcoding cards isn't the greatest & probably should be factored out count += InPlay[predicate].Count(c => !(c is Cards.Adventures.Champion || c is Cards.Adventures.Hireling || c is Cards.Adventures2ndEdition.Champion)); count += SetAside.SelectMany(ic => ic.LookThrough(predicate)).Count(); count += SetAside[predicate].Count(c => !(c is Cards.Adventures.Champion || c is Cards.Adventures.Hireling || c is Cards.Adventures2ndEdition.Champion)); count += SetAside.SelectMany(ic => ic.LookThrough(predicate)).Count(); count += EndgamePile[ip => ip is Card c && predicate(c)].Count; foreach (var deckType in PlayerMats.Keys) { // Don't count cards on mats that you can't obtain cards from if (onlyObtainable && !PlayerMats[deckType].IsObtainable) continue; var matchingCards = PlayerMats[deckType].LookThrough(predicate); // Conditionally, cards on the Tavern Mat are obtainable (any Reserve other than Distant Lands) if (deckType == Cards.Adventures.TypeClass.TavernMat) matchingCards = matchingCards.FindAll(c => c.Category.HasFlag(Categories.Reserve) && c.Type != Cards.Adventures.TypeClass.DistantLands); count += matchingCards.Count; } } return count; } public int SumAll(IPlayer fromPlayer, Predicate filterPredicate, Func sumSelector, bool onlyObtainable = true, bool onlyCurrentlyDrawable = false) { var sum = 0; sum += DiscardPile.LookThrough(filterPredicate).Sum(sumSelector); sum += DrawPile.LookThrough(filterPredicate).Sum(sumSelector); if (onlyCurrentlyDrawable) return sum; sum += Hand[filterPredicate].Sum(sumSelector); sum += Private[filterPredicate].Sum(sumSelector); sum += Revealed[filterPredicate].Sum(sumSelector); sum += InPlay[filterPredicate].Sum(sumSelector); sum += InPlay.SelectMany(ic => ic.LookThrough(filterPredicate)).Sum(sumSelector); sum += SetAside[filterPredicate].Sum(sumSelector); sum += SetAside.SelectMany(ic => ic.LookThrough(filterPredicate)).Sum(sumSelector); sum += EndgamePile[filterPredicate].Sum(sumSelector); sum += Takeables.FindAll(filterPredicate).Sum(sumSelector); sum += PlayerMats.Keys.Where(deckType => !onlyObtainable || PlayerMats[deckType].IsObtainable) .Sum(deckType => PlayerMats[deckType].LookThrough(filterPredicate).Sum(sumSelector)); return sum; } public double SumAll(IPlayer fromPlayer, Predicate filterPredicate, Func sumSelector, bool onlyObtainable = true, bool onlyCurrentlyDrawable = false) { double sum = 0; sum += DiscardPile.LookThrough(filterPredicate).Sum(sumSelector); sum += DrawPile.LookThrough(filterPredicate).Sum(sumSelector); if (!onlyCurrentlyDrawable) { sum += Hand[filterPredicate].Sum(sumSelector); sum += Private[filterPredicate].Sum(sumSelector); sum += Revealed[filterPredicate].Sum(sumSelector); sum += InPlay[filterPredicate].Sum(sumSelector); sum += InPlay.SelectMany(ic => ic.LookThrough(filterPredicate)).Sum(sumSelector); sum += SetAside[filterPredicate].Sum(sumSelector); sum += SetAside.SelectMany(ic => ic.LookThrough(filterPredicate)).Sum(sumSelector); sum += EndgamePile[filterPredicate].Sum(sumSelector); sum += PlayerMats.Keys.Where(deckType => !onlyObtainable || PlayerMats[deckType].IsObtainable) .Sum(deckType => PlayerMats[deckType].LookThrough(filterPredicate).Sum(sumSelector)); } return sum; } public int CountVictoryPoints() { return SumAll(this, ip => true, ip => ip.ComputeVictoryPoints(this, GetEnumeratorIPoints()), false) + (int)VictoryChits; } public void AddToken(Token token) { Contract.Requires(token != null, "token cannot be null"); TokenPiles.Add(token, this); } public void AddTokens(TokenCollection tokens) { TokenPiles.AddCollection(tokens, this); } public void RemoveToken(Token token) { Contract.Requires(token != null, "token cannot be null"); TokenPiles.Remove(token, this); } internal void RemoveTokens(IEnumerable tokens) { TokenPiles.Remove(tokens); } public void RemoveTokens(Type tokenType, int count) { if (count <= 0) return; var finalCount = count > TokenPiles[tokenType].Count ? TokenPiles[tokenType].Count : count; TokenPiles.Remove(TokenPiles[tokenType].Take(finalCount)); } public void SetupDeckAs(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); var cards = new ItemCollection(RetrieveCardsFrom(DeckLocation.Hand).Union(RetrieveCardsFrom(DeckLocation.Deck))); foreach (var sourceCard in player.Hand) { var myFoundCard = cards.First(c => c.Type == sourceCard.Type); cards.Remove(myFoundCard); AddCardInto(DeckLocation.Hand, myFoundCard); } AddCardsInto(DeckLocation.Deck, cards); } public virtual XmlNode GenerateXml(XmlDocument doc) { Contract.Requires(doc != null, "doc cannot be null"); var xePlayer = doc.CreateElement("player"); var xe = doc.CreateElement("name"); xe.InnerText = Name; xePlayer.AppendChild(xe); xe = doc.CreateElement("uniqueid"); xe.InnerText = UniqueId.ToString(); xePlayer.AppendChild(xe); xe = doc.CreateElement("playertype"); xe.InnerText = PlayerType.ToString(); xePlayer.AppendChild(xe); xe = doc.CreateElement("type"); xe.InnerText = GetType().ToString(); xePlayer.AppendChild(xe); xe = doc.CreateElement("phase"); var phase = Phase; if (phase == PhaseEnum.Action) phase = PhaseEnum.Starting; xe.InnerText = phase.ToString(); xePlayer.AppendChild(xe); xe = doc.CreateElement("mode"); xe.InnerText = PlayerMode.ToString(); xePlayer.AppendChild(xe); xe = doc.CreateElement("actions"); xe.InnerText = Actions.ToString(CultureInfo.InvariantCulture); xePlayer.AppendChild(xe); xe = doc.CreateElement("buys"); xe.InnerText = Buys.ToString(CultureInfo.InvariantCulture); xePlayer.AppendChild(xe); xe = doc.CreateElement("currency"); xe.InnerText = Currency.ToString(); xePlayer.AppendChild(xe); xePlayer.AppendChild(Hand.LookThrough(c => true).GenerateXml(doc, "hand")); xePlayer.AppendChild(DrawPile.LookThrough(c => true).GenerateXml(doc, "deck")); xePlayer.AppendChild(DiscardPile.LookThrough(c => true).GenerateXml(doc, "discard")); xePlayer.AppendChild(InPlay.LookThrough(c => true).GenerateXml(doc, "inplay")); xePlayer.AppendChild(SetAside.LookThrough(c => true).GenerateXml(doc, "setaside")); xePlayer.AppendChild(Revealed.LookThrough(c => true).GenerateXml(doc, "revealed")); xePlayer.AppendChild(Private.LookThrough(c => true).GenerateXml(doc, "private")); xePlayer.AppendChild(PlayerMats.GenerateXml(doc, "cardmats")); xePlayer.AppendChild(TokenPiles.GenerateXml(doc, "tokenpiles")); return xePlayer; } internal static Player Load(IGame game, XmlNode xnPlayer) { var xnName = xnPlayer.SelectSingleNode("name"); var xnPlayerType = xnPlayer.SelectSingleNode("playertype"); var xnType = xnPlayer.SelectSingleNode("type"); var xnPhase = xnPlayer.SelectSingleNode("phase"); var xnMode = xnPlayer.SelectSingleNode("mode"); var xnActions = xnPlayer.SelectSingleNode("actions"); var xnBuys = xnPlayer.SelectSingleNode("buys"); var xnCurrency = xnPlayer.SelectSingleNode("currency"); if (xnName == null || xnPlayerType == null || xnType == null) return null; var name = xnName.InnerText; var playerType = (PlayerType)Enum.Parse(typeof(PlayerType), xnPlayerType.InnerText, true); var type = Type.GetType(xnType.InnerText); var phase = (PhaseEnum)Enum.Parse(typeof(PhaseEnum), xnPhase.InnerText, true); var playerMode = (PlayerMode)Enum.Parse(typeof(PlayerMode), xnMode.InnerText, true); var actions = int.Parse(xnActions.InnerText, CultureInfo.InvariantCulture); var buys = int.Parse(xnBuys.InnerText, CultureInfo.InvariantCulture); var currency = new Currency(xnCurrency.InnerText); Player player = null; switch (playerType) { case PlayerType.Human: player = new Human(game, xnName.InnerText); break; case PlayerType.Computer: player = (Player)Activator.CreateInstance(type, game, name); break; default: break; } if (player == null) return null; player.Load(xnPlayer); player.Phase = phase; player.PlayerMode = playerMode; player.Actions = actions; player.Buys = buys; player.Currency = currency; return player; } internal virtual void Load(XmlNode xnPlayer) { var xnUniqueId = xnPlayer.SelectSingleNode("uniqueid"); if (xnUniqueId != null) UniqueId = new Guid(xnUniqueId.InnerText); foreach (DeckLocation location in Enum.GetValues(typeof(DeckLocation))) { switch (location) { case DeckLocation.InPlayAndSetAside: continue; default: var xnLocation = xnPlayer.SelectSingleNode(location.ToString().ToLower()); if (xnLocation == null) continue; var cards = ItemCollection.Load(xnLocation); AddCardsInto(location, cards, DeckPosition.Bottom); cards.ObtainedBy(this); break; } } // Temporary solution for upgrade (changing Tableau -> InPlay and PreviousTableau -> SetAside foreach (var locationString in new string[] { "tableau", "previoustableau" }) { var location = DeckLocation.Discard; switch (locationString) { case "tableau": location = DeckLocation.InPlay; break; case "previoustableau": location = DeckLocation.SetAside; break; } var xnLocation = xnPlayer.SelectSingleNode(location.ToString().ToLower()); if (xnLocation == null) continue; var cards = ItemCollection.Load(xnLocation); AddCardsInto(location, cards, DeckPosition.Bottom); cards.ObtainedBy(this); } PlayerMats.Load(xnPlayer.SelectSingleNode("cardmats")); TokenPiles.Load(xnPlayer.SelectSingleNode("tokenpiles")); } } }