1. 程式人生 > >(Java專案)人機五子棋對戰

(Java專案)人機五子棋對戰

本文將在控制檯五子棋的基礎上繼續完善,改寫成具有介面的人機對戰五子棋遊戲。

專案主要內容

  • 1、書寫棋盤介面,關鍵是繪製棋盤和棋子
  • 2、點選棋盤下棋,繫結事件,書寫機器落子核心邏輯程式碼。關鍵是判斷輸贏及計算每個空位評分,確定每一步機器落子位置。

其他都是細節問題,筆者不打算贅述,具體可以參看筆者的原始碼。

這個評分表演算法的大致思路如博文中筆者描述,五子棋棋盤15X15大小,橫豎斜四個方向共有572個五元組,給每個五元組一個評分,這個五元組將為它的每個位置貢獻的分數就是這個五元組自身的得分。對整個棋盤來說,每個位置的得分就是該位置所在的橫豎斜四個方向的所有五元組的得分之和。然後從所有空位置中選得分最高的位置就是機器落子位置了!!

筆者話費兩天時間完成了人機五子棋程式碼實現,得出的結果正如這位博主所說,我很驚訝,基本被機器打敗了。。

效果圖

下面是機器先手時,與筆者的對局,結果:筆者輸了。。 在這裡插入圖片描述

專案開發軟體環境 Windows10+Java8+記事本

專案程式碼架構 在這裡插入圖片描述 在這裡插入圖片描述 上面圖片來源自百度圖片。

詳細原始碼 UI.java

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

//介面類,這是遊戲主體框架
public class UI{
	
	private JFrame frame;//五子棋遊戲視窗
	
	//五子棋盤【關鍵】
	private Chessboard chessboard = new Chessboard();//五子棋盤
	//*****五子棋業務邏輯【關鍵】	
	private Chess chess = new Chess();	

	private JMenuBar menu;//選單欄
	private JMenu option;//選單欄中的“選項”選單
	private Action replayOption;//“選項”下拉項中的“重玩一盤”選項
	private Action AIFirstOption;//“選項”下拉項中的“機器先手”選項
	private Action HumanFirstOption;//“選項”下拉項中的“人類先手”選項

	//遊戲執行入口
	public static void main(String[] args){
		new UI().init();
	}	

	//完成五子棋遊戲介面
	public void init(){

		frame = new JFrame("人機對戰五子棋");//建立遊戲介面視窗
		menu = new JMenuBar();//建立選單欄
		option = new JMenu("選項");//建立選單欄中的“選項”選單
		
		//把“選項”選單加入到選單欄
		menu.add(option);
		
		//把“重玩一盤”、“機器先手”、“人類先手”加入“選項”下拉項中
		replayOptionInit();
		option.add(replayOption);
		AIFirstOptionInit();
		option.add(AIFirstOption);
		HumanFirstOptionInit();
		option.add(HumanFirstOption);		

		frame.setJMenuBar(menu);//把menu設定為frame的選單欄
		frame.add(chessboard);//把五子棋盤加入到frame

		//初始化棋盤
		chessboard.init();
		chess.init();	
		
		//【【【最核心】】】繫結滑鼠事件,要下棋了,為了避免寫無用的抽象方法的實現,用介面卡
		chessboard.addMouseListener(new MouseAdapter(){
			public void mouseClicked(MouseEvent e){
				//滑鼠點選引發下棋事件,處理下棋事件比較繁瑣,為此開一個方法
				play(e);
			}
		});	

		//設定frame視窗左上角圖示
		frame.setIconImage(frame.getToolkit().getImage("image/gobang.png"));
		frame.setSize(518, 565);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		//frame.pack();
		frame.setVisible(true);
	}
	
	

	//“重玩一盤”選項繫結相應的處理事件
	public void replayOptionInit(){
		replayOption = new AbstractAction("重玩一盤", new ImageIcon("image/replay.png")){
			public void actionPerformed(ActionEvent e){
				chessboard.init();//介面方面:初始化重來
				chess.init();//邏輯業務方面:初始化重來
			}
		};
	}

	//“機器先手”選項繫結相應的處理事件
	public void AIFirstOptionInit(){
		AIFirstOption = new AbstractAction("機器先手", new ImageIcon("image/robot.png")){
			public void actionPerformed(ActionEvent e){
				//棋盤還沒有落子的時候可以選擇“機器先手”,一旦有落子,選擇“機器先手”失效
				if(chessboard.isEmpty()){
					Chess.FIRST = -1;
					//機器先手,則先在中間位置下一個棋子
					chessboard.addChessman(7, 7, -1);
					chess.addChessman(7, 7, -1);
				}
			}
		};
	}

	//“人類先手”選項繫結相應的處理事件
	public void HumanFirstOptionInit(){
		HumanFirstOption = new AbstractAction("人類先手", new ImageIcon("image/human.png")){
			public void actionPerformed(ActionEvent e){
				//棋盤還沒有落子的時候可以選擇“人類先手”,一旦有落子,選擇“人類先手”失效
				if(chessboard.isEmpty()){	
					Chess.FIRST = 1;
				}
			}
		};
	}
	
	//【【【核心業務邏輯】】】處理滑鼠落子事件
	public void play(MouseEvent e){
		int cellSize = chessboard.getCellSize();//每個格子的邊長
		int x = (e.getX() - 5) / cellSize;//畫素值轉換成棋盤座標
		int y = (e.getY() - 5) / cellSize;//畫素值轉換成棋盤座標
		//判斷落子是否合法
		boolean isLegal = chess.isLegal(x, y);
		//如果落子合法
		if(isLegal){
			chessboard.addChessman(x, y, 1);//介面方面加一個棋子
			chess.addChessman(x, y, 1);//邏輯業務方面加一個棋子
			
			//判斷人類是否勝利
			if(chess.isWin(x, y, 1)){
				JOptionPane.showMessageDialog(frame, "人類獲勝", "Congratulations,您贏了!", JOptionPane.PLAIN_MESSAGE);
				chessboard.init();
				chess.init();
				return;
			}
			
			//機器落子
			Location loc = chess.searchLocation();
			chessboard.addChessman(loc);
			chess.addChessman(loc.getX(), loc.getY(), loc.getOwner());

			//判斷機器是否勝利
			if(chess.isWin(loc.getX(), loc.getY(), -1)){
				JOptionPane.showMessageDialog(frame, "機器獲勝", "Congratulations,您輸了!", JOptionPane.PLAIN_MESSAGE);
				chessboard.init();
				chess.init();
				return;
			}
		}
	}
	
}

Chessboard.java

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

import java.util.*;

public class Chessboard extends JPanel{

	public static final int CHESSBOARD_SIZE = 15;//棋盤大小15X15
	private ArrayList<Location> locationList = new ArrayList<>();//棋盤上所有可以落子的位置座標等資訊
	private Color backgroundColor = new Color(255, 245, 186);//棋盤背景色
	private Color lineColor = new Color(66, 66, 66);//棋盤線條顏色
	private int margin = 20;//棋盤邊緣長度
	//private int cellSize = (this.getWidth() - 2*margin)/(CHESSBOARD_SIZE - 1);//每個格子的邊長,這麼做是錯的!!

	//初始化棋盤
	public void init(){
		locationList.clear();
		repaint();
	}
	
	//覆蓋paint方法
	public void paint(Graphics g){
		super.paint(g);
		drawChessboard(g);
		drawChessman(g);
	}

	//畫棋盤
	public void drawChessboard(Graphics g){
		//先畫背景
		g.setColor(backgroundColor);
		g.fillRect(0, 0, this.getWidth(), this.getHeight());

		//畫線
		g.setColor(lineColor);
		int cellSize = (this.getWidth() - 2*margin)/(CHESSBOARD_SIZE - 1);//每個格子的邊長
		for(int i = 0; i < CHESSBOARD_SIZE; i++){
			g.drawLine(margin, margin + i*cellSize, this.getWidth() - margin, margin + i*cellSize);//畫橫線
			g.drawLine(margin + i*cellSize, margin, margin + i*cellSize, this.getHeight() - margin);//畫縱線
		}
	}
	
	//畫棋子
	public void drawChessman(Graphics g){
		for(int i = 0; i < locationList.size(); i++){
			Location loc = locationList.get(i);
			//根據先後手設定棋子為黑色和白色
			if(loc.getOwner() == Chess.FIRST){
				g.setColor(Color.BLACK);	
			}else{
				g.setColor(Color.WHITE);
			}
			
			int cellSize = (this.getWidth() - 2*margin)/(CHESSBOARD_SIZE - 1);//每個格子的邊長
			//畫棋子
			g.fillOval(margin + cellSize*loc.getX() - cellSize/2, margin + cellSize*loc.getY() - cellSize/2, cellSize, cellSize);
			
		}
	}

	//落子
	public void addChessman(int x, int y, int owner){
		locationList.add(new Location(x, y, owner));
		repaint();
	}

	//落子
	public void addChessman(Location loc){
		locationList.add(loc);
		repaint();
	}

	//計算棋盤每個小格子的大小
	public int getCellSize(){
		return (this.getWidth() - 2*margin)/(CHESSBOARD_SIZE - 1);
	}
	
	//判斷棋盤是否還沒有棋子
	public boolean isEmpty(){
		return locationList.size() == 0 ? true : false;
	}
}

Location.java


public class Location{

	private int x;//某個棋盤位置橫座標,0-14
	private int y;//某個棋盤位置縱座標,0-14
	
	private int owner;//佔據該位置的棋手方,1是人類,-1是機器,0是空
	private int score;//對該位置的打的分數

	public Location(){}
	public Location(int x, int y, int owner){
		this.x = x;
		this.y = y;
		this.owner = owner;
	}
	public Location(int x, int y, int owner, int score){
		this(x, y, owner);
		this.score = score;
	}

	public int getX(){return this.x;}
	public void setX(int x){this.x = x;}
	
	public int getY(){return this.y;} 
	public void setY(int y){this.y = y;}

	public int getOwner(){return this.owner;}
	public void setOwner(int owner){this.owner = owner;}

	public int getScore(){return this.score;}
	public void setScore(int score){this.score = score;}
	

}

Chess.java


//下棋業務核心類,與介面棋盤對應,業務放在這裡,可以和介面程式碼分離
public class Chess{
	public static final int CHESSBOARD_SIZE = 15;
	public static int FIRST = 1;//先手,-1表示機器,1表示人類,與Location類中的對應
	private int[][] chessboard = new int[CHESSBOARD_SIZE][CHESSBOARD_SIZE];//與介面棋盤對應,0代表空,-1代表機器,1代表人類
	private int[][] score = new int[CHESSBOARD_SIZE][CHESSBOARD_SIZE];//每個位置得分

	public Chess(){}	
	
	public void init(){
		FIRST = 1;//預設人類先手
		for(int i = 0; i  < CHESSBOARD_SIZE; i++){
			for(int j = 0; j < CHESSBOARD_SIZE; j++){
				chessboard[i][j] = 0;
				score[i][j] = 0;
			}
		}
	}	
	
	//落子
	public void addChessman(int x, int y, int owner){
		chessboard[x][y] = owner;
	}

	//判斷落子位置是否合法
	public boolean isLegal(int x, int y){
		if(x >=0 && x < CHESSBOARD_SIZE && y >= 0 && y < CHESSBOARD_SIZE && chessboard[x][y] == 0){
			return true;
		}
		return false;
	}

	//判斷哪方贏了(必定有剛落的子引發,因此只需判斷剛落子的周圍),owner為-1代表機器,owner為1代表人類
	public boolean isWin(int x, int y, int owner){
		int sum = 0;
		//判斷橫向左邊
		for(int i = x - 1; i >= 0; i--){
			if(chessboard[i][y] == owner){sum++;}
			else{break;}
		}	
		//判斷橫向右邊
		for(int i = x + 1; i < CHESSBOARD_SIZE; i++){
			if(chessboard[i][y] == owner){sum++;}
			else{break;}
		}
		if(sum >= 4) {return true;}
		
		sum = 0;
		//判斷縱向上邊
		for(int i = y - 1; i >= 0; i--){
			if(chessboard[x][i] == owner){sum++;}
			else{break;}
		}
		//判斷縱向下邊
		for(int i = y + 1; i < CHESSBOARD_SIZE; i++){
			if(chessboard[x][i] == owner){sum++;}
			else{break;}
		}
		if(sum >= 4) {return true;}
	
		sum = 0;
		//判斷左上角到右下角方向上側
		for(int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j-- ){
			if(chessboard[i][j] == owner){sum++;}
			else{break;}
		}
		//判斷左上角到右下角方向下側
		for(int i = x + 1, j = y + 1; i < CHESSBOARD_SIZE && j < CHESSBOARD_SIZE; i++, j++ ){
			if(chessboard[i][j] == owner){sum++;}
			else{break;}
		}
		if(sum >= 4) {return true;}
		
		sum = 0;
		//判斷右上角到左下角方向上側
		for(int i = x + 1, j = y - 1; i < CHESSBOARD_SIZE && j >= 0; i++, j-- ){
			if(chessboard[i][j] == owner){sum++;}
			else{break;}
		}
		//判斷右上角到左下角方向下側
		for(int i = x - 1, j = y + 1; i >= 0 && j < CHESSBOARD_SIZE; i--, j++ ){
			if(chessboard[i][j] == owner){sum++;}
			else{break;}
		}
		if(sum >= 4) {return true;}

		return false;
		
	}


	//【【【【【*******整個遊戲的核心*******】】】】】______確定機器落子位置
	//使用五元組評分演算法,該演算法參考部落格地址:https://blog.csdn.net/u011587401/article/details/50877828
	//演算法思路:對15X15的572個五元組分別評分,一個五元組的得分就是該五元組為其中每個位置貢獻的分數,
	//	   一個位置的分數就是其所在所有五元組分數之和。所有空位置中分數最高的那個位置就是落子位置。
	public Location searchLocation(){
		//每次都初始化下score評分陣列
		for(int i = 0; i  < CHESSBOARD_SIZE; i++){
			for(int j = 0; j < CHESSBOARD_SIZE; j++){
				score[i][j] = 0;
			}
		}
		
		//每次機器找尋落子位置,評分都重新算一遍(雖然算了很多多餘的,因為上次落子時候算的大多都沒變)
		//先定義一些變數
		int humanChessmanNum = 0;//五元組中的黑棋數量
		int machineChessmanNum = 0;//五元組中的白棋數量
		int tupleScoreTmp = 0;//五元組得分臨時變數
		
		int goalX = -1;//目標位置x座標
		int goalY = -1;//目標位置y座標
		int maxScore = -1;//最大分數

		//1.掃描橫向的15個行
		for(int i = 0; i < 15; i++){
			for(int j = 0; j < 11; j++){
				int k = j;
				while(k < j + 5){
					
					if(chessboard[i][k] == -1) machineChessmanNum++;
					else if(chessboard[i][k] == 1)humanChessmanNum++;
				
					k++;
				}
				tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
				//為該五元組的每個位置新增分數
				for(k = j; k < j + 5; k++){
					score[i][k] += tupleScoreTmp;
				}
				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數
			}
		}
		
		//2.掃描縱向15行
		for(int i = 0; i < 15; i++){
			for(int j = 0; j < 11; j++){
				int k = j;
				while(k < j + 5){
					if(chessboard[k][i] == -1) machineChessmanNum++;
					else if(chessboard[k][i] == 1)humanChessmanNum++;
				
					k++;
				}
				tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
				//為該五元組的每個位置新增分數
				for(k = j; k < j + 5; k++){
					score[k][i] += tupleScoreTmp;
				}
				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數
			}
		}

		//3.掃描右上角到左下角上側部分
		for(int i = 14; i >= 4; i--){
			for(int k = i, j = 0; j < 15 && k >= 0; j++, k--){
				int m = k;
				int n = j;
				while(m > k - 5 && k - 5 >= -1){
					if(chessboard[m][n] == -1) machineChessmanNum++;
					else if(chessboard[m][n] == 1)humanChessmanNum++;
					
					m--;
					n++;
				}
				//注意斜向判斷的時候,可能構不成五元組(靠近四個角落),遇到這種情況要忽略掉
				if(m == k-5){
					tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
					//為該五元組的每個位置新增分數
					for(m = k, n = j; m > k - 5 ; m--, n++){
						score[m][n] += tupleScoreTmp;
					}
				}

				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數

			}
		}
		
		//4.掃描右上角到左下角下側部分
		for(int i = 1; i < 15; i++){
			for(int k = i, j = 14; j >= 0 && k < 15; j--, k++){
				int m = k;
				int n = j;
				while(m < k + 5 && k + 5 <= 15){
					if(chessboard[n][m] == -1) machineChessmanNum++;
					else if(chessboard[n][m] == 1)humanChessmanNum++;
					
					m++;
					n--;
				}
				//注意斜向判斷的時候,可能構不成五元組(靠近四個角落),遇到這種情況要忽略掉
				if(m == k+5){
					tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
					//為該五元組的每個位置新增分數
					for(m = k, n = j; m < k + 5; m++, n--){
						score[n][m] += tupleScoreTmp;
					}
				}
				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數

			}
		}

		//5.掃描左上角到右下角上側部分
		for(int i = 0; i < 11; i++){
			for(int k = i, j = 0; j < 15 && k < 15; j++, k++){
				int m = k;
				int n = j;
				while(m < k + 5 && k + 5 <= 15){
					if(chessboard[m][n] == -1) machineChessmanNum++;
					else if(chessboard[m][n] == 1)humanChessmanNum++;
					
					m++;
					n++;
				}
				//注意斜向判斷的時候,可能構不成五元組(靠近四個角落),遇到這種情況要忽略掉
				if(m == k + 5){
					tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
					//為該五元組的每個位置新增分數
					for(m = k, n = j; m < k + 5; m++, n++){
						score[m][n] += tupleScoreTmp;
					}
				}

				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數

			}
		}	
	
		//6.掃描左上角到右下角下側部分
		for(int i = 1; i < 11; i++){
			for(int k = i, j = 0; j < 15 && k < 15; j++, k++){
				int m = k;
				int n = j;
				while(m < k + 5 && k + 5 <= 15){
					if(chessboard[n][m] == -1) machineChessmanNum++;
					else if(chessboard[n][m] == 1)humanChessmanNum++;
					
					m++;
					n++;
				}
				//注意斜向判斷的時候,可能構不成五元組(靠近四個角落),遇到這種情況要忽略掉
				if(m == k + 5){
					tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
					//為該五元組的每個位置新增分數
					for(m = k, n = j; m < k + 5; m++, n++){
						score[n][m] += tupleScoreTmp;
					}
				}

				//置零
				humanChessmanNum = 0;//五元組中的黑棋數量
				machineChessmanNum = 0;//五元組中的白棋數量
				tupleScoreTmp = 0;//五元組得分臨時變數

			}
		}	
	
		//從空位置中找到得分最大的位置
		for(int i = 0; i < 15; i++){
			for(int j = 0; j < 15; j++){
				if(chessboard[i][j] == 0 && score[i][j] > maxScore){
					goalX = i;
					goalY = j;
					maxScore = score[i][j];
				}
			}
		}		

		if(goalX != -1 && goalY != -1){
			return new Location(goalX, goalY, -1);
		}
		
		//沒找到座標說明平局了,筆者不處理平局
		return new Location(-1, -1, -1);
	}
	
	//各種五元組情況評分表
	public int tupleScore(int humanChessmanNum, int machineChessmanNum){
		//1.既有人類落子,又有機器落子,判分為0
		if(humanChessmanNum > 0 && machineChessmanNum > 0){
			return 0;
		}
		//2.全部為空,沒有落子,判分為7
		if(humanChessmanNum == 0 && machineChessmanNum == 0){
			return 7;
		}
		//3.機器落1子,判分為35
		if(machineChessmanNum == 1){
			return 35;
		}
		//4.機器落2子,判分為800
		if(machineChessmanNum == 2){
			return 800;
		}
		//5.機器落3子,判分為15000
		if(machineChessmanNum == 3){
			return 15000;
		}
		//6.機器落4子,判分為800000
		if(machineChessmanNum == 4){
			return 800000;
		}
		//7.人類落1子,判分為15
		if(humanChessmanNum == 1){
			return 15;
		}
		//8.人類落2子,判分為400
		if(humanChessmanNum == 2){
			return 400;
		}
		//9.人類落3子,判分為1800
		if(humanChessmanNum == 3){
			return 1800;
		}
		//10.人類落4子,判分為100000
		if(humanChessmanNum == 4){
			return 100000;
		}
		return -1;//若是其他結果肯定出錯了。這行程式碼根本不可能執行
	}
	
}