我目前正在开发一个解决方案,一个基于技巧的纸牌游戏称为Skat在一个完美的信息情况。虽然大多数人可能不知道这个游戏,但请容忍我,我的问题是一般性的。
SKAT简介:
基本上,每个玩家轮流玩一张牌,每三张牌就形成一个戏法。每张卡都有特定的价值。一个玩家所获得的分数是将其所赢得的每一张牌的价值相加的结果。我漏掉了一些对我的问题不重要的事情,例如谁对谁,我什么时候赢一个把戏。
我们应该记住的是,有一个连续的分数,谁在调查某个位置时打了什么(->它的历史)与该分数有关。
我用Java编写了一个alpha-beta算法,它看起来工作得很好,但是速度太慢了最有希望的第一个改进是使用换位表。我读到在搜索skat游戏树的时候,你会遇到很多已经被调查过的位置。
这就是我的问题所在:如果我找到一个之前已经调查过的位置,那么导致这个位置的移动是不同的。因此,一般来说,分数(和α或β)也会有所不同。
这就引出了我的问题:如果我知道同一个职位的价值,但又有不同的历史,我如何确定这个职位的价值?
换言之:如何将子树从其路径分离到根,以便将其应用到新路径?
我的第一个冲动是这是不可能的,因为alpha或beta可能受到其他路径的影响,这些路径可能不适用于当前位置,但是……
似乎已经有了解决办法
…我好像不明白在Sebastion Kupferschmid关于Skat解算器的硕士论文中,我发现了这段代码(可能是C-ish/伪代码?)以下内容:
def ab_tt(p, alpha, beta):
if p isa Leaf:
return 0
if hash.lookup(p, val, flag):
if flag == VALID:
return val
elif flag == LBOUND:
alpha = max(alpha, val)
elif flag == UBOUND:
beta = min(beta, val)
if alpha >= beta:
return val
if p isa MAX_Node:
res = alpha
else:
res = beta
for q in succ(p):
if p isa MAX_Node:
succVal = t(q) + ab_tt(q, res - t(q), beta - t(q))
res = max(res, succVal)
if res >= beta:
hash.add(p, res, LBOUND)
return res
elif p isa MIN_Node:
succVal = t(q) + ab_tt(q, alpha - t(q), res - t(q))
res = min(res, succVal)
if res <= alpha:
hash.add(p, res, UBOUND)
return res
hash.add(p, res, VALID)
return res
这应该是非常不言而喻的。
succ(p)
是返回当前位置的所有可能移动的函数t(q)
是我认为的各自位置的连续得分(到目前为止由宣布者获得的分数)。既然我不喜欢在不理解的情况下抄袭,这应该是对任何想帮助我的人的帮助。当然,我已经对这段代码做了一些思考,但我不能只考虑一件事:在再次调用函数之前,先从alpha/beta中减去当前的分数[例如
ab_tt(q, res - t(q), beta - t(q))
],这似乎是某种脱钩但是如果我们把位置的值存储在换位表中,而不在这里做同样的减法,到底有什么好处呢?如果我们发现了一个先前调查过的位置,为什么我们可以返回它的值(如果它是VALID
)或者使用alpha或beta的绑定值?在我看来,从换位表中存储和检索值并不能解释这些位置的特定历史还是会呢?文献:
在skat游戏中,几乎没有英文资料可以处理人工智能,但我找到了这个:A Skat Player Based on Monte Carlo Simulation by Kupferschmid, Helmert遗憾的是,整篇论文,特别是换位表的阐述是相当紧凑的。
编辑:
为了让每个人都能更好地想象,在玩完所有的牌之前,通过Skat游戏,分数是如何发展的,这里有一个example游戏过程显示在下表中,每行一个技巧每一个技巧后的实际得分在它的左边,其中+x是说唱者的得分(-y是防守队的得分,这与α-β无关)。如我所说,一个技巧的赢家(庄家或防守队)会将这个技巧中每张牌的价值加到他们的得分上。
卡值为:
Rank J A 10 K Q 9 8 7
Value 2 11 10 4 3 0 0 0
最佳答案
我解决了这个问题。根据我问题中引用的建议,每次递归调用都要做奇怪的减法运算,只有在换位表中存储一个位置时,我才会从得到的alpha beta值中减去运行分数:
对于精确的值(位置尚未修剪):
transpo.put(hash, new int[] { TT_VALID, bestVal - node.getScore()});
如果节点导致beta中断:
transpo.put(hash, new int[] { TT_LBOUND, bestVal - node.getScore()});
如果节点导致alpha截断:
transpo.put(hash, new int[] { TT_UBOUND, bestVal - node.getScore()});
哪里:
transpo
是一个HashMap<Long, int[]>
hash
是表示该位置的long
值bestVal
是精确值或导致截断的值TT_VALID
、TT_LBOUND
和TT_UBOUND
是简单的常量,用于描述换位表条目的类型然而,这本身并不起作用在gamedev.net上发布了同样的问题之后,一位名叫阿尔瓦罗的用户给了我一个决定性的提示:
当存储精确的分数(
TT_VALID
)时,我应该只存储位置,这样可以提高alpha。关于algorithm - 如何在换位表中说明头寸历史,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/21688552/