井字游戏完美AI算法:更深层次的“创建分叉”步骤

2022-09-03 02:50:27

我已经在StackOverflow上阅读了许多井字游戏主题。我发现维基百科上的策略适合我的演示项目:

玩家可以玩完美的井字游戏,如果他们选择下表中优先级最高的动作[3]。

1)赢:如果你连续有两个,玩第三个,连续得到三个。

2)格挡:如果对手连续有两个,打第三个来挡住他们。

3)分叉:创造一个机会,你可以通过两种方式获胜。

4)阻止对手的叉子:

选项1:连续创建两个以迫使对手进行防守,只要不会导致他们创建分叉或获胜。例如,如果“X”有一个角,“O”有中心,“X”也有相反的角,“O”一定不能为了获胜而打角。(在这种情况下,玩一个角落会为“X”创建一个分叉以获胜。

选项2:如果有对手可以分叉的配置,请阻止该分叉。

5)居中:玩中。

6)对角:如果对手在角,则打对角。

7)空角:玩空角。

8)空面:玩空面。

我遵循了这些步骤,计算机永远不会丢失。但是,它的攻击方式并不完美。因为我不知道如何做步骤3。以下是我在步骤3中执行的操作:扫描每个单元格,检查在该单元格上放置令牌是否创建了一个分支,然后将其放在那里。

private void step3() // Create Fork.
{
    int[] dummyField = (int[])field.Clone();
    // Try Level 1 Dummy
    for (int i = 0; i < 9; i++)
    {
        if (dummyField[i] != 0) continue;
        dummyField[i] = 2;
        if (countFork(dummyField, 2) >= 2)
        {
            nextCell = i;
            return;
        }
        dummyField[i] = 0;
    }

}

请给我一些关于此步骤的建议。

EDIT1:计数分叉将计算计算机有多少个分叉(计算机的令牌为2,玩家令牌为1,因为我在步骤4中也使用了该方法,因此函数中有一个令牌参数)。countFork

编辑2:我说它不完美的原因是这个(CPU先走,它的细胞是蓝色的,人类细胞是红色的)。enter image description here如您所见,如果我放入顶部单元格,计算机将获胜。但是,如果我把右边的单元格放进去,这是一个平局,尽管计算机仍然可以获胜。

编辑3:不知道为什么,但我注释掉了步骤3,电脑播放...完全!我真的很惊讶!这是我的countFork函数(我需要将此代码移植到Alice,它不支持2维数组,所以我使用getNumberFromXY将2维数组转换为1维):

private int countFork(int[] field, int token)
{
    int result = 0;

    // Vertical
    int cpuTokenCount;
    int spareCell;
    for (int x = 0; x < 3; x++)
    {
        cpuTokenCount = 0;
        spareCell = -1;
        for (int y = 0; y < 3; y++)
        {
            if (field[getNumberFromXY(x, y)] == token)
                cpuTokenCount++;
            else if (field[getNumberFromXY(x, y)] == 0)
                spareCell = getNumberFromXY(x, y);
        }
        if (cpuTokenCount == 2 && spareCell != -1) result++;
    }

    // Horizontal
    for (int y = 0; y < 3; y++)
    {
        cpuTokenCount = 0;
        spareCell = -1;
        for (int x = 0; x < 3; x++)
        {
            if (field[getNumberFromXY(x, y)] == token)
                cpuTokenCount++;
            else if (field[getNumberFromXY(x, y)] == 0)
                spareCell = getNumberFromXY(x, y);
        }
        if (cpuTokenCount == 2 && spareCell != -1) result++;
    }

    // Top-Left To Lower-Right Diagonal
    cpuTokenCount = 0;
    spareCell = -1;
    for (int i = 0; i < 3; i++)
    {
        if (field[getNumberFromXY(i, i)] == token)
            cpuTokenCount++;
        else if (field[getNumberFromXY(i, i)] == 0)
            spareCell = getNumberFromXY(i, i);
    }
    if (cpuTokenCount == 2 && spareCell != -1) result++;

    // Top-Right To Lower-Left Diagonal
    cpuTokenCount = 0;
    spareCell = -1;
    for (int i = 0; i < 3; i++)
    {
        if (field[getNumberFromXY(2 - i, i)] == token)
            cpuTokenCount++;
        else if (field[getNumberFromXY(2 - i, i)] == 0)
            spareCell = getNumberFromXY(2 - i, i);
    }
    if (cpuTokenCount == 2 && spareCell != -1) result++;

    return result;
}

EDIT4:根据soandos修复了错误,并在EDIT 3上更新了代码,现在它工作得很好!


答案 1

我不确定这是最优雅的方法,但这里有一个两步法来看待分叉。

如果计算机无法赢得下一回合,并且它不是第一回合或第二回合,则分叉可能是可能的(这不涉及为分叉创建设置,而只是查找分叉)。

对于每个空单元格,填充它,然后运行步骤 1 函数(查看一行中是否有两个)。如果它找到两个地方,恭喜你,你有一个叉子。如果没有,则不会。


答案 2