# 遊戲說明
這是一個五子棋遊戲,規則就如同大家熟悉的那樣,先使 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