在井字游戏中确定获胜者时,如何避免重复?

When determining the winner in a Tic Tac Toe game, how do I avoid repetition?

注意:我是 Java 的初学者(2 - 3 个月的经验)。

在 JetBrains/Hyperskill 上做一个关于制作 Tic Tac Toe 游戏的项目时,我发现自己在尝试确定游戏的获胜者时重复了相当多的代码。为了将游戏表示为坐标系(因此 1,1 位于左下角,3,3 位于右上角),我使用了二维数组。 这是决定获胜者的功能:

public String determineWinner() {
    int countX = 0; // amount of X's in a row
    int countO = 0; // amount of O's in a row

    for (int y = 0; y <= 2; y++) { // for all horizontal rows
        countX = 0;
        countO = 0;
        for (int x = 0; x <= 2; x++) { // loop through all x-coordinates
            String value = this.field[x][y]; 
            if (value.equals("X")) { // if the value at that coordinate equals "X", add 1 to the count
                countX++;
            }
            if (value.equals("O")) { // same here
                countO++;
            }
        }
        if (countX == 3) { // if the count is 3 (thus 3 X's in a row), X has won
            return "X wins";
        }
        if (countO == 3) { // same here
            return "O wins";
        }
    }

    // Same thing, but for all vertical columns
    for (int x = 0; x <= 2; x++) {
        countX = 0;
        countO = 0;
        for (int y = 0; y <= 2; y++) {
            String value = this.field[x][y];
            if (value.equals("X")) {
                countX++;
            }
            if (value.equals("O")) {
                countO++;
            }
        }
        if (countX == 3) {
            return "X wins";
        }
        if (countO == 3) {
            return "O wins";
        }
    }

    // Same thing, but for diagonal
    countX = 0;
    countO = 0;
    for (int i = 0; i <= 2; i++) {
        String value = this.field[i][i];
        if (value.equals("X")) {
            countX++;
        }
        if (value.equals("O")) {
            countO++;
        }
    }
    if (countX == 3) {
        return "X wins";
    }
    if (countO == 3) {
        return "O wins";
    }

    // Same thing, but for other diagonal
    countX = 0;
    countO = 0;
    for (int i = 0; i <= 2; i++) {
        String value = this.field[i][2-i];
        if (value.equals("X")) {
            countX++;
        }
        if (value.equals("O")) {
            countO++;
        }
    }
    if (countX == 3) {
        return "X wins";
    }
    if (countO == 3) {
        return "O wins";
    }

    if (this.getNumberOfMoves() == 9) { // if the number of moves equals 9, the game is over and it is a draw
        return "draw";
    }

    return "game not finished";
}

目前,该代码允许您设置起始板(所有 O 和 X 的起始排列),然后让您进行 1 次移动。在此之后,比赛决定谁是赢家或者是平局等等

很快就会注意到,该函数太长而且有相当一部分重复,但我无法想出任何方法来缩短它。

有人有什么建议吗?或者任何适用于所有代码的指南?

免责声明:抱歉,如果我的回答在最后开始变得马虎。 另外,我在底部有一个代码,显示了我在行动中谈到的所有事情。

我想我能说的最简单的事情就是使用更多的方法,可能 classes。首先,避免所有代码重复的方法之一是使用面向对象编程来编写它们。这就是让多个 class 都与主 class 交互以协助编写代码的想法。我不会在这里谈论它,但是如果您有兴趣使您的代码整洁并且 "clean",我强烈建议您查找它。此外,还有一本关于这个主题的好书,叫做 Clean Code by Robert C. Martin。我将简单地展示如何利用方法来缩短代码并清理代码。你重复得最多的事情之一是这个

if (countX == 3) {
        return "X wins";
}
if (countO == 3) {
    return "O wins";
}

你的countX和countO每次都不一样,所以你重写了。我更简单和更有效的方法是使用一个方法。我建议您研究 Java 的语法,因为您不知道如何制作方法或 classes,但您确实使用了 determineWinner() 方法的语法,所以我假设您理解它。您可以使函数具有实质上是可以在整个函数中访问和修改的输入的参数。 (顺便说一句,您不能在 Java 中的方法内部创建方法,因此您需要将下一个方法放在 class 中其他地方的外面。)

public String checkCounts() {
    if (countX == 3) {
        return "X wins";
    }
    if (countO == 3) {
        return "O wins";
    }
    else return "N/A";
}

*您想检查它是否 returns "N/A" 每当您使用带有 if 语句的方法时。如果是这样,你应该忽略它,因为没有人赢。

whoWon = checkCounts();
//In the code I put at the bottom I will make whoWon a global variable, which is why I'm not defining it here.
//It will be already defined at the top of the code.
if (!whoWon.equals("N/A")) return whoWon;

*这个!符号表示不,a.k.a 如果 whoWon 不等于 "N/A",return whoWon.

这样,任何时候您需要写出 if 语句代码,您只需编写 checkCounts 并插入您刚刚从数组中获得的两个变量。你会写 checkCounts();在这种情况下。现在,如果你只说 return checkCounts();然后代码将 运行 所有这些 if 语句,而无需您将它们全部键入并 return 结果。你实际上也重复了很多其他事情。这两行

String value = this.field[x][y];
if (value.equals("X")) {
    countX++;
}
if (value.equals("O")) {
    countO++;
}

与这些行非常相似

String value = this.field[i][i];
if (value.equals("X")) {
    countX++;
}
if (value.equals("O")) {
    countO++;
}

和这些行

String value = this.field[i][2-i];
 if (value.equals("X")) {
     countX++;
 }
 if (value.equals("O")) {
     countO++;
 }

因此您可以将它们全部压缩为一种具有三种不同输入的方法。该方法将 return 0、1 或 2。目标是使用给定的字符串输入检查 return 是哪一个,然后将其转换为要加 1 的变量。

如果是0,忽略,如果是1,countX++,如果是2,countY++。

public int checkString(String value) {
    int whichCount = 0;
    //if whichCount is 1, it means X
    //if whichCount is 2, it means O
    if (value.equals("X")) {
        whichCount = 1;
    }
    if (value.equals("O")) {
        whichCount = 2;
    }
    return whichCount;
}

Switch 语句可能有点高级,但它们在概念上非常简单。它是一堆 if 语句,采用非常方便的语法。括号内的值是您的输入,或要检查的内容。案例说,当它等于这个时,就这样做。当您需要在 for 循环中递增 countX 或 countY 时,您可以编写

switch (checkString(this.field[coord1][coord2])) {
    case 1 -> countX++;
    case 2 -> countO++;
}

案例 1 说,如果 addToCount() returns 1 然后做箭头右边的事情,案例 2 说如果它 returns 2 做箭头右边的事情那个箭头在您的 for 循环中,coord1 和 coord2 可以是从 [x][y] 到 [i][i] 到 [i][2-i] 的任何值,因此您可以在执行 switch 语句时随时更改它。

此外,您可以将 switch 语句本身变成一个方法。

public void adjustCounts(String stringFromArray) {
    switch (checkString(stringFromArray)) {
        case 1 -> countX++;
        case 2 -> countO++;
    }
}

您还可以通过缩短 if 语句来减少几行。如果 if 语句中的内容只有一行那么你可以直接放在它旁边。

if (bool) {
   doSomething();
}
//Change that to this
if (bool) doSomething();

你经常重复的另一件事是这个

countX = 0;
countO = 0;

我只是做了一个非常简单的方法,没有参数。

public void resetCounts() {
    countX = 0;
    countO = 0;
}

重复就差不多了,但我认为您的 determineWinner 方法仍然太大。即使您不再重复任何代码,对其进行大量更改并将其分成更小的部分也可以使其更易于阅读和理解。

我添加了一堆只包含您的 for 循环的方法。他们将在我想出的这个决赛 class 的最底部。它有 85 行长,所以从技术上讲它只是 4 行的改进,但它更简洁了。此外,如果你要将它嵌入到你的实际 class 中,而不是只嵌入到一个方法中(因为你不能将它全部放在一个方法中)那么它会更有效,因为你可以访问所有 classes 全局变量。这是我想出的代码,但我强烈建议对面向对象编程进行额外研究,以真正改进您的代码。

public class TicTacToe {
    String[][] field = new String[3][3];
    int countX, countO = 0; // amount of X's and O's in a row
    String whoWon = "N/A";

    public int getNumberOfMoves() {return 0;} //Whatever you method did that determined this. Obviously it didn't really just return 0.
    public String determineWinner() {
        String columns = checkColumnsForWinner();
        String rows = checkRowsForWinner();
        String diagonal1 = checkDiagonal(1, 0);
        String diagonal2 = checkDiagonal(-1, 2);

        if (checkForNA(columns)) return columns;
        if (checkForNA(rows)) return rows;
        if (checkForNA(diagonal1)) return diagonal1;
        if (checkForNA(diagonal2)) return diagonal2;
        if (this.getNumberOfMoves() == 9) return "draw"; // if the number of moves equals 9, the game is over and it is a draw
        return "game not finished";
    }
    public String checkCounts(int countX, int countO) {
        if (countX == 3) return "X wins";
        if (countO == 3) return "O wins";
        else return "N/A";
    }
    public int checkString(String value) {
        int whichCount = 0;
        //if whichCount is 1, it means X
        //if whichCount is 2, it means O
        if (value.equals("X")) whichCount = 1;
        if (value.equals("O")) whichCount = 2;
        return whichCount;
    }
    public void adjustCounts(String stringFromArray) {
        switch (checkString(stringFromArray)) {
            case 1 -> countX++;
            case 2 -> countO++;
        }
    }
    public void resetCounts() {
        countX = 0;
        countO = 0;
    }
    public String checkRowsForWinner() {
        for (int y = 0; y <= 2; y++) { // for all horizontal rows
            resetCounts();
            for (int x = 0; x <= 2; x++) { // loop through all x-coordinates
                adjustCounts(field[x][y]);
            }
            whoWon = checkCounts(countX, countO);
            if (!whoWon.equals("N/A")) return whoWon;
        }
        return "N/A";
    }
    public String checkColumnsForWinner() {
        for (int x = 0; x <= 2; x++) {
            resetCounts();
            for (int y = 0; y <= 2; y++) {
                adjustCounts(field[x][y]);
            }
            whoWon = checkCounts(countX, countO);
            if (!whoWon.equals("N/A")) return whoWon;
        }
        return "N/A";
    }
    public String checkDiagonal(int mutiply, int add) {
        resetCounts();
        for (int i = 0; i <= 2; i++) {
            adjustCounts(field[i][i*mutiply + add]);
        }
        whoWon = checkCounts(countX, countO);
        if (!whoWon.equals("N/A")) return whoWon;
        return "N/A";
    }
    public boolean checkForNA(String string) {return !string.equals("N/A");}
}

关于面向对象编程,我能看到您在此示例中付诸实践的最佳示例是抽象。这是一个非常笼统的概念,但我认为在这种情况下它会有很大帮助。在我上面的程序中,我有一个 TicTacToe class,我的所有代码都在里面。问题是,您看到了很多样板文件来获取 运行 的代码。最大的例子是您拥有的 2D Array 对象。你必须做很多事情才能从中得到 X 或 O。制作一个新的 class 可能会更好(意见),也许叫做 Board。它将包含一个私有二维数组对象,以及从该对象获取值的 public 方法。此外,(这真的只是我的意见)我建议使用枚举而不是字符串来表示数组值。例如

public enum BoardValues {
    X,
    O,
    EMPTY
}

然后您可以创建一个 class 以将这些棋盘值放置在一个 3x3 网格中。

public class Board {
    private BoardValues[][] values = new BoardValues[3][3];

    public BoardValues getValue(int x, int y) {
        return values[x][y];
    }

    public BoardValues[] getRow(int rowNumber) {
        BoardValues[] rowValues = new BoardValues[3];
        for (int i = 0; i < values.length; i++) {
            rowValues[i] = getValue(i, rowNumber);
        }
        return rowValues;
    }

    public BoardValues[] getColumn(int columnNumber) {
        BoardValues[] columnValues = new BoardValues[3];
        for (int i = 0; i < values.length; i++) {
            columnValues[i] = getValue(columnNumber, i);
        }
        return columnValues;
    }

    public void setValues(BoardValues[][] values) {
        this.values = values;
    }

    public void setValue(int x, int y, BoardValues value) {
        values[x][y] = value;
    }
}

现在不用使用那个讨厌的旧二维数组,您只需创建一个棋盘对象并在需要时随意设置和获取它的值。另外,我没有添加对角线,但你仍然可以很容易地得到,我的只是为了概念证明。这就是抽象,可能是最容易掌握的 OOP 概念,因为它非常通用。我只是隐藏了您在编写游戏代码时不需要看到的信息。