我认为对于任何人来说,“傻瓜”(以下单词都会用小写字母且没有引号)是俄罗斯和前苏联国家/地区最受欢迎的纸牌游戏(尽管在外面几乎是未知的)对任何人都是秘密。 尽管它的名字和相当简单的规则,但赢得它仍然更多地取决于玩家的技能,而不是纸牌的随机分布(用英语术语来说,两种类型的游戏分别称为技能 游戏和机会游戏 。)更多的技巧游戏 )。
本文的目的是为游戏编写一个简单的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),其次,如果您击败了然后一张有三把王牌的牌,就有机会有人将三张黑桃扔给你(因为通常没有持有这样的牌的意思),而你会“抓住”这五张牌。
为了实现这些策略,我们修改了算法:在这里,我们考虑每套西装的张数和优势...
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):
最后,我们考虑到了手头上的卡片数量之类的平庸事物。 实际上,在游戏开始时拥有12张好牌是非常好的(特别是因为它们仍然只能被扔出不超过6张),但是在游戏结束时,除了您之外只有2张牌的对手时,情况并非如此。
我们总结一下-完整的评估函数如下所示:
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)
因此,我们已经准备好评估功能。 在下一部分中,计划描述一个更有趣的任务-基于这样的评估做出决策。
谢谢大家的关注!
PS此代码是作者在业余时间开发的应用程序的一部分。 它在GitHub上可用 (适用于台式机和Android的二进制版本,对于后者,该应用程序也可以在F-Droid上使用 )。