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



# 介面介紹
畫面中間為13 * 13大小的棋盤
左下角有一個text area,會顯示各種訊息
右下角有四個按鈕,分別為:
vs player: 玩家與玩家對戰vs computer: 玩家與電腦對戰theme: 更換主題restart: 重新開始遊戲
# 遊戲方式介紹
遊戲未開始前,能先選擇主題,總共有三種主題,每按一次theme按鈕就會跳至下一個主題
基本主題: 與一般五子棋一樣岩石主題: 在13 * 13的場地中會隨機掉落10顆落石,落石掉落處視為牆壁,無法下棋子海洋主題: 在13 * 13的場地中會隨機產生10顆泡泡(可能重疊),當有一名玩家下到7顆泡泡所在的格子時,立即獲得勝利
有兩種對戰模式可以選擇,皆為玩家先攻,按下對應的按鈕即開始遊戲
玩家 vs 玩家玩家 vs 電腦
任何時候都能按下restart按鈕重新開始遊戲
# 程式碼說明
以下會說明程式的思路
- 初始化、變數說明
- 按鈕事件
- 滑鼠事件
- 電腦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)
顧名思義就是按下按鈕會發生動作的事件
總共有四個按鈕,會執行相對應的動作
- 按鈕(vs player)
- 按鈕(vs computer)
- 按鈕(restart)
- 按鈕(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
這個部分是整個程式中最複雜的部份
它的原理是會遍歷棋盤上所有未走過的點,根據兩方的棋子各打出一個分數,找出分數最高的點即為最佳點
棋型有以下幾種:
ooooo五連線_oooo_活四_oooo、oooo_死四(強)oo_oo、ooo_o、o_ooo死四(弱)__ooo_、_ooo__活三(強)_o_oo_、_oo_o_活三(弱)__ooo、ooo__、_ooo_、_o_oo、oo_o_、_oo_o、o_oo_、o__oo、oo__o、o_o_o死三_oo___、__oo__、___oo_活二___oo、oo___死二
而這些棋型的組合及數量決定分數:
1分什麼也沒有2分1個死二3分1個死三4分1個活二5分2個活二6分1個死四(弱)7分1個死四(強)8分1個活三(弱)9分1個活三(強)10分1個死四 + 1個活三(弱)11分1個死四 + 1個活三(強)12分2個活三13分1個活四 , 2個死四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
# 遊戲作者
Ping's notes
C++ programing, UVa