public IAction ChooseAction(GameState gameState)
        {
            /* PRE-CALCULATIONS */

            var me           = gameState.CurrentPlayer;
            var otherPlayers = gameState.Players.Where(p => p != me);

            bool CanBuy(Card card) => BuyCard.CanAffordCard(me, card);

            var allFaceUpCards = gameState.Tiers.SelectMany(t => t.ColumnSlots)
                                 .Select(s => s.Value)
                                 .Where(card => card != null)
                                 .ToArray();

            var faceUpAndMyReserved = allFaceUpCards.Concat(me.ReservedCards).ToArray();

            var cardsICanBuy = faceUpAndMyReserved.Where(CanBuy).ToArray();

            var myOrderedCardStudy = AnalyseCards(me, faceUpAndMyReserved, gameState)
                                     .OrderBy(s => s.Repulsion).ToArray();

            var myTargetCard = myOrderedCardStudy.FirstOrDefault();

            /* BEHAVIOUR */

            // Check to see if a player can win (including me)
            if (_options.IsThieving)
            {
                foreach (var player in gameState.Players.OrderByDescending(p => p == me))
                {
                    var score = player.VictoryPoints;

                    var nobleDangerMap = new List <TokenColour>();
                    foreach (var noble in gameState.Nobles)
                    {
                        var deficit = player.Bonuses.DeficitFor(noble.Cost);
                        if (deficit.Sum != 1)
                        {
                            continue;
                        }
                        nobleDangerMap.Add(deficit.Colours().Single());
                    }

                    if (score < 10 && !nobleDangerMap.Any())
                    {
                        continue;
                    }

                    var winCards = allFaceUpCards.Where(c => c.VictoryPoints + score + (nobleDangerMap.Contains(c.BonusGiven) ? 3 : 0) >= 15)
                                   .Where(c => BuyCard.CanAffordCard(player, c))
                                   .OrderByDescending(c => c.VictoryPoints + (nobleDangerMap.Contains(c.BonusGiven) ? 3 : 0))
                                   .ToArray();

                    if (winCards.Length == 0)
                    {
                        continue;
                    }
                    if (player != me && winCards.Length > 1)
                    {
                        break;                                  // We're screwed if he has more than one winning move.
                    }
                    var winningCard = winCards.First();
                    if (cardsICanBuy.Contains(winningCard))
                    {
                        return(new BuyCard(winningCard, BuyCard.CreateDefaultPaymentOrNull(me, winningCard)));
                    }
                    if (player != me && me.ReservedCards.Count < 3)
                    {
                        return(new ReserveCard(winningCard));
                    }
                }
            }

            // If the second best card isn't very appealing, then check to see if I should reserve the first.
            if (_options.IsVeryTheiving && myOrderedCardStudy.Length > 1)
            {
                var second              = myOrderedCardStudy.Skip(1).First();
                var firstIsAmazing      = myTargetCard.Repulsion < -5m;
                var sedcondIsntAmazing  = second.Repulsion > -5m;
                var otherPlayerCanBuy   = otherPlayers.Any(p => BuyCard.CanAffordCard(p, myTargetCard.Card));
                var iCanAffordIfReserve = myTargetCard.DeficitWithGold == 1;
                if (firstIsAmazing &&
                    sedcondIsntAmazing &&
                    otherPlayerCanBuy &&
                    iCanAffordIfReserve &&
                    me.ReservedCards.Count < 3 &&
                    !me.ReservedCards.Contains(myTargetCard.Card))
                {
                    return(new ReserveCard(myTargetCard.Card));
                }
            }

            // Buy favourite card if possible
            if (myTargetCard?.DeficitWithGold == 0)
            {
                return(new BuyCard(myTargetCard.Card, BuyCard.CreateDefaultPaymentOrNull(me, myTargetCard.Card)));
            }

            // If I have 9 or more coins, or there aren't many coins left to take, buy any reasonable card I can at all
            if (me.Purse.Sum > 8 || gameState.Bank.Colours(includeGold: false).Count() < 3)
            {
                foreach (var study in myOrderedCardStudy.Where(s => s.DeficitWithGold == 0))
                {
                    var payment = BuyCard.CreateDefaultPaymentOrNull(me, study.Card);
                    return(new BuyCard(study.Card, payment));
                }
            }

            /* more precalc */
            CardFeasibilityStudy myNextTargetCard = null;

            if (_options.LooksAhead && myTargetCard != null)
            {
                var nextCards = me.CardsInPlay.ToList();
                nextCards.Add(myTargetCard.Card);
                var nextMe = me with {
                    CardsInPlay = nextCards
                };
                myNextTargetCard = AnalyseCards(nextMe, faceUpAndMyReserved.Except(new[] { myTargetCard.Card }).ToArray(), gameState)
                                   .OrderBy(s => s.Repulsion)
                                   .FirstOrDefault();
            }

            // Take some coins - but if there are coins to return then favour reserving a card
            var takeTokens = GetTakeTokens(gameState, myTargetCard, myNextTargetCard);

            if (takeTokens != null && !takeTokens.TokensToReturn.Colours().Any())
            {
                return(takeTokens);
            }

            // Do a reserve
            if (myTargetCard != null && !me.ReservedCards.Contains(myTargetCard.Card) && me.ReservedCards.Count < 3)
            {
                return(new ReserveCard(myTargetCard.Card));
            }

            // Do a random reserve if not maxxed out.
            var action = ChooseRandomCardOrNull(gameState);

            if (action != null && _options.RandomReserves)
            {
                return(action);
            }

            // do the give/take coins if possible
            return(takeTokens ?? (IAction) new NoAction());
        }
        private TakeTokens GetTakeTokens(GameState gameState, CardFeasibilityStudy firstChoice, CardFeasibilityStudy secondChoice)
        {
            if (firstChoice == null)
            {
                return(null);
            }

            var me = gameState.CurrentPlayer;

            var coloursByPreference = gameState.Bank.Colours(includeGold: false).ToList();

            var coinsCountICanTake = Math.Min(Math.Min(10 - me.Purse.Sum, 3), coloursByPreference.Count);


            var coloursNeeded  = firstChoice.Deficit.Colours().ToList();
            var coloursNeeded2 = secondChoice?.Deficit.Colours().ToList();
            var ordering       = coloursByPreference.OrderByDescending(col => coloursNeeded.Contains(col));

            if (secondChoice != null)
            {
                ordering = ordering.ThenByDescending(col => coloursNeeded2.Contains(col));
            }
            coloursByPreference = ordering.ToList();

            if (coinsCountICanTake > 0)
            {
                var deficitColours = firstChoice.Deficit.Colours().ToArray();

                var transaction = new Pool();

                if (_options.CanTakeTwo &&
                    deficitColours.Length == 1 &&
                    deficitColours.Any(col => firstChoice.Deficit[col] >= 2) &&
                    coinsCountICanTake > 1)
                {
                    var neededColour = deficitColours.Single();
                    if (gameState.Bank[neededColour] > 3)
                    {
                        transaction[neededColour] = 2;
                        return(new TakeTokens(transaction));
                    }
                }
                foreach (var colour in coloursByPreference.Take(coinsCountICanTake))
                {
                    transaction[colour] = 1;
                }
                return(new TakeTokens(transaction));
            }

            // otherwise just swap a single coin towards what we need
            if (coloursByPreference.Count == 0)
            {
                return(null);
            }
            var colourToTake     = coloursByPreference.First();
            var colourToGiveBack = me.Purse.Colours()
                                   .OrderBy(c => c == TokenColour.Gold || c == colourToTake)
                                   .Cast <TokenColour?>()
                                   .FirstOrDefault();

            if (!colourToGiveBack.HasValue)
            {
                return(null);
            }
            var take = new Pool();
            var give = new Pool();

            take[colourToTake]           = 1;
            give[colourToGiveBack.Value] = 1;

            return(new TakeTokens(take, give));
        }