我目前正在开发一个名为Skat的基于技巧的纸牌游戏的求解器,处于完美信息情况。尽管大多数人可能不了解这个游戏,请耐心听我说;我的问题具有普遍性。
Skat简介:
基本来说,每个玩家轮流出一张牌,每三张牌构成一个技巧。每张牌都有特定的价值。玩家获得的分数是赢得的技巧中每张牌的价值总和。我省略了一些对我的问题不重要的细节,例如谁与谁对抗或何时赢得一个技巧。
我们应该记住的是有一个正在进行的分数,并且在研究某个位置时,之前谁出了什么牌(->它的历史)对这个分数是相关的。
我用Java编写了一个alpha-beta算法,看起来运行良好,但速度太慢。最有前景的第一个改进似乎是使用置换表。我读到在搜索Skat游戏的树时,会遇到很多已经研究过的位置。
这就是我的问题所在:如果我发现一个之前已经研究过的位置,导致这个位置的移动是不同的。因此,一般来说,分数(和alpha或beta)也会不同。
这引出了我的问题:如果我知道同一个位置但有不同历史的值,我如何确定这个位置的值?
换句话说:我如何将一个子树从其到根的路径中分离出来,以便它可以应用到一个新的路径上?
我的第一反应是这根本不可能,因为alpha或beta可能受到其他路径的影响,这些路径可能不适用于当前位置,但是…
似乎已经有一个解决方案
…我似乎不理解。在Sebastion Kupferschmid关于Skat求解器的硕士论文中,我找到了这段代码(可能是C语言风格/伪代码):
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游戏中人工智能的英文来源,但我找到了这个:基于蒙特卡洛模拟的Skat玩家,作者Kupferschmid, Helmert。不幸的是,整篇论文,尤其是关于置换表的详细说明相当简洁。
编辑:
为了让大家更好地想象Skat游戏中直到所有牌都打完的分数发展情况,这里有一个例子。游戏的进程显示在下表中,每行一个技巧。每个技巧后的实际分数显示在左侧,其中+X是庄家的分数(-Y是防守队的分数,这对alpha-beta不重要)。如我所说,技巧的赢家(庄家或防守队)将这个技巧中每张牌的价值加到他们的分数上。
牌的价值是:
等级 J A 10 K Q 9 8 7价值 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上发布相同的问题后,一个名为Álvaro的用户给了我决定性的提示:
当存储精确分数(TT_VALID
)时,我应该只存储改进了alpha的位置。