簡單五子棋對戰(zhàn)(AI生成)
簡單五子棋對戰(zhàn)(AI生成)
- 簡單五子棋對戰(zhàn)(AI生成)
- 一、目標(biāo)
- 二、包結(jié)構(gòu)
- 三、代碼設(shè)計(jì)與講解
- 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
- 運(yùn)行結(jié)果展示
- 總結(jié)
一、目標(biāo)
通過AI,生成五子棋游戲的代碼,并理解代碼含義。
二、包結(jié)構(gòu)
gomoku/
├── Main.java // 程序入口
├── model/ // 數(shù)據(jù)模型層
│ ├── Chessboard.java // 棋盤數(shù)據(jù)模型
│ ├── Player.java // 玩家抽象類
│ ├── HumanPlayer.java // 人類玩家
│ ├── AIPlayer.java // AI玩家
│ ├── GameRecord.java // 儲存游戲數(shù)據(jù)
│ └── RankList.java // 排行榜對話框
├── view/ . // 視圖層
│ ├── MainFrame.java // 主窗口
│ ├── ChessboardPanel.java // 棋盤繪制面板
│ ├── MenuPanel.java // 菜單面板
│ ├── TimerPanel.java // 計(jì)時器面板
│ ├── BoardPreview.java // 預(yù)覽棋盤對話框
│ ├── TowPlayerInputDialog.java // 人人對戰(zhàn)玩家輸入對話框
│ ├── UsernameDialog.java // 人機(jī)對戰(zhàn)玩家輸入對話框
│ └── RankDialog.java // 排行榜對話框
└── util/ // 工具類
└── GameUtils.java // 游戲工具類
三、代碼設(shè)計(jì)與講解
3.1 Main.java
點(diǎn)擊查看代碼
package gomoku;
import gomoku.view.MainFrame;
import javax.swing.SwingUtilities;
public class Main {
public static void main(String[] args) {
// 在事件調(diào)度線程中啟動GUI
SwingUtilities.invokeLater(() -> {
MainFrame mainFrame = new MainFrame("五子棋");
mainFrame.setVisible(true); // 必須調(diào)用此方法,窗口才能顯示
});
}
}
-
使用多線程來確保程序的同時運(yùn)行性
-
使用SwingUtilities.invokeLater()來確保每一步操作在正確的線程上安全地執(zhí)行
3.2 Chessboard.java
點(diǎn)擊查看代碼
package gomoku.model;
public class Chessboard {
// 棋子狀態(tài)常量
public static final int EMPTY = 0;
public static final int BLACK = 1;
public static final int WHITE = 2;
private int[][] board; // 棋盤數(shù)組
private int size; // 棋盤大小(如15x15)
private int currentPlayer; // 當(dāng)前落子玩家(默認(rèn)黑棋先行)
private int lastRow = -1; // 最后落子的行
private int lastCol = -1; // 最后落子的列
private boolean isGameOver; // 游戲是否結(jié)束
private int winner; // 獲勝者(EMPTY表示未分勝負(fù))
// 構(gòu)造方法:初始化指定大小的棋盤
public Chessboard(int size) {
this.size = size;
this.board = new int[size][size];
this.currentPlayer = BLACK; // 黑棋先行
this.isGameOver = false;
this.winner = EMPTY;
}
// 落子方法:在指定位置放置當(dāng)前玩家的棋子(成功返回true,失敗返回false)
public boolean placePiece(int row, int col) {
// 校驗(yàn)條件:游戲未結(jié)束 + 位置合法 + 該位置為空
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;
// 方向數(shù)組:上、下、左、右、左上、右下、右上、左下(共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; // 當(dāng)前位置已占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];
}
// 連續(xù)5個及以上則獲勝
if (count >= 5) return true;
}
return false;
}
// 切換當(dāng)前玩家(黑→白,白→黑)- 僅在游戲未結(jié)束時有效
public void switchPlayer() {
if (!isGameOver) {
currentPlayer = (currentPlayer == BLACK) ? WHITE : BLACK;
}
}
// 重置棋盤(清空所有棋子,恢復(fù)初始狀態(tài))
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;
}
// 手動設(shè)置游戲結(jié)束狀態(tài)(用于特殊場景)
public void setGameOver(boolean gameOver) {
isGameOver = gameOver;
}
}
-
該代碼用于更新棋盤、判斷棋子位置的合法性、檢測是否勝利、切換玩家、重置棋盤
-
使用構(gòu)造函數(shù)來初始化棋盤大小和棋子的樣式
-
使用多種判斷方法,使代碼更加簡潔
3.3 Player.java
點(diǎn)擊查看代碼
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();
// 設(shè)置落子后的回調(diào)接口
public interface MoveListener {
void onMoveMade
(int row, int col);
}
}
-
玩家抽象類,設(shè)定玩家下棋的行為
-
設(shè)置接口MoveListener(響應(yīng)事件),監(jiān)聽玩家下棋棋子的位置(監(jiān)聽成功之后,判斷勝負(fù),刷新面板,切換回合)
-
設(shè)置落子后的回調(diào)接口時默認(rèn)隱藏了public abstract修飾符
3.4 HumanPlayer.java
點(diǎn)擊查看代碼
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() {
// 人類玩家通過界面點(diǎn)擊落子,此處空實(shí)現(xiàn)
}
// 處理人類玩家的點(diǎn)擊
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,可以保存外部傳入的回調(diào)“通信渠道”,使之持有任何 MoveListener 接口的實(shí)現(xiàn)類的對象(監(jiān)聽位置)
-
人類對戰(zhàn)時背后邏輯判斷
-
人類玩家的落子觸發(fā)方式是 “鼠標(biāo)點(diǎn)擊”(被動觸發(fā)),而 makeMove() 是抽象父類Player定義的 “主動落子方法”(用于 AI 主動找位置落子),所以是空實(shí)現(xiàn);而畫圖會在ChessboardPanel中實(shí)現(xiàn)
-
handleClick()判斷落子的合法性,并監(jiān)聽落子位置
3.5 AIPlayer.java
點(diǎn)擊查看代碼
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; // 落子后的回調(diào)接口
private Random random; // 隨機(jī)數(shù)生成器
private int delayMillis = 800; // 延遲時間,單位:毫秒(可調(diào)整)
// 構(gòu)造方法:接收顏色、棋盤和回調(diào)
public AIPlayer(int color, Chessboard chessboard, MoveListener listener) {
this.color = color;
this.chessboard = chessboard;
this.listener = listener;
this.random = new Random(); // 初始化隨機(jī)數(shù)生成器
}
// 帶延遲的AI隨機(jī)落子邏輯
public void makeMove() {
// 校驗(yàn):游戲未結(jié)束且當(dāng)前是AI回合
if (!chessboard.isGameOver() && chessboard.getCurrentPlayer() == color) {
// 使用新線程實(shí)現(xiàn)延遲,避免阻塞UI
new Thread(() -> {
try {
// AI"思考"延遲
Thread.sleep(delayMillis);
// 在UI線程中執(zhí)行落子操作(Swing要求UI操作在事件調(diào)度線程中執(zhí)行)
SwingUtilities.invokeLater(() -> {
// 獲取所有空位置
List<int[]> emptyPositions = findAllEmptyPositions();
if (!emptyPositions.isEmpty()) {
// 從空位置中隨機(jī)選擇一個
int randomIndex = random.nextInt(emptyPositions.size());
int[] randomPos = emptyPositions.get(randomIndex);
int row = randomPos[0];
int col = randomPos[1];
// 落子并通知回調(diào)
if (chessboard.placePiece(row, col)) {
listener.onMoveMade(row, col);
}
}
});
} catch (InterruptedException e) {
// 處理中斷異常(恢復(fù)中斷狀態(tài))
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;
}
// 設(shè)置延遲時間(允許外部調(diào)整)
public void setDelayMillis(int millis) {
if (millis > 0) {
this.delayMillis = millis;
}
}
// 落子回調(diào)接口
public interface MoveListener {
void onMoveMade(int row, int col);
}
}
-
創(chuàng)建新的線程對象,避免 AI 延遲落子阻塞 UI 線程
-
Thread.sleep(800) 阻塞面板800ns,營造一種AI在思考的感覺
-
使用List來存儲空為位置,并使用random,在空位置中隨機(jī)生成落子,可以修改此處代碼,使機(jī)器的落子更加復(fù)雜
3.6 GameRecord.java
點(diǎn)擊查看代碼
package gomoku.model;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 游戲記錄模型,存儲單局游戲的信息,包括用戶名和棋盤狀態(tài)
*/
public class GameRecord {
private String gameType; // "人機(jī)對戰(zhàn)" 或 "人人對戰(zhàn)"
private LocalDateTime startTime; // 開始時間
private LocalDateTime endTime; // 結(jié)束時間
private String winner; // 獲勝方 ("黑棋" 或 "白棋")
private Duration duration; // 游戲時長
private String username; // 玩家用戶名
private int[][] boardState; // 結(jié)束時的棋盤狀態(tài)
private int boardSize; // 棋盤大小
public GameRecord(String gameType, int boardSize) {
this.gameType = gameType;
this.startTime = LocalDateTime.now();
this.boardSize = boardSize;
}
// 結(jié)束游戲并計(jì)算時長,保存棋盤狀態(tài)
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;
// 深拷貝棋盤狀態(tài)
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; }
}
-
結(jié)束游戲時,創(chuàng)建獨(dú)立的棋盤副本,記錄落子位置、時間和勝者
-
對原數(shù)組的每一行,使用System.arraycopy來把原數(shù)組每一行元素直接復(fù)制到新數(shù)組的對應(yīng)行
3.7 RankList.java
點(diǎn)擊查看代碼
package gomoku.model;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 排行榜數(shù)據(jù)管理類,負(fù)責(zé)記錄和排序游戲記錄
*/
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);
}
// 獲取人機(jī)對戰(zhàn)排行榜 (按時長升序,即最快獲勝)
public List<GameRecord> getAiRankList() {
return allRecords.stream()
.filter(r -> r.getGameType().equals("人機(jī)對戰(zhàn)"))
.sorted(Comparator.comparing(GameRecord::getDuration))
.collect(Collectors.toList());
}
// 獲取人人對戰(zhàn)排行榜
public List<GameRecord> getPvpRankList() {
return allRecords.stream()
.filter(r -> r.getGameType().equals("人人對戰(zhàn)"))
.sorted(Comparator.comparing(GameRecord::getDuration))
.collect(Collectors.toList());
}
// 獲取總排行榜
public List<GameRecord> getTotalRankList() {
return allRecords.stream()
.sorted(Comparator.comparing(GameRecord::getDuration))
.collect(Collectors.toList());
}
}
-
使用List來儲存記錄每一局對戰(zhàn)數(shù)據(jù),添加到排行榜上
-
使用Stream流+Collection來比較對局時間,進(jìn)行排序
3.8 MainFrame.java
點(diǎn)擊查看代碼
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 {
// 全局樣式配置(統(tǒng)一管理,方便修改)
private static final Color BG_COLOR = new Color(245, 240, 230); // 米白色背景
private static final Color BOARD_COLOR = new Color(210, 180, 140); // 木質(zhì)棋盤色
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);
// 新增:調(diào)整窗口和棋盤尺寸,確保足夠顯示
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);
// 基礎(chǔ)窗口配置
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT); // 使用調(diào)整后的高度
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);
// 關(guān)鍵:設(shè)置棋盤面板的首選尺寸和最小尺寸
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);
// 中間區(qū)域:使用面板包裹棋盤,確保居中且不被壓縮
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("監(jiān)聽器收到:人人對戰(zhàn)");
startPVPGame();
}
@Override
public void onPvaiSelected() {
System.out.println("監(jiān)聽器收到:人機(jī)對戰(zhàn)");
startPVAGame();
}
@Override
public void onRestartSelected() {
System.out.println("監(jiān)聽器收到:重新開始");
restartGame();
}
@Override
public void onRankSelected() {
System.out.println("監(jiān)聽器收到:排行榜");
new RankDialog(MainFrame.this).setVisible(true);
}
@Override
public void onExitSelected() {
System.out.println("監(jiān)聽器收到:退出");
System.exit(0);
}
});
chessPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (!gameStarted) return;
// 優(yōu)化:動態(tài)計(jì)算棋盤坐標(biāo),適配實(shí)際面板尺寸
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("人人對戰(zhàn)", 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("人人對戰(zhàn) · 黑方回合");
gameStarted = true;
System.out.println("已進(jìn)入人人對戰(zhàn)模式");
}
private void startPVAGame() {
isVsAI = true;
chessboard.reset();
currentRecord = new GameRecord("人機(jī)對戰(zhàn)", 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("人機(jī)對戰(zhàn) · 您(黑棋)回合");
gameStarted = true;
System.out.println("已進(jìn)入人機(jī)對戰(zhàn)模式");
}
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(),
"游戲結(jié)束", 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(),
"游戲結(jié)束", JOptionPane.INFORMATION_MESSAGE);
}
RankList.getInstance().addRecord(currentRecord);
return;
}
if (GameUtils.isDraw(chessboard)) {
timerPanel.stop();
menuPanel.updateStatus("平局!");
gameStarted = false;
JOptionPane.showMessageDialog(this, "本局戰(zhàn)平!", "游戲結(jié)束", JOptionPane.INFORMATION_MESSAGE);
return;
}
chessboard.switchPlayer();
if (isVsAI) {
menuPanel.updateStatus("人機(jī)對戰(zhàn) · AI(白棋)思考中...");
aiPlayer.makeMove();
menuPanel.updateStatus("人機(jī)對戰(zhàn) · 您(黑棋)回合");
} else {
String currentPlayer = chessboard.getCurrentPlayer() == Chessboard.BLACK ? "黑方" : "白方";
menuPanel.updateStatus("人人對戰(zhàn) · " + currentPlayer + "回合");
}
}
// 優(yōu)化棋盤繪制邏輯,確保完整顯示
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);
// 關(guān)鍵:動態(tài)計(jì)算繪制區(qū)域,適配面板實(shí)際大小
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);
// 繪制網(wǎng)格
drawBoardGrid(g2d, startX, startY, cellSize);
// 繪制棋子
drawChessPieces(g2d, startX, startY, cellSize);
}
// 繪制棋盤網(wǎng)格(使用動態(tài)計(jì)算的參數(shù))
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);
}
}
}
// 繪制棋子(使用動態(tài)計(jì)算的參數(shù))
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);
// 最后一步標(biāo)記
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("五子棋 - 優(yōu)化版").setVisible(true);
});
}
}
-
MainFrame繪制圖形界面面板
-
initComponents()初始化面板,可以在這里修改面板大小
-
使用邊界布局,添加組件,在圖形界面按動時,通過監(jiān)聽功能,運(yùn)行內(nèi)在邏輯
-
兩種模式:人人對戰(zhàn)、人機(jī)對戰(zhàn)
3.9 ChessboardPanel.java
點(diǎn)擊查看代碼
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;
// 新增方法:設(shè)置最后落子位置
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); // 繪制最后落子標(biāo)記
}
// 新增方法:繪制最后落子標(biāo)記(紅色圓點(diǎn))
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) {
// 計(jì)算點(diǎn)擊位置對應(yīng)的棋盤坐標(biāo)
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();
// 繪制網(wǎng)格線
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棋盤的星位坐標(biāo)
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);
}
}
}
}
// 繪制小圓點(diǎn)(用于星位)
private void drawDot(Graphics g, int x, int y, int radius) {
g.fillOval(x - radius / 2, y - radius / 2, radius, radius);
}
// 設(shè)置點(diǎn)擊監(jiān)聽器
public void setClickListener(ClickListener listener) {
this.clickListener = listener;
}
// 點(diǎn)擊監(jiān)聽器接口
@FunctionalInterface
public interface ClickListener {
void onClick(int row, int col);
}
}
-
繪制落子后的圖形界面(棋盤、棋子),對于最后落子加個紅色邊框
-
落子成功后,MainFrame 調(diào)用 setLastMove 更新最后落子位置,并調(diào)用 repaint()
3.10 MenuPanel.java
點(diǎn)擊查看代碼
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;
// 監(jiān)聽器接口(保持不變)
public interface MenuActionListener {
void onPvpSelected();
void onPvaiSelected();
void onRestartSelected();
void onRankSelected();
void onExitSelected();
}
// 構(gòu)造方法(核心修改:解決布局?jǐn)D壓問題)
public MenuPanel() {
// 1. 改用BoxLayout(垂直布局):先放按鈕行,再放狀態(tài)標(biāo)簽,避免橫向擠壓
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBackground(new Color(245, 240, 230));
// 2. 強(qiáng)制面板最小尺寸(確保不會被壓縮到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,進(jìn)一步減少總寬度)
int gap = 10;
buttonContainer.add(Box.createHorizontalGlue()); // 左側(cè)空白(自動填充,使按鈕居中)
buttonContainer.add(createStyledButton("人人對戰(zhàn)"));
buttonContainer.add(Box.createRigidArea(new Dimension(gap, 0))); // 按鈕間距
buttonContainer.add(createStyledButton("人機(jī)對戰(zhàn)"));
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()); // 右側(cè)空白(自動填充)
// 4. 添加按鈕容器到面板(垂直方向間距10px)
add(Box.createVerticalStrut(10)); // 頂部空白
add(buttonContainer);
add(Box.createVerticalStrut(10)); // 按鈕和標(biāo)簽間距
// 5. 狀態(tài)標(biāo)簽(保持不變,但調(diào)整寬度適配面板)
statusLabel = new JLabel("請選擇對戰(zhàn)模式", SwingConstants.CENTER);
statusLabel.setFont(MAIN_FONT);
statusLabel.setForeground(BUTTON_COLOR);
statusLabel.setPreferredSize(new Dimension(800, 30));
add(statusLabel);
// 調(diào)試:確認(rèn)組件已添加(運(yùn)行后看控制臺)
System.out.println("MenuPanel組件數(shù):" + getComponentCount());
System.out.println("按鈕數(shù):" + buttonContainer.getComponentCount());
}
// 按鈕創(chuàng)建方法(保持不變,僅寬度已在常量中調(diào)整)
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);
// 狀態(tài)色判斷
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) {}
});
// 按鈕點(diǎn)擊事件(保持不變)
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if ("人人對戰(zhàn)".equals(text) && listener != null) {
listener.onPvpSelected();
} else if ("人機(jī)對戰(zhàn)".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;
}
// 設(shè)置監(jiān)聽器(保持不變)
public void setMenuListener(MenuActionListener listener) {
this.listener = listener;
}
// 更新狀態(tài)(保持不變)
public void updateStatus(String text) {
if (statusLabel != null) {
statusLabel.setText(text);
}
}
}
-
繪制最上部的菜單欄
-
通過按鈕來實(shí)現(xiàn)人人對戰(zhàn)/人機(jī)對戰(zhàn)
-
使用多種方法,增強(qiáng)了封裝性
3.11 TimerPanel .java
點(diǎn)擊查看代碼
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);
}
// 開始計(jì)時
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);
}
}
// 重置計(jì)時
public void reset() {
if (timer != null) {
timer.cancel();
timer = null;
}
seconds = 0;
timerLabel.setText("游戲時間:00:00");
}
// 停止計(jì)時
public void stop() {
if (timer != null) {
timer.cancel();
timer = null;
}
}
}
- 記錄游戲時長,并使用timerLabel.setText(String.format("游戲時間:%02d:%02d", minutes, secs))來規(guī)范化展示
3.12 BoardPreview.java
點(diǎn)擊查看代碼
package gomoku.view;
import gomoku.model.GameRecord;
import gomoku.model.Chessboard;
import javax.swing.*;
import java.awt.*;
/**
* 棋盤預(yù)覽對話框,用于顯示歷史棋局
*/
public class BoardPreviewDialog extends JDialog {
private GameRecord record;
private int cellSize = 20; // 預(yù)覽棋盤的單元格大小
private int margin = 10; // 邊距
// 正確的構(gòu)造函數(shù),接受父窗口和游戲記錄
public BoardPreviewDialog(Dialog parent, GameRecord record) {
super(parent, "棋局預(yù)覽", true);
this.record = record;
initComponents();
setupLayout();
pack();
setLocationRelativeTo(parent);
}
private void initComponents() {
// 創(chuàng)建信息面板顯示游戲詳情
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()));
// 創(chuàng)建棋盤預(yù)覽面板
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);
// 添加關(guān)閉按鈕
JButton closeBtn = new JButton("關(guān)閉");
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);
}
// 繪制預(yù)覽棋盤
private void drawPreviewBoard(Graphics g) {
int[][] board = record.getBoardState();
int size = record.getBoardSize();
// 繪制網(wǎng)格
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);
}
}
}
}
}
-
使用邊界布局
-
通過點(diǎn)擊,調(diào)用GameRecord展示歷史數(shù)據(jù)
3.13 TowPlayerInputDialog .java
點(diǎn)擊查看代碼
package gomoku.view;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 雙人對戰(zhàn)結(jié)束時輸入兩個玩家用戶名的對話框
*/
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, "游戲結(jié)束 - 輸入玩家信息", 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("確認(rèn)");
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);
// 重置網(wǎng)格寬度
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);
}
/**
* 獲取玩家輸入的用戶名,為空時使用默認(rèn)值
*/
private String getPlayerName(String input, String defaultName) {
String name = input.trim();
return name.isEmpty() ? defaultName : name;
}
/**
* 顯示對話框并返回結(jié)果
* @return 長度為2的數(shù)組,[0]是黑方用戶名,[1]是白方用戶名
*/
public String[] showDialog() {
setVisible(true);
return result;
}
}
-
雙人對戰(zhàn)結(jié)束時輸入兩個玩家用戶名的對話框,使用String[]來存儲用戶名稱
-
使用邊界布局
-
最開始默認(rèn)顯示黑方玩家/白方玩家,可以在文本框中輸入不同用戶名
-
使用GridBagLayout自由調(diào)整界面布局,使界面布局更加美觀
3.14 UsernameDialog.java
點(diǎn)擊查看代碼
package gomoku.view;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 游戲結(jié)束后輸入用戶名的對話框
*/
public class UsernameDialog extends JDialog {
private JTextField usernameField;
private String username;
private boolean confirmed;
public UsernameDialog(Frame parent) {
super(parent, "游戲結(jié)束", 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));
// 提示標(biāo)簽
JLabel label = new JLabel("請輸入您的用戶名:");
panel.add(label);
// 輸入框
usernameField = new JTextField();
usernameField.setText("玩家1"); // 默認(rèn)用戶名
panel.add(usernameField);
// 按鈕面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton confirmButton = new JButton("確認(rèn)");
confirmButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
username = usernameField.getText().trim();
if (username.isEmpty()) {
username = "匿名玩家";
}
confirmed = true;
dispose();
}
});
buttonPanel.add(confirmButton);
// 布局設(shè)置
getContentPane().setLayout(new BorderLayout());
getContentPane().add(panel, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
}
// 顯示對話框并返回用戶名
public String showDialog() {
setVisible(true);
return confirmed ? username : "匿名玩家";
}
}
-
人機(jī)模式下的玩家名稱輸入框,默認(rèn)顯示玩家1
-
邊界布局
-
使用setVisible(true)來鎖定界面,直到點(diǎn)擊確定/取消才結(jié)束
-
使用confirmed來標(biāo)記,判斷返回的用戶名
3.15 RankDialog .java
點(diǎn)擊查看代碼
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("人機(jī)對戰(zhàn)", createRankTable("人機(jī)對戰(zhàn)"));
tabbedPane.addTab("人人對戰(zhàn)", createRankTable("人人對戰(zhàn)"));
tabbedPane.addTab("總排行榜", createRankTable("全部"));
}
private void setupLayout() {
setLayout(new BorderLayout());
add(tabbedPane, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
JButton closeBtn = new JButton("關(guān)閉");
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(closeBtn);
add(buttonPanel, BorderLayout.SOUTH);
}
private JComponent createRankTable(String type) {
// 修正數(shù)組初始化方式
String[] columns = type.equals("人人對戰(zhàn)") ?
new String[]{"排名", "黑方玩家", "白方玩家", "獲勝方", "時長", "日期", "操作"} :
new String[]{"排名", "玩家", "游戲類型", "獲勝方", "時長", "日期", "操作"};
// 確保變量名唯一,不重復(fù)
DefaultTableModel tableModel = new DefaultTableModel(columns, 0) {
// 為內(nèi)部類添加序列化版本號
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 "人機(jī)對戰(zhàn)":
records = rankList.getAiRankList();
break;
case "人人對戰(zhàn)":
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("人人對戰(zhàn)")) {
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);
}
};
// 設(shè)置列寬
TableColumnModel columnModel = table.getColumnModel();
if (type.equals("人人對戰(zhàn)")) {
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
點(diǎn)擊查看代碼
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; // 棋盤已滿,平局
}
}
- 通過檢驗(yàn)棋盤上是否還有空格(調(diào)用chessboard.getPiece)來判斷平局
運(yùn)行結(jié)果展示
1.界面展示

2.人機(jī)對戰(zhàn)過程展示

3.人機(jī)對戰(zhàn)對話框展示

4.人機(jī)對戰(zhàn)勝利界面展示

5.人人對戰(zhàn)過程展示

6.人人對戰(zhàn)勝利界面展示

7.排行榜展示




8.對戰(zhàn)記錄展示

總結(jié)
- 該游戲采用邊界布局,主要同通過監(jiān)聽的方式來判斷落子的位置,在根據(jù)其落子的合法性來判斷是否刷新頁面,在頁面上生成該落子。
- 人機(jī)對戰(zhàn)中AI的落子是隨機(jī)生成的,模式較為簡單,可以優(yōu)化其落子算法,使游戲趣味性更加豐富。

浙公網(wǎng)安備 33010602011771號