I think it is not a secret to anyone that “Fool” (hereinafter this word will be written in small letters and without quotes) is the most popular card game in Russia and the countries of the former USSR (although almost unknown outside of it). Despite its name and rather simple rules, the gain in it still depends more on the player’s skill than on the random layout of the cards (in English terminology, games of both types are called game of skill and game of chance, respectively. So, the fool in more game of skill ).
The purpose of the article is to write a simple AI for the game. The word "simple" means the following:
(Strictly speaking, the first paragraph no longer gives such an AI the right to be called artificial intelligence per se , but only a pseudo-AI. But such terminology in game development is well-established, so we will not change it.)
I think the rules of the game are known to everyone, so I’ll not remind them again. For those who want to check, I advise you to contact Wikipedia , there is a pretty good article on this topic.
So, let's begin. Obviously, the more foolish a card is, the more profitable it is to have it in your hand. Therefore, we will build an algorithm on the classical assessment of the strength of the hand and making a decision (for example, about throwing one or another card) on the basis of this assessment. We assign cards values, for example:
(We use numbers that are multiples of 100 in order to get rid of floating-point in calculations and operate only with whole numbers. For what we need negative estimates, see later in the article.)
The trump cards are more valuable than any simple ones (even a trump deuce beats an "ordinary" ace), and the hierarchy in the trump suit is the same, so for their evaluation we simply add 1300 to the "base" value - then, for example, the trump deuce will cost 600 + 1300 = 700 points (that is, just a little more than an uncensored ace).
In the code (all code examples in the article will be on Kotlin), it looks like this (the relativaCardValue()
function returns that same estimate, and RANK_MULTIPLIER
is just a factor of 100):
for (c in hand) { val r = c.rank val s = c.suit res += ((relativeCardValue(r.value)) * RANK_MULTIPLIER).toInt() if (s === trumpSuit) res += 13 * RANK_MULTIPLIER // еще не все, продолжение чуть ниже }
Alas, that's not all. It is also important to consider the following evaluation rules:
$$ display $$ \ clubsuit 2 \ spadesuit 2 \ diamondsuit Q \ heartsuit Q \ clubsuit Q \ spadesuit Q $$ display $$ almost perfect (of course, if the opponent does not go under your kings or aces): you will be beaten off by the ladies, after which hang rival shoulder straps hand him a pair of twos.
$$ display $$ \ spadesuit 5 \ spadesuit J \ spadesuit A \ diamondsuit 6 \ diamondsuit 9 \ diamondsuit K $$ display $$ very unsuccessful - even if the opponent does not “knock out” your trump card with the first move and goes with the card of the peak suit, then all other cards thrown up will be of other suits, and you will have to give trumps to them. In addition, it is very likely that the top five will remain unclaimed - all the trump cards you have are worth more than five, so under no circumstances (unless, of course, you initially went to the card under) you will not be able to cover it with any other card - the probability of taking a very is high. On the other hand, we replace the jack with a ten of clubs, and the trump six with a triple:
$$ display $$ \ spadesuit 5 \ clubsuit 10 \ spadesuit A \ diamondsuit 3 \ diamondsuit 9 \ diamondsuit K $$ display $$ Despite the fact that we replaced the cards with younger ones, such a hand is much better - firstly, you will not have to give away a trump card to the trekuit (and you can be more likely to use the ace). And secondly, if you beat then the card is your trump card, there is a possibility that someone will throw a peak at you (for there’s usually no point in holding such a card), and you will have to buy five.
To implement these strategies, we modify our algorithm: Here we count the number of cards of each suit and value ...
/* бонусные коэффициенты в зависимости от количества карт одного достоинства - например, если нет ни оной карты какого-то достоинства или она только одна, бонусы не начисляются, а за все 4 карты коэффициент равен 1.25 */ val bonuses = doubleArrayOf(0.0, 0.0, 0.5, 0.75, 1.25) var res = 0 val countsByRank = IntArray(13) val countsBySuit = IntArray(4) for (c in hand) { val r = c.rank val s = c.suit res += ((relativeCardValue(r.value)) * RANK_MULTIPLIER).toInt() if (s === trumpSuit) res += 13 * RANK_MULTIPLIER countsByRank[r.value - 1]++ countsBySuit[s.value]++ }
... here we add bonuses for them (calling Math.max
needed in order not to charge negative bonuses for junior cards - because in this case it is also profitable) ...
for (i in 1..13) { res += (Math.max(relativeCardValue(i), 1.0) * bonuses[countsByRank[i - 1]]).toInt() }
... and here, on the contrary, we are fined for an unbalanced hand (the value of UNBALANCED_HAND_PENALTY
empirically set to 200):
// считаем среднее количество карт некозырных мастей... var avgSuit = 0.0 for (c in hand) { if (c.suit !== trumpSuit) avgSuit++ } avgSuit /= 3.0 for (s in Suit.values()) { if (s !== trumpSuit) { // и вычитаем штрафы в зависимости от отклонения от этого среднего по каждой масти val dev = Math.abs((countsBySuit[s.value] - avgSuit) / avgSuit) res -= (UNBALANCED_HAND_PENALTY * dev).toInt() } }
Finally, let us take into account such a banal thing as the number of cards in your hand. In fact, it’s very good to have 12 good cards at the beginning of the game (especially since they can still throw no more than 6), but at the end of the game, when there’s only a 2-card opponent left behind you, it’s not like that.
// считаем количество оставшихся в игре карт (в колоде и на руках у игроков) var cardsInPlay = cardsRemaining for (p in playerHands) cardsInPlay += p cardsInPlay -= hand.size // вычисляем, какая доля из них у игрока, и определяем величину штрафа (здесь MANY_CARDS_PENALTY = 600) val cardRatio = if (cardsInPlay != 0) (hand.size / cardsInPlay).toDouble() else 10.0 res += ((0.25 - cardRatio) * MANY_CARDS_PENALTY).toInt() return res
We summarize - in full, the evaluation function looks like this:
private fun handValue(hand: ArrayList<Card>, trumpSuit: Suit, cardsRemaining: Int, playerHands: Array<Int>): Int { if (cardsRemaining == 0 && hand.size == 0) { return OUT_OF_PLAY } val bonuses = doubleArrayOf(0.0, 0.0, 0.5, 0.75, 1.25) // for cards of same rank var res = 0 val countsByRank = IntArray(13) val countsBySuit = IntArray(4) for (c in hand) { val r = c.rank val s = c.suit res += ((relativeCardValue(r.value)) * RANK_MULTIPLIER).toInt() if (s === trumpSuit) res += 13 * RANK_MULTIPLIER countsByRank[r.value - 1]++ countsBySuit[s.value]++ } for (i in 1..13) { res += (Math.max(relativeCardValue(i), 1.0) * bonuses[countsByRank[i - 1]]).toInt() } var avgSuit = 0.0 for (c in hand) { if (c.suit !== trumpSuit) avgSuit++ } avgSuit /= 3.0 for (s in Suit.values()) { if (s !== trumpSuit) { val dev = Math.abs((countsBySuit[s.value] - avgSuit) / avgSuit) res -= (UNBALANCED_HAND_PENALTY * dev).toInt() } } var cardsInPlay = cardsRemaining for (p in playerHands) cardsInPlay += p cardsInPlay -= hand.size val cardRatio = if (cardsInPlay != 0) (hand.size / cardsInPlay).toDouble() else 10.0 res += ((0.25 - cardRatio) * MANY_CARDS_PENALTY).toInt() return res }
So, we have the evaluation function ready. In the next part, it is planned to describe a more interesting problem - making decisions based on such an assessment.
Thank you all for your attention!
PS This code is part of the application developed by the author in his free time. It is available on GitHub (binary releases for Desktop and Android, the latest application is also available on F-Droid ).
Source: https://habr.com/ru/post/437346/