Java小游戏源代码示例:用控制台制作一个五子棋程序

Java小游戏源代码示例:用控制台制作一个五子棋程序

Java小游戏源代码示例:用控制台制作一个五子棋程序

五子棋的规则简单,但很适合练习 Java 基础:棋盘可以用二维数组表示,玩家输入需要经过校验,落子后要判断是否获胜或平局。本文从一个可直接运行的控制台版本开始,说明代码结构、规则边界和常见问题。示例不依赖第三方库,适用于本地练习、课堂演示以及命令行环境。

这个示例适合做什么

程序实现双人轮流落子的基本流程:棋盘为 15×15,双方在同一台电脑上输入坐标;程序拒绝格式错误、越界和重复占用的落子;每次有效落子后检查胜负;棋盘填满且无人获胜时判定平局。玩家可以输入 q 提前结束。

这里采用的是简化的自由规则:任意一方在横、竖或斜线上形成连续五枚或更多棋子,即获胜。程序不实现禁手、三手交换、计时、悔棋或联网对战。如果目标是复现某个比赛规则,需要先明确规则细节,再相应扩展判断逻辑。

准备环境并运行

示例使用 Java 8 及以上版本支持的语法。将下面的代码保存为 Gomoku.java,在该文件所在目录打开终端,执行:

javac Gomoku.java
java Gomoku

若系统安装了多个 Java 版本,可以用 java -version 和 javac -version 检查当前命令对应的版本。部分环境需要设置好 JDK 的 PATH,否则可能出现找不到命令的提示。

完整可运行代码

import java.util.Scanner;

public class Gomoku {
    private static final int SIZE = 15;
    private static final char EMPTY = '.';

    private final char[][] board = new char[SIZE][SIZE];

    public static void main(String[] args) {
        new Gomoku().play();
    }

    private void play() {
        Scanner scanner = new Scanner(System.in);
        char current = 'X';

        while (true) {
            printBoard();
            System.out.println("轮到 " + current + "。请输入行和列,例如:8 8;输入 q 退出。");
            System.out.print("> ");

            if (!scanner.hasNextLine()) {
                System.out.println("输入结束,游戏退出。");
                return;
            }

            String line = scanner.nextLine().trim();

            if (line.equalsIgnoreCase("q")) {
                System.out.println("游戏已退出。");
                return;
            }

            String[] parts = line.split("\\s+");
            if (parts.length != 2) {
                System.out.println("输入格式错误,请输入两个整数,例如:8 8。");
                continue;
            }

            int row;
            int col;
            try {
                row = Integer.parseInt(parts[0]) - 1;
                col = Integer.parseInt(parts[1]) - 1;
            } catch (NumberFormatException e) {
                System.out.println("行和列必须是整数,请重新输入。");
                continue;
            }

            if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) {
                System.out.println("坐标超出范围,请输入 1 到 " + SIZE + " 之间的行和列。");
                continue;
            }

            if (board[row][col] != EMPTY) {
                System.out.println("这个位置已经有棋子,请选择空位。");
                continue;
            }

            board[row][col] = current;

            if (hasWon(row, col, current)) {
                printBoard();
                System.out.println(current + " 获胜!");
                return;
            }

            if (isBoardFull()) {
                printBoard();
                System.out.println("棋盘已满,平局。");
                return;
            }

            current = (current == 'X') ? 'O' : 'X';
        }
    }

    private boolean hasWon(int row, int col, char player) {
        int[][] directions = {
            {0, 1},   // 横向
            {1, 0},   // 纵向
            {1, 1},   // 右下斜线
            {1, -1}   // 左下斜线
        };

        for (int[] direction : directions) {
            int count = 1;
            count += countInDirection(
                row, col, direction[0], direction[1], player
            );
            count += countInDirection(
                row, col, -direction[0], -direction[1], player
            );

            if (count >= 5) {
                return true;
            }
        }

        return false;
    }

    private int countInDirection(
        int row, int col, int rowStep, int colStep, char player
    ) {
        int count = 0;
        int r = row + rowStep;
        int c = col + colStep;

        while (r >= 0 && r < SIZE
                && c >= 0 && c < SIZE
                && board[r][c] == player) {
            count++;
            r += rowStep;
            c += colStep;
        }

        return count;
    }

    private boolean isBoardFull() {
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                if (board[row][col] == EMPTY) {
                    return false;
                }
            }
        }
        return true;
    }

    private void printBoard() {
        System.out.print("    ");
        for (int col = 1; col <= SIZE; col++) {
            System.out.printf("%2d ", col);
        }
        System.out.println();

        for (int row = 0; row < SIZE; row++) {
            System.out.printf("%2d  ", row + 1);
            for (int col = 0; col < SIZE; col++) {
                System.out.print(" " + board[row][col] + " ");
            }
            System.out.println();
        }
    }
}

代码的主要部分

用二维数组保存棋盘

board 是一个 char[15][15] 二维数组。数组下标从 0 开始,而棋盘给玩家显示的行列编号从 1 开始,因此读取输入后要分别减去 1,再用转换后的坐标访问数组。空位使用 . 表示,玩家棋子使用 X 和 O 表示。

在 Java 中,新建的 char 数组元素默认为空字符 '\u0000',并不会自动等于 '.'。本例没有额外初始化棋盘,因此若直接拿数组内容和 EMPTY 比较,会发现所有位置都不是空位。要避免这个问题,可以显式填充数组,或者在整个程序中统一以默认值代表空位。本例通过构造数组后补充初始化更直观;代码需要在创建实例时把每个格子设为点号。可将字段初始化部分替换为以下构造器:

private Gomoku() {
    for (int row = 0; row < SIZE; row++) {
        for (int col = 0; col < SIZE; col++) {
            board[row][col] = EMPTY;
        }
    }
}

将这个构造器添加到类中,例如放在 main 方法之前。由于 main 通过 new Gomoku() 创建对象,初始化会在游戏开始前完成。完整程序必须包含这一段初始化代码,下面也给出了整理后的完整版本,运行时请使用该版本。

在 Java 中,若字段声明为 new char[SIZE][SIZE],元素初值确实是空字符,不是点号。为了避免复制时遗漏初始化,后续应使用下面这个完整版本替换上一段示例代码。

完整版本:包含棋盘初始化

import java.util.Scanner;

public class Gomoku {
    private static final int SIZE = 15;
    private static final char EMPTY = '.';

    private final char[][] board = new char[SIZE][SIZE];

    private Gomoku() {
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                board[row][col] = EMPTY;
            }
        }
    }

    public static void main(String[] args) {
        new Gomoku().play();
    }

    private void play() {
        Scanner scanner = new Scanner(System.in);
        char current = 'X';

        while (true) {
            printBoard();
            System.out.println("轮到 " + current + "。请输入行和列,例如:8 8;输入 q 退出。");
            System.out.print("> ");

            if (!scanner.hasNextLine()) {
                System.out.println("输入结束,游戏退出。");
                return;
            }

            String line = scanner.nextLine().trim();
            if (line.equalsIgnoreCase("q")) {
                System.out.println("游戏已退出。");
                return;
            }

            String[] parts = line.split("\\s+");
            if (parts.length != 2) {
                System.out.println("输入格式错误,请输入两个整数,例如:8 8。");
                continue;
            }

            int row;
            int col;
            try {
                row = Integer.parseInt(parts[0]) - 1;
                col = Integer.parseInt(parts[1]) - 1;
            } catch (NumberFormatException e) {
                System.out.println("行和列必须是整数,请重新输入。");
                continue;
            }

            if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) {
                System.out.println("坐标超出范围,请输入 1 到 " + SIZE + " 之间的行和列。");
                continue;
            }

            if (board[row][col] != EMPTY) {
                System.out.println("这个位置已经有棋子,请选择空位。");
                continue;
            }

            board[row][col] = current;

            if (hasWon(row, col, current)) {
                printBoard();
                System.out.println(current + " 获胜!");
                return;
            }

            if (isBoardFull()) {
                printBoard();
                System.out.println("棋盘已满,平局。");
                return;
            }

            current = (current == 'X') ? 'O' : 'X';
        }
    }

    private boolean hasWon(int row, int col, char player) {
        int[][] directions = {
            {0, 1}, {1, 0}, {1, 1}, {1, -1}
        };

        for (int[] direction : directions) {
            int count = 1;
            count += countInDirection(
                row, col, direction[0], direction[1], player
            );
            count += countInDirection(
                row, col, -direction[0], -direction[1], player
            );

            if (count >= 5) {
                return true;
            }
        }
        return false;
    }

    private int countInDirection(
        int row, int col, int rowStep, int colStep, char player
    ) {
        int count = 0;
        int r = row + rowStep;
        int c = col + colStep;

        while (r >= 0 && r < SIZE
                && c >= 0 && c < SIZE
                && board[r][c] == player) {
            count++;
            r += rowStep;
            c += colStep;
        }
        return count;
    }

    private boolean isBoardFull() {
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                if (board[row][col] == EMPTY) {
                    return false;
                }
            }
        }
        return true;
    }

    private void printBoard() {
        System.out.print("    ");
        for (int col = 1; col <= SIZE; col++) {
            System.out.printf("%2d ", col);
        }
        System.out.println();

        for (int row = 0; row < SIZE; row++) {
            System.out.printf("%2d  ", row + 1);
            for (int col = 0; col < SIZE; col++) {
                System.out.print(" " + board[row][col] + " ");
            }
            System.out.println();
        }
    }
}

上一段“完整可运行代码”是为了先展示整体结构,但它遗漏了数组初始化,不能单独正确运行。实际编译时请使用这里包含构造器的完整版本。棋盘初始化是容易忽略的细节:数组已分配内存,不代表元素自动采用程序自定义的空位标记。

解析输入并拒绝无效落子

程序使用 nextLine() 读取整行文本,再用空白字符拆分。这样可以接受 8 8、8  8 等形式,也能先检查参数数量,再尝试把两个参数转换为整数。若输入 hello、只输入一个数字或输入超出 int 范围的数字,转换会抛出 NumberFormatException,程序捕获异常后提示用户重试,而不会直接崩溃。

坐标转换后,程序先检查行列是否都在 0 到 14 之间,再访问数组。这一步顺序很重要:如果先访问 board[row][col],负数或过大的下标会导致 ArrayIndexOutOfBoundsException。只有坐标合法且位置为空时,程序才写入棋子。输入错误时使用 continue 回到本轮开头,不会切换当前玩家。

从最后一枚棋子检查胜负

每次落子后,只需要检查经过新棋子的四条线:横向、纵向、左上到右下,以及右上到左下。若逐一扫描整个棋盘也能判断胜负,但本例采用从新棋子出发的方式,代码更直接,而且每次检查的工作量很小。

countInDirection 从落子位置的相邻格开始,沿指定方向统计连续的同色棋子。hasWon 对每条线分别统计正反两个方向,再加上刚放下的这枚棋子。例如横向左边连续 2 枚、右边连续 2 枚,加上当前棋子共 5 枚,就判定获胜。

边界判断写在访问数组之前。方向越过棋盘边缘时,循环结束,不会读取无效位置。这个判断既适用于棋盘中央,也适用于四条边和四个角。

规则边界:五枚还是五枚以上

示例使用 count >= 5,表示连续五枚及以上都算胜利,即“长连也获胜”的自由规则。某些规则或线上平台可能对特定一方的长连另有处理;若要严格遵循相关规则,不能只把条件改成 count == 5 就认为规则已经完整,因为禁手、先后手等规则也可能影响判定。

同样,本程序把棋盘填满且没有赢家视为平局。胜负判断先于平局判断,因此最后一格落子若构成胜利,会先宣布获胜。这种顺序符合程序所采用的基本流程。

常见错误与排查方法

  • 忘记初始化空格。 Java 的 char 数组默认填充空字符,而不是 '.'。如果没有构造器中的初始化循环,程序会把所有格子判断为已占用。
  • 混淆输入坐标和数组下标。 展示给玩家的是从 1 开始的行列号,数组下标从 0 开始。输入后减 1,显示时加 1,两个方向都要处理。
  • 越界检查放在数组访问之后。 必须先验证坐标范围,再读取或写入棋盘数组。
  • 无效输入后错误地切换玩家。 只有成功放置棋子后才切换回合。格式错误、越界或占用位置都不算有效落子。
  • 只检查一个方向。 五枚棋子可能分布在落点两侧,不能只向一个方向计数。也不能遗漏两条斜线。
  • 漏算当前落子。 方向统计从相邻格开始,所以需要初始化 count 为 1,把刚放下的棋子计入总数。
  • 把“连续五枚”误写成“任意五枚”。 判断必须沿同一条直线连续统计,不能只统计棋盘上某种棋子的总数。
  • 文件名与公开类名不一致。 文件应命名为 Gomoku.java。Java 要求含 public 类的源文件名与类名对应。

可用几组输入手动测试:输入 8 8 后再输入 8 8,应提示位置已被占用且仍轮到同一方;输入 0 1 或 16 1,应提示越界;连续在同一行下满五枚同色棋子(期间让另一方在别处落子),应宣布获胜;在棋盘边缘完成五连,也应能正确识别。

参数限制与安全说明

棋盘大小在代码中固定为 15,用户输入的行列范围因此固定为 1 到 15。程序接受两个可解析为 Java int 的整数作为坐标;超过整数表示范围的文本会被捕获并拒绝。输入读取是一行一行进行的,适合交互式控制台使用,并未设计为处理批量文件、超长输入或高并发请求的服务程序。

这是本地控制台小游戏,不会联网、写文件或执行用户输入的内容。输入只会被解析为整数坐标,无法直接变成 Java 代码或系统命令。不过,这不等于适用于所有安全场景:如果将程序改造成网页、网络服务或支持文件导入的工具,需要另外考虑身份验证、请求限制、资源消耗、日志与数据处理等问题。本文的输入校验只解决本地棋盘坐标和格式问题,不提供通用的安全保障。

后续可以怎样扩展

在保持基本程序可运行的前提下,可以依次增加悔棋记录、重新开始、胜局统计、电脑随机落子,或者将棋盘大小做成启动参数。增加悔棋时,至少要保存每一步的行、列和玩家;加入电脑对手时,则需要明确策略与计算限制。若改成图形界面,可以继续复用棋盘状态和胜负判断,把输入输出部分替换为界面事件处理。

如果把棋盘尺寸改为用户可配置,除了修改数组大小,还要同步调整坐标提示、边界检查、棋盘显示以及胜利规则的适用范围。将棋盘大小存入实例字段通常比继续使用固定常量更方便;还应限制可选尺寸,避免接受不合理的大值造成不必要的内存占用或输出量。

通过这个例子可以练习二维数组、循环、条件判断、异常处理、方法拆分和终端输入。实现时最值得反复检查的不是棋盘画得是否整齐,而是状态是否一致:只有合法落子才改变棋盘,只有有效落子才轮换玩家,并且胜负判断要覆盖四个方向和棋盘边缘。

© 版权声明

相关文章