承接上一篇,本文將深入解析《雙人掃雷 1.0》的核心程式碼實作。我們將涵蓋幫助界面、主遊戲畫面繪製、關鍵遊戲邏輯(佈雷、遞歸展開、勝負判定)、遊戲中選單,以及最終的程式碼整合。這是一個基於 MIDP 1.0 的完整雙人對戰掃雷範例,適合初學者研究 J2ME 遊戲開發的基礎架構。
幫助界面採用簡單的文字陣列繪製方式。將說明文字預先存入 String[],在 paintHelpScreen() 中逐行繪製。若內容超出螢幕,可透過按鍵翻頁(實際上只是重繪陣列的不同區段)。
// MiningCanvas.java 成員變數
final String[] strGamehelp = new String[10];
public MiningCanvas(MiningMIDlet miningMIDlet) {
// ...
strGamehelp[0] = "此遊戲為雙人對戰遊戲";
strGamehelp[1] = "與經典掃雷不同,這";
strGamehelp[2] = "個遊戲中,我們要挖出";
strGamehelp[3] = "雷,挖錯雷則交換玩家。";
strGamehelp[4] = "一共有 52 顆雷,256 個格";
strGamehelp[5] = "子,最先挖到 27 顆雷者";
strGamehelp[6] = "獲勝。1. 上、下、左、右分";
strGamehelp[7] = "別為數字鍵 2、8、4、6,挖";
strGamehelp[8] = "雷為 5;2. 螢幕外框的顏";
strGamehelp[9] = "色為當前玩家;";
}
private void paintHelpScreen(Graphics g) {
g.setColor(0x00FFFFFF);
g.fillRect(0, 0, canvasW, canvasH);
g.setFont(lowFont);
g.setColor(0x00000000);
for (int i = 0; i < strGamehelp.length; i++) {
g.drawString(strGamehelp[i], 5, 5 + (lowFont.getHeight() + 3) * i, Graphics.TOP | Graphics.LEFT);
}
}
// keyPressed 中處理幫助狀態
case GAMESTATE_HELP:
gamestate = GAMESTATE_MENU; // 任意鍵返回主選單
break;
主畫面分為四大區塊:
Bomb[][] 陣列狀態繪製(未翻開、已翻開數字、玩家 1/2 的雷)。paintX、paintY 控制可視區域左上角在雷區中的座標。(selectedX, selectedY) 若超出可視範圍,即調整 paintX/Y 使其回到視野內。(x, y) 對應陣列 bombs[y][x],讀寫時務必留意行列互換。bombs 宣告為 new Bomb[18][18](miningMapGrid+2),真實雷區位於索引 1~16,外圍一圈作為哨兵,避免周邊檢查時發生 ArrayIndexOutOfBoundsException。
// 遊戲主畫面繪製入口
private void paintGameScreen(Graphics g) {
paintPlayer(g, isPlayer1); // 外框與旗幟
paintMiningMap(g); // 雷區
paintInfo(g); // 資訊欄
paintSelected(g); // 選擇框
}
// 繪製雷區核心邏輯
public void paintMiningMap(Graphics g) {
for (int i = 0; i < bombMapH; i++) {
for (int j = 0; j < bombMapW; j++) {
Bomb b = bombs[i + paintY + 1][j + paintX + 1];
int dx = miningMapX + j * miningGridLength;
int dy = miningMapY + i * miningGridLength;
if (!b.hasFound) {
g.drawImage(unFoundGroundImg, dx, dy, Graphics.TOP | Graphics.LEFT);
} else if (!b.isBomb) {
// 依周圍雷數繪製 0~8 圖片
switch (b.bombaround) {
case 0: g.drawImage(foundGroundImg, dx, dy, Graphics.TOP | Graphics.LEFT); break;
case 1: g.drawImage(bomb1Img, dx, dy, Graphics.TOP | Graphics.LEFT); break;
// ... case 2~7 同理
default: g.drawImage(foundGroundImg, dx, dy, Graphics.TOP | Graphics.LEFT); break;
}
} else {
// 已翻開且是雷:顯示屬於哪位玩家
g.drawImage(b.isPlayer1 ? player1BombImg : player2BombImg, dx, dy, Graphics.TOP | Graphics.LEFT);
}
}
}
}
bombInit()建立 18×18 的 Bomb 物件陣列,隨機在 1~16 範圍內放置 52 顆雷,並同步更新周圍 8 格的 bombaround 計數器。
public void bombInit() {
int bombindex = 0;
Random random = new Random();
// 初始化所有 Bomb 物件
for (int i = 0; i < miningMapGrid + 2; i++)
for (int j = 0; j < miningMapGrid + 2; j++)
bombs[i][j] = new Bomb();
while (bombindex < bombNum) {
int x = Math.abs(random.nextInt() % 16) + 1;
int y = Math.abs(random.nextInt() % 16) + 1;
if (!bombs[x][y].isBomb) {
try {
bombs[x][y].isBomb = true;
bombindex++;
// 更新周圍 8 格計數
bombs[x-1][y].bombaround++;
bombs[x-1][y-1].bombaround++;
bombs[x][y-1].bombaround++;
bombs[x+1][y-1].bombaround++;
bombs[x+1][y].bombaround++;
bombs[x+1][y+1].bombaround++;
bombs[x][y+1].bombaround++;
bombs[x-1][y+1].bombaround++;
} catch (ArrayIndexOutOfBoundsException e) {}
}
}
}
bombOut(x, y)經典掃雷規則:點擊無雷且周圍無雷的格子,自動展開相連的安全區域及其邊緣一圈。
public void bombOut(int x, int y) {
if (!bombs[x][y].hasFound && x >= 1 && y >= 1 && x <= miningMapGrid && y <= miningMapGrid) {
bombs[x][y].hasFound = true;
if (bombs[x][y].bombaround == 0) {
bombOut(x-1, y);
bombOut(x-1, y-1);
bombOut(x, y-1);
bombOut(x+1, y-1);
bombOut(x+1, y);
bombOut(x+1, y+1);
bombOut(x, y+1);
bombOut(x-1, y+1);
}
}
}
checkWin()任一方挖到 27 顆雷(超過一半)即獲勝,彈出 Alert 並自動重置遊戲。
public void checkWin() {
if (player1Found > 26) { winString = "Player1 has won!"; sbWon = true; }
if (player2Found > 26) { winString = "Player2 has won!"; sbWon = true; }
if (sbWon) {
winAlert = new Alert("", winString + "\nPlayer1 " + player1Found + " : " + player2Found + " Player2", null, AlertType.INFO);
gameInit(); // 重置下一局
winAlert.setTimeout(Alert.FOREVER);
MiningMIDlet.display.setCurrent(winAlert);
}
}
gameInit()重置所有狀態變數、分數、選擇框位置、可視區域,並呼叫 bombInit() 重新佈雷。
遊戲採用狀態機設計:GAMESTATE_MENU、GAMESTATE_HELP、GAMESTATE_GAMEING、GAMESTATE_GAMEMENU 等。在 keyPressed() 的 switch(gamestate) 中分派處理。
case GAMESTATE_GAMEING:
if (keyCode == FullCanvas.KEY_SOFTKEY1) {
gamestate = GAMESTATE_GAMEMENU;
} else if (action == FullCanvas.LEFT) {
selectedX = (--selectedX + miningMapGrid) % miningMapGrid;
} else if (action == FullCanvas.RIGHT) {
selectedX = (++selectedX) % miningMapGrid;
} else if (action == FullCanvas.UP) {
selectedY = (--selectedY + miningMapGrid) % miningMapGrid;
} else if (action == FullCanvas.DOWN) {
selectedY = (++selectedY) % miningMapGrid;
} else if (action == FullCanvas.FIRE) {
Bomb target = bombs[selectedY+1][selectedX+1];
if (!target.hasFound) {
if (target.isBomb) {
// 挖到雷:得分、標記歸屬、切換玩家
target.hasFound = true;
target.isPlayer1 = isPlayer1;
if (isPlayer1) player1Found++; else player2Found++;
bombLeft--;
checkWin();
} else {
// 未挖到雷:遞歸展開並交換玩家
bombOut(selectedY+1, selectedX+1);
isPlayer1 = !isPlayer1;
}
}
}
// 視野跟隨選擇框
if ((selectedX - paintX) < 0) paintX = selectedX;
else if ((selectedX - paintX) >= bombMapW) paintX = selectedX - bombMapW + 1;
if ((selectedY - paintY) < 0) paintY = selectedY;
else if ((selectedY - paintY) >= bombMapH) paintY = selectedY - bombMapH + 1;
break;
提供「返回遊戲、重新開始、幫助、主選單、退出」五個選項。結構與主選單類似,此處略述。已知問題:從幫助界面返回會直接跳至主選單,可透過增加狀態變數修復。
至此,雙人掃雷的完整邏輯已全部呈現。雖然核心簡單,但已具備完整遊戲循環。後續可擴展:
原作者 yinowl 於 2005 年發布,雖屬早期 J2ME 範例,但其狀態機、座標映射、遞歸展開等設計模式至今仍具參考價值。