【问题标题】:Determine the winner in 4x4 tic tac toe确定 4x4 井字游戏的获胜者
【发布时间】:2021-09-18 13:08:29
【问题描述】:

4x4tic-tac-toe 在四行(从上到下编号为 0 到 3)和四列(从左到右编号为 0 到 3)的棋盘上进行。 X 总是会迈出第一步。
游戏由第一个在同一行、列或对角线(仅 2 个主对角线)上获得四个棋子的玩家获胜。如果棋盘已满且没有玩家获胜,则游戏为平局。

假设轮到X 移动,如果X 可以移动,那么X 被称为强制获胜,X 可以赢。这并不一定意味着X 将在下一步中获胜,尽管这是有可能的。这意味着X 有一个制胜策略,无论O 做什么,都能保证最终获胜。

鉴于与X 的部分完成的游戏接下来要移动,我必须确定X 是否有强制获胜。该游戏将代表一个真实世界的游戏,并且不存在任何错误(例如 x 的数量多于 o 的数量等)。如果有这样的强制获胜,应该报告第一个发现的这样的强制获胜。对于这个问题,第一次强制获胜是由棋盘位置决定的。

通过检查位置 (0,0), (0, 1), (0, 2), (0, 3),(1, 0), (1, 1), 来搜索强制获胜。 .., (2, 2),...(3, 3) 依次输出找到的第一个强制获胜。请注意,X 的许多动作可能会导致强制获胜 - 因此请以这种方式找到第一个此类强制获胜。

例子:

....
.xo.
.ox.
....

这里,X 没有强制获胜。但这是另一个例子:

o...
.ox.
.xxx
xooo

(0, 1) by X 是强制获胜。 (即使在 (0, 3) 和 (2, 0) 标记 x 将使 x 在同一个动作中获胜,但我们不允许跳转到任何特定位置。必须对每个位置执行顺序扫描并且顺序如上所述。)

我的方法: 因此,假设对于上面提到的第二个示例,如果我在 (0, 1) {这是可以进行移动的第一个空位} 处移动 x,我将递归调用此方法来确定是否通过标记这个地方是x 并在 3 个不同的子程序下检查以下内容,

  1. x 赢了吗?
  2. 有平局吗?
  3. o赢了吗?

这 3 个条件将在单独的方法中进行评估,每个方法都返回一个布尔值。如果 1 的返回值为真,我会立即将其作为答案返回。但是如果 2 为真,那么对于x 在 (0, 1) 处移动的当前棋盘,我将移动 o 以防止 x 获胜。可以根据作为参数传递给函数的当前布尔值进行轮次更改。为了确定这一举动,我必须扫描整个棋盘(4 行、4 列和 2 条对角线),看看棋盘上是否有任何地方x 有 4 个标记中的 3 个。

我无法弄清楚接下来的步骤,我强烈觉得我走错了方向。我知道它涉及递归和回溯。(如果没有,请纠正我)。任何形式的指导将不胜感激。

更新:

我试图自己弄清楚一些事情。

  1. 如果比赛中的标记少于 6 个,则为平局。
  2. 因此,即使对已经存在 6 个标记的游戏使用蛮力,复杂度也将是 9!对于每场分析的比赛(如果模拟到最后。但一旦我们知道这是x 的强胜,我们可以立即返回)。
  3. 如果标记 x 生成超过 2 行得分为 3 或更高,并且没有得分为 -3 或更高的行 o,则它是 x 的强制获胜(在板上,我是用1表示标记为x-1表示标记为o0表示空白)。
  4. 如果标记 0 生成超过 2 行的得分为 -3 或更高,则强制 ox 丢失。
  5. 除了这些点之外,无需检查。
  6. 在每次移动前进行平局检查。

更新 2: Tricont 建议我在这里使用 minmax 算法方法。但我相信这对于这个问题来说可能是一种矫枉过正。这不是人类与人工智能的游戏。我只需要确定导致x强制获胜的位置。如果不存在这样的位置,那么绝对不是强制获胜(可能是x输掉比赛或平局)。

【问题讨论】:

  • 你看极小极大算法了吗?
  • 让我直说。 Input - 板子的当前状态。 输出 - 查找X在当前状态下是否强制获胜?
  • 不,我没有。我什至不知道它是什么。
  • @AKSingh 准确!

标签: algorithm tic-tac-toe


【解决方案1】:

Minimax 是这个问题的解决方案。

这里最重要的想法是玩家 O 可以对平局感到满意,并且当它发出 o 时至少可以确定平局在网格上任何可能的四行中的至少一个单元格中。有 10 条这样的线(4 条水平线、4 条垂直线和 2 条对角线)。 O 要在所有 10 行中出现并不难。比如O占据了这4个位置,那么X是不可能赢的:

...o
.o..
..o.
o...

这意味着 O 很容易阻止 X 获胜,因此预计在棋盘出现之前搜索不必下棋。完全填满,因为两位玩家中的一个在游戏的早期就已经达到了他们的目标。

评价函数

minimax 实现需要一个评估函数。在这种情况下,它可以决定最后下棋的玩家是否达到了他们的目标。

  • X 的目标是获胜。
  • O 的目标是赢或平。

因此,评估函数可以检查所有 10 条四线,看看最后播放的玩家是否已经完全占据了这条线。如果找到,评估函数应该返回 1——表明玩家已经达到了他们的目标。

接下来,评估函数应该检查 O 是否是最后一个玩家。如果是这样,它可以再次检查所有 10 个四行并查看 O 是否在 每个 行中至少存在一个。如果是这种情况,它也应该返回 1——表示 O 已经达到了他们的目标。

在所有其他情况下,评估函数可能返回 0——表示还没有明确的答案。

由于唯一的结果是 0 或 1,因此评估函数也可以返回布尔值。我将调用该函数happy

极小极大

极小极大函数返回一个值。在这种情况下,我们将支持值 -1、0 或 1:

  • -1:最后出场的玩家无法达到目标
  • 0:未定
  • 1:最后出场的玩家可以强行达到目标。

Minimax 将执行所有对手的动作并找出哪一个给出“最佳”结果,在这种情况下,它的评估值为 1。如果找到这样的动作,那么前一个玩家将获得否定的评估结果他们的最后一步,即-1。对于可以从minimax 的递归调用返回的其他评估值,也会发生类似的逻辑。

董事会代表

有很多方法可以代表游戏。我选择为两个玩家使用一个位图,每个位置一个位。所以每个玩家都有一个 16 位的位图,位图中的每个 1 位代表他们所做的一个动作。导致特定状态的移动顺序无关。

四行也可以表示为位图,以便通过将玩家的位图与它们叠加来轻松检测胜利。

移动用数字表示,从 0 到 15。

记忆

Minimax 可以从记忆中受益,因此它不需要通过相同的搜索树来两次评估相同的棋盘状态。由于我选择用两个 16 位数字来表示电路板,因此很容易通过 32 位数字来识别(散列)状态。

实施

这是一个 JavaScript 实现。您可以在此处运行它,它将输出您给出的两个示例的结果。一个非负整数的输出,意味着这一步是强制获胜。输出 -1 表示不可能强制获胜。

class TicTacToe {
    static memo = new Map; // For memoization

    constructor(str) {
        str = str.toLowerCase().replace(/[^.xo]/gi, "");
        if (str.length !== 16) throw "Invalid board size";
        this.players = [0x0000, 0x0000]; // bitmaps for both players
        this.lines = [ // bitmaps of winning lines
            0xF000, 0x0F00, 0x00F0, 0x000F,
            0x1111, 0x2222, 0x4444, 0x8888,
            0x1248, 0x8421
        ];
        this.turn = 0; // 0 or 1
        // collect moves into bitmaps
        let bit = 1;
        let balance = 0;
        for (let ch of str) {
            let player = "xo".indexOf(ch);
            if (player >= 0) {
                this.players[player] ^= bit;
                balance += player || -1;
            }
            bit *= 2;
        }
        if (balance != 0) throw "Number of X and O should be equal";
    }
    doMove(move) {
        this.players[this.turn] ^= 1 << move;
        this.turn ^= 1;
    }
    undoMove(move) {
        this.turn ^= 1;
        this.players[this.turn] ^= 1 << move;
    }
    happy() {
        // Returns whether the player (that last moved) has reached their goal (boolean)
        // For "X" this means they have 4-in-a-row. 
        // For "O" this means they have 4-in-a-row OR have a presence in all 10 lines
        let mask = this.players[1 - this.turn];
        for (let line of this.lines) {
            if ((line & mask) === line) return true; // Game over. Last move was winner.
        }
        if (this.turn == 1) return false;
        // Check whether last player ("O") cannot lose:
        for (let line of this.lines) {
            if ((line & mask) == 0) return false; // Opponent could still win here
        }        
        return true; // Second player is happy with draw
    }
    getMoveList() {
        let occupied = this.players[0] ^ this.players[1];
        let moveList = []; 
        for (let move = 0; move < 16; move++) {
            if (((occupied >> move) & 1) == 0) moveList.push(move);
        }
        return moveList;
    }
    minimax() { // Return value for player that just played (-1, 0 or 1).
        let key = (this.players[0] << 16) + this.players[1];
        let value = TicTacToe.memo.get(key); // Using memoization
        if (value === undefined) { // Not encountered before
            value = 1;
            if (!this.happy()) { // Undecided?
                for (let move of this.getMoveList()) {
                    this.doMove(move);   
                    // What is good for opponent is bad for current player:
                    value = Math.min(value, -this.minimax());
                    this.undoMove(move);
                    if (value == -1) break; // Opponent can reach their goal
                }
            }
            TicTacToe.memo.set(key, value); // Memoize...
        }
        return value;
    }
    bestMove() { // Returns winning move (0..15) for X or -1 if none found
        if (this.happy()) return -1; // "O" already ensured at least a draw.
        for (let move of this.getMoveList()) {
            this.doMove(move);
            let value = this.minimax();
            this.undoMove(move);
            if (value == 1) return move; // forced win: return this winning move
        }
        return -1; // no forced win found
    }
    toString() {  // To ease printing...
        let str = "";
        for (let bit = 1; bit < 0x10000; bit *= 2) {
            if (this.players[0] & bit) str += "x";
            else if (this.players[1] & bit) str += "o";
            else str += ".";
            if (bit & 0x8888) str += "\n";
        }
        return str;
    }
}

let game;

// Test case 1:
game = new TicTacToe(`
o...
.ox.
.xxx
xooo`);
console.log(game.toString());
console.log("bestMove() returned", game.bestMove());

// Test case 2:
game = new TicTacToe(`
....
.ox.
.xo.
....`);
console.log(game.toString());
console.log("bestMove() returned", game.bestMove());

人们可以想到很多优化,但由于这已经在可接受的时间内运行,我就这样离开了。

【讨论】:

    【解决方案2】:

    每个字段都被玩家 1、玩家 2 覆盖,或者是空的。那是 $3^{16}$ 的可能性,远低于 1 亿。

    根据 X 与 Y 覆盖的字段数将每个位置标记为 X、Y 或“不允许”:相同的数字 -> x 正在播放,多一个 X 字段 -> Y 正在播放,其他所有内容 -> 不是允许。

    将每个允许的位置标记为赢、输、平或未知:四个对手颜色 = 输,四个你自己的颜色 = 不允许,所有填充 = 平局,否则未知。

    现在遍历所有未知位置:任何移动到“丢失”(对于对手)位置 -> 获胜。任何移动到“未知”位置 -> 未知。任何移动到“绘制” -> 绘制。否则,所有动作都为对手赢得 -> 输了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-02-11
      • 1970-01-01
      • 1970-01-01
      • 2020-07-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多