繼上週實現了C++控制檯版的五子棋之後,這周開始學習Java,順便花了兩三天時間,做出了一直想做的圖形化界面的五子棋小遊戲。同時在原來C++控制檯程序的基礎上對AI的算法進行了一定修正,修復了一些bug,並加入了悔棋的功能
C++五子棋系列傳送門:
C++之簡單五子棋的設計思路
C++之簡單五子棋的語言設計實現
C++簡單五子棋的AI設計及實現
五子棋的算法規則和類的設計在C++相關中講過了,所以現在主要總結一下如何轉成圖形化界面。由於第一次接觸java,頂層設計做的不是很好,還是帶有濃厚的面向過程的設計風格,勉強合格吧。
效果如下
界面主要使用swing組件搭建,具體如下:
MyChessBoard :面板類,繼承JPanel類。主要用來實現存儲棋盤信息,記錄下棋過程和顯示棋局。考慮到各個組件都要對棋盤進行操作,所以對棋局進行記錄操作的相關函數和變量均設爲靜態類成員。具體包括:
- 存儲棋盤信息的15*15二維數組chess[][],用0代表空,1代表白,2代表黑;
- 記錄最近兩步走棋信息的2*2二維數組lastChess[][],用於悔棋;
- 悔棋操作函數reDo(),將lastChess數組中存儲的座標處的棋子標記爲空,然後調用repaint()進行重繪;
- 標誌棋盤是否爲空的布爾變量isEmpty
- 負責接收座標,更新棋盤信息的函數record(int,int);
- clearBoard()函數負責對棋盤進行清除,並初始化相關變量;
除此之外,在構造函數中對各變量進行初始化,lastChess[][]初始化爲全變量-1,有棋子落子時在record函數裏對其進行更新,採用隊列的數據結構,存儲落子的座標。
重寫父類的paintComponent用來完成棋盤和棋子的繪製。
setChess(int,int,int)函數用來接收鼠標點擊的座標和當前所下的棋子顏色,通過點擊座標計算出落子座標,調用record函數進行記錄,調用repaint函數進行重繪。
//面板,用來打印15*15的棋盤和當前在棋盤上的棋子,提供一個15*15的int數組,標記每個節點處的棋子狀態
class MyChessBoard extends JPanel{
protected BufferedImage bg = null;
static public int[][] chess ;//0爲空,1爲白,2爲黑
static protected int [][] lastChess = {{-1,-1},{-1,-1}};
static protected boolean isEmpty = true;
static public void record(int x,int y) {
lastChess[1][0] = lastChess[0][0];
lastChess[1][1] = lastChess[0][1];
lastChess[0][0] = x;
lastChess[0][1] = y;
ChessGame.reserve();
MyControlBoard.redo = false;
MyControlBoard.undo.setText("Undo");
isEmpty = false;
}
// default constructor
public MyChessBoard(){
super();
lastChess[0][0] = -1;
lastChess[0][1] = -1;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
chess = new int[15][15];
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
chess[i][j] = 0;
}
}
try {
bg = ImageIO.read(new File("D:\\administrator-\\Documents\\eclipse-workspace\\practice\\src\\Windows\\renju.png"));
}catch(Exception e) {
e.printStackTrace();
}
repaint();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
setBackground(new Color(222,184,135));
Graphics2D g1 = (Graphics2D)g;
//add background
g1.drawImage(bg, 0, 0,getWidth(),getHeight(), null);
//draw chess board
g1.setColor(Color.red);
BasicStroke seg = new BasicStroke(2,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND);
g1.setStroke(seg);
int width = getWidth();
int height = getHeight();
int vgap = height/16;
int hgap = width/16;
for(int i = 1;i<16;i++) {
g1.drawLine(hgap, i*vgap, 15*hgap, i*vgap);
g1.drawLine(i*hgap,vgap , i*hgap, 15*vgap);
}
//draw chess
int chessWidth = hgap*3/4;
int chessHeight = vgap*3/4;
for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if(chess[i][j] == 2) {
g1.setColor(Color.BLACK);
g1.fillOval((1+j)*hgap-chessWidth/2,(1+i)*vgap-chessHeight/2,chessWidth, chessHeight);
}
else{
if(chess[i][j] == 1) {
g1.setColor(Color.white);
g1.fillOval((1+j)*hgap-chessWidth/2,(1+i)*vgap-chessHeight/2,chessWidth, chessHeight);
}
}
}
}
}
//清空棋盤並重繪
static public void clearBoard() {
isEmpty = true;
lastChess[0][0] = -1;
lastChess[0][1] = -1;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
chess[i][j] = 0;
}
}
}
//悔棋
static public void reDo() {
int count = 0;
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if(chess[i][j] != 0)
count++;
}
}
if(count == 1) {
chess[lastChess[0][0]][lastChess[0][1]] = 0;
ChessGame.reserve();
lastChess[0][0] = -1;
lastChess[0][1] = -1;
isEmpty = true;
}
else{
if(count == 2) isEmpty = true;
chess[lastChess[0][0]][lastChess[0][1]] = 0;
chess[lastChess[1][0]][lastChess[1][1]] = 0;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
lastChess[0][0] = -1;
lastChess[0][1] = -1;
}
}
//繪製一顆給定顏色和座標的棋子
public void setChess(int x,int y,int white) {
int width = getWidth();
int height = getHeight();
int vgap = height/16;
int hgap = width/16;
int chessWidth = hgap*3/4;
int chessHeight = vgap*3/4;
for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if( x >= ((1 + j)*hgap - chessWidth/2) && x <= ((1 + j)*hgap + chessWidth/2)) {
if(y >= ((1 + i)*vgap - chessHeight/2) && y <= ((1 + i)*vgap + chessHeight/2)) {
while(chess[i][j] == 0) {
if(white == 1)
chess[i][j] = 1;
else
chess[i][j] = 2;
record(i, j);
return;
}
}
}
}
}
repaint();
}
}
- MyControlBoard類繼承Jpanel類,負責繪製控制面板。同樣將一些各組件都需要調用的變量設置爲靜態的類成員變量。如標誌棋局是否開局的boolean變量started,用戶執黑還是執白的標誌變量whiteC,控制面板復原函數initialize()函數。MyControlBoard類中加入三個Jbutton類的對象,分別負責接收開局、認輸和悔棋的用戶單擊輸入。通過內部類ButtonAct實現接口ActionListener,來針對按鈕的點擊做出反應。具體反應邏輯參見代碼
- -
//Control Board,控制開局,認輸和投降,分別對應三個布爾變量started,redo,surrendered,並且提供int型變量whiteC標記先後手
@SuppressWarnings("serial")
class MyControlBoard extends JPanel{
protected static JButton start;
protected JButton giveUp;
protected static JButton undo;
protected JLabel b1,b2;
protected ButtonAct cbut = new ButtonAct();
static boolean started = false;
static boolean redo = false;
static boolean surrendered = false;
static int whiteC = 0; // 爲0表示用戶執黑先手,爲1表示用戶執白後走
//constructor
public MyControlBoard(){
super();
this.setBackground(Color.darkGray);
GridLayout bl = new GridLayout(5,1);
this.setLayout(bl);
b1 = new JLabel();
b2 = new JLabel();
start = new JButton("Start");
start.setBackground(Color.yellow);
start.setSize(getWidth(), getHeight()/10);
start.addActionListener(cbut);
giveUp = new JButton("Give up");
giveUp.setBackground(Color.lightGray);
giveUp.setSize(getWidth(), getHeight()/10);
giveUp.addActionListener(cbut);
undo = new JButton("Undo");
undo.setBackground(Color.cyan);
undo.setSize(getWidth(), getHeight()*6/10);
undo.addActionListener(cbut);
add(start);
add(b1);
add(giveUp);
add(b2);
add(undo);
}
//復原爲初始狀態
static public void initialize() {
started = false;
redo = false;
surrendered = false;
whiteC = 0;
start.setText("Start");
undo.setText("Undo");
}
private class ButtonAct implements ActionListener {
public void actionPerformed(ActionEvent e) {
// TODO 自動生成的方法存根
if(e.getActionCommand().equals("Start")) {
start.setText("Started");
started = true;
whiteC = JOptionPane.showConfirmDialog(start, "是否執黑先行?(若選擇執白單擊棋盤後棋局開始)", "先後手確認", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
}
if(e.getActionCommand().equals("Undo")) {
if(started && !MyChessBoard.isEmpty) {
redo = true;
undo.setText("Undone");
MyChessBoard.reDo();
}
}
if(e.getActionCommand().equals("Give up")) {
if(started) {
JOptionPane.showMessageDialog(giveUp, "你已認輸!","結果",JOptionPane.INFORMATION_MESSAGE);
surrendered = true;
started = false;
start.setText("Start");
MyChessBoard.clearBoard();
initialize();
ChessGame.takeTurn = 0;
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
}
}
}
}
}
其實這裏有一些設計上的失誤,因爲之後的棋盤事件偵聽需要綜合棋盤點擊和控制面板點擊進行判定先後手,所以其實將MyControlBoard類設計爲MyChessBoad類的內部類比較好,可以通過設置佈局管理器爲BorderLayout來實現添加子面板。
接下來設計實現ChessGame類,將各個組件組合在一起,實現下棋流程。並在ChessGame類中實現MyChessBoad類的事件偵聽程序。
ChessGame類的靜態變量主要包括標記當前輪到哪方落子的交替標誌量takeTurn和對其進行反轉的函數rserve();record函數每一次記錄的時候都調用reserve函數反轉標誌量,從而使得每次會接受一次鼠標點擊。
在ChessGame中有用來檢查是否有連成五個的CheckFive函數和用來判斷局勢的judge函數。並定義內部類ClickAct繼承MouseAdapter適配器,用來對鼠標點擊做出反應。在該類中定義一個初始化爲真的布爾變量goAI,用來區分先後手,每次鼠標點擊之後,先進行先後手檢測,當棋局已開局、棋盤爲空且用戶棋子顏色與反轉標誌量不一致,則調用AI進行走棋,同時goAI值置爲假,當goAI值爲真時,則先接收用戶點擊,計算出點擊座標對應的棋子座標,調用setChess函數,再調用judge函數判斷局勢,如果沒有獲勝方,再調用AI下棋,再進行局勢判斷,再根據局勢彈出相應對話框或者什麼都不做。
代碼如下:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class ChessGame {
JFrame frame;
MyChessBoard chessBoard;//棋盤面板
MyControlBoard control;//控制面板
static int takeTurn = 0;//交替標誌量,記錄當前輪到哪方落子
//反轉交替標誌量
static public void reserve() {
if(takeTurn == 0)
takeTurn = 1;
else
takeTurn = 0;
}
public ChessGame() {
frame =new JFrame("五子棋");
frame.setSize(850, 710);
control = new MyControlBoard();
frame.getContentPane().add(control,BorderLayout.EAST);
control.setVisible(true);
chessBoard = new MyChessBoard();
}
public void play() {
ClickAct playChess = new ClickAct();
frame.getContentPane().add(chessBoard,BorderLayout.CENTER);
chessBoard.repaint();
chessBoard.setVisible(true);
chessBoard.addMouseListener(playChess);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//判斷棋局形式,返回-1繼續,0表示平局,1表示白棋獲勝,2表示黑棋獲勝
private int judge() {
boolean full = true;
if(checkFive() >= 5) {
return MyChessBoard.chess[MyChessBoard.lastChess[0][0]][MyChessBoard.lastChess[0][1]];
}
loop: for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++)
{
if(MyChessBoard.chess[i][j] == 0){
full = false;
break loop;
}
}
}
if(full) {
return 0;
}
else {
return -1;
}
}
//檢查連子情況
private int checkFive()
{
int x = MyChessBoard.lastChess[0][0], y = MyChessBoard.lastChess[0][1];
if(x <= 0 || x >= 15)
return 1;
if(y <= 0 || y >= 15)
return 1;
int bd[][] = MyChessBoard.chess;
if(MyChessBoard.isEmpty)
return 0;
int count = 1;//計數器,統計同色個數
int sum[] = {0,0,0,0};
boolean locked = false;//邏輯標記量,用來標記是否遇到了非同色節點
//水平方向檢測
for(int i = 1;i < 5 && ((x - i) >= 0) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
{
if(bd[x][y] == bd[x - i][y]) {
count++;
}
else
locked = true;
}
locked = false;
for(int i = 1;i < 5&&((x + i) <= 14) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x + i][y])
count++;
else
locked = true;
sum[0]=count;
if(count >= 5)
return count;
//豎直方向檢測
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y - i) >= 0) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x][y - i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((y + i) <= 14) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x][y + i])
count++;
else
locked = true;
sum[1]=count;
if(count>=5)
return count;
//左上到右下斜向檢測
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y - i) >= 0) && ((x - i) >= 0) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x - i][y - i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((x + i) <= 14) && ((y + i) <= 14) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x + i][y + i])
count++;
else
locked = true;
sum[2] = count;
if(count >= 5)
return count;
//左下到右上斜向檢測
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y + i) <= 14) && ((x - i) >= 0) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x - i][y + i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((x + i) <= 14) && ((y - i) >= 0) && (!locked);i++)//終止循環條件:同色超過4個或觸碰到棋盤邊界或遇到非同色節點
if(bd[x][y] == bd[x + i][y - i])
count++;
else
locked = true;
sum[3] = count;
if(count >= 5)
return count;
return MAX(sum,4);
}
//求最值
private int MAX(int[] a, int n) {
int max = a[0];
for(int i =1; i < n ;i++)
{
if(a[i] > max)
max = a[i];
}
return max;
}
//棋盤的事件響應類
protected class ClickAct extends MouseAdapter{
public void mousePressed(MouseEvent e) {
int x = e.getX();
int y = e.getY();
int state = judge();
AI ai = new AI();
boolean goAI = true;
if(MyControlBoard.started && MyControlBoard.whiteC == 1 && MyChessBoard.isEmpty) {
ai.play();
chessBoard.repaint();
goAI = false;
}
if(MyControlBoard.started && (takeTurn == MyControlBoard.whiteC)&&goAI)
{
chessBoard.setChess(x, y, MyControlBoard.whiteC);
state = judge();
if((state == -1) && (takeTurn != MyControlBoard.whiteC)&&goAI) {
ai.play();
chessBoard.repaint();
state = judge();
// System.out.println("state="+state);
}
switch(state) {
case 1:
if(MyControlBoard.whiteC == 1)
JOptionPane.showMessageDialog(frame, "恭喜:你贏了!","結果",JOptionPane.INFORMATION_MESSAGE);
else
JOptionPane.showMessageDialog(frame, "抱歉:你輸了!","結果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
case 2:
if(MyControlBoard.whiteC == 0)
JOptionPane.showMessageDialog(frame, "恭喜:你贏了!","結果",JOptionPane.INFORMATION_MESSAGE);
else
JOptionPane.showMessageDialog(frame, "抱歉:你輸了!","結果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
case 0:
JOptionPane.showMessageDialog(frame, "平局!","結果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
default:
break;
}
}
}
}
}
AI類和main類實現下篇繼續