1. 引言

本文将介绍如何用Java实现经典游戏四子棋 我们会先了解游戏规则和玩法,然后逐步实现这些规则。

2. 什么是四子棋?

在实现游戏前,必须先理解游戏规则。

四子棋规则相对简单:玩家轮流将棋子放入任意一列的顶部。每次落子后,如果某玩家的棋子在水平、垂直或对角线方向连成四子,则该玩家获胜:

四子棋示例

若无人获胜,则轮到下一位玩家。游戏持续进行直到某玩家获胜或棋盘填满(平局)。

⚠️ 关键规则:玩家可自由选择落子的列,但棋子必须落在该列的最上方空位,不能自由选择行。

实现这个游戏需要考虑三个核心组件:游戏棋盘、落子机制和胜负判定。 我们将逐一实现这些功能。

3. 定义游戏棋盘

开始游戏前,需要先创建游戏棋盘。 棋盘包含所有可落子的格子,并记录已放置的棋子位置。

首先定义玩家棋子的枚举类型:

public enum Piece {
    PLAYER_1,
    PLAYER_2
}

这里假设游戏只有两名玩家,这是四子棋的典型设定。

接下来创建棋盘类:

public class GameBoard {
    private final List<List<Piece>> columns;
    private final int rows;

    public GameBoard(int columns, int rows) {
        this.rows = rows;
        this.columns = new ArrayList<>();

        for (int i = 0; i < columns; ++i) {
            this.columns.add(new ArrayList<>());
        }
    }

    public int getRows() {
        return rows;
    }

    public int getColumns() {
        return columns.size();
    }
}

这里使用嵌套的List表示棋盘:每个内层List代表一列,列表中的元素表示该列中的棋子。

棋子必须从底部向上堆叠,因此无需处理中间空隙——所有空隙都在已放置棋子的上方。 实际存储时,我们按棋子加入列的顺序保存。

添加辅助方法获取指定位置的棋子:

public Piece getCell(int x, int y) {
    assert(x >= 0 && x < getColumns());
    assert(y >= 0 && y < getRows());

    List<Piece> column = columns.get(x);

    if (column.size() > y) {
        return column.get(y);
    } else {
        return null;
    }
}

参数说明:

  • x:列索引(从0开始)
  • y:行索引(从底部开始为0) 返回该位置的棋子,若为空则返回null

4. 实现落子功能

有了棋盘后,需要实现落子功能。 玩家通过将棋子放入指定列的顶部来落子:

public void move(int x, Piece player) {
    assert(x >= 0 && x < getColumns());

    List<Piece> column = columns.get(x);

    if (column.size() >= this.rows) {
        throw new IllegalArgumentException("该列已满");
    }

    column.add(player);
}

✅ 关键点:

  1. 检查列索引是否有效
  2. 检查该列是否已满(棋子数超过行数)
  3. 将新棋子添加到列的末尾(即顶部)

5. 胜负判定逻辑

玩家落子后,需要检查是否获胜。 获胜条件是棋子在水平、垂直或对角线方向连成四子。

但我们可以优化检查逻辑,利用游戏规则的特点:

  1. 只有刚落子的玩家可能获胜(游戏在有人获胜时立即结束)
  2. 获胜线必须包含刚落子的棋子(无需检查整个棋盘)
  3. 利用列堆叠特性排除不可能情况(例如垂直线要求棋子至少在第4行)

最终只需检查以下13条可能的线:

  • 1条垂直线:从当前棋子向下延伸3格
  • 4条水平线:分别从当前棋子左侧3格到右侧3格(滑动检查)
  • 4条主对角线线:从当前棋子左上3格到右下3格
  • 4条副对角线线:从当前棋子左下3格到右上3格

实际检查时,部分线可能超出棋盘边界(无效线)

棋盘边界示例

如图所示,部分线会超出棋盘范围,这些线不可能形成获胜条件。

5.1 检查单条线

首先实现检查单条线的方法:给定起点和方向,检查该线上四个格子是否都属于当前玩家:

private boolean checkLine(int x1, int y1, int xDiff, int yDiff, Piece player) {
    for (int i = 0; i < 4; ++i) {
        int x = x1 + (xDiff * i);
        int y = y1 + (yDiff * i);

        if (x < 0 || x > columns.size() - 1) {
            return false;
        }

        if (y < 0 || y > rows - 1) {
            return false;
        }

        if (player != getCell(x, y)) {
            return false;
        }
    }

    return true;
}

✅ 优化点:

  • 循环中同时检查边界和棋子归属
  • 发现任何不匹配立即返回false
  • 仅检查4个格子,无需预计算边界

5.2 检查所有可能线

接下来检查所有可能的获胜线:只要有一条线满足条件即判定获胜:

private boolean checkWin(int x, int y, Piece player) {
    // 垂直线
    if (checkLine(x, y, 0, -1, player)) {
        return true;
    }

    for (int offset = 0; offset < 4; ++offset) {
        // 水平线
        if (checkLine(x - 3 + offset, y, 1, 0, player)) {
            return true;
        }

        // 主对角线(左上到右下)
        if (checkLine(x - 3 + offset, y + 3 - offset, 1, -1, player)) {
            return true;
        }

        // 副对角线(左下到右上)
        if (checkLine(x - 3 + offset, y - 3 + offset, 1, 1, player)) {
            return true;
        }
    }

    return false;
}

🔍 算法解析:

  • 垂直线:从当前棋子向下检查
  • 水平线:滑动检查4条可能的线(从左3格到右3格)
  • 对角线:类似水平线,但需同时调整行和列

最后更新move()方法,返回是否获胜:

public boolean move(int x, Piece player) {
    // 前面的代码保持不变

    return checkWin(x, column.size() - 1, player);
}

5.3 游戏演示

至此已实现完整游戏逻辑。创建棋盘并轮流落子,直到有人获胜:

GameBoard gameBoard = new GameBoard(8, 6);

assertFalse(gameBoard.move(3, Piece.PLAYER_1));
assertFalse(gameBoard.move(2, Piece.PLAYER_2));

assertFalse(gameBoard.move(4, Piece.PLAYER_1));
assertFalse(gameBoard.move(3, Piece.PLAYER_2));

assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(6, Piece.PLAYER_2));

assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(4, Piece.PLAYER_2));

assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(5, Piece.PLAYER_2));

assertFalse(gameBoard.move(6, Piece.PLAYER_1));
assertTrue(gameBoard.move(4, Piece.PLAYER_2));

这段落子序列与文章开头的示例一致,最后一步落子后PLAYER_2获胜。

6. 总结

本文介绍了四子棋的游戏规则,并展示了如何用Java实现核心逻辑。 不妨尝试自己动手实现,并扩展成完整的游戏应用。

本文的完整代码可在GitHub获取。