愚人游戏(不是)。 我们为“傻瓜”编写AI(第1部分)

我认为对于任何人来说,“傻瓜”(以下单词都会用小写字母且没有引号)是俄罗斯和前苏联国家/地区最受欢迎的纸牌游戏(尽管在外面几乎是未知的)对任何人都是秘密。 尽管它的名字和相当简单的规则,但赢得它仍然更多地取决于玩家的技能,而不是纸牌的随机分布(用英语术语来说,两种类型的游戏分别称为技能 游戏和机会游戏 。)更多的技巧游戏 )。


本文的目的是为游戏编写一个简单的AI。 “简单”一词的含义如下:


  • 直观的决策算法(因此,没有机器学习将算法深深地隐藏在“幕后”);
  • 缺乏状态(也就是说,算法仅以当前时间的数据为准,简而言之,它不会记住任何东西(例如,它不会“计数”离开游戏的牌)。

(严格来说,第一段不再授予这种AI 本身被称为人工智能的权利,而只是伪AI。但是此术语已在游戏开发中确立,因此我们不会对其进行更改。)


我认为游戏规则是众所周知的,因此我不会再提醒他们。 那些想要检查的人,我建议您与Wikipedia联系,有关此主题的文章不错。


因此,让我们开始吧。 显然,愚蠢的人认为卡片越老越好。 因此,我们将基于对手部力量的经典评估来构建算法,并基于该评估做出决策(例如,扔出特定的牌)。 例如,我们将值分配给地图,如下所示:


  • ace (A)-+600点,
  • 国王 (K)-+500,
  • (Q)-+400,
  • 杰克 (J)-+300,
  • (10)-+200,
  • (9)-+100,
  • (8)-0,
  • (7)--100,
  • (6)--200,
  • (5)--300
  • (4)--400,
  • (3)--500,
  • 最后,降低(2)--600分。

(为了避免浮点数在计算中使用,我们使用100的整数倍,而仅使用整数。为此,我们需要负的额定值,请参阅本文下面的内容。)


特朗普牌比任何简单的牌都有价值(即使是王牌大局也能击败“普通”王牌),王牌中的等级也是相同的,因此要对其进行评估,只需在“基本”值上加上1300-例如,王牌大局将“花费” -600 + 1300 = 700点(即,仅比一张无王牌王牌多一点)。


在代码中(本文中的所有代码示例均在Kotlin中),看起来像这样( relativaCardValue()函数返回相同的估计值,而RANK_MULTIPLIER只是一个等于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 //   ,    } 

las,还不是全部。 考虑以下评估规则也很重要:


  • 拥有许多同等价值的卡牌是有利的-不仅因为它们可以“填满”对手,而且还可以轻松地击退攻击(尤其是在卡牌价值很高时)。 例如,在游戏结束时,一只手(为简单起见,我们假设以下的王牌是手鼓)

    $$显示$$ \ Clubsuit 2 \ spadesuit 2 \ Diamondsuit Q \ heartsuit Q \ clubsuit Q \ spadesuit Q $$ display $$ 几乎完美(当然,如果对手没有用国王或王牌击败您):您将被女士们击败,之后 死对头 给他一副演说。


    但是,许多相同(当然是非王牌)西装的卡却有一个缺点-它们会彼此“干扰”。 例如手

    $$显示$$ \ spadesuit 5 \ spadesuit J \ spadesuit A \ Diamondsuit 6 \ Diamondsuit 9 \ Diamondsuit K $$显示$$ 非常不幸的是-即使对手没有在第一步中“淘汰”您的王牌,而是拿到了顶峰套装的牌,那么其他所有扔出的牌也将是其他套牌,他们将不得不提供王牌。 此外,很有可能五次比赛都无人认领-您的所有王牌的尊严都超过五分,因此在任何情况下(当然,除非您最初使用的是较年轻的卡片),否则您将无法使用其他任何卡片进行覆盖-这很可能会导致高。 另一方面,我们用十个棍棒代替了黑桃杰克,而用三重棍代替了王牌六:

    $$显示$$ \ spadesuit 5 \ clubsuit 10 \ spadesuit A \ Diamondsuit 3 \ Diamondsuit 9 \ Diamondsuit K $$显示$$ 尽管我们用较低的牌替换了牌,但这样的手要好得多-首先,您不必打王牌(并且您更有可能使用黑桃A),其次,如果您击败了然后一张有三把王牌的牌,就有机会有人将三张黑桃扔给你(因为通常没有持有这样的牌的意思),而你会“抓住”这五张牌。



    为了实现这些策略,我们修改了算法:在这里,我们考虑每套西装的张数和优势...


     /*          - ,      -     ,   ,    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]++ } 

    ...在这里,我们为他们添加了奖金( Math.max调用是必需的,以便不为低卡产生负奖金-因为在这种情况下,这也是有益的)...


     for (i in 1..13) { res += (Math.max(relativeCardValue(i), 1.0) * bonuses[countsByRank[i - 1]]).toInt() } 

    ……与此相反,我们对西装中不平衡的西装罚款( UNBALANCED_HAND_PENALTY的值实验设置为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() } } 

    最后,我们考虑到了手头上的卡片数量之类的平庸事物。 实际上,在游戏开始时拥有12张好牌是非常好的(特别是因为它们仍然只能被扔出不超过6张),但是在游戏结束时,除了您之外只有2张牌的对手时,情况并非如此。


     //       (      ) 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 

    我们总结一下-完整的评估函数如下所示:


     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 } 

    因此,我们已经准备好评估功能。 在下一部分中,计划描述一个更有趣的任务-基于这样的评估做出决策。


    谢谢大家的关注!


    PS此代码是作者在业余时间开发的应用程序的一部分。 它在GitHub上可用 (适用于台式机和Android的二进制版本,对于后者,该应用程序也可以在F-Droid上使用 )。

Source: https://habr.com/ru/post/zh-CN437346/


All Articles