【Android】自己动手做个扫雷游戏

扫雷是玩法极其简单的小游戏,点击玩家认为不存在雷的区域,标记出全部地雷所在的区域,即可获得胜利。当点击不包含雷的块的时候,可能它底下存在一个数,也可能是一个空白块。当点击中有数字的块时,游戏会展现当前点击块所包含的数字。当点击空白块时,地图会展开,形成一个大小和形状不规则的图形,该图形的边界是数字块,也可以想成展开的是一个被数字包围着的不规则图形。

pic

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类

该类表示游戏中的“块”,我们给它声明三个成员。

short    value; 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函数生成地图。否则就进行展开地图等操作。

(5)MainView类

视图类,负责绘图和操作Mine对象。

3.代码示例 Mine.java public class Mine { public int x;//地图的在屏幕上的坐标点 public int y;//地图的在屏幕上的坐标点 public int mapCol;//矩阵宽 public int mapRow;//矩阵高 private int mineNum ; public static short EMPTY=0;//空 public static short MINE=-1;//雷 public Tile[][] tile;//地图矩阵 public int tileWidth;//块宽 private Paint textPaint; private Paint bmpPaint; private Paint tilePaint; private Paint rectPaint; private Paint minePaint; private Random rd=new Random(); public int mapWidth;//绘图区宽 public int mapHeight;//绘图区高 public boolean isDrawAllMine=false;//标记是否画雷 private int[][] dir={ {-1,1},//左上角 {0,1},//正上 {1,1},//右上角 {-1,0},//正左 {1,0},//正右 {-1,-1},//左下角 {0,-1},//正下 {1,-1}//右下角 };//表示八个方向 public class 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]; } /** * 初始化地图 */ public void 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 */ public void 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 { private Mine mine; private boolean isFirst=true;//标记是否是本局第一次点击屏幕 private Context context; private final int mineNum=10;//产生的雷的个数 private final int ROW=15;//要生成的矩阵高 private final int COL=8;//要生成的矩阵宽 private int TILE_WIDTH=50;//块大小 private boolean isFalse=false; public MainView(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 { public static int W; public static int 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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyzgpp.html