从零开始开发Android版2048 (3)逻辑判断

从零开始开发Android版2048 (三)逻辑判断

          最近工作比较忙,所以更新的慢了一点,今天的主要内容是关于Android版2048的逻辑判断,经过本篇的讲解,基本上完成了这个游戏的主体部分。

         首先还是看一下,我在实现2048时用到的一些存储的数据结构。我在实现时,为了省事存储游戏过程中的变量主要用到的是List。

比如说:List<Integer> spaceList = new ArrayList<Integer>();这个spaceList主要用于保存,所有空白格的位置,也就是空白格在GridLayout中的位置(从0到15)

         对于数字格,以及格子对应的数据,我写了一个类如下:

package com.example.t2048;

import java.util.ArrayList;
import java.util.List;

import android.util.Log;

/**
 * 用于保存数字格,已经数字格对应的数字
 * @author Mr.Wang
 * 
 */
public class NumberList {

	//这个list用于保存所有不为空的格子的坐标(在GridLayout中的位置从0到15)
	private List<Integer> stuffList = new ArrayList<Integer>();
	//这个list用于保存所有不为空的格子对应的数字(以2为底数的指数)
	private List<Integer> numberList = new ArrayList<Integer>();
	
	/**
	 * 新加入的数字格
	 * @param index   数字格对应的位置
	 * @param number  对应数字的指数(以2为底数)
	 */
	public void add(int index, int number){
		stuffList.add(index);
		numberList.add(number);
	}
	
	/**
	 * 用于判断当前位置是否为数字格
	 * @param index  当前位置
	 * @return true表示是
	 */
	public boolean contains(int index){
		return stuffList.contains(index);
	}
	
	/**
	 * 将当前的格子从数字列表中去掉
	 * @param index
	 */
	public void remove(int index){
		int order = stuffList.indexOf(index);
		numberList.remove(order);
		stuffList.remove(order);
	}
	
	/**
	 * 将当前格子对应的数字升级,指数加1
	 * @param index
	 */
	public void levelup(int index){
		int order = stuffList.indexOf(index);
		numberList.set(order, numberList.get(order)+1);
	}
	
	/**
	 * 将当前格子对应的位置置换为新的位置
	 * @param index      当前位置
	 * @param newIndex   新的位置
	 */
	public void changeIndex(int index, int newIndex){
		stuffList.set(stuffList.indexOf(index), newIndex);
	}
	
	/**
	 * 通过格子对应的位置获取其对应的数字
	 * @param index   当前位置
	 * @return        格子对应数字的指数
	 */
	public int getNumberByIndex(int index){
		int order = stuffList.indexOf(index);
		return numberList.get(order) ;
	}
	
	
	public String toString(){
		return stuffList.toString()+numberList.toString();
	}
	
	public void printLog(){
		Log.i("stuffList", stuffList.toString());
		Log.i("numberList", numberList.toString());
	}
}

             这个类主要是我对数字格、数字格对应数字的保存,和增删改等操作。其实就是两个list,我为了操作起来方便,所以把他们写在一个类里。


然后,我们来讲一下这个游戏的逻辑。

比如,我们在游戏过程中执行了一次向右滑动的操作,在这个操作中,我们要对所有可以移动和合并的格子进行判断和相应的操作:

1、数字格的右边如果是空白格,则数字格与空白格交换

2、数字格右边如果有多个空白格,则数字格与连续的最后一个空白格做交换

3、数字格的右边如果存在与之相同的数字格,则本格置空,右边的数字格升级(指数加一)

4、如果滑动方向连续存在多个相同的数字格,右的格子优先升级

5、在一次滑动中,每个格子最多升级一次

当一个格子存在上述前四种中的任意一种时,则完成了对它的操作

我们试着把上面的判断规则翻译成代码,首先,明确在GridLayout中的坐标位置,我在GridLayout中采用的是水平布局,所以每个格子对应的位置如下

从零开始开发Android版2048 (3)逻辑判断

在这个基础上,我建立如下的坐标轴,以左上角为原点,x轴为横轴,y轴为竖轴:

从零开始开发Android版2048 (3)逻辑判断


当向右滑动的时候,从上面逻辑来看,为了方便,我们应当从右向左遍历格子

			for(int y=0;y<4;y++){
				for(int x=2;x>=0;x--){
					int thisIdx = 4*y +x;
					Change(thisIdx,direction);
				}
			}

每遍历到一个新的格子,执行一次change()方法,其实应该是每遍历到一个非空的格子,执行一次change()但是我为了省事,把非空判断加到了change的代码里,我们看一下change()这个方法的实现,这个方法主要是用来判断,一个格子是需要移动、合并,还是什么都不操作。:

	/**
	 * 该方法,为每个符合条件的格子执行变动的操作,如置换,升级等
	 * @param thisIdx     当前格子的坐标
	 * @param direction   滑动方向
	 */
	public void Change(int thisIdx,int direction){
		if(numberList.contains(thisIdx)){
						
			int nextIdx = getLast(thisIdx, direction);
			if(nextIdx == thisIdx){
				//不能移动
				return;
			}else if(spaceList.contains(nextIdx)){
				//存在可以置换的空白格
				replace(thisIdx,nextIdx);
			}else{				
				if(numberList.getNumberByIndex(thisIdx) == numberList.getNumberByIndex(nextIdx)){
					//可以合并
					levelup(thisIdx, nextIdx);
				}else{
					int before = getBefore(nextIdx, direction);
					if(before != thisIdx){
						//存在可以置换的空白格
						replace(thisIdx,before);
					}
				}
			}
		}
	}

        其中getLast()方法,用于获取当前格子在移动方向的可以移动或者合并的最后一个格子,如果返回值还是当前的格子,则表示不能移动。其中调用的getNext()方法是为了获取当前格子在移动方向的下个格子的位置。

	/**
	 * 用于获取移动方向上最后一个空白格之后的位置
	 * @param index      当前格子的坐标
	 * @param direction  移动方向
	 * @return
	 */
	public int getLast(int thisIdx, int direction){
		 int nextIdx = getNext(thisIdx, direction);
		 if(nextIdx < 0)
			 return thisIdx;
		 else{
			 if(spaceList.contains(nextIdx))
				 return getLast(nextIdx, direction);
			 else
				 return nextIdx;
		 }		
	}
        然后是replace(int thisIdx, int nextIdx),这个方法是执行两个格子互换位置,内容主要是对两个格子中的view更换背景图片,然后操作空白格的list和数字格的list:

	/**
	 * 该方法用来交换当前格与目标空白格的位置
	 * @param thisIdx 当前格子的坐标
	 * @param nextIdx 目标空白格的坐标
	 */
	public void replace(int thisIdx, int nextIdx){
		moved = true;
		//获取当前格子的view,并将其置成空白格
		View thisView = gridLayout.getChildAt(thisIdx);
		ImageView image = (ImageView) thisView.findViewById(R.id.image);
		image.setBackgroundResource(icons[0]);
		
		//获取空白格的view,并将其背景置成当前格的背景
		View nextView = gridLayout.getChildAt(nextIdx);
		ImageView nextImage = (ImageView) nextView.findViewById(R.id.image);
		nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(thisIdx)]);
		
		//在空白格列表中,去掉目标格,加上当前格
		spaceList.remove(spaceList.indexOf(nextIdx));
		spaceList.add(thisIdx);
		
		//在数字格列表中,当前格的坐标置换成目标格的坐标
		numberList.changeIndex(thisIdx, nextIdx);
	}


        levelup(int thisIdx, int nextIdx)这个方法是为了实现相同数字格的合并操作,其实就是将当前的格子置成空白格,将移动方向上下一个格子对应的背景置成下一个背景:

	/**
	 * 刚方法用于合并在移动方向上两个相同的格子
	 * @param thisIdx 当前格子的坐标
	 * @param nextIdx 目标格子的坐标
	 */
	public void levelup(int thisIdx, int nextIdx){
		
		//一次移动中,每个格子最多只能升级一次
		if(!changeList.contains(nextIdx)){
			moved = true;
			//获取当前格子的view,并将其置成空白格
			View thisView = gridLayout.getChildAt(thisIdx);
			ImageView image = (ImageView) thisView.findViewById(R.id.image);
			image.setBackgroundResource(icons[0]);
			
			
			//获取目标格的view,并将其背景置成当前格升级后的背景
			View nextView = gridLayout.getChildAt(nextIdx);
			ImageView nextImage = (ImageView) nextView.findViewById(R.id.image);
			nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(nextIdx)+1]);
			
			//在空白格列表中加入当前格
			spaceList.add(thisIdx);
			//在数字列中删掉第一个格子
			numberList.remove(thisIdx);
			//将数字列表对应的内容升级
			numberList.levelup(nextIdx);
			
			changeList.add(nextIdx);
		}

	}


写完这些,基本完成了主要的判断,但是还有两个问题:1是如果每次滑动没有格子移动(合并),那么就不应该新随机生成格子;2每个格子只能合并一次。

为解决这两个问题,我又加了两个变量

	//用于保存每次操作时,已经升级过的格子
	List<Integer> changeList = new ArrayList<Integer>();
	
	//用于表示本次滑动是否有格子移动过
	boolean moved = false;


其中changeList在每次滑动前清空,然后加入本次移动中发生过合并的格子,在每次合并的判断时首先看看要合并的格子是不是在这个list中,如果在的话,说明已经合并过,那么就不执行合并的操作了。

还有个波尔型的moved变量,这个也是在每次滑动前置为false,如果在本次滑动中,有格子移动或者合并,就置为ture,在滑动的最后,通过这个变量判断是否要随机生产新的格子。


下面是完整的Activity中的代码:

package com.example.t2048;


import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import android.app.Activity;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.GridLayout;
import android.widget.ImageView;

public class MainActivity extends Activity {
	
	final static int LEFT = -1;
	final static int RIGHT = 1;
	final static int UP = -4;
	final static int DOWN = 4;

	GridLayout gridLayout = null;
	
	//用于保存空格的位置
	List<Integer> spaceList = new ArrayList<Integer>();
	
	//所有非空的格子
	NumberList numberList = new NumberList();
	
	//用于保存每次操作时,已经升级过的格子
	List<Integer> changeList = new ArrayList<Integer>();
	
	//用于表示本次滑动是否有格子移动过
	boolean moved = false;
	
	GestureDetector gd = null;
	
	/**
	 * 图标数组
	 */
	private final int[] icons = { R.drawable.but_empty, R.drawable.but2,
			R.drawable.but4, R.drawable.but8, R.drawable.but16,
			R.drawable.but32, R.drawable.but64, R.drawable.but128,
			R.drawable.but256, R.drawable.but512, R.drawable.but1024,
			R.drawable.but2048, R.drawable.but4096 };
	
	protected void onCreate(Bundle savedInstanceState) {
		System.out.println("程序启动");
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		gridLayout = (GridLayout) findViewById(R.id.GridLayout1);
		
		init();
		
		MygestureDetector mg = new MygestureDetector();

		gd = new GestureDetector(mg);
		gridLayout.setOnTouchListener(mg);
		gridLayout.setLongClickable(true);		
	}
	
	//初始化界面
	public void init(){
		System.out.println("初始化");
		
		//首先在16个各种都填上空白的图片
		for(int i=0;i<16;i++){
			View view = View.inflate(this, R.layout.item, null);
			ImageView image = (ImageView) view.findViewById(R.id.image);
			
			image.setBackgroundResource(icons[0]);
			spaceList.add(i);
			gridLayout.addView(view);	
		}
		
		//在界面中随机加入两个2或者4
		addRandomItem();
		addRandomItem();
	}
	
	//从空格列表中随机获取位置
	public int getRandomIndex(){
		Random random = new Random();
		if(spaceList.size()>0)
		 return random.nextInt(spaceList.size());
		else 
		 return -1;	
	}
	
	//在空白格中随机加入数字2或4
	public void addRandomItem(){
		int index = getRandomIndex();
		if(index!=-1){
			System.out.println("随机生成数字 位置"+spaceList.get(index));
			//获取对应坐标所对应的View
			View view = gridLayout.getChildAt(spaceList.get(index));
			ImageView image = (ImageView) view.findViewById(R.id.image);
			//随机生成数字1或2
			int i = (int) Math.round(Math.random()+1);
			//将当前格子的图片置换为2或者4
			image.setBackgroundResource(icons[i]);	
		
			//在numList中加入该格子的信息
			numberList.add(spaceList.get(index), i);
			
			//在空白列表中去掉这个格子
			spaceList.remove(index);
		
		}
	}
	
	public class MygestureDetector implements OnGestureListener,OnTouchListener{

		@Override
		public boolean onTouch(View v, MotionEvent event) {
			// TODO Auto-generated method stub		
			return gd.onTouchEvent(event);
		}

		@Override
		public boolean onDown(MotionEvent e) {
			// TODO Auto-generated method stub
			return false;
		}

		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
				float velocityY) {
			
	        // 参数解释:      
	        // e1:第1个ACTION_DOWN MotionEvent      
	        // e2:最后一个ACTION_MOVE MotionEvent      
	        // velocityX:X轴上的移动速度,像素/秒      
	        // velocityY:Y轴上的移动速度,像素/秒      
	        
	        // 触发条件 :      
	        // X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒    
			
			if(e1.getX()-e2.getX()>100){
				System.out.println("向左");
				move(LEFT);
				return true;
			}else	if(e1.getX()-e2.getX()<-100){
				System.out.println("向右");
				move(RIGHT);
				return true;
			}else	if(e1.getY()-e2.getY()>100){
				System.out.println("向上");
				move(UP);
				return true;
			}else	if(e1.getY()-e2.getY()<-00){
				System.out.println("向下");
				move(DOWN);
				return true;
			}
			return false;
		}

		@Override
		public void onLongPress(MotionEvent e) {
			// TODO Auto-generated method stub
			
		}

		@Override
		public boolean onScroll(MotionEvent e1, MotionEvent e2,
				float distanceX, float distanceY) {
			// TODO Auto-generated method stub
			return false;
		}

		@Override
		public void onShowPress(MotionEvent e) {
			// TODO Auto-generated method stub
			
		}

		@Override
		public boolean onSingleTapUp(MotionEvent e) {
			// TODO Auto-generated method stub
			return false;
		}
		
	}
	
	/**
	 * 用于获取移动方向上下一个格子的位置
	 * @param index      当前格子的位置
	 * @param direction  滑动方向
	 * @return 如果在边界在返回-1
	 */
	public int getNext(int index,int direction){
		
		int y = index/4;
		int x = index%4;
		
		if(x==3 && direction==RIGHT)	
			return -1;			
		if(x==0 && direction==LEFT)
			return -1;
		if(y==0 && direction==UP)
			return -1;
		if(y==3 && direction==DOWN)
			return -1;	
		return index+direction;	
	}
	
	/**
	 * 用于获取移动方向上前一个格子的位置
	 * @param index      当前格子的位置
	 * @param direction  滑动方向
	 * @return 如果在边界在返回-1
	 */
	public int getBefore(int index,int direction){
		
		int y = index/4;
		int x = index%4;
		
		if(x==0 && direction==RIGHT)	
			return -1;			
		if(x==3 && direction==LEFT)
			return -1;
		if(y==3 && direction==UP)
			return -1;
		if(y==0 && direction==DOWN)
			return -1;	
		return index-direction;	
	}
	

	/**
	 * 该方法用来交换当前格与目标空白格的位置
	 * @param thisIdx 当前格子的坐标
	 * @param nextIdx 目标空白格的坐标
	 */
	public void replace(int thisIdx, int nextIdx){
		moved = true;
		//获取当前格子的view,并将其置成空白格
		View thisView = gridLayout.getChildAt(thisIdx);
		ImageView image = (ImageView) thisView.findViewById(R.id.image);
		image.setBackgroundResource(icons[0]);
		
		//获取空白格的view,并将其背景置成当前格的背景
		View nextView = gridLayout.getChildAt(nextIdx);
		ImageView nextImage = (ImageView) nextView.findViewById(R.id.image);
		nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(thisIdx)]);
		
		//在空白格列表中,去掉目标格,加上当前格
		spaceList.remove(spaceList.indexOf(nextIdx));
		spaceList.add(thisIdx);
		
		//在数字格列表中,当前格的坐标置换成目标格的坐标
		numberList.changeIndex(thisIdx, nextIdx);
	}
	
	/**
	 * 刚方法用于合并在移动方向上两个相同的格子
	 * @param thisIdx 当前格子的坐标
	 * @param nextIdx 目标格子的坐标
	 */
	public void levelup(int thisIdx, int nextIdx){
		
		//一次移动中,每个格子最多只能升级一次
		if(!changeList.contains(nextIdx)){
			moved = true;
			//获取当前格子的view,并将其置成空白格
			View thisView = gridLayout.getChildAt(thisIdx);
			ImageView image = (ImageView) thisView.findViewById(R.id.image);
			image.setBackgroundResource(icons[0]);
			
			
			//获取目标格的view,并将其背景置成当前格升级后的背景
			View nextView = gridLayout.getChildAt(nextIdx);
			ImageView nextImage = (ImageView) nextView.findViewById(R.id.image);
			nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(nextIdx)+1]);
			
			//在空白格列表中加入当前格
			spaceList.add(thisIdx);
			//在数字列中删掉第一个格子
			numberList.remove(thisIdx);
			//将数字列表对应的内容升级
			numberList.levelup(nextIdx);
			
			changeList.add(nextIdx);
		}

	}
	
	/**
	 * 该方法为不同的滑动方向,执行不同的遍历顺序
	 * @param direction 滑动方向
	 */
	public void move(int direction){
		
		moved = false;
		
		changeList.clear();
		
		numberList.printLog();
		
		switch(direction){
		case RIGHT:
			for(int y=0;y<4;y++){
				for(int x=2;x>=0;x--){
					int thisIdx = 4*y +x;
					Change(thisIdx,direction);
				}
			}
			break;
		case LEFT:
			for(int y=0;y<4;y++){
				for(int x=1;x<=3;x++){
					int thisIdx = 4*y +x;
					Change(thisIdx,direction);
				}
			}
			break;
		case UP:
			for(int x=0;x<4;x++){
				for(int y=1;y<=3;y++){
					int thisIdx = 4*y +x;
					Change(thisIdx,direction);
				}
			}
			break;	
		case DOWN:
			for(int x=0;x<4;x++){
				for(int y=2;y>=0;y--){
					int thisIdx = 4*y +x;
					Change(thisIdx,direction);
				}
			}
			break;
		}
		
		//如果本次滑动有格子移动过,则随机填充新的格子
		if(moved)
			addRandomItem();

	}
	
	/**
	 * 该方法,为每个符合条件的格子执行变动的操作,如置换,升级等
	 * @param thisIdx     当前格子的坐标
	 * @param direction   滑动方向
	 */
	public void Change(int thisIdx,int direction){
		if(numberList.contains(thisIdx)){
						
			int nextIdx = getLast(thisIdx, direction);
			if(nextIdx == thisIdx){
				//不能移动
				return;
			}else if(spaceList.contains(nextIdx)){
				//存在可以置换的空白格
				replace(thisIdx,nextIdx);
			}else{				
				if(numberList.getNumberByIndex(thisIdx) == numberList.getNumberByIndex(nextIdx)){
					//可以合并
					levelup(thisIdx, nextIdx);
				}else{
					int before = getBefore(nextIdx, direction);
					if(before != thisIdx){
						//存在可以置换的空白格
						replace(thisIdx,before);
					}
				}
			}
		}
	}
	
	/**
	 * 用于获取移动方向上最后一个空白格之后的位置
	 * @param index      当前格子的坐标
	 * @param direction  移动方向
	 * @return
	 */
	public int getLast(int thisIdx, int direction){
		 int nextIdx = getNext(thisIdx, direction);
		 if(nextIdx < 0)
			 return thisIdx;
		 else{
			 if(spaceList.contains(nextIdx))
				 return getLast(nextIdx, direction);
			 else
				 return nextIdx;
		 }		
	}
	
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

}


         写到这里,做为我学习Android以来,第一个自己写的程序已经完成一半了。逻辑判断这部分写的时候,还是费了一点时间,因为总有一些情况没有考虑进来,到现在基本上已经实现了。但是也反应出来一个很重要的问题,那就是自己在数据结构和算法方面还是很薄弱,整个读一下自己写的代码,为了完成对各种情况的判断,整个代码看起来十分冗余,而且效率之低就更不用说了。再看看别人写的代码,感觉自己在开发方面还是有很长的路要走的。

        接下来的时间,我会利用工作之余的时间不断去完善这个程序,并尽可能的去优化。大家共勉吧!

代码写成这样,我也不藏拙了,我把代码打包上传了,需要的朋友可以下载,也希望大家多多指正

   下载地址http://download.csdn.net/detail/johnsonwce/7269315

1楼x359981514昨天 17:48
算法好复杂,而且拓展性不太好,这是我的思路,大家可以一起学习下nhttp://blog.csdn.net/eclipsexys/article/details/24582957
Re: johnsonwce昨天 19:05
回复x359981514n嗯,我看过你的文章,很有参考价值,咱们的思路还是比较接近的,我把四个方向写成常量,当时也是考虑要不要扩展的问题。但是写的过程中犯懒有的没有严格地写下去