public void CannotAffordCard_if_wrong_coins() { var card = new Card(1, 0, new Pool(0, 1, 2, 0, 0, 0), TokenColour.Black); var budget = new Pool(0, 1, 1, 0, 0, 1); var player = new Player("").Clone(withPurse: budget); Assert.False(BuyCard.CanAffordCard(player, card)); }
public void CanAffordCard_uses_gold() { // Costs 1 white, 2 blue. var card = new Card(1, 0, new Pool(0, 1, 2, 0, 0, 0), TokenColour.Black); var budget = new Pool(1, 1, 1, 0, 0, 0); var player = new Player("").Clone(withPurse: budget); Assert.True(BuyCard.CanAffordCard(player, card)); }
private char GetBuyIndicator(Card card, Player player) { if (card == null) { return(' '); } // '*' can afford // '·' can almost afford // ' ' simply cannot afford var buyIndicator = BuyCard.CanAffordCard(player, card) ? '*' : player.Budget.DeficitFor(card.Cost).Sum <= 3 ? '·' : ' '; return(buyIndicator); }
public IAction ChooseAction(GameState gameState) { var me = gameState.CurrentPlayer; bool CanBuy(Card card) => BuyCard.CanAffordCard(me, card); var allFaceUpCards = gameState.Tiers .SelectMany(t => t.ColumnSlots) .Select(s => s.Value) .Where(card => card != null) // If a stack runs out of cards, a slot will be null .ToArray(); // First, if I can buy a card that gives victory points, I always do. foreach (var card in allFaceUpCards.Concat(me.ReservedCards) .Where(c => c.VictoryPoints > 0) .OrderByDescending(c => c.VictoryPoints) .Where(CanBuy)) { var payment = BuyCard.CreateDefaultPaymentOrNull(me, card); return(new BuyCard(card, payment)); } // I look at all the cards I can see and choose one that looks the best in terms of accessibility var bestCardStudy = AnalyseCards(me, allFaceUpCards.Concat(me.ReservedCards), gameState) .OrderBy(s => s.DifficultyRating) .FirstOrDefault(); // Second, if the most accessible card is purchasable, I buy it. if (CanBuy(bestCardStudy.Card)) { return(new BuyCard(bestCardStudy.Card, BuyCard.CreateDefaultPaymentOrNull(me, bestCardStudy.Card))); } // Third, I try and get rid of reserved cards. foreach (var card in me.ReservedCards.Where(CanBuy)) { return(new BuyCard(card, BuyCard.CreateDefaultPaymentOrNull(me, card))); } // Fourth, I check if I've got loads of coins and if so, I buy any card I can afford if (me.Purse.Sum > 8) { foreach (var card in allFaceUpCards.Where(CanBuy)) { var payment = BuyCard.CreateDefaultPaymentOrNull(me, card); return(new BuyCard(card, payment)); } } // Fifth, I top up my coins, favouring colours needed by the most accessible card. var coloursAvailable = gameState.Bank.Colours(includeGold: false).ToList(); var coinsCountICanTake = Math.Min(Math.Min(10 - me.Purse.Sum, 3), coloursAvailable.Count); if (coinsCountICanTake > 0) { if (bestCardStudy != null) { var coloursNeeded = bestCardStudy.Deficit.Colours().ToArray(); coloursAvailable = coloursAvailable.OrderByDescending(col => coloursNeeded.Contains(col)).ToList(); } else { coloursAvailable.Shuffle(); } var transaction = new Pool(); if (bestCardStudy.Deficit.Colours().Any(col => bestCardStudy.Deficit[col] >= 2) && coinsCountICanTake > 1) { var neededColour = bestCardStudy.Deficit.Colours().First(col => bestCardStudy.Deficit[col] >= 2); if (gameState.Bank[neededColour] > 3) { transaction[neededColour] = 2; return(new TakeTokens(transaction)); } } foreach (var colour in coloursAvailable.Take(coinsCountICanTake)) { transaction[colour] = 1; } return(new TakeTokens(transaction)); } // Sixth, if it comes to it, I just reserve the best looking card I can see. if (!me.ReservedCards.Contains(bestCardStudy.Card) && me.ReservedCards.Count < 3) { return(new ReserveCard(bestCardStudy.Card)); } // Seventh, If I've already reserved the best looking card, I take a gamble. var action = ChooseRandomCardOrNull(gameState); if (action != null) { return(action); } // Lastly, if I think I can't do anything at all I just pass the turn. return(new NoAction()); }
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 IAction GetActionFromInput(string input, GameState state) { var i = input.ToLowerInvariant().Trim(); if (i.StartsWith("h")) { Console.WriteLine("Commands: refer to cards buy their tier and index, e.g. '1-2' for tier 1, 2nd card."); Console.WriteLine(" t: take tokens, e.g. 't ugb' for take blUe, Green, Black."); Console.WriteLine(" b: buy card. e.g. 'b 1-2', or 'b res' to buy a reserved card (choice made for you)."); Console.WriteLine(" r: reserve card. e.g. 'r 1-2', or just 'r 1' to reserve a random face down card from tier 1."); Console.WriteLine("Instruments:"); Console.WriteLine(" *: you can afford this card."); Console.WriteLine(" ·: you can almost afford this card."); return(null); } if (i.StartsWith("t")) { var whiteSpace = i.IndexOf(' '); var codes = i.Substring(whiteSpace); var tokens = TokenPoolFromInput(codes); IPool tokensToReturn = new Pool(); var commaIndex = codes.IndexOf(','); if (commaIndex != -1) { tokensToReturn = TokenPoolFromInput(codes.Substring(commaIndex + 1)); } return(new TakeTokens(tokens, tokensToReturn)); } ; if (i.StartsWith("b")) { var whiteSpace = i.IndexOf(' '); var args = i.Substring(whiteSpace).Trim(); if (args.StartsWith("res")) { var resCard = state.CurrentPlayer.ReservedCards.OrderByDescending(c => c.VictoryPoints).FirstOrDefault(c => BuyCard.CanAffordCard(state.CurrentPlayer, c)); if (resCard == null) { throw new ArgumentException("You can't afford any of your reserved cards."); } var resPayment = BuyCard.CreateDefaultPaymentOrNull(state.CurrentPlayer, resCard); if (resPayment == null) { throw new ArgumentException("You cannot afford this card or did not specify sufficient payment."); } return(new BuyCard(resCard, resPayment)); } if (args[1] != '-') { throw new ArgumentException($"Syntax error. Usage: 'b 1-2' for buying the second card in tier one."); } var tier = int.Parse(args[0].ToString()); var cardIndex = int.Parse(args[2].ToString()); var card = state.Tiers.Single(t => t.Tier == tier).ColumnSlots[cardIndex]; if (card == null) { throw new ArgumentException("There's no card in that slot. The tier {tier} deck has been exhasted."); } var payment = args.Length > 3 ? TokenPoolFromInput(args.Substring(3)) : BuyCard.CreateDefaultPaymentOrNull(state.CurrentPlayer, card); if (payment == null) { throw new ArgumentException("You cannot afford this card or did not specify sufficient payment."); } return(new BuyCard(card, payment)); } if (i.StartsWith("r")) { var whiteSpace = i.IndexOf(' '); var args = i.Substring(whiteSpace).Trim(); var tier = int.Parse(args[0].ToString()); IPool tokensToReturn = new Pool(); var commaIndex = args.IndexOf(','); if (commaIndex != -1) { tokensToReturn = TokenPoolFromInput(args.Substring(commaIndex + 1)); } var colourToReturn = tokensToReturn.Colours().Cast <TokenColour?>().SingleOrDefault(); if (args.Length == 1) { return(new ReserveFaceDownCard(tier, colourToReturn)); } if (args[1] != '-') { throw new ArgumentException($"Syntax error. Usage: 'r 1-2' for reserving the second card in tier one."); } var cardIndex = int.Parse(args[2].ToString()); var card = state.Tiers.Single(t => t.Tier == tier).ColumnSlots[cardIndex]; return(new ReserveCard(card, colourToReturn)); } throw new NotImplementedException("Don't know that one."); }