Java版Tic-Tac-Toe井字棋AI落子逻辑实现与代码优化
井字棋AI功能实现与代码修正指南
原有代码问题梳理
- 方法定义错误:
switchVal()方法声明返回值为boolean,但方法内无任何返回语句,直接编译失败,需要将返回值类型改为void。 - 全局变量滥用:
row、col、symbol、numPlayers这类仅在单次操作中使用的变量全部定义为类成员变量,多方法调用时很容易出现值被意外篡改的问题,这类变量应当作为局部变量定义在对应方法内,需要跨方法传递时通过参数传入即可。 - 重复创建对象冗余:
play()、singlePlayer()、twoPlayer()方法中都重复新建了TicTacToe实例,导致最初初始化的游戏实例完全没有被使用,额外占用内存还容易出现状态不同步的问题,直接操作当前实例即可。 - 缺少输入校验:用户输入行列值时,没有校验输入范围是否在1-3之间,也没有校验选中位置是否为空,用户输入非法值时会触发数组越界异常,或者覆盖已有棋子。
- 单人模式胜负判断缺失:现有单人模式逻辑只判断了玩家获胜和平局两种情况,电脑获胜的场景完全没有处理,电脑赢了也会提示玩家获胜,逻辑错误。
- 获胜判断逻辑硬编码:8条获胜线的判断全部重复硬编码,可读性差,后续修改成本高,可以抽成通用方法,传入待判断的棋子符号即可返回是否获胜。
AI落子逻辑实现方案
按照要求的优先级实现AI决策,核心思路是先写一个通用的制胜点查找辅助方法:遍历棋盘所有空位,模拟在空位放入指定棋子,检查放入后是否满足获胜条件,如果满足就返回该位置坐标,找不到则返回空。
AI落子的三层判断顺序:
- 优先查找AI自身(持'o'棋子)一步获胜的空位,找到则直接落子取胜
- 不存在制胜点时,查找玩家(持'x'棋子)一步获胜的空位,找到则落子封堵
- 两种情况都不满足时,随机选择空位落子
修正后的完整代码
Driver 主类文件
public class DriverTicTacToe { public static void main(String[] args) { TicTacToe game = new TicTacToe(); game.play(); } }
TicTacToe 逻辑类文件
import java.util.Scanner; public class TicTacToe { // 棋盘、输入Scanner设为成员变量,其他流程变量全部改为局部变量 private final char[][] board = new char[3][3]; private final Scanner kb = new Scanner(System.in); private static final char EMPTY = '-'; private static final char PLAYER_SYMBOL = 'x'; private static final char AI_SYMBOL = 'o'; // 构造方法初始化空棋盘 public TicTacToe() { for(int i = 0; i <3; i++) { for(int j = 0; j < 3; j++) board[i][j] = EMPTY; } } // 获取玩家数量,增加输入校验 private int getNumPlayers() { int input; do { System.out.println("请选择游玩人数(输入1或2):"); while (!kb.hasNextInt()) { System.out.println("输入非法,请输入数字1或2:"); kb.next(); } input = kb.nextInt(); } while (input !=1 && input !=2); return input; } // 打印棋盘 private void printBoard() { System.out.println("当前棋盘状态:"); for(int i = 0; i < 3; i++) { for(int j = 0; j<3; j++) System.out.print(board[i][j] + " "); System.out.println(); } } // 判断棋盘是否还有空位 private boolean boardNotFilled() { for(int i = 0; i < 3; i++) { for(int j = 0; j < 3; j++) { if(board[i][j] == EMPTY) return true; } } return false; } // 通用获胜判断:传入棋子符号,判断该符号持有者是否获胜 private boolean checkWin(char symbol) { // 三行判断 for (int i = 0; i < 3; i++) { if (board[i][0] == symbol && board[i][1] == symbol && board[i][2] == symbol) return true; } // 三列判断 for (int j = 0; j < 3; j++) { if (board[0][j] == symbol && board[1][j] == symbol && board[2][j] == symbol) return true; } // 两条对角线判断 if (board[0][0] == symbol && board[1][1] == symbol && board[2][2] == symbol) return true; if (board[0][2] == symbol && board[1][1] == symbol && board[2][0] == symbol) return true; return false; } // 查找指定棋子的一步制胜位置,找不到返回null private int[] findWinningSpot(char symbol) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == EMPTY) { // 模拟落子 board[i][j] = symbol; boolean win = checkWin(symbol); // 还原棋盘 board[i][j] = EMPTY; if (win) { return new int[]{i, j}; } } } } return null; } // 获取用户合法落子位置 private int[] getPlayerMove() { int row, col; while (true) { System.out.println("请输入落子行号(1-3):"); while (!kb.hasNextInt()) { System.out.println("输入非法,请输入1-3之间的数字:"); kb.next(); } row = kb.nextInt() - 1; System.out.println("请输入落子列号(1-3):"); while (!kb.hasNextInt()) { System.out.println("输入非法,请输入1-3之间的数字:"); kb.next(); } col = kb.nextInt() - 1; // 校验范围和位置是否为空 if (row >=0 && row <3 && col >=0 && col <3 && board[row][col] == EMPTY) { return new int[]{row, col}; } System.out.println("输入位置非法或已被占用,请重新输入!"); } } // AI落子逻辑,按优先级决策 private void compMove() { int[] move; // 第一步:找自己的制胜点 move = findWinningSpot(AI_SYMBOL); if (move != null) { board[move[0]][move[1]] = AI_SYMBOL; return; } // 第二步:找玩家的制胜点封堵 move = findWinningSpot(PLAYER_SYMBOL); if (move != null) { board[move[0]][move[1]] = AI_SYMBOL; return; } // 第三步:随机落子 int row, col; do { row = (int)(Math.random()*3); col = (int)(Math.random()*3); } while (board[row][col] != EMPTY); board[row][col] = AI_SYMBOL; } // 单人模式逻辑 private void singlePlayer() { boolean isPlayerTurn = true; while (true) { printBoard(); if (isPlayerTurn) { System.out.println("轮到你落子:"); int[] playerMove = getPlayerMove(); board[playerMove[0]][playerMove[1]] = PLAYER_SYMBOL; // 判断玩家是否获胜 if (checkWin(PLAYER_SYMBOL)) { printBoard(); System.out.println("恭喜你获胜了!"); return; } } else { System.out.println("电脑落子中..."); compMove(); // 判断电脑是否获胜 if (checkWin(AI_SYMBOL)) { printBoard(); System.out.println("电脑获胜,再接再厉!"); return; } System.out.println("电脑落子完成,轮到你了"); } // 判断平局 if (!boardNotFilled()) { printBoard(); System.out.println("平局!"); return; } isPlayerTurn = !isPlayerTurn; } } // 双人模式逻辑 private void twoPlayer() { boolean isP1Turn = true; while (true) { printBoard(); char currentSymbol = isP1Turn ? PLAYER_SYMBOL : AI_SYMBOL; System.out.printf("轮到%s落子:%n", isP1Turn ? "玩家1(x)" : "玩家2(o)"); int[] move = getPlayerMove(); board[move[0]][move[1]] = currentSymbol; // 判断当前玩家是否获胜 if (checkWin(currentSymbol)) { printBoard(); System.out.printf("恭喜%s获胜!%n", isP1Turn ? "玩家1" : "玩家2"); return; } // 判断平局 if (!boardNotFilled()) { printBoard(); System.out.println("平局!"); return; } isP1Turn = !isP1Turn; } } // 游戏启动入口 public void play() { int numPlayers = getNumPlayers(); if(numPlayers == 1) singlePlayer(); else twoPlayer(); kb.close(); } }
额外编码建议
- 类的成员变量如果不需要对外暴露,尽量加
private修饰符,符合封装原则 - 魔法值(比如'-'、'x'、'o'、3这类重复出现的固定值)可以定义为常量,后续修改的时候只需要改一处
- 方法功能尽量单一,一个方法只做一件事,不要把输入、落子、判断胜负全耦合在一个方法里,后续维护和改bug会方便很多
- 涉及用户输入的场景一定要做合法性校验,避免非法输入导致程序崩溃
内容的提问来源于stack exchange,提问作者Leraner
相关产品推荐
相关产品推荐

