【Android】自己動手做個掃雷遊戲
1. 遊戲規則
掃雷是玩法極其簡單的小遊戲,點選玩家認為不存在雷的區域,標記出全部地雷所在的區域,即可獲得勝利。當點選不包含雷的塊的時候,可能它底下存在一個數,也可能是一個空白塊。當點選中有數字的塊時,遊戲會展現當前點選塊所包含的數字。當點選空白塊時,地圖會展開,形成一個大小和形狀不規則的圖形,該圖形的邊界是數字塊,也可以想成展開的是一個被數字包圍著的不規則圖形。

1.1 數字生成規則
掃雷遊戲中是通過數字來判斷雷的位置的,那麼,數字的生成規則是什麼呢?假設遊戲中只有一個雷,那麼,他的將被1這個數字包圍著,如圖:
1 | 1 | 1 |
---|---|---|
1 | 雷 | 1 |
1 | 1 | 1 |
如果遇到邊界就忽略
雷 | 1 |
---|---|
1 | 1 |
可見,遊戲是先生成雷然後再根據雷的位置生成數字的,我們再看下面的圖:
1 | 1 | 1 |
---|---|---|
1 | 雷 | 2 |
1 | 2 | 雷 |
1 | 1 | 1 |
在上圖中,塊中有兩個數字為2的塊,它是數字疊加的結果,圍繞著雷的區域重合了,重合的區域塊的數字相加,該塊的數字就會變成相加後的數字。
1.2 本博文的例子掃雷的規則
玩家需要把所有的空白塊點開,留下玩家認為有雷的塊,當所剩餘的塊數和雷的數量相等時,玩家勝利。如果在此之前,點到有雷的方塊,玩家失敗。
2. 遊戲的演算法和資料結構
2.1 空白塊展開演算法
空白塊的展開幾乎是掃雷遊戲的核心了。上面說到,掃雷遊戲時,點中空白塊,遊戲的地圖塊就會展開,我們可以觀察到:空白塊是一層一層展開的,所以,地圖展開演算法我們就用廣度優先搜尋。也許有人會問:可以用深度優先搜尋演算法嗎?答案是可以的,但是如果在這裡用的話,效率會比廣度優先搜尋演算法效率低。
2.2 掃雷的資料結構
(1)方向陣列
int[][] dir={ {-1,1},//左上角 {0,1},//正上 {1,1},//右上角 {-1,0},//正左 {1,0},//正右 {-1,-1},//左下角 {0,-1},//正下 {1,-1}//右下角 };
方向陣列在展開空白塊的時候回用到,因為廣度優先遍歷就是在地圖中朝各個方向走。
(2)Tile類
該類表示遊戲中的“塊”,我們給它宣告三個成員。
shortvalue; boolean flag; boolean open;
value儲存該塊的值。-1表示雷塊;0表示空白塊;>0代表數字塊。
flag儲存該雷是否被玩家標記(在本例子中無作用,保留,方便擴充套件)。
open儲存該塊是否被使用者點開過。
(3)Tile陣列
Tile陣列代表塊的集合,及遊戲的地圖,儲存著遊戲的主要資料。
(4)Point類
Point類代表“位置”,宣告Point類方便我們在地圖中生成隨機位置的雷。Point類還要重寫hashCode和equals方法,為了比較位置與位置是否相同。
(5)Mine類
對上面的資料結構的封裝。
Mine建構函式:對遊戲地圖的引數設定,比如繪製的位置,繪製的大小,塊的大小,生成的雷數等。
init()方法:清空並初始化遊戲地圖。
create(Point p)方法:在地圖中隨機生成雷的位置,併產生數字。引數p是不產生雷的位置,p點可以傳入使用者第一次點選時的位置。生成隨機位置的雷比較快速的辦法是:先把地圖中除p位置外所有的位置加入到連結串列中,然後生成0到連結串列大小-1之間的隨機數,根據生成的隨機數在連結串列中取元素,取完元素就把該位置從連結串列中移除,並把Tile陣列中該位置的Tile的value設為-1。重複執行以上操作,直到生成的雷個數滿足要求。產生數字的辦法:遍歷Tile陣列,遇到雷就將他身邊的八個的位置的value值加1,如果八個位置中有雷,或者該位置不存在,不執行任何操作。
open(Point p,boolean isFirst)方法:p代表點開某個位置的塊,即Tile陣列的索引。isFirst傳入是否是第一次點選螢幕。該方法要對是不是第一次點選而作不同的操作,當玩家第一次點選塊時,呼叫create函式生成地圖。否則就進行展開地圖等操作。
(6)MainView類
檢視類,負責繪圖和操作Mine物件。
3.程式碼示例
Mine.java
public class Mine { publicint x;//地圖的在螢幕上的座標點 publicint y;//地圖的在螢幕上的座標點 publicint mapCol;//矩陣寬 publicint mapRow;//矩陣高 privateint mineNum ; public static short EMPTY=0;//空 public static short MINE=-1;//雷 public Tile[][] tile;//地圖矩陣 publicint tileWidth;//塊寬 privatePaint textPaint; private Paint bmpPaint; privatePaint tilePaint; privatePaint rectPaint; privatePaint minePaint; private Random rd=new Random(); publicint mapWidth;//繪圖區寬 public int mapHeight;//繪圖區高 public boolean isDrawAllMine=false;//標記是否畫雷 privateint[][] dir={ {-1,1},//左上角 {0,1},//正上 {1,1},//右上角 {-1,0},//正左 {1,0},//正右 {-1,-1},//左下角 {0,-1},//正下 {1,-1}//右下角 };//表示八個方向 publicclass Tile{ short value; boolean flag; boolean open; public Tile() { this.value=0; this.flag=false; this.open=false; } } public static class Point{ private int x; private int y; public Point(int x,int y) { this.x=x; this.y=y; } @Override public int hashCode() { // TODO Auto-generated method stub return 2*x+y; } @Override public boolean equals(Object obj) { // TODO Auto-generated method stub return this.hashCode()==((Point)(obj)).hashCode(); } }//表示每個雷塊 public Mine(int x, int y, int mapCol, int mapRow, int mineNum, int tileWidth) { this.x=x; this.y=y; this.mapCol = mapCol; this.mapRow = mapRow; this.mineNum=mineNum; this.tileWidth=tileWidth; mapWidth=mapCol*tileWidth; mapHeight=mapRow*tileWidth; textPaint=new Paint(); textPaint.setAntiAlias(true); textPaint.setTextSize(MainActivity.W/10); textPaint.setColor(Color.RED); bmpPaint=new Paint(); bmpPaint.setAntiAlias(true); bmpPaint.setColor(Color.DKGRAY); tilePaint =new Paint(); tilePaint.setAntiAlias(true); tilePaint.setColor(0xff1faeff); minePaint =new Paint(); minePaint.setAntiAlias(true); minePaint.setColor(0xffff981d); rectPaint =new Paint(); rectPaint.setAntiAlias(true); rectPaint.setColor(0xff000000); rectPaint.setStyle(Paint.Style.STROKE); tile=new Tile[mapRow][mapCol]; } /** * 初始化地圖 */ publicvoid init() { for (int i = 0; i< mapRow; i++) { for (int j = 0; j< mapCol; j++) { tile[i][j]=new Tile(); tile[i][j].value=EMPTY; tile[i][j].flag=false; tile[i][j].open=false; isDrawAllMine=false; } } } /** * 生成雷 * @param exception 排除的位置,該位置不生成雷 */ public void create(Point exception) { List<Point> allPoint=new LinkedList<Point>(); //把所有位置加入連結串列 for (int i = 0; i< mapRow; i++)//y { for (int j = 0; j < mapCol; j++)//x { Point point=new Point(j,i); if(!point.equals(exception)) { allPoint.add(point); } } } List<Point> minePoint=new LinkedList<Point>(); //隨機產生雷 for (int i=0; i< mineNum; i++) { int idx=rd.nextInt(allPoint.size()); minePoint.add(allPoint.get(idx)); allPoint.remove(idx);//取了之後,從所有集合中移除 } //在矩陣中標記雷的位置 for(Iterator<Point> it=minePoint.iterator();it.hasNext();) { Point p=it.next(); tile[p.y][p.x].value=MINE; } //給地圖新增數字 for (int i = 0; i< mapRow; i++)//y { for (int j = 0; j< mapCol; j++)//x { short t=tile[i][j].value; if(t==MINE) { for (int k=0;k<8;k++) { int offsetX=j+dir[k][0],offsetY=i+dir[k][1]; if(offsetX>=0&&offsetX< mapCol &&offsetY>=0&&offsetY< mapRow ) { if (tile[offsetY][offsetX].value != -1) tile[offsetY][offsetX].value += 1; } } } } } } /** * 開啟某個位置 * @param op * @param isFirst 標記是否是第一次開啟 */ public void open(Point op,boolean isFirst) { if(isFirst) { create(op); } tile[op.y][op.x].open=true; if( tile[op.y][op.x].value==-1) return; else if( tile[op.y][op.x].value>0)//點中數字塊 { return; } //廣度優先遍歷用佇列 Queue<Point> qu=new LinkedList<Point>(); //加入第一個點 qu.offer(new Point(op.x,op.y)); //朝8個方向遍歷 for (int i=0;i<8;i++) { int offsetX=op.x+dir[i][0],offsetY=op.y+dir[i][1]; //判斷越界和是否已訪問 boolean isCan=offsetX>=0&&offsetX< mapCol &&offsetY>=0&&offsetY< mapRow; if(isCan) { if(tile[offsetY][offsetX].value==0 &&!tile[offsetY][offsetX].open) { qu.offer(new Point(offsetX, offsetY)); } else if(tile[offsetY][offsetX].value>0) { tile[offsetY][offsetX].open=true; } } } while(qu.size()!=0) { Point p=qu.poll(); tile[p.y][p.x].open=true; for (int i=0;i<8;i++) { int offsetX=p.x+dir[i][0],offsetY=p.y+dir[i][1]; //判斷越界和是否已訪問 boolean isCan=offsetX>=0&&offsetX< mapCol &&offsetY>=0&&offsetY< mapRow; if(isCan) { if( tile[offsetY][offsetX].value==0&&!tile[offsetY][offsetX].open) { qu.offer(new Point(offsetX, offsetY)); } else if(tile[offsetY][offsetX].value>0) { tile[offsetY][offsetX].open=true; } } } } } /** * 繪製地圖 * @param canvas */ publicvoid draw(Canvas canvas) { for (int i = 0; i< mapRow; i++) { for (int j = 0; j< mapCol; j++) { Tile t=tile[i][j]; if(t.open){ if(t.value>0) { canvas.drawText(t.value+"",x+j*tileWidth,y+i*tileWidth+tileWidth,textPaint); } }else { //標記,備用 if(t.flag) { }else { //畫矩形方塊 RectF reactF=new RectF(x+j*tileWidth,y+i*tileWidth,x+j*tileWidth+tileWidth,y+i*tileWidth+tileWidth); canvas.drawRoundRect(reactF,0,0, tilePaint); } } //是否畫出所有雷 if( isDrawAllMine&&tile[i][j].value==-1) { canvas.drawCircle((x + j * tileWidth) + tileWidth / 2, (y + i * tileWidth) + tileWidth / 2, tileWidth / 2, bmpPaint); } } } //畫邊框 canvas.drawRect(x,y,x+mapWidth,y+mapHeight, rectPaint); //畫橫線 for (int i = 0; i< mapRow; i++) { canvas.drawLine(x,y+i*tileWidth,x+mapWidth,y+i*tileWidth, rectPaint); } //畫豎線 for (int i = 0;i < mapCol; i++) { canvas.drawLine(x+i*tileWidth,y,x+i*tileWidth,y+mapHeight, rectPaint); } } }
MainView.java
public class MainView extends View { privateMine mine; privateboolean isFirst=true;//標記是否是本局第一次點選螢幕 privateContext context; private final int mineNum=10;//產生的雷的個數 privatefinal int ROW=15;//要生成的矩陣高 privatefinal int COL=8;//要生成的矩陣寬 privateint TILE_WIDTH=50;//塊大小 privateboolean isFalse=false; publicMainView(Context context) { super(context); this.context=context; TILE_WIDTH=MainActivity.W/10; mine=new Mine((MainActivity.W-COL*TILE_WIDTH)/2,(MainActivity.H-ROW*TILE_WIDTH)/2,COL,ROW,mineNum,TILE_WIDTH); try { mine.init(); }catch (Exception e){ e.printStackTrace(); } } /** * 遊戲邏輯 */ public void logic() { int count=0; for (int i=0;i<mine.mapRow;i++) { for (int j=0;j<mine.mapCol;j++) { if(!mine.tile[i][j].open) { count++; } } } //邏輯判斷是否勝利 if(count==mineNum) { new AlertDialog.Builder(context) .setMessage("恭喜你,你找出了所有雷") .setCancelable(false) .setPositiveButton("繼續", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mine.init(); invalidate(); isFirst=true; } }) .setNegativeButton("退出", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { System.exit(0); } }) .create() .show(); } } /** * 重新整理View * @param canvas */ @Override protected void onDraw(Canvas canvas) { mine.draw(canvas); } /** * 點選螢幕事件 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { if(event.getAction()==MotionEvent.ACTION_DOWN) { int x=(int)event.getX(); int y=(int)event.getY(); //判斷是否點在範圍內 if(x>=mine.x&&y>=mine.y&&x<=(mine.mapWidth+mine.x)&&y<=(mine.y+mine.mapHeight)) { int idxX=(x-mine.x)/mine.tileWidth; int idxY=(y-mine.y)/mine.tileWidth; mine.open(new Mine.Point(idxX,idxY),isFirst); isFirst=false; if(mine.tile[idxY][idxX].value==-1) { mine.isDrawAllMine=true; new AlertDialog.Builder(context) .setCancelable(false) .setMessage("很遺憾,你踩到雷了!") .setPositiveButton("繼續", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mine.init(); isFalse=true; isFirst=true; invalidate(); } }) .setNegativeButton("退出", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { System.exit(0); } }) .create() .show(); } if(isFalse) { isFalse=false; invalidate(); return true; } logic(); invalidate(); } } return true; } }
MainActivity.java
public class MainActivity extends Activity { publicstaticint W; publicstaticint H; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); W = dm.widthPixels;//寬度 H = dm.heightPixels ;//高度 setContentView(new MainView(this)); new AlertDialog.Builder(this) .setCancelable(false) .setTitle("遊戲規則") .setMessage("把你認為不是雷的位置全部點開,只留著有雷的位置,每局遊戲有10個雷。\n\n--臥槽工作室") .setPositiveButton("我知道了",null) .create() .show(); } }
完整程式碼:https://github.com/luoyesiqiu/Mine