参见->游戏规则
此游戏是否有必不败策略?(由策梅洛定理,TixTax必定存在必不败策略)
那么,先手是必胜、必和,还是必败呢?
抽了个寂寞
借用了位棋盘的思想,用81位的整型来存储棋盘上的落子情况,红方落子记为reds, 蓝方落子记为blues。例如,reds = 111000......000代表棋盘前三个小格子都为红色。用pos(0-8)代表一方落子时被限制在哪个中格子里,pos = -1表示无限制。color表示当前轮到哪个玩家, 奇数表示红方,偶数表示蓝方。于是乎,游戏进行中的每一个局面都可以通过(reds, blues, pos, color)唯一描述。例如,开始局面为(000......000, 000......000, -1, 1),当红方落子在棋盘正中央的小格时,局面变为(000...1...000, 000......000, 4, 2)。
思路一:穷举。利用dfs从开始局面一步步深搜下去,对每一层所得结果汇总后得出本层结果并返回给上一层。具体实现见tixtax.cpp。实现起来较简单,但最大的问题在于TixTax种类数太多了!一个不紧确上界是81!(这大约有10的120次方),另一个估算是9^81(这大约有10的77次方)。明显可见,穷举并不是一个解决该问题的好方法。
思路二:正如象棋一样,虽然情况数非常多,我们也有方法能评估一个局面。但是这样我们能得到的仅是一个数值。数值的大小代表了我们能获胜的可能性,但这不意味着我们一定获胜。换言之,在没有穷举所有局面的情况下,可能会有一条路径使后手必胜,但我们的评估函数认为此局面先手有很大优势。
思路三:也许我们能从数学上通过一定的技巧得出必不败策略(得益于TixTax简单而又富有美感的游戏规则),但这需要进一步思考。
实现穷举时,对策梅洛定理有了点直观的看法。无论是之前的finger-game,还是这次的TixTax,都是符合策梅洛定理前提的游戏。这些游戏都可以从结局反推,一步步往开始局面倒退,得出过程中每个局面的必不败策略,从而一直推到开始局面的必不败策略如何。例如最终局面是红方赢,那么上一局面若是轮到红方落子的,则上一局面一定是红方必胜。如果上一局面是蓝方落子,蓝方无论如何落子都会导致败局,则上一局面也是红方必胜。简言之,对于一方落子时,若其所有落子结果中存在 其必胜的局面,则本局面它必胜。若全是 输的结果,则它必败。若上述两条都不满足,则必和。因此一个局面的胜负性是必定存在的,因此可以倒着导出开始局面的必胜性。
在我看来,一个符合策梅洛定理前提的(双人)游戏,可以用以下的通用模型(半伪代码)来穷举:
class Game {
enum Result {
WIN, TIE, LOSE, // 分别表示先手必赢、和、输
NULL // 表示一局面尚未分出胜负(仅在搜索过程中才会出现,不可能作为一个局面最终结果)
}
Result move(Scene scene, int player); // 一方进行游戏
Result check(Scene scene) //判断一个局面是否结束了
Result dfs(Scene scene, int player) {
if(check(scene) == WIN or TIE or LOSE) //此局面已经结束
return check(scene); //将结果返回上一层来决策
vector<Result> all_results;
for(canMove(player)) // 对于该玩家所有能走的局面
all_results.push_back(dfs(scene, next_player()));
if(player == first) {
// 如果此局面轮到先手下
if(all_results.contains(WIN))
// 如果结果中有先手必胜,则先手必胜
return WIN;
else if(all_results.contains(TIE))
// 不存在先手必胜的结果,但有先手必和的结果,则此局面先手必和。
return TIE;
else
// 无论怎么走都会导致后手胜利
return LOSE;
}
else {
// 此局面轮到后手下,同理
if(all_results.contains(LOSE))
// 如果结果中有后手必胜,则先手必败
return LOSE;
else if(all_results.contains(TIE))
// 不存在后手必胜的结果,但有后手必和的结果,则此局面先手必和。
return TIE;
else
// 无论怎么走都会导致先手胜利
return WIN;
}
throw error; // 走到这说明这游戏不符合策梅洛定理前提
return NULL;
}
}