# 遊戲說明

這是一個五子棋遊戲,規則就如同大家熟悉的那樣,先使 5 顆棋子連成一條線就獲勝

gobang
gobang rock theme
gobang ocean theme

# 介面介紹

畫面中間為 13 * 13 大小的棋盤
左下角有一個 text area ,會顯示各種訊息
右下角有四個按鈕,分別為:

  • vs player : 玩家與玩家對戰
  • vs computer : 玩家與電腦對戰
  • theme : 更換主題
  • restart : 重新開始遊戲

# 遊戲方式介紹

遊戲未開始前,能先選擇主題,總共有三種主題,每按一次 theme 按鈕就會跳至下一個主題

  1. 基本主題 :與一般五子棋一樣
  2. 岩石主題 :在 13 * 13 的場地中會隨機掉落 10 顆落石,落石掉落處視為牆壁,無法下棋子
  3. 海洋主題 :在 13 * 13 的場地中會隨機產生 10 顆泡泡 (可能重疊),當有一名玩家下到 7 顆泡泡所在的格子時,立即獲得勝利

有兩種對戰模式可以選擇,皆為玩家先攻,按下對應的按鈕即開始遊戲

  1. 玩家 vs 玩家
  2. 玩家 vs 電腦

任何時候都能按下 restart 按鈕重新開始遊戲

# 程式碼說明

以下會說明程式的思路

  1. 初始化、變數說明
  2. 按鈕事件
  3. 滑鼠事件
  4. 電腦 AI

以下是簡單的流程:
初始化 -> 選擇主題 -> 選擇對戰模式 -> LOOP ( 下棋 -> 判斷是否勝利 ) -> 遊戲結束

# 初始化、變數說明

將一些會用到的變數初始化

// 控制遊戲處於哪個階段,一開始設為 0
// 0 為遊戲尚未開始
// 1 為玩家 vs 玩家
// 2 為玩家 vs 電腦
//-1 為遊戲結束
static int game_start;
game_start = 0;
// 控制棋子的顏色,一開始設為白與黑
static Color chess[] = new Color[2];
chess[0] = Color.white;
chess[1] = Color.black;
// 控制輪到哪一方,一開始設為 0
static int ch;
ch = 0;
// 儲存棋盤的資料,一開始全部設為 0
static int board[][] = new int[13][13];
for (int i = 0; i < 13; ++i) for (int j = 0; j < 13; ++j) board[i][j] = 0;

將所有會用到的圖片素材從檔案讀入

import java.io.*;
import javax.imageio.*;
static Image image[] = new Image[3];
static Image sourse[] = new Image[3];
//image 儲存對應三種不同的主題
try { image[0] = ImageIO.read(new File("wood.png")); }
catch(Exception ex) { System.out.println("No image"); }
try { image[1] = ImageIO.read(new File("ground.jpg")); }
catch(Exception ex) { System.out.println("No image"); }
try { image[2] = ImageIO.read(new File("water.png")); }
catch(Exception ex) { System.out.println("No image"); }
//sourse 儲存落石及泡泡的圖片素材
try { sourse[1] = ImageIO.read(new File("a_rock.png")); }
catch(Exception ex) { System.out.println("No image"); }
try { sourse[2] = ImageIO.read(new File("bubble.png")); }
catch(Exception ex) { System.out.println("No image"); }

設定 JFrame 視窗

static final_project frm = new final_project();
// 使右上角 X 能夠關閉視窗
frm.addWindowListener(new WindowAdapter(){public void windowClosing(WindowEvent e){System.exit(0);}});
// 設定視窗標題、大小、背景顏色
frm.setTitle("Gobang");
frm.setSize(800, 850);
frm.setVisible(true);
frm.setBackground(new Color(255,255,224));
// 使視窗增加滑鼠事件
frm.addMouseListener(frm);
frm.addMouseMotionListener(frm);

布局 text area 及四個按鈕

static JButton player  = new JButton("vs player");
static JButton computer = new JButton("vs computer");
static JButton restart = new JButton("restart");
static JButton theme = new JButton("theme");
static TextArea txa = new TextArea("Choose a game mode.", 1, 50, TextArea.SCROLLBARS_VERTICAL_ONLY);
static JPanel toolbar = new JPanel();
// 將 `text area` 與四個按鈕以 FlowLayout 的方式排版,並設置在視窗的底部
toolbar.setLayout(new FlowLayout());
toolbar.add(txa);
toolbar.add(player);
toolbar.add(computer);
toolbar.add(theme);
toolbar.add(restart);
frm.add(toolbar, BorderLayout.SOUTH);
// 使視窗增加按鈕事件
player.addActionListener(frm);
computer.addActionListener(frm);
restart.addActionListener(frm);
theme.addActionListener(frm);

paint 會將整個棋盤繪製出
此函式會自動先執行一次,後續如果要使 paint 再次執行,可以呼叫 repaint()

public void paint(Graphics g)
{
	Graphics2D g2 = (Graphics2D)g;
	
	// 繪製主題
	g2.drawImage(image[current_theme], 50, 50, 700, 700, null);
	// 設定粗細及顏色
	g2.setStroke(new BasicStroke(2));
	if (current_theme == 0) g2.setColor(Color.black);
	else if (current_theme == 1) g2.setColor(Color.white);
	// 繪製線條
	for (int i = 100; i <= 700; i += 50) 
	{ 
		g2.drawLine(i, 100, i, 700);
		g2.drawLine(100, i, 700, i);
	}
	
	// 繪製圓點
	g2.fillOval(245, 245, 10, 10);
	g2.fillOval(545, 545, 10, 10);
	g2.fillOval(245, 545, 10, 10);
	g2.fillOval(545, 245, 10, 10);
	g2.fillOval(395, 395, 10, 10);
}

# 按鈕事件 public void actionPerformed (ActionEvent e)

顧名思義就是按下按鈕會發生動作的事件
總共有四個按鈕,會執行相對應的動作

  1. 按鈕 (vs player)
  2. 按鈕 (vs computer)
  3. 按鈕 (restart)
  4. 按鈕 (theme)
// 取得按下的按鈕
JButton b = (JButton) e.getSource();
// 按鈕 (vs player),只有當遊戲尚未開始時才有用
if (game_start == 0 && b == player)
{
	txa.setText("Player vs player. Game started! White turn.");
	game_start = 1;
	
	// 若主題為岩石或海洋,則繪製落石或泡泡
	if (current_theme >= 1) draw_item();
}
// 按鈕 (vs computer),只有當遊戲尚未開始時才有用
else if (game_start == 0 && b == computer)
{
	txa.setText("Player vs computer. Game started! White turn.");
	game_start = 2;
	
	// 若主題為岩石或海洋,則繪製落石或泡泡
	if (current_theme >= 1) draw_item();
}
// 按鈕 (restart),隨時有用
else if (b == restart)
{
	txa.setText("Game restart. Choose a game mode.");
	init();
	repaint();
}
// 按鈕 (theme),只有當遊戲尚未開始時才有用
else if (game_start == 0 && b == theme)
{
	// 切換至下一個主題
	++current_theme;
	if (current_theme == 3) current_theme = 0;
	
	// 根據主題設定棋子顏色
	if (current_theme == 0)
	{
		chess[0] = Color.white;
		chess[1] = Color.black;
	}
	else if (current_theme == 1)
	{
		chess[0] = Color.red;
		chess[1] = Color.blue;
	}
	else if (current_theme == 2)
	{
		chess[0] = Color.green;
		chess[1] = Color.yellow;
	}
	repaint();
}

draw_item 函式會根據 岩石主題海洋主題 繪製落石或泡泡,若為 基本主題 則不繪製

static Random rand = new Random();
static int bubbles[][] = new int[10][2];
public void draw_item()
{
	Graphics2D g = (Graphics2D)getGraphics();
	int cnt = 0, x, y;
		  
	do
	{
		// 隨機產生一個座標
		x = rand.nextInt(13);
		y = rand.nextInt(13);
		if (board[y][x] != 0) continue;
		
		// 繪製落石或泡泡
		g.drawImage(sourse[current_theme], 70 + x * 50, 70 + y * 50, 60, 60, null);
		
		// 若為岩石主題,則將棋盤對應位置設為無法下
		// 若為海洋主題,則儲存座標至 bubbles
		if (current_theme == 1) board[y][x] = 3;
		else if (current_theme == 2)
		{
			bubbles[cnt][0] = x;
			bubbles[cnt][1] = y;
		}
		
		++cnt;
	} while (cnt != 10);
}

# 滑鼠事件 public void mouseClicked (MouseEvent e)

由於只會使用滑鼠點擊事件,所以其他滑鼠事件設為空

public void mouseMoved(MouseEvent e){}
public void mouseReleased(MouseEvent e){}
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e){}
public void mouseDragged(MouseEvent e){}
public void mousePressed(MouseEvent e){}

滑鼠點擊事件會取得座標

// 若遊戲尚未開始或遊戲已經結束,則滑鼠點擊無效
if (game_start <= 0) return;
// 根據滑鼠點擊的位置繪製棋子
if (find_and_draw(e.getX(), e.getY(), 1) && game_start == 2)
{
	// 若為玩家 vs 電腦,則電腦產生一個位置並繪製
	int pos[] = new int[2];
	get_computer(pos);
	find_and_draw(pos[0], pos[1], 2);
}

尋找並繪製指定位置的棋子,若成功繪製,則再判斷是否有達成勝利條件

public boolean find_and_draw(int x, int y, int h)
{
	Graphics2D g = (Graphics2D)getGraphics();
	int cur = 0;
	// 因為電腦產生的座標為 (0 ~ 13, 0 ~ 13),所以要轉為點擊棋盤的座標
	if (h == 2)
	{
		x = x * 50 + 80;
		y = y * 50 + 80;
	}
	for (int i = 75, cnt_j = 0; i <= 675; i += 50, ++cnt_j)
		for (int j = 75, cnt_i = 0; j <= 675; j += 50, ++cnt_i)
			// 若沒有超出邊界與當前座標位置還未下棋,進入 if
			if (x >= j && x <= j + 50 && y >= i && y <= i + 50 && board[cnt_j][cnt_i] == 0)
			{
				cur = board[cnt_j][cnt_i] = ch + 1;
				// 繪製棋子及外框
				g.setColor(chess[ch]);
				g.fillOval(j + 5, i + 5, 40, 40);
				ch ^= 1;
				g.setColor(chess[ch]);
				g.drawOval(j + 5, i + 5, 40, 40);
				// 判斷遊戲是否結束
				if (isEnd(cnt_i, cnt_j, cur) == true)
				{
					// 若是由五子連線獲勝,則繪製出連線
					if (bbwin == false)
					{
						g.setColor(Color.red);
						g.setStroke(new BasicStroke(4));
						g.drawLine(result[0] * 50 + 100, result[1] * 50 + 100, result[2] * 50 + 100, result[3] * 50 + 100);
					} 
					if (game_start == 1)
					{
						if (cur == 1) txa.setText("Player 1 wins!");
						else txa.setText("Player 2 wins!");
					}
					else
					{
						if (cur == 1) txa.setText("Player wins!");
						else txa.setText("Computer wins!");
					}           
					// 遊戲結束
					game_start = -1;
				}
				else
				{
					if (game_start == 1)
					{
						if (cur == 1) txa.setText("Player 1 goes (" + cnt_i + ", " + cnt_j  + ")  It's Player 2's turn.");
						else txa.setText("Player 2 goes (" + cnt_i + ", " + cnt_j  + ")  It's Player 1's turn."); 
					}
					else
					{
						if (cur == 1) txa.setText("Player goes (" + cnt_i + ", " + cnt_j  + ")  It's Computer's turn.");
						else txa.setText("Computer goes (" + cnt_i + ", " + cnt_j  + ")  It's Player's turn.");
					}
					
				}
				
				// 繪製成功
				return true;
			} 
	// 繪製失敗
	return false;
}

根據下棋的座標及輪到何方判斷是否達成勝利

// 判斷是否是由下到 7 個泡泡的位置而獲勝,一開始設為 false
static boolean bbwin;
bbwin = false;
// 分別代表橫線、直線、兩個方向的斜線
static int move[][] = <!--swig0-->;
// 儲存某方達成五子連線時的起點與終點座標
// 四個 int 分別為 (x1, y1) (x2, y2)
static int result[] = new int[4];
public static boolean isEnd(int x, int y, int cur)
{
	// 只有在海洋主題中,才會進行泡泡的判斷
	if (game_start == 2)
	{
		int cnt = 0;
		for (int i = 0; i < 10; ++i)
		{
			int bx = bubbles[i][0];
			int by = bubbles[i][1];
			if (board[by][bx] == cur) ++cnt;
		}
		if (cnt >= 7)
		{
			bbwin = true;
			return true;
		}
	}
	for (int i = 0; i < 4; ++i)
	{
		int cnt = 0;
		for (int j = -4; j <= 4; ++j)
		{
			int nx = x + move[i][0] * j;
			int ny = y + move[i][1] * j;
			// 根據基準座標的正負四顆棋子 (共 9 顆棋子) 判斷是否存在連續 5 顆棋子連線
			if (nx >= 0 && nx < 13 && ny >= 0 && ny < 13 && board[ny][nx] == cur) ++cnt;
			else cnt = 0;
			
			// 儲存連線起點
			if (cnt == 1)
			{
				result[0] = nx;
				result[1] = ny;
			}
			// 儲存連線終點,並回傳結束
			else if (cnt == 5) 
			{ 
				result[2] = nx;
				result[3] = ny;
				return true;
			}
		}
	}
	return false;
}

# 電腦 AI

這個部分是整個程式中最複雜的部份
它的原理是會遍歷棋盤上所有未走過的點,根據兩方的棋子各打出一個分數,找出分數最高的點即為最佳點

棋型有以下幾種:

  1. ooooo 五連線
  2. _oooo_ 活四
  3. _oooooooo_ 死四 (強)
  4. oo_ooooo_oo_ooo 死四 (弱)
  5. __ooo__ooo__ 活三 (強)
  6. _o_oo__oo_o_ 活三 (弱)
  7. __oooooo___ooo__o_oooo_o__oo_oo_oo_o__oooo__oo_o_o 死三
  8. _oo_____oo_____oo_ 活二
  9. ___oooo___ 死二

而這些棋型的組合及數量決定分數:

  1. 1分 什麼也沒有
  2. 2分 1 個死二
  3. 3分 1 個死三
  4. 4分 1 個活二
  5. 5分 2 個活二
  6. 6分 1 個死四 (弱)
  7. 7分 1 個死四 (強)
  8. 8分 1 個活三 (弱)
  9. 9分 1 個活三 (強)
  10. 10分 1 個死四 + 1 個活三 (弱)
  11. 11分 1 個死四 + 1 個活三 (強)
  12. 12分 2 個活三
  13. 13分 1 個活四,2 個死四
  14. 100分 達成五連線

取得棋盤上所有點中最佳的點

public void get_computer(int[] pos)
{
	int best_attack[] = new int[2];
	int best_defence[] = new int[2];
	int tmp1[] = new int[2];
	int tmp2[] = new int[2];
	// 遍歷棋盤上所有點
	for (int i = 0; i < 13; ++i) for (int j = 0 ; j < 13; ++j)
	{
		if (board[i][j] != 0) continue;
		// 取得攻擊及防守分數
		int c_ = score(j, i, 2);
		int p_ = score(j, i, 1);
		// 紀錄最佳攻擊及防守的座標,一共有兩種模式
		// 1. 攻擊為主,防禦為輔
		// 2. 防禦為主,攻擊為輔
		if (c_ > best_attack[0] || (c_ == best_attack[0] && p_ > best_attack[1]))
		{
			best_attack[0] = c_;
			best_attack[1] = p_;
			tmp1[0] = j;
			tmp1[1] = i;
		}
		if (p_ > best_defence[0] || (p_ == best_defence[0] && c_ > best_defence[1]))
		{
			best_defence[0] = p_;
			best_defence[1] = c_;
			tmp2[0] = j;
			tmp2[1] = i;
		}
	}
	// 優先採取為主分數較高的那一個點
	// 若為主分數一樣,則採取為輔分數較高的那一個點 (攻擊優先)
	if (best_attack[0] > best_defence[0])
	{
		pos[0] = tmp1[0];
		pos[1] = tmp1[1];
	}
	else if (best_defence[0] > best_attack[0])
	{
		pos[0] = tmp2[0];
		pos[1] = tmp2[1];
	}
	else if (best_attack[1] >= best_defence[1])
	{
		pos[0] = tmp1[0];
		pos[1] = tmp1[1];
	}
	else
	{
		pos[0] = tmp2[0];
		pos[1] = tmp2[1];
	}
}

取得此點的分數

public int score(int x, int y, int cur)
    {
        int opposite = (cur == 1 ? 2 : 1);
        int five = 0;
        int four_alive = 0, four_die1 = 0, four_die2 = 0;
        int three_alive1 = 0, three_alive2 = 0, three_die = 0;
        int two_alive = 0, two_die = 0;
		// 同樣根據 move 尋找四個方向
        for (int i = 0; i < 4; ++i)
        {
            int cnt = 1;
            int l = 0, r = 0;
            int left[] = new int[4];
            int right[] = new int[4];
            // 找出基準點右邊連線的點
            for (int j = 1; j <= 4; ++j)
            {
                int nx = x + move[i][0] * j;
                int ny = y + move[i][1] * j;
                if (nx >= 0 && nx < 13 && ny >= 0 && ny < 13 && board[ny][nx] == cur) ++cnt;
                else 
                {
                    r = j;
                    break;
                }
            }
            // 找出基準點左邊連線的點
            for (int j = -1; j >= -4; --j)
            {
                int nx = x + move[i][0] * j;
                int ny = y + move[i][1] * j;
                if (nx >= 0 && nx < 13 && ny >= 0 && ny < 13 && board[ny][nx] == cur) ++cnt;
                else
                {
                    l = j;
                    break;
                }
            }
            // 將此連線的座左端及最右端再取得四個位置,分別存入 left 與 right,若為牆壁則設為對手的棋子
            for (int j = 0; j < 4; ++j, --l, ++r)
            {
                int nx = x + move[i][0] * l;
                int ny = y + move[i][1] * l;
                if (nx >= 0 && nx < 13 && ny >= 0 && ny < 13) left[j] = board[ny][nx];
                else left[j] = opposite;
                nx = x + move[i][0] * r;
                ny = y + move[i][1] * r;
                if (nx >= 0 && nx < 13 && ny >= 0 && ny < 13) right[j] = board[ny][nx];
                else right[j] = opposite;
            }
			// 判斷為哪一種棋型
            if (cnt == 5) ++five;
            else if (cnt == 4)
            {
                if (left[0] == 0 && right[0] == 0) ++four_alive; // _oooo_
                else if (left[0] == 0 || right[0] == 0) ++four_die1; // _oooo, oooo_
            }
            else if (cnt == 3)
            {
                if ((left[0] == 0 && left[1] == cur) || (right[0] == 0 && right[1] == cur)) ++four_die2; // o_ooo, ooo_o
                else if  (left[0] == 0 && right[0] == 0 && (left[1] == 0 || right[1] == 0)) ++three_alive1; // __ooo_, _ooo__
                else if ((left[0] == 0 && left[1] == 0) || (right[0] == 0 && right[1] == 0)) ++three_die; // __ooo, ooo__
                else if (left[0] == 0 && right[0] == 0) ++three_die; // _ooo_
            }
            else if (cnt == 2)
            {
                if ((left[0] == 0 && left[1] == cur && left[2] == cur) || (right[0] == 0 && right[1] == cur && right[2] == cur)) ++four_die2; // oo_oo
                else if ((left[0] == 0 && right[0] == 0) && (left[1] == cur && left[2] == 0) || (right[1] == cur && right[2] == 0)) ++three_alive2; // _o_oo_, _oo_o_
                else if ((left[0] == 0 && left[1] == cur && left[2] == 0) || (right[0] == 0 && right[1] == cur && right[2] == 0)) ++three_die; //_o_oo, oo_o_
                else if ((left[0] == 0 && left[1] == 0 && left[2] == cur) || (right[0] == 0 && right[1] == 0 && right[2] == cur)) ++three_die; // o__oo, oo__o
                else if (left[0] == 0 && right[0] == 0 && (left[1] == 0 && left[2] == 0) || (left[1] == 0 && right[1] == 0) || (right[1] == 0 && right[2] == 0)) ++two_alive; // _oo___, __oo__, ___oo_
                else if ((left[0] == 0 && left[1] == 0 && left[2] == 0) && (right[0] == 0 && right[1] == cur && right[2] == 0)) ++two_die; // ___oo, oo___
            }
            else if (cnt == 1)
            {
                if ((left[0] == 0 && left[1] == cur && left[2] == cur && left[3] == cur) || (right[0] == 0 && right[1] == cur && right[2] == cur && right[3] == cur)) ++four_die2; // ooo_o, o_ooo
                else if ((left[0] == 0 && right[0] == 0) && ((left[1] == cur && left[2] == cur && left[3] == 0) || ( right[1] == cur && right[2] == cur && right[3] == 0))) ++three_alive2; // _oo_o_, _o_oo_
                else if (left[0] == 0 && right[0] == 0 && ((left[1] == cur && left[2] == cur) || (right[1] == cur && right[2] == cur))) ++three_die; // oo_o_, _o_oo
                else if ((left[0] == 0 && left[1] == cur && left[2] == cur && left[3] == 0) || (right[0] == 0 && right[1] == cur && right[2] == cur && right[3] == 0)) ++three_die; // _oo_o, o_oo_
                else if ((left[0] == 0 && left[1] == 0 && left[2] == cur && left[3] == cur) || (right[0] == 0 && right[1] == 0 && right[2] == cur && right[3] == cur)) ++three_die; // oo__o, o__oo
                else if ((left[0] == 0 && left[1] == cur && left[2] == 0 && left[3] == cur) || (right[0] == 0 && right[1] == cur && right[2] == 0 && right[3] == cur)) ++three_die; // o_o_o
            }
        }
		// 根據棋型的數量得出最後的分數
        if (five >= 1) return 100; // ooooo
        else if (four_alive >= 1 || (four_die1 + four_die2) >= 2 || ((four_die1 + four_die2) >= 1 && (three_alive1 + three_alive2) >= 1)) return 13;
        else if ((three_alive1 + three_alive2)  >= 2) return 12;
        else if (three_alive1 >= 1 && (four_die1 + four_die2) >= 1) return 11;
        else if (three_alive2 >= 1 && (four_die1 + four_die2) >= 1) return 10;
        else if (three_alive1 >= 1) return 9;
        else if (three_alive2 >= 1) return 8;
        else if (four_die1 >= 1) return 7;
        else if (four_die2 >= 1) return 6;
        else if (two_alive >= 2) return 5;
        else if (two_alive >= 1) return 4;
        else if (three_die >= 1) return 3;
        else if (two_die >= 1) return 2;
        else return 1;
    }

詳細的程式碼與圖片素材放在 github

# 遊戲作者