简单五子棋对战(AI生成)

简单五子棋对战(AI生成)


一、目标

通过AI,生成五子棋游戏的代码,并理解代码含义。

二、包结构


gomoku/
├── Main.java                       // 程序入口
├── model/                          // 数据模型层
│   ├── Chessboard.java             // 棋盘数据模型
│   ├── Player.java                 // 玩家抽象类
│   ├── HumanPlayer.java            // 人类玩家
│   ├── AIPlayer.java               // AI玩家
│   ├── GameRecord.java             // 储存游戏数据
│   └── RankList.java               // 排行榜对话框
├── view/                   .       // 视图层
│   ├── MainFrame.java              // 主窗口
│   ├── ChessboardPanel.java        // 棋盘绘制面板
│   ├── MenuPanel.java              // 菜单面板
│   ├── TimerPanel.java             // 计时器面板
│   ├── BoardPreview.java           // 预览棋盘对话框
│   ├── TowPlayerInputDialog.java   // 人人对战玩家输入对话框
│   ├── UsernameDialog.java         // 人机对战玩家输入对话框
│   └── RankDialog.java             // 排行榜对话框
└── util/                           // 工具类
    └── GameUtils.java              // 游戏工具类

三、代码设计与讲解

3.1 Main.java

点击查看代码
package gomoku;

import gomoku.view.MainFrame;
import javax.swing.SwingUtilities;

public class Main {
    public static void main(String[] args) {
        // 在事件调度线程中启动GUI
        SwingUtilities.invokeLater(() -> {
            MainFrame mainFrame = new MainFrame("五子棋");
            mainFrame.setVisible(true); // 必须调用此方法,窗口才能显示
        });
    }
}
  • 使用多线程来确保程序的同时运行性

  • 使用SwingUtilities.invokeLater()来确保每一步操作在正确的线程上安全地执行

3.2 Chessboard.java

点击查看代码
package gomoku.model;

public class Chessboard {
    // 棋子状态常量
    public static final int EMPTY = 0;
    public static final int BLACK = 1;
    public static final int WHITE = 2;
    
    private int[][] board;       // 棋盘数组
    private int size;            // 棋盘大小(如15x15)
    private int currentPlayer;   // 当前落子玩家(默认黑棋先行)
    private int lastRow = -1;    // 最后落子的行
    private int lastCol = -1;    // 最后落子的列
    private boolean isGameOver;  // 游戏是否结束
    private int winner;          // 获胜者(EMPTY表示未分胜负)

    // 构造方法:初始化指定大小的棋盘
    public Chessboard(int size) {
        this.size = size;
        this.board = new int[size][size];
        this.currentPlayer = BLACK; // 黑棋先行
        this.isGameOver = false;
        this.winner = EMPTY;
    }

    // 落子方法:在指定位置放置当前玩家的棋子(成功返回true,失败返回false)
    public boolean placePiece(int row, int col) {
        // 校验条件:游戏未结束 + 位置合法 + 该位置为空
        if (isGameOver || row < 0 || row >= size || col < 0 || col >= size || board[row][col] != EMPTY) {
            return false; // 落子失败
        }
        
        // 放置棋子
        board[row][col] = currentPlayer;
        this.lastRow = row;
        this.lastCol = col;
        
        // 检查是否获胜
        if (checkWin(row, col)) {
            isGameOver = true;
            winner = currentPlayer;
        }
        
        return true; // 落子成功
    }

    // 检查指定位置落子后是否获胜(五子连珠判定)
    public boolean checkWin(int row, int col) {
        int player = board[row][col];
        if (player == EMPTY) return false;

        // 方向数组:上、下、左、右、左上、右下、右上、左下(共4组对向)
        int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1}};
        
        for (int i = 0; i < 4; i++) {
            int count = 1; // 当前位置已占1个棋子
            int[] dir1 = dirs[2*i];   // 第一个方向(如向上)
            int[] dir2 = dirs[2*i +1];// 对向方向(如向下)
            
            // 检查第一个方向
            int r = row + dir1[0];
            int c = col + dir1[1];
            while (r >= 0 && r < size && c >= 0 && c < size && board[r][c] == player) {
                count++;
                r += dir1[0];
                c += dir1[1];
            }
            
            // 检查对向方向
            r = row + dir2[0];
            c = col + dir2[1];
            while (r >= 0 && r < size && c >= 0 && c < size && board[r][c] == player) {
                count++;
                r += dir2[0];
                c += dir2[1];
            }
            
            // 连续5个及以上则获胜
            if (count >= 5) return true;
        }
        return false;
    }

    // 切换当前玩家(黑→白,白→黑)- 仅在游戏未结束时有效
    public void switchPlayer() {
        if (!isGameOver) {
            currentPlayer = (currentPlayer == BLACK) ? WHITE : BLACK;
        }
    }

    // 重置棋盘(清空所有棋子,恢复初始状态)
    public void reset() {
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                board[i][j] = EMPTY;
            }
        }
        currentPlayer = BLACK;
        lastRow = -1;
        lastCol = -1;
        isGameOver = false;
        winner = EMPTY;
    }

    // ------------------- Getter和Setter方法 -------------------
    public int getLastRow() {
        return lastRow;
    }

    public int getLastCol() {
        return lastCol;
    }

    public void setLastMove(int row, int col) {
        this.lastRow = row;
        this.lastCol = col;
    }

    public int[][] getBoard() {
        return board;
    }

    public int getSize() {
        return size;
    }

    public int getCurrentPlayer() {
        return currentPlayer;
    }

    public int getPiece(int row, int col) {
        if (row >= 0 && row < size && col >= 0 && col < size) {
            return board[row][col];
        }
        return -1;
    }

    public boolean isGameOver() {
        return isGameOver;
    }

    public int getWinner() {
        return winner;
    }

    // 手动设置游戏结束状态(用于特殊场景)
    public void setGameOver(boolean gameOver) {
        isGameOver = gameOver;
    }
}

  • 该代码用于更新棋盘、判断棋子位置的合法性、检测是否胜利、切换玩家、重置棋盘

  • 使用构造函数来初始化棋盘大小和棋子的样式

  • 使用多种判断方法,使代码更加简洁

3.3 Player.java

点击查看代码
package gomoku.model;

public abstract class Player {
    protected int color;
    protected Chessboard chessboard;
    
    public Player(int color, Chessboard chessboard) {
        this.color = color;
        this.chessboard = chessboard;
    }
    
    public int getColor() {
        return color;
    }
    
    // 抽象方法:下棋
    public abstract void makeMove();
    
    // 设置落子后的回调接口
    public interface MoveListener {
        void onMoveMade
        (int row, int col);
    }
}
  • 玩家抽象类,设定玩家下棋的行为

  • 设置接口MoveListener(响应事件),监听玩家下棋棋子的位置(监听成功之后,判断胜负,刷新面板,切换回合)

  • 设置落子后的回调接口时默认隐藏了public abstract修饰符

3.4 HumanPlayer.java

点击查看代码
package gomoku.model;

public class HumanPlayer extends Player {
    private MoveListener listener;
    
    public HumanPlayer(int color, Chessboard chessboard, MoveListener listener) {
        super(color, chessboard);
        this.listener = listener;
    }
    
    @Override
    public void makeMove() {
        // 人类玩家通过界面点击落子,此处空实现
    }
    
    // 处理人类玩家的点击
    public void handleClick(int row, int col) {
        if (chessboard.getPiece(row, col) == Chessboard.EMPTY && 
            chessboard.getCurrentPlayer() == color) {
            
            if (chessboard.placePiece(row, col)) {
                listener.onMoveMade(row, col);
            }
        }
    }
}
  • 声明了MoveListener listener,可以保存外部传入的回调“通信渠道”,使之持有任何 MoveListener 接口的实现类的对象(监听位置)

  • 人类对战时背后逻辑判断

  • 人类玩家的落子触发方式是 “鼠标点击”(被动触发),而 makeMove() 是抽象父类Player定义的 “主动落子方法”(用于 AI 主动找位置落子),所以是空实现;而画图会在ChessboardPanel中实现

  • handleClick()判断落子的合法性,并监听落子位置

3.5 AIPlayer.java

点击查看代码
package gomoku.model;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.SwingUtilities;

public class AIPlayer {
    private int color; // AI的棋子颜色(黑/白)
    private Chessboard chessboard;
    private MoveListener listener; // 落子后的回调接口
    private Random random; // 随机数生成器
    private int delayMillis = 800; // 延迟时间,单位:毫秒(可调整)

    // 构造方法:接收颜色、棋盘和回调
    public AIPlayer(int color, Chessboard chessboard, MoveListener listener) {
        this.color = color;
        this.chessboard = chessboard;
        this.listener = listener;
        this.random = new Random(); // 初始化随机数生成器
    }

    // 带延迟的AI随机落子逻辑
    public void makeMove() {
        // 校验:游戏未结束且当前是AI回合
        if (!chessboard.isGameOver() && chessboard.getCurrentPlayer() == color) {
            // 使用新线程实现延迟,避免阻塞UI
            new Thread(() -> {
                try {
                    // AI"思考"延迟
                    Thread.sleep(delayMillis);
                    
                    // 在UI线程中执行落子操作(Swing要求UI操作在事件调度线程中执行)
                    SwingUtilities.invokeLater(() -> {
                        // 获取所有空位置
                        List<int[]> emptyPositions = findAllEmptyPositions();
                        
                        if (!emptyPositions.isEmpty()) {
                            // 从空位置中随机选择一个
                            int randomIndex = random.nextInt(emptyPositions.size());
                            int[] randomPos = emptyPositions.get(randomIndex);
                            int row = randomPos[0];
                            int col = randomPos[1];
                            
                            // 落子并通知回调
                            if (chessboard.placePiece(row, col)) {
                                listener.onMoveMade(row, col);
                            }
                        }
                    });
                } catch (InterruptedException e) {
                    // 处理中断异常(恢复中断状态)
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }).start();
        }
    }

    // 查找棋盘上所有的空位置
    private List<int[]> findAllEmptyPositions() {
        List<int[]> emptyPositions = new ArrayList<>();
        int size = chessboard.getSize();
        
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (chessboard.getPiece(i, j) == Chessboard.EMPTY) {
                    emptyPositions.add(new int[]{i, j});
                }
            }
        }
        
        return emptyPositions;
    }

    // 设置延迟时间(允许外部调整)
    public void setDelayMillis(int millis) {
        if (millis > 0) {
            this.delayMillis = millis;
        }
    }

    // 落子回调接口
    public interface MoveListener {
        void onMoveMade(int row, int col);
    }
}
    
  • 创建新的线程对象,避免 AI 延迟落子阻塞 UI 线程

  • Thread.sleep(800) 阻塞面板800ns,营造一种AI在思考的感觉

  • 使用List来存储空为位置,并使用random,在空位置中随机生成落子,可以修改此处代码,使机器的落子更加复杂

3.6 GameRecord.java

点击查看代码
package gomoku.model;

import java.time.Duration;
import java.time.LocalDateTime;

/**
 * 游戏记录模型,存储单局游戏的信息,包括用户名和棋盘状态
 */
public class GameRecord {
    private String gameType; // "人机对战" 或 "人人对战"
    private LocalDateTime startTime; // 开始时间
    private LocalDateTime endTime; // 结束时间
    private String winner; // 获胜方 ("黑棋" 或 "白棋")
    private Duration duration; // 游戏时长
    private String username; // 玩家用户名
    private int[][] boardState; // 结束时的棋盘状态
    private int boardSize; // 棋盘大小

    public GameRecord(String gameType, int boardSize) {
        this.gameType = gameType;
        this.startTime = LocalDateTime.now();
        this.boardSize = boardSize;
    }

    // 结束游戏并计算时长,保存棋盘状态
    public void finishGame(String winner, int[][] boardState, String username) {
        this.endTime = LocalDateTime.now();
        this.winner = winner;
        this.duration = Duration.between(startTime, endTime);
        this.username = username;
        // 深拷贝棋盘状态
        this.boardState = new int[boardSize][boardSize];
        for (int i = 0; i < boardSize; i++) {
            System.arraycopy(boardState[i], 0, this.boardState[i], 0, boardSize);
        }
    }

    // 获取时长的字符串表示 (mm:ss)
    public String getDurationStr() {
        long seconds = duration.getSeconds();
        long minutes = seconds / 60;
        seconds %= 60;
        return String.format("%02d:%02d", minutes, seconds);
    }

    // getter方法
    public String getGameType() { return gameType; }
    public String getWinner() { return winner; }
    public Duration getDuration() { return duration; }
    public LocalDateTime getStartTime() { return startTime; }
    public String getUsername() { return username; }
    public int[][] getBoardState() { return boardState; }
    public int getBoardSize() { return boardSize; }
}

  • 结束游戏时,创建独立的棋盘副本,记录落子位置、时间和胜者

  • 对原数组的每一行,使用System.arraycopy来把原数组每一行元素直接复制到新数组的对应行

3.7 RankList.java

点击查看代码
package gomoku.model;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 排行榜数据管理类,负责记录和排序游戏记录
 */
public class RankList {
    private static RankList instance; // 单例模式
    private List<GameRecord> allRecords; // 所有记录

    private RankList() {
        allRecords = new ArrayList<>();
    }

    // 单例获取
    public static synchronized RankList getInstance() {
        if (instance == null) {
            instance = new RankList();
        }
        return instance;
    }

    // 添加记录
    public void addRecord(GameRecord record) {
        allRecords.add(record);
    }

    // 获取人机对战排行榜 (按时长升序,即最快获胜)
    public List<GameRecord> getAiRankList() {
        return allRecords.stream()
                .filter(r -> r.getGameType().equals("人机对战"))
                .sorted(Comparator.comparing(GameRecord::getDuration))
                .collect(Collectors.toList());
    }

    // 获取人人对战排行榜
    public List<GameRecord> getPvpRankList() {
        return allRecords.stream()
                .filter(r -> r.getGameType().equals("人人对战"))
                .sorted(Comparator.comparing(GameRecord::getDuration))
                .collect(Collectors.toList());
    }

    // 获取总排行榜
    public List<GameRecord> getTotalRankList() {
        return allRecords.stream()
                .sorted(Comparator.comparing(GameRecord::getDuration))
                .collect(Collectors.toList());
    }
}

  • 使用List来储存记录每一局对战数据,添加到排行榜上

  • 使用Stream流+Collection来比较对局时间,进行排序

3.8 MainFrame.java

点击查看代码
package gomoku.view;

import gomoku.model.Chessboard;
import gomoku.model.AIPlayer;
import gomoku.model.HumanPlayer;
import gomoku.model.GameRecord;
import gomoku.model.RankList;
import gomoku.util.GameUtils;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class MainFrame extends JFrame {
    // 全局样式配置(统一管理,方便修改)
    private static final Color BG_COLOR = new Color(245, 240, 230); // 米白色背景
    private static final Color BOARD_COLOR = new Color(210, 180, 140); // 木质棋盘色
    private static final Color BUTTON_COLOR = new Color(139, 69, 19); // 棕色按钮
    private static final Color BUTTON_TEXT_COLOR = Color.WHITE; // 按钮文字白
    private static final Font MAIN_FONT = new Font("微软雅黑", Font.PLAIN, 14);
    private static final Font TITLE_FONT = new Font("微软雅黑", Font.BOLD, 18);

    // 新增:调整窗口和棋盘尺寸,确保足够显示
    private static final int MAIN_WINDOW_WIDTH = 900;
    private static final int MAIN_WINDOW_HEIGHT = 850; // 增加高度避免压缩
    private static final int BOARD_MARGIN = 25; // 棋盘边缘留白

    private Chessboard chessboard;
    private ChessboardPanel chessPanel;
    private MenuPanel menuPanel;
    private TimerPanel timerPanel;
    private HumanPlayer humanPlayer1;
    private HumanPlayer humanPlayer2;
    private AIPlayer aiPlayer;
    private boolean isVsAI;
    private boolean gameStarted;
    private GameRecord currentRecord;

    public MainFrame(String title) {
        super(title);
        // 基础窗口配置
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT); // 使用调整后的高度
        setLocationRelativeTo(null);
        setResizable(false);
        getContentPane().setBackground(BG_COLOR);

        initComponents();
        setupLayout();
        setupListeners();
    }

    private void initComponents() {
        menuPanel = new MenuPanel();
        timerPanel = new TimerPanel();

        chessboard = new Chessboard(15);
        chessPanel = new ChessboardPanel(chessboard);
        // 关键:设置棋盘面板的首选尺寸和最小尺寸
        chessPanel.setPreferredSize(new Dimension(600, 600));
        chessPanel.setMinimumSize(new Dimension(500, 500)); // 防止被压缩
        chessPanel.setBackground(BG_COLOR);
    }

    private void setupLayout() {
        JPanel mainPanel = new JPanel(new BorderLayout(0, 20));
        mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        mainPanel.setBackground(BG_COLOR);

        mainPanel.add(menuPanel, BorderLayout.NORTH);
        
        // 中间区域:使用面板包裹棋盘,确保居中且不被压缩
        JPanel boardContainer = new JPanel(new FlowLayout(FlowLayout.CENTER));
        boardContainer.setBackground(BG_COLOR);
        // 为容器添加最小尺寸约束
        boardContainer.setMinimumSize(new Dimension(600, 600));
        boardContainer.add(chessPanel);
        mainPanel.add(boardContainer, BorderLayout.CENTER);

        mainPanel.add(timerPanel, BorderLayout.SOUTH);

        this.setContentPane(mainPanel);
    }

    private void setupListeners() {
        if (menuPanel == null) {
            menuPanel = new MenuPanel();
            System.err.println("menuPanel为空,已重新初始化");
        }

        menuPanel.setMenuListener(new MenuPanel.MenuActionListener() {
            @Override
            public void onPvpSelected() {
                System.out.println("监听器收到:人人对战");
                startPVPGame();
            }

            @Override
            public void onPvaiSelected() {
                System.out.println("监听器收到:人机对战");
                startPVAGame();
            }

            @Override
            public void onRestartSelected() {
                System.out.println("监听器收到:重新开始");
                restartGame();
            }

            @Override
            public void onRankSelected() {
                System.out.println("监听器收到:排行榜");
                new RankDialog(MainFrame.this).setVisible(true);
            }

            @Override
            public void onExitSelected() {
                System.out.println("监听器收到:退出");
                System.exit(0);
            }
        });

        chessPanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (!gameStarted) return;

                // 优化:动态计算棋盘坐标,适配实际面板尺寸
                int panelSize = Math.min(chessPanel.getWidth(), chessPanel.getHeight());
                int cellSize = (panelSize - 2 * BOARD_MARGIN) / 14;
                int startX = (chessPanel.getWidth() - panelSize) / 2 + BOARD_MARGIN;
                int startY = (chessPanel.getHeight() - panelSize) / 2 + BOARD_MARGIN;

                int col = (e.getX() - startX + cellSize/2) / cellSize;
                int row = (e.getY() - startY + cellSize/2) / cellSize;

                if (row >= 0 && row < 15 && col >= 0 && col < 15) {
                    if (isVsAI) {
                        humanPlayer1.handleClick(row, col);
                    } else {
                        if (chessboard.getCurrentPlayer() == Chessboard.BLACK) {
                            humanPlayer1.handleClick(row, col);
                        } else {
                            humanPlayer2.handleClick(row, col);
                        }
                    }
                }
            }
        });
    }

    private void startPVPGame() {
        isVsAI = false;
        chessboard.reset();
        currentRecord = new GameRecord("人人对战", 15);
        timerPanel.reset();
        timerPanel.start();

        humanPlayer1 = new HumanPlayer(Chessboard.BLACK, chessboard, this::onMoveMade);
        humanPlayer2 = new HumanPlayer(Chessboard.WHITE, chessboard, this::onMoveMade);

        chessPanel.repaint();
        menuPanel.updateStatus("人人对战 · 黑方回合");
        gameStarted = true;
        System.out.println("已进入人人对战模式");
    }

    private void startPVAGame() {
        isVsAI = true;
        chessboard.reset();
        currentRecord = new GameRecord("人机对战", 15);
        timerPanel.reset();
        timerPanel.start();

        humanPlayer1 = new HumanPlayer(Chessboard.BLACK, chessboard, this::onMoveMade);
        aiPlayer = new AIPlayer(Chessboard.WHITE, chessboard, this::onMoveMade);
        aiPlayer.setDelayMillis(800);

        chessPanel.repaint();
        menuPanel.updateStatus("人机对战 · 您(黑棋)回合");
        gameStarted = true;
        System.out.println("已进入人机对战模式");
    }

    private void restartGame() {
        timerPanel.stop();
        if (isVsAI) {
            startPVAGame();
        } else {
            startPVPGame();
        }
    }

    private void onMoveMade(int row, int col) {
        chessboard.setLastMove(row, col);
        chessPanel.repaint();

        if (chessboard.checkWin(row, col)) {
            timerPanel.stop();
            String winner = chessboard.getCurrentPlayer() == Chessboard.BLACK ? "黑棋" : "白棋";
            menuPanel.updateStatus(winner + "获胜!");
            gameStarted = false;

            if (isVsAI) {
                String username = new UsernameDialog(this).showDialog();
                currentRecord.finishGame(winner, chessboard.getBoard(), username);
                JOptionPane.showMessageDialog(this, 
                        "恭喜!" + username + "(" + winner + ")获胜!\n用时:" + currentRecord.getDurationStr(),
                        "游戏结束", JOptionPane.INFORMATION_MESSAGE);
            } else {
                TwoPlayerInputDialog inputDialog = new TwoPlayerInputDialog(this, winner);
                String[] usernames = inputDialog.showDialog();
                String combinedName = usernames[0] + "|" + usernames[1];
                currentRecord.finishGame(winner, chessboard.getBoard(), combinedName);
                JOptionPane.showMessageDialog(this, 
                        "恭喜!" + (winner.equals("黑棋") ? usernames[0] : usernames[1]) + "获胜!\n用时:" + currentRecord.getDurationStr(),
                        "游戏结束", JOptionPane.INFORMATION_MESSAGE);
            }

            RankList.getInstance().addRecord(currentRecord);
            return;
        }

        if (GameUtils.isDraw(chessboard)) {
            timerPanel.stop();
            menuPanel.updateStatus("平局!");
            gameStarted = false;
            JOptionPane.showMessageDialog(this, "本局战平!", "游戏结束", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        chessboard.switchPlayer();
        if (isVsAI) {
            menuPanel.updateStatus("人机对战 · AI(白棋)思考中...");
            aiPlayer.makeMove();
            menuPanel.updateStatus("人机对战 · 您(黑棋)回合");
        } else {
            String currentPlayer = chessboard.getCurrentPlayer() == Chessboard.BLACK ? "黑方" : "白方";
            menuPanel.updateStatus("人人对战 · " + currentPlayer + "回合");
        }
    }

    // 优化棋盘绘制逻辑,确保完整显示
    private class ChessboardPanel extends JPanel {
        private Chessboard chessboard;

        public ChessboardPanel(Chessboard chessboard) {
            this.chessboard = chessboard;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g;
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            // 关键:动态计算绘制区域,适配面板实际大小
            int panelSize = Math.min(getWidth(), getHeight());
            int drawAreaSize = panelSize - 2 * BOARD_MARGIN;
            int cellSize = drawAreaSize / 14; // 15x15棋盘有14个间隔
            int startX = (getWidth() - panelSize) / 2 + BOARD_MARGIN;
            int startY = (getHeight() - panelSize) / 2 + BOARD_MARGIN;

            // 绘制棋盘阴影
            g2d.setColor(new Color(0, 0, 0, 20));
            g2d.fillRoundRect(startX - 5, startY - 5, drawAreaSize + 10, drawAreaSize + 10, 8, 8);

            // 绘制棋盘边框
            g2d.setColor(new Color(160, 82, 45));
            g2d.drawRoundRect(startX, startY, drawAreaSize, drawAreaSize, 8, 8);

            // 绘制棋盘底色
            g2d.setColor(BOARD_COLOR);
            g2d.fillRoundRect(startX + 2, startY + 2, drawAreaSize - 4, drawAreaSize - 4, 6, 6);

            // 绘制网格
            drawBoardGrid(g2d, startX, startY, cellSize);

            // 绘制棋子
            drawChessPieces(g2d, startX, startY, cellSize);
        }

        // 绘制棋盘网格(使用动态计算的参数)
        private void drawBoardGrid(Graphics2D g2d, int startX, int startY, int cellSize) {
            g2d.setColor(Color.BLACK);
            g2d.setStroke(new BasicStroke(1.2f));

            // 画横线和竖线
            for (int i = 0; i < 15; i++) {
                // 横线
                g2d.drawLine(startX, startY + i * cellSize, startX + 14 * cellSize, startY + i * cellSize);
                // 竖线
                g2d.drawLine(startX + i * cellSize, startY, startX + i * cellSize, startY + 14 * cellSize);
            }

            // 绘制天元和星位
            int[] starPoints = {3, 7, 11};
            g2d.setStroke(new BasicStroke(1f));
            for (int x : starPoints) {
                for (int y : starPoints) {
                    int px = startX + x * cellSize;
                    int py = startY + y * cellSize;
                    g2d.fillOval(px - 4, py - 4, 8, 8);
                }
            }
        }

        // 绘制棋子(使用动态计算的参数)
        private void drawChessPieces(Graphics2D g2d, int startX, int startY, int cellSize) {
            int chessSize = cellSize - 4;
            int lastRow = chessboard.getLastRow();
            int lastCol = chessboard.getLastCol();

            for (int i = 0; i < 15; i++) {
                for (int j = 0; j < 15; j++) {
                    if (chessboard.getBoard()[i][j] != Chessboard.EMPTY) {
                        int px = startX + j * cellSize;
                        int py = startY + i * cellSize;

                        // 棋子阴影
                        if (chessboard.getBoard()[i][j] == Chessboard.BLACK) {
                            g2d.setColor(new Color(0, 0, 0, 30));
                        } else {
                            g2d.setColor(new Color(0, 0, 0, 15));
                        }
                        g2d.fillOval(px - chessSize/2 + 2, py - chessSize/2 + 2, chessSize, chessSize);

                        // 棋子本体
                        if (chessboard.getBoard()[i][j] == Chessboard.BLACK) {
                            g2d.setColor(Color.BLACK);
                        } else {
                            g2d.setColor(Color.WHITE);
                            g2d.drawOval(px - chessSize/2, py - chessSize/2, chessSize, chessSize);
                        }
                        g2d.fillOval(px - chessSize/2, py - chessSize/2, chessSize, chessSize);

                        // 最后一步标记
                        if (lastRow != -1 && lastCol != -1 && i == lastRow && j == lastCol) {
                            g2d.setColor(new Color(255, 0, 0, 80));
                            g2d.setStroke(new BasicStroke(2f));
                            g2d.drawOval(px - chessSize/2 - 2, py - chessSize/2 - 2, chessSize + 4, chessSize + 4);
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
            e.printStackTrace();
        }

        SwingUtilities.invokeLater(() -> {
            new MainFrame("五子棋 - 优化版").setVisible(true);
        });
    }
}

  • MainFrame绘制图形界面面板

  • initComponents()初始化面板,可以在这里修改面板大小

  • 使用边界布局,添加组件,在图形界面按动时,通过监听功能,运行内在逻辑

  • 两种模式:人人对战、人机对战

3.9 ChessboardPanel.java

点击查看代码
package gomoku.view;

import gomoku.model.Chessboard;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ChessboardPanel extends JPanel {
    private static final int CELL_SIZE = 30;  // 每个格子的大小
    private static final int MARGIN = 20;     // 边距
    private Chessboard chessboard;
    private ClickListener clickListener;
    private int lastRow = -1;
    private int lastCol = -1;

    // 新增方法:设置最后落子位置
    public void setLastMove(int row, int col) {
        this.lastRow = row;
        this.lastCol = col;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        drawChessboard(g2d);
        drawPieces(g2d);
        drawLastMoveMarker(g2d); // 绘制最后落子标记
    }

    // 新增方法:绘制最后落子标记(红色圆点)
    private void drawLastMoveMarker(Graphics2D g) {
        if (lastRow != -1 && lastCol != -1) {
            int x = MARGIN + lastCol * CELL_SIZE;
            int y = MARGIN + lastRow * CELL_SIZE;
            g.setColor(Color.RED);
            g.fillOval(x - 4, y - 4, 8, 8);
        }
    }

    
    public ChessboardPanel(Chessboard chessboard) {
        this.chessboard = chessboard;
        setPreferredSize(new Dimension(
            chessboard.getSize() * CELL_SIZE + MARGIN * 2,
            chessboard.getSize() * CELL_SIZE + MARGIN * 2
        ));
        setBackground(Color.WHITE);
        
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (clickListener != null) {
                    // 计算点击位置对应的棋盘坐标
                    int row = (e.getY() - MARGIN + CELL_SIZE / 2) / CELL_SIZE;
                    int col = (e.getX() - MARGIN + CELL_SIZE / 2) / CELL_SIZE;
                    
                    if (row >= 0 && row < chessboard.getSize() && 
                        col >= 0 && col < chessboard.getSize()) {
                        clickListener.onClick(row, col);
                    }
                }
            }
        });
    }
    
   
    // 绘制棋盘
    private void drawChessboard(Graphics g) {
        int size = chessboard.getSize();
        
        // 绘制网格线
        g.setColor(Color.BLACK);
        for (int i = 0; i < size; i++) {
            // 横线
            g.drawLine(MARGIN, MARGIN + i * CELL_SIZE,
                      MARGIN + (size - 1) * CELL_SIZE, MARGIN + i * CELL_SIZE);
            // 竖线
            g.drawLine(MARGIN + i * CELL_SIZE, MARGIN,
                      MARGIN + i * CELL_SIZE, MARGIN + (size - 1) * CELL_SIZE);
        }
        
        // 绘制天元和星位
        int[] stars = {3, 7, 11};  // 15x15棋盘的星位坐标
        for (int x : stars) {
            for (int y : stars) {
                drawDot(g, MARGIN + x * CELL_SIZE, MARGIN + y * CELL_SIZE, 5);
            }
        }
    }
    
    // 绘制棋子
    private void drawPieces(Graphics g) {
        int size = chessboard.getSize();
        int[][] board = chessboard.getBoard();
        
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (board[i][j] != Chessboard.EMPTY) {
                    int x = MARGIN + j * CELL_SIZE;
                    int y = MARGIN + i * CELL_SIZE;
                    
                    // 绘制棋子
                    if (board[i][j] == Chessboard.BLACK) {
                        g.setColor(Color.BLACK);
                    } else {
                        g.setColor(Color.WHITE);
                        g.drawOval(x - CELL_SIZE / 2, y - CELL_SIZE / 2, 
                                  CELL_SIZE, CELL_SIZE);
                    }
                    g.fillOval(x - CELL_SIZE / 2, y - CELL_SIZE / 2, 
                              CELL_SIZE, CELL_SIZE);
                    
                    // 绘制棋子边框
                    g.setColor(Color.BLACK);
                    g.drawOval(x - CELL_SIZE / 2, y - CELL_SIZE / 2, 
                              CELL_SIZE, CELL_SIZE);
                }
            }
        }
    }
    
    // 绘制小圆点(用于星位)
    private void drawDot(Graphics g, int x, int y, int radius) {
        g.fillOval(x - radius / 2, y - radius / 2, radius, radius);
    }
    
    // 设置点击监听器
    public void setClickListener(ClickListener listener) {
        this.clickListener = listener;
    }
    
    // 点击监听器接口
    @FunctionalInterface
    public interface ClickListener {
        void onClick(int row, int col);
    }
  
}
  • 绘制落子后的图形界面(棋盘、棋子),对于最后落子加个红色边框

  • 落子成功后,MainFrame 调用 setLastMove 更新最后落子位置,并调用 repaint()

3.10 MenuPanel.java

点击查看代码
package gomoku.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MenuPanel extends JPanel {
    // 样式常量(保持不变)
    private static final Color BUTTON_COLOR = new Color(139, 69, 19);
    private static final Color BUTTON_TEXT_COLOR = Color.WHITE;
    private static final Color HOVER_COLOR = new Color(160, 82, 45);
    private static final Color PRESSED_COLOR = new Color(101, 67, 33);
    private static final Font MAIN_FONT = new Font("微软雅黑", Font.PLAIN, 14);
    private static final int BUTTON_WIDTH = 110; // 缩小按钮宽度(原120→110,减少总宽度)
    private static final int BUTTON_HEIGHT = 35;
    private static final int CORNER_RADIUS = 8;

    private JLabel statusLabel;
    private MenuActionListener listener;

    // 监听器接口(保持不变)
    public interface MenuActionListener {
        void onPvpSelected();
        void onPvaiSelected();
        void onRestartSelected();
        void onRankSelected();
        void onExitSelected();
    }

    // 构造方法(核心修改:解决布局挤压问题)
    public MenuPanel() {
        // 1. 改用BoxLayout(垂直布局):先放按钮行,再放状态标签,避免横向挤压
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        setBackground(new Color(245, 240, 230));
        
        // 2. 强制面板最小尺寸(确保不会被压缩到0)
        setMinimumSize(new Dimension(850, 120));
        setPreferredSize(new Dimension(850, 120));

        // 3. 按钮容器(横向排列,用BoxLayout确保按钮不换行且居中)
        JPanel buttonContainer = new JPanel();
        buttonContainer.setLayout(new BoxLayout(buttonContainer, BoxLayout.X_AXIS));
        buttonContainer.setBackground(new Color(245, 240, 230));
        
        // 按钮间添加固定间距(原15→10,进一步减少总宽度)
        int gap = 10;
        buttonContainer.add(Box.createHorizontalGlue()); // 左侧空白(自动填充,使按钮居中)
        buttonContainer.add(createStyledButton("人人对战"));
        buttonContainer.add(Box.createRigidArea(new Dimension(gap, 0))); // 按钮间距
        buttonContainer.add(createStyledButton("人机对战"));
        buttonContainer.add(Box.createRigidArea(new Dimension(gap, 0)));
        buttonContainer.add(createStyledButton("重新开始"));
        buttonContainer.add(Box.createRigidArea(new Dimension(gap, 0)));
        buttonContainer.add(createStyledButton("排行榜"));
        buttonContainer.add(Box.createRigidArea(new Dimension(gap, 0)));
        buttonContainer.add(createStyledButton("退出游戏"));
        buttonContainer.add(Box.createHorizontalGlue()); // 右侧空白(自动填充)

        // 4. 添加按钮容器到面板(垂直方向间距10px)
        add(Box.createVerticalStrut(10)); // 顶部空白
        add(buttonContainer);
        add(Box.createVerticalStrut(10)); // 按钮和标签间距

        // 5. 状态标签(保持不变,但调整宽度适配面板)
        statusLabel = new JLabel("请选择对战模式", SwingConstants.CENTER);
        statusLabel.setFont(MAIN_FONT);
        statusLabel.setForeground(BUTTON_COLOR);
        statusLabel.setPreferredSize(new Dimension(800, 30));
        add(statusLabel);

        // 调试:确认组件已添加(运行后看控制台)
        System.out.println("MenuPanel组件数:" + getComponentCount());
        System.out.println("按钮数:" + buttonContainer.getComponentCount());
    }

    // 按钮创建方法(保持不变,仅宽度已在常量中调整)
    private JButton createStyledButton(String text) {
        JButton button = new JButton(text);
        button.setFont(MAIN_FONT);
        button.setForeground(BUTTON_TEXT_COLOR);
        button.setBackground(BUTTON_COLOR);
        button.setPreferredSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
        button.setBorderPainted(false);
        button.setFocusPainted(false);
        button.setContentAreaFilled(false);
        button.setRolloverEnabled(true);

        // 自定义圆角UI(保持不变)
        button.setUI(new javax.swing.plaf.basic.BasicButtonUI() {
            @Override
            public void paint(Graphics g, JComponent c) {
                AbstractButton b = (AbstractButton) c;
                Graphics2D g2 = (Graphics2D) g.create();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

                // 状态色判断
                if (b.getModel().isPressed()) {
                    g2.setColor(PRESSED_COLOR);
                } else if (b.getModel().isRollover()) {
                    g2.setColor(HOVER_COLOR);
                } else {
                    g2.setColor(b.getBackground());
                }

                // 绘制圆角背景
                g2.fillRoundRect(0, 0, b.getWidth(), b.getHeight(), CORNER_RADIUS, CORNER_RADIUS);
                g2.dispose();
                super.paint(g, c);
            }

            @Override
            protected void paintFocus(Graphics g, AbstractButton b, Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {}
        });

        // 按钮点击事件(保持不变)
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if ("人人对战".equals(text) && listener != null) {
                    listener.onPvpSelected();
                } else if ("人机对战".equals(text) && listener != null) {
                    listener.onPvaiSelected();
                } else if ("重新开始".equals(text) && listener != null) {
                    listener.onRestartSelected();
                } else if ("排行榜".equals(text) && listener != null) {
                    listener.onRankSelected();
                } else if ("退出游戏".equals(text) && listener != null) {
                    listener.onExitSelected();
                }
            }
        });

        return button;
    }

    // 设置监听器(保持不变)
    public void setMenuListener(MenuActionListener listener) {
        this.listener = listener;
    }

    // 更新状态(保持不变)
    public void updateStatus(String text) {
        if (statusLabel != null) {
            statusLabel.setText(text);
        }
    }
}
  • 绘制最上部的菜单栏

  • 通过按钮来实现人人对战/人机对战

  • 使用多种方法,增强了封装性

3.11 TimerPanel .java

点击查看代码
package gomoku.view;

import javax.swing.*;
import java.awt.*;
import java.util.Timer;
import java.util.TimerTask;

public class TimerPanel extends JPanel {
    private JLabel timerLabel;
    private Timer timer;
    private int seconds = 0;

    public TimerPanel() {
        setBackground(new Color(245, 240, 230));
        timerLabel = new JLabel("游戏时间:00:00", SwingConstants.CENTER);
        timerLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        timerLabel.setForeground(new Color(139, 69, 19));
        add(timerLabel);
    }

    // 开始计时
    public void start() {
        if (timer == null) {
            timer = new Timer();
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    seconds++;
                    int minutes = seconds / 60;
                    int secs = seconds % 60;
                    timerLabel.setText(String.format("游戏时间:%02d:%02d", minutes, secs));
                }
            }, 1000, 1000);
        }
    }

    // 重置计时
    public void reset() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        seconds = 0;
        timerLabel.setText("游戏时间:00:00");
    }

    // 停止计时
    public void stop() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }
}
  • 记录游戏时长,并使用timerLabel.setText(String.format("游戏时间:%02d:%02d", minutes, secs))来规范化展示

3.12 BoardPreview.java

点击查看代码
package gomoku.view;

import gomoku.model.GameRecord;
import gomoku.model.Chessboard;

import javax.swing.*;
import java.awt.*;

/**
 * 棋盘预览对话框,用于显示历史棋局
 */
public class BoardPreviewDialog extends JDialog {
    private GameRecord record;
    private int cellSize = 20; // 预览棋盘的单元格大小
    private int margin = 10;   // 边距

    // 正确的构造函数,接受父窗口和游戏记录
    public BoardPreviewDialog(Dialog parent, GameRecord record) {
        super(parent, "棋局预览", true);
        this.record = record;
        initComponents();
        setupLayout();
        pack();
        setLocationRelativeTo(parent);
    }

    private void initComponents() {
        // 创建信息面板显示游戏详情
    	JPanel infoPanel = new JPanel(new GridLayout(4, 2, 10, 5));
        infoPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        
        infoPanel.add(new JLabel("玩家:"));
        infoPanel.add(new JLabel(record.getUsername()));
        
        infoPanel.add(new JLabel("游戏类型:"));
        infoPanel.add(new JLabel(record.getGameType()));
        
        infoPanel.add(new JLabel("获胜方:"));
        infoPanel.add(new JLabel(record.getWinner()));
        
        infoPanel.add(new JLabel("用时:"));
        infoPanel.add(new JLabel(record.getDurationStr()));

        // 创建棋盘预览面板
        JPanel boardPanel = new JPanel() {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                drawPreviewBoard(g);
            }
            
            @Override
            public Dimension getPreferredSize() {
                int size = record.getBoardSize() * cellSize + margin * 2;
                return new Dimension(size, size);
            }
        };
        boardPanel.setBackground(Color.WHITE);

        // 添加关闭按钮
        JButton closeBtn = new JButton("关闭");
        closeBtn.addActionListener(e -> dispose());
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(closeBtn);

        // 组装界面
        setLayout(new BorderLayout(10, 10));
        add(infoPanel, BorderLayout.NORTH);
        add(new JScrollPane(boardPanel), BorderLayout.CENTER);
        add(buttonPanel, BorderLayout.SOUTH);
    }

    private void setupLayout() {
        setResizable(false);
    }

    // 绘制预览棋盘
    private void drawPreviewBoard(Graphics g) {
        int[][] board = record.getBoardState();
        int size = record.getBoardSize();
        
        // 绘制网格
        g.setColor(Color.BLACK);
        for (int i = 0; i < size; i++) {
            // 横线
            g.drawLine(margin, margin + i * cellSize,
                    margin + (size - 1) * cellSize, margin + i * cellSize);
            // 竖线
            g.drawLine(margin + i * cellSize, margin,
                    margin + i * cellSize, margin + (size - 1) * cellSize);
        }
        
        // 绘制棋子
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (board[i][j] != Chessboard.EMPTY) {
                    int x = margin + j * cellSize;
                    int y = margin + i * cellSize;
                    
                    if (board[i][j] == Chessboard.BLACK) {
                        g.setColor(Color.BLACK);
                    } else {
                        g.setColor(Color.WHITE);
                        g.drawOval(x - cellSize / 2, y - cellSize / 2, 
                                cellSize, cellSize);
                    }
                    g.fillOval(x - cellSize / 2, y - cellSize / 2, 
                            cellSize, cellSize);
                    
                    // 绘制边框
                    g.setColor(Color.BLACK);
                    g.drawOval(x - cellSize / 2, y - cellSize / 2, 
                            cellSize, cellSize);
                }
            }
        }
    }
}
  • 使用边界布局

  • 通过点击,调用GameRecord展示历史数据

3.13 TowPlayerInputDialog .java

点击查看代码
package gomoku.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * 双人对战结束时输入两个玩家用户名的对话框
 */
public class TwoPlayerInputDialog extends JDialog {
    private JTextField blackPlayerField;
    private JTextField whitePlayerField;
    private String[] result; // 存储两个玩家的用户名,索引0为黑方,1为白方
    private String winnerColor; // 获胜方颜色("黑棋"或"白棋")

    public TwoPlayerInputDialog(Frame parent, String winnerColor) {
        super(parent, "游戏结束 - 输入玩家信息", true);
        this.winnerColor = winnerColor;
        this.result = new String[2];
        initComponents();
        setupLayout();
        pack();
        setLocationRelativeTo(parent);
    }

    private void initComponents() {
        // 黑方玩家输入
        JLabel blackLabel = new JLabel("黑方玩家:");
        blackPlayerField = new JTextField(15);
        blackPlayerField.setText("黑方玩家");

        // 白方玩家输入
        JLabel whiteLabel = new JLabel("白方玩家:");
        whitePlayerField = new JTextField(15);
        whitePlayerField.setText("白方玩家");

        // 获胜信息提示
        JLabel winnerLabel = new JLabel("<html><font color='red'>" + winnerColor + "获胜!</font></html>");
        winnerLabel.setFont(new Font("SimHei", Font.BOLD, 14));

        // 按钮
        JButton confirmBtn = new JButton("确认");
        confirmBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                result[0] = getPlayerName(blackPlayerField.getText(), "黑方玩家");
                result[1] = getPlayerName(whitePlayerField.getText(), "白方玩家");
                dispose();
            }
        });

        // 布局
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 5, 5, 5);
        gbc.anchor = GridBagConstraints.WEST;

        // 获胜信息行
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.gridwidth = 2;
        panel.add(winnerLabel, gbc);

        // 重置网格宽度
        gbc.gridwidth = 1;
        
        // 黑方玩家行
        gbc.gridx = 0;
        gbc.gridy = 1;
        panel.add(blackLabel, gbc);
        gbc.gridx = 1;
        panel.add(blackPlayerField, gbc);

        // 白方玩家行
        gbc.gridx = 0;
        gbc.gridy = 2;
        panel.add(whiteLabel, gbc);
        gbc.gridx = 1;
        panel.add(whitePlayerField, gbc);

        // 按钮行
        gbc.gridx = 0;
        gbc.gridy = 3;
        gbc.gridwidth = 2;
        gbc.anchor = GridBagConstraints.CENTER;
        panel.add(confirmBtn, gbc);

        add(panel);
    }

    private void setupLayout() {
        setResizable(false);
    }

    /**
     * 获取玩家输入的用户名,为空时使用默认值
     */
    private String getPlayerName(String input, String defaultName) {
        String name = input.trim();
        return name.isEmpty() ? defaultName : name;
    }

    /**
     * 显示对话框并返回结果
     * @return 长度为2的数组,[0]是黑方用户名,[1]是白方用户名
     */
    public String[] showDialog() {
        setVisible(true);
        return result;
    }
}

  • 双人对战结束时输入两个玩家用户名的对话框,使用String[]来存储用户名称

  • 使用边界布局

  • 最开始默认显示黑方玩家/白方玩家,可以在文本框中输入不同用户名

  • 使用GridBagLayout自由调整界面布局,使界面布局更加美观

3.14 UsernameDialog.java

点击查看代码
package gomoku.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * 游戏结束后输入用户名的对话框
 */
public class UsernameDialog extends JDialog {
    private JTextField usernameField;
    private String username;
    private boolean confirmed;

    public UsernameDialog(Frame parent) {
        super(parent, "游戏结束", true);
        initComponents();
        setSize(300, 150);
        setLocationRelativeTo(parent);
    }

    private void initComponents() {
        JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10));
        panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));

        // 提示标签
        JLabel label = new JLabel("请输入您的用户名:");
        panel.add(label);

        // 输入框
        usernameField = new JTextField();
        usernameField.setText("玩家1"); // 默认用户名
        panel.add(usernameField);

        // 按钮面板
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        JButton confirmButton = new JButton("确认");
        confirmButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                username = usernameField.getText().trim();
                if (username.isEmpty()) {
                    username = "匿名玩家";
                }
                confirmed = true;
                dispose();
            }
        });
        buttonPanel.add(confirmButton);

        // 布局设置
        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(panel, BorderLayout.CENTER);
        getContentPane().add(buttonPanel, BorderLayout.SOUTH);
    }

    // 显示对话框并返回用户名
    public String showDialog() {
        setVisible(true);
        return confirmed ? username : "匿名玩家";
    }
}

  • 人机模式下的玩家名称输入框,默认显示玩家1

  • 边界布局

  • 使用setVisible(true)来锁定界面,直到点击确定/取消才结束

  • 使用confirmed来标记,判断返回的用户名

3.15 RankDialog .java

点击查看代码
package gomoku.view;

import gomoku.model.GameRecord;
import gomoku.model.RankList;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.List;

public class RankDialog extends JDialog {
    private JTabbedPane tabbedPane;
    private RankList rankList;
    // 添加序列化版本号解决第一个警告
    private static final long serialVersionUID = 1L;

    public RankDialog(Frame owner) {
        super(owner, "游戏排行榜", true);
        rankList = RankList.getInstance();
        initComponents();
        setupLayout();
        pack();
        setLocationRelativeTo(owner);
    }

    private void initComponents() {
        tabbedPane = new JTabbedPane();
        tabbedPane.addTab("人机对战", createRankTable("人机对战"));
        tabbedPane.addTab("人人对战", createRankTable("人人对战"));
        tabbedPane.addTab("总排行榜", createRankTable("全部"));
    }

    private void setupLayout() {
        setLayout(new BorderLayout());
        add(tabbedPane, BorderLayout.CENTER);
        
        JPanel buttonPanel = new JPanel();
        JButton closeBtn = new JButton("关闭");
        closeBtn.addActionListener(e -> dispose());
        buttonPanel.add(closeBtn);
        add(buttonPanel, BorderLayout.SOUTH);
    }

    private JComponent createRankTable(String type) {
        // 修正数组初始化方式
        String[] columns = type.equals("人人对战") ? 
            new String[]{"排名", "黑方玩家", "白方玩家", "获胜方", "时长", "日期", "操作"} :
            new String[]{"排名", "玩家", "游戏类型", "获胜方", "时长", "日期", "操作"};
        
        // 确保变量名唯一,不重复
        DefaultTableModel tableModel = new DefaultTableModel(columns, 0) {
            // 为内部类添加序列化版本号
            private static final long serialVersionUID = 2L;
            
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
            
            @Override
            public Class<?> getColumnClass(int column) {
                return (column == columns.length - 1) ? JButton.class : Object.class;
            }
        };

        List<GameRecord> records;
        switch (type) {
            case "人机对战":
                records = rankList.getAiRankList();
                break;
            case "人人对战":
                records = rankList.getPvpRankList();
                break;
            default:
                records = rankList.getTotalRankList();
                break;
        }

        for (int i = 0; i < records.size(); i++) {
            GameRecord record = records.get(i);
            JButton viewButton = new JButton("查看棋局");
            final int index = i;

            viewButton.addActionListener(e -> {
                new BoardPreviewDialog(RankDialog.this, records.get(index)).setVisible(true);
            });

            Object[] row;
            if (type.equals("人人对战")) {
                String[] players = record.getUsername().split("\\|");
                String blackPlayer = players.length > 0 ? players[0] : "黑方玩家";
                String whitePlayer = players.length > 1 ? players[1] : "白方玩家";
                
                row = new Object[]{
                        i + 1,
                        blackPlayer,
                        whitePlayer,
                        record.getWinner(),
                        record.getDurationStr(),
                        record.getStartTime().toLocalDate().toString(),
                        viewButton
                };
            } else {
                row = new Object[]{
                        i + 1,
                        record.getUsername(),
                        record.getGameType(),
                        record.getWinner(),
                        record.getDurationStr(),
                        record.getStartTime().toLocalDate().toString(),
                        viewButton
                };
            }
            tableModel.addRow(row); // 使用修改后的变量名
        }

        JTable table = new JTable(tableModel) { // 使用修改后的变量名
            private static final long serialVersionUID = 3L;
            
            @Override
            public TableCellRenderer getCellRenderer(int row, int column) {
                if (column == columns.length - 1) {
                    return new DefaultTableCellRenderer() {
                        private static final long serialVersionUID = 4L;
                        
                        @Override
                        public Component getTableCellRendererComponent(JTable table, Object value,
                                                                      boolean isSelected, boolean hasFocus,
                                                                      int row, int column) {
                            if (value instanceof JButton) {
                                JButton btn = (JButton) value;
                                if (isSelected) {
                                    btn.setBackground(table.getSelectionBackground());
                                    btn.setForeground(table.getSelectionForeground());
                                } else {
                                    btn.setBackground(table.getBackground());
                                    btn.setForeground(table.getForeground());
                                }
                                return btn;
                            }
                            return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                        }
                    };
                }
                return super.getCellRenderer(row, column);
            }

            @Override
            protected void processMouseEvent(MouseEvent e) {
                int clickRow = rowAtPoint(e.getPoint());
                int clickCol = columnAtPoint(e.getPoint());

                if (clickRow != -1 && clickCol == columns.length - 1 && e.getButton() == MouseEvent.BUTTON1) {
                    Object cellValue = getModel().getValueAt(clickRow, clickCol);
                    if (cellValue instanceof JButton) {
                        ((JButton) cellValue).doClick();
                        return;
                    }
                }
                super.processMouseEvent(e);
            }
        };

        // 设置列宽
        TableColumnModel columnModel = table.getColumnModel();
        if (type.equals("人人对战")) {
            columnModel.getColumn(0).setPreferredWidth(50);
            columnModel.getColumn(1).setPreferredWidth(100);
            columnModel.getColumn(2).setPreferredWidth(100);
            columnModel.getColumn(3).setPreferredWidth(80);
            columnModel.getColumn(4).setPreferredWidth(80);
            columnModel.getColumn(5).setPreferredWidth(120);
            columnModel.getColumn(6).setPreferredWidth(100);
        } else {
            columnModel.getColumn(0).setPreferredWidth(50);
            columnModel.getColumn(1).setPreferredWidth(100);
            columnModel.getColumn(2).setPreferredWidth(100);
            columnModel.getColumn(3).setPreferredWidth(80);
            columnModel.getColumn(4).setPreferredWidth(80);
            columnModel.getColumn(5).setPreferredWidth(120);
            columnModel.getColumn(6).setPreferredWidth(100);
        }

        table.getTableHeader().setResizingAllowed(false);
        table.setRowHeight(30);
        table.setFocusable(false);
        
        return new JScrollPane(table);
    }
}
  • 展示排行榜

3.16 GameUtils.java

点击查看代码
package gomoku.util;

import gomoku.model.Chessboard;

public class GameUtils {
    // 检查是否平局
    public static boolean isDraw(Chessboard chessboard) {
        int size = chessboard.getSize();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (chessboard.getPiece(i, j) == Chessboard.EMPTY) {
                    return false;  // 还有空位,不是平局
                }
            }
        }
        return true;  // 棋盘已满,平局
    }
}
  • 通过检验棋盘上是否还有空格(调用chessboard.getPiece)来判断平局

运行结果展示

1.界面展示

6316df4e500c9b87057fcbc41ed7f76b

2.人机对战过程展示

e33dfa7999016bd754fdf9306a3d5b42

3.人机对战对话框展示

b7a90f753e2ac32676da732a23e591d8

4.人机对战胜利界面展示

3b09c463cbcc84c1a477183281c7122f

5.人人对战过程展示

d5fb75f0e8e053476e6c542b44b7550c

6.人人对战胜利界面展示

08f4f09a8011a7f1c1c988baa1b84426

7.排行榜展示

f57758b57f6b04fa1a72c052cceccba2
e760c59b74d31f5d5e3fdcf69dab3205
e760c59b74d31f5d5e3fdcf69dab3205
532e34b46d9dc0490fb7cafa2792c793

8.对战记录展示

04e727f7677eb1a024d84912727e7eb4

总结

  • 该游戏采用边界布局,主要同通过监听的方式来判断落子的位置,在根据其落子的合法性来判断是否刷新页面,在页面上生成该落子。
  • 人机对战中AI的落子是随机生成的,模式较为简单,可以优化其落子算法,使游戏趣味性更加丰富。
posted @ 2025-11-03 23:08  穗和  阅读(7)  评论(0)    收藏  举报