简单五子棋对战(AI生成)
简单五子棋对战(AI生成)
- 简单五子棋对战(AI生成)
- 一、目标
- 二、包结构
- 三、代码设计与讲解
- 3.1 Main.java
- 3.2 Chessboard.java
- 3.3 Player.java
- 3.4 HumanPlayer.java
- 3.5 AIPlayer.java
- 3.6 GameRecord.java
- 3.7 RankList.java
- 3.8 MainFrame.java
- 3.9 ChessboardPanel.java
- 3.10 MenuPanel.java
- 3.11 TimerPanel .java
- 3.12 BoardPreview.java
- 3.13 TowPlayerInputDialog .java
- 3.14 UsernameDialog.java
- 3.15 RankDialog .java
- 3.16 GameUtils.java
- 运行结果展示
- 总结
一、目标
通过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.界面展示

2.人机对战过程展示

3.人机对战对话框展示

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

5.人人对战过程展示

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

7.排行榜展示




8.对战记录展示

总结
- 该游戏采用边界布局,主要同通过监听的方式来判断落子的位置,在根据其落子的合法性来判断是否刷新页面,在页面上生成该落子。
- 人机对战中AI的落子是随机生成的,模式较为简单,可以优化其落子算法,使游戏趣味性更加丰富。

浙公网安备 33010602011771号