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



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

在JetBrains/Hyperskill上做一个关于制作井字游戏的项目时,我发现自己在试图确定游戏的获胜者时重复了相当多的代码。为了将游戏表示为坐标系(因此 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 个移动。在此之后,游戏决定谁是赢家,或者是否是平局等。

正如人们很快注意到的那样,该功能太长了,而且有相当一部分重复,但我无法想出任何缩短它的方法。

有人有什么提示吗?或者任何适用于所有代码的准则?

免责声明:对不起,如果我的答案在最后开始变得草率。 另外,我在底部有一个代码,显示了我在行动中谈论的所有事情。

我认为我能说的最简单的事情是使用更多的方法和可能的类。首先,避免所有代码重复的方法之一是使用面向对象编程编写它们。这是拥有多个类的想法,这些类都与主类交互以帮助编写代码。我不会在这里讨论这个问题,但如果你有兴趣让你的代码整洁和"干净",我强烈建议你查找一下。此外,还有一本关于这个主题的好书,叫做罗伯特·C·马丁的《清洁代码》。我将简单地展示如何利用方法来缩短代码并清理它。你重复最多的一件事是这个

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

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

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

*您想检查它是否在将该方法与 if 语句一起使用时返回"N/A"。如果是这样,你应该忽略它,因为没有人赢。

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;

*! 符号表示 not,即如果 whoWon 不等于"N/A",则返回 whoWon。

这样,无论何时需要写出 if 语句代码,都可以编写 checkCount 并插入刚刚从 Array 获得的两个变量。你会写 checkCounts();在这种情况下。现在,如果您只是说返回检查计数();然后,代码将运行所有这些 if 语句,而无需键入所有语句并返回结果。你实际上也重复了很多其他事情。这几行

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++;
}

因此,您可以将它们全部压缩为具有三个不同输入的方法。该方法将返回 0、1 或 2。目标是检查它使用给定的字符串输入返回哪一个,然后将其转换为要添加 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() 返回 1,则在箭头右侧执行操作,案例 2 表示如果它返回该箭头右侧的事物 2。在你的 for 循环中,coord1 和 coord2 可以是从 [x][y] 到 [i][i] 到 [i][2-i] 的任何值,因此您可以随时更改它。

此外,还可以将该 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 循环的方法。他们将在我想出的最后一堂课的最底层。它有 85 行长,所以从技术上讲,它只有 4 行的改进,但它要干净得多。此外,如果您要将其嵌入到实际类中,而不仅仅是嵌入到单个方法中(因为不能将其全部放在一个方法中),那么它将更有效,因为您可以访问所有类全局变量。这是我想出的代码,但我强烈建议对面向对象编程进行额外的研究,以真正改进您的代码。

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类,里面有我所有的代码。问题是,您看到很多样板文件来运行代码。最大的示例是您拥有的 2D 数组对象。你必须做很多事情才能摆脱X或O。创建一个新类会更好(意见),也许称为董事会。它将包含一个私有的 2D 数组对象,以及从该对象获取值的公共方法。此外,(这实际上只是我的意见)我建议使用枚举而不是字符串来获取数组值。例如

public enum BoardValues {
X,
O,
EMPTY
}

然后,您可以创建一个类,将这些板值放置在本质上的 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;
}
}

现在,您无需使用讨厌的旧 2D 数组,您只需创建一个板对象并在需要时随意设置和获取其值。另外,我没有添加对角线,但您仍然可以轻松,我的只是为了概念验证。这就是抽象,可能是最容易掌握的OOP概念,因为它太通用了。我只是在掩盖你在尝试编写游戏代码时不需要看到的信息。

最新更新