移动迷宫——拼图游戏

Posted 黄飞_hf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动迷宫——拼图游戏相关的知识,希望对你有一定的参考价值。

拼图游戏就是将用户选择的图片根据选择的难度进行分割,并抽出一张图片用空白块代替,同时随机打乱顺序,作为拼图的初始游戏界面。当用户点击空白块周围上下左右相邻的图片时,可以移动该图片,当整个图片的位置都与原始图片一致时,拼图完成。

拼图算法

这个拼图游戏用到了人工智能领域的一个非常著名的算法——N Puzzle问题。随机交换图片的位置之后,生成的拼图游戏很多是无解的,且这个比例高达50%左右,所以必须先判断生成的拼图是否有解。

我们将图片暂时用数字来代替,这样整个拼图游戏就变成了一个数字矩阵,假设随机得到了这个矩阵,其中X用来代表空格的图片,如下所示:

12758111X1310496214153
对图片拼图的还原,与还原上图中的数字矩阵,其实是一个道理。现在,将这个矩阵写成一维数组的形式,并将其记为序列A, 如下所示。

A = 12, 1, 10, 2, 7, 11, 4, 14, 5, X, 9, 15, 8, 13, 6, 3

再定义一个“倒置变量值”的算法—— T Ti表示序列 A 中位于第i位之后,比Ai小的元素的个数(不算X)。例如对上面的序列 A 中的每个元素进行“倒置变量值”的计算,其结果如下所示。

11, 0, 8, 0, 4, 6, 1, 6, 1, 3, 4, 2, 2, 1

最后,求得所有“倒置变量值”的和SumT=49在N Puzzle算法中,使用如下两个原则来判断一个N Puzzle问题是否有解。

  • 如果序列A的宽度为奇数,那么每个可解的问题所定义的“倒置变量值”的和—— SumT 必须为偶数。
  • 如果序列A的宽度为偶数,那么当空格X位于从下往上数的奇数行中时,定义的“倒置变量值”的和—— SumT 必须为偶数;当空格X位于从下往上数的偶数行中时,定义的“倒置变量值”的和—— SumT 必须为奇数。

图片工具

通过这两步处理,就把图片分割成了NxN个小的Item,并将最后一个图片剔除,用于显示要移动的空格图片(即数字X)。最后,通过GridView的数据适配器,将这些图片添加到拼图游戏的GridView中显示出来。

/**
 * Created by Administrator on 2016/6/14.
 * 图像工具类:实现图像的分割与自适应
 */
public class ImagesUtil 

    /**
     * 切图、初始状态(正常顺序)
     *
     * @param type        游戏种类
     * @param picSelected 选择的图片
     * @param context     context
     */
    public void createInitBitmaps(int type, Bitmap picSelected, Context context) 
        ItemBean itemBean = null;
        Bitmap bitmap = null;
        List<Bitmap> bitmapItems = new ArrayList<Bitmap>();
        // 每个Item的宽高
        int itemWidth = picSelected.getWidth() / type;
        int itemHeight = picSelected.getHeight() / type;
        for (int i = 1; i <= type ; i++) 
            for (int j = 1; j <= type; j++) 
                bitmap = Bitmap.createBitmap(picSelected, (j - 1) * itemWidth, (i -1) * itemHeight,
                        itemWidth, itemHeight);
                bitmapItems.add(bitmap);
                itemBean = new ItemBean((i - 1) * type + j, (i - 1) * type + j, bitmap);
                GameUtil.mItemBeans.add(itemBean);
            
        
        // 保存最后一个图片在拼图完成时填充
        PuzzleActivity.mLastBitmap = bitmapItems.get(type * type -1);
        // 设置最后一个为空Item
        bitmapItems.remove(type * type -1);
        GameUtil.mItemBeans.remove(type * type -1);
        Bitmap blankBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.blank);
        blankBitmap = Bitmap.createBitmap(blankBitmap, 0 , 0, itemWidth, itemHeight);

        bitmapItems.add(blankBitmap);
        GameUtil.mItemBeans.add(new ItemBean(type * type, 0, blankBitmap));
        GameUtil.mBlankItemBean = GameUtil.mItemBeans.get(type * type - 1);
    

    /**
     * 处理图片 放大、缩小到合适位置
     *
     * @param newWidth  缩放后Width
     * @param newHeight 缩放后Height
     * @param bitmap    bitmap
     * @return bitmap
     */
    public Bitmap resizeBitmap(float newWidth, float newHeight, Bitmap bitmap) 
        Matrix matrix = new Matrix();
        matrix.postScale(newWidth / bitmap.getWidth(), newHeight/ bitmap.getHeight());
        Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        return newBitmap;
    

生成游戏

前面获取了按顺序分割的图片,下面通过一个循环来随机交换Item中的图片,从而生成杂乱的拼图游戏。由于随机生成的拼图游戏有将近50%都是无解的,所以还需要判断当前生成的游戏是否有解。

 // 游戏信息单元格Bean
    public static List<ItemBean> mItemBeans = new ArrayList<ItemBean>();
    // 空格单元格
    public static ItemBean mBlankItemBean = new ItemBean();

    /**
     * 生成随机的Item
     */
    public static void getPuzzleGenerator() 
        int index = 0;
        // 随机打乱顺序
        for (int i = 0; i < mItemBeans.size(); i++) 
            index = (int) (Math.random() * PuzzleActivity.TYPE * PuzzleActivity.TYPE);
            swapItems(mItemBeans.get(index), mBlankItemBean);
        

        List<Integer> data = new ArrayList<Integer>();
        for (int i = 0; i < mItemBeans.size(); i++) 
            data.add(mItemBeans.get(i).getBitmapId());
        

        // 判断生成是否有解
        if (!canSolve(data))
            getPuzzleGenerator();
    


    /**
     * 交换空格与点击Item的位置
     *
     * @param from  交换图
     * @param blank 空白图
     */
    public static void swapItems(ItemBean from, ItemBean blank) 
        ItemBean tempItemBean = new ItemBean();
        // 交换BitmapId
        tempItemBean.setBitmapId(from.getBitmapId());
        from.setBitmapId(blank.getBitmapId());
        blank.setBitmapId(tempItemBean.getBitmapId());
        // 交换Bitmap
        tempItemBean.setBitmap(from.getBitmap());
        from.setBitmap(blank.getBitmap());
        blank.setBitmap(tempItemBean.getBitmap());
        // 设置新的Blank
        GameUtil.mBlankItemBean = from;
    

    /**
     * 该数据是否有解
     *
     * @param data 拼图数组数据
     * @return 该数据是否有解
     */
    public static boolean canSolve(List<Integer> data) 
        //获取空格Id
        int blankId = mBlankItemBean.getItemId();
        // 可行性原则
        if (data.size() % 2 == 1) 
            return getInversions(data) % 2 == 0;
         else 
            // 从底往上数,空格位于奇数行
            if (((blankId - 1) / PuzzleActivity.TYPE) % 2 == 1) 
                return getInversions(data) % 2 == 0;
             else // 从底往上数,空位位于偶数行
                return getInversions(data) % 2 == 1;
            
        
    

    /**
     * 计算倒置和算法
     *
     * @param data 拼图数组数据
     * @return 该序列的倒置和
     */
    public static int getInversions(List<Integer> data) 
        int inversions = 0;
        int inversionCount = 0;
        for (int i = 0; i < data.size(); i++) 
            for (int j = i + 1; j < data.size(); j++) 
                int index = data.get(i);
                if (data.get(j) != 0 && data.get(j) < index)
                    inversionCount++;
            
            inversions += inversionCount;
            inversionCount = 0;
        
        return inversions;
    

移动图片

生成游戏后,就可以给GridView的每个Item来响应点击事件了。当点击的图片是空格图片上下左右的图片时,就可以交换两个Item,实现移动的效果。如果点击的是其他图片,自然是不能移动。

    /**
     * 判断点击的Item是否可移动
     *
     * @param position position
     * @return 能否移动
     */
    public static boolean isMoveable(int position) 
        int type = PuzzleActivity.TYPE;
        // 获取空格Item
        int blankId = mBlankItemBean.getItemId() - 1;
        // 不同行 相差为type
        if (Math.abs(blankId - position) == type) 
            return true;
        

        // 相同行 相差为1
        if ((blankId / type == position / type) && Math.abs(blankId - position) == 1) 
            return true;
        

        return false;
    

如果判读可以移动,则使用swapItems方法进行交换。每次交换后,还需要对当前游戏进行判断。判断是否已经还原成功,即当前图片Item的ID与初始状态下图片的ID是否相同。如果还原成功,那么就将最后缺失的那一块图片补齐。

    /**
     * 是否拼图成功
     *
     * @return 是否拼图成功
     */
    public static boolean isSuccess() 
        for (ItemBean tempBean : mItemBeans) 
            if(tempBean.getBitmapId() != 0 && tempBean.getItemId() == tempBean.getBitmapId())
                continue;
            else if(tempBean.getBitmapId() == 0 &&
                    tempBean.getItemId() == PuzzleActivity.TYPE * PuzzleActivity.TYPE)
                continue;
             else 
                return false;
            
        
        return true;
    

程序运行后效果图如下所示:

代码地址

以上是关于移动迷宫——拼图游戏的主要内容,如果未能解决你的问题,请参考以下文章

拼图游戏(数码还原游戏)的实现

走出迷宫小游戏

A*算法拼图游戏

C# 拼图游戏(超详细)

拼图问题可解性探究

java大作业之拼图游戏