iOS 滑块拼图游戏(Puzzle8)

作者: Mr_Wendao | 来源:发表于2017-08-31 13:24 被阅读230次

    效果图&DEMO

    效果图

    一、准备工作

    先了解一个定义和定理

    定义:在一个1,2,...,n的排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。逆序数为偶数的排列称为偶排列;逆序数为奇数的排列称为奇排列。如2431中,21,43,41,31是逆序,逆序数是4,为偶排列。——这是北大《高等代数》上的定义。

    定理:交换一个排列中的两个数,则排列的奇偶性发生改变。

    二、实现过程

    以3*3拼图为例进行分析

    1、随机打乱拼图

    1)初始化从0-8的数组initializeNums

    NSMutableArray *initializeNums = [NSMutableArray array];//初始化0-n数字
    for (int i = 0; i < _puzzleCount; i++) {
        [initializeNums addObject:@(i)];
    }
    

    2)从initializeNums随机抽取数字add到数组randomNums,得到随机数组

    NSMutableArray *randomNums = [NSMutableArray array];//随机数组
    for (int i = 0; i < _puzzleCount; i++) {   
        int randomNum = arc4random() % initializeNums.count;
        [randomNums addObject:initializeNums[randomNum]];
        [initializeNums removeObjectAtIndex:randomNum];   
    }
    

    3)判断拼图是否可还原

    图1,通过移动要还原到的拼图状态
    图2,随机打乱的拼图状态
    图3,将图2中的空白块移动到拼图右下角的拼图状态,用来计算打乱的拼图是否可以还原
    ④ 空白块处相当于数字8
    ⑤ 我们的目的是把打乱拼图如图2通过移动(空白块与相邻数字块位置交换)还原到图1状态
    ⑥ 不是每个随机打乱的拼图都能还原到图1状态(根据定义定理有50%概率随机打乱的拼图不能还原)
    ⑦ 根据定义定理图1的逆序数为0,为偶排列。所以只有图3也为偶排列,图2才有可能还原到图1状态

    图1 图2

    如何计算图3的逆序数

    ① 先计算图2的逆序数
    ② 再计算图2图3变换步数
    ③ 将两者相加即得图3逆序数

    图3

    判断图2是否可还原代码:

    //判断是否可还原拼图
    inverCount = 0;
    int curNum = 0;
    int nextNum = 0;
    for (int i = 0; i < _puzzleCount; i++) {
        curNum = [randomNums[i] intValue];
        if (curNum == _puzzleCount - 1) {
            inverCount += _difficulty - 1 - (i / _difficulty);
            inverCount += _difficulty - 1 - (i % _difficulty);
        }
        for (int j = i + 1; j < _puzzleCount; j++) {
            nextNum = [randomNums[j] intValue];
            if (curNum > nextNum) {
                inverCount++;
            }
        }
        
    }
    if (!(inverCount % 2)) {//对2求余,余0,逆序数为偶数,即偶排列;否则,为奇排列
        return randomNums;
    }
    

    获得随机可还原的数组randomNums

    - (NSMutableArray *)getNewAvailableRandomNums {
        
        //随机数字
        int inverCount = 0;
        while (1) {
            NSMutableArray *initializeNums = [NSMutableArray array];//初始化0-n数字
            for (int i = 0; i < _puzzleCount; i++) {
                [initializeNums addObject:@(i)];
            }
            
            NSMutableArray *randomNums = [NSMutableArray array];//随机数组
            for (int i = 0; i < _puzzleCount; i++) {
                
                int randomNum = arc4random() % initializeNums.count;
                
                [randomNums addObject:initializeNums[randomNum]];
                
                [initializeNums removeObjectAtIndex:randomNum];
                
            }
            //判断是否可还原拼图
            inverCount = 0;
            int curNum = 0;
            int nextNum = 0;
            for (int i = 0; i < _puzzleCount; i++) {
                curNum = [randomNums[i] intValue];
                if (curNum == _puzzleCount - 1) {
                    inverCount += _difficulty - 1 - (i / _difficulty);
                    inverCount += _difficulty - 1 - (i % _difficulty);
                }
                for (int j = i + 1; j < _puzzleCount; j++) {
                    nextNum = [randomNums[j] intValue];
                    if (curNum > nextNum) {
                        inverCount++;
                    }
                }
                
            }
            if (!(inverCount % 2)) {//对2求余,余0,逆序数为偶数,即偶排列;否则,为奇排列
                return randomNums;
            }
            
        }
    }
    
    2、初始化拼图UI (九宫格)

    代码:

    - (void)customUI {
        CGFloat puzzleBgViewX = 0;
        CGFloat puzzleBgViewY = 64 + 20;
        CGFloat puzzleBgViewW = [UIScreen mainScreen].bounds.size.width;
        CGFloat puzzleBgViewH = puzzleBgViewW;
        
        _puzzleBgView = [[UIView alloc] initWithFrame:CGRectMake(puzzleBgViewX, puzzleBgViewY, puzzleBgViewW, puzzleBgViewH)];
        _puzzleBgView.backgroundColor = [UIColor lightGrayColor];
        [self.view addSubview:_puzzleBgView];
        
        CGFloat puzzleBtnX = 0;
        CGFloat puzzleBtnY = 0;
        CGFloat puzzleBtnW = puzzleBgViewW / _difficulty - kPuzzleBtnGap * 2;
        CGFloat puzzleBtnH = puzzleBtnW;
        
        for (int i = 0; i < _puzzleCount; i++) {
            puzzleBtnX = i % _difficulty * (puzzleBtnW + kPuzzleBtnGap * 2) + kPuzzleBtnGap;
            puzzleBtnY = i / _difficulty * (puzzleBtnH + kPuzzleBtnGap * 2) + kPuzzleBtnGap;
            UIButton *puzzleBtn = [UIButton buttonWithType:UIButtonTypeCustom];
            puzzleBtn.frame = CGRectMake(puzzleBtnX, puzzleBtnY, puzzleBtnW, puzzleBtnH);
            puzzleBtn.tag = i;
            puzzleBtn.clipsToBounds = YES;
            [_puzzleBgView addSubview:puzzleBtn];
    
            int  puzzleValue = [self.randomNums[i] intValue];
            if (puzzleValue == _puzzleCount - 1) {
                puzzleBtn.backgroundColor = [UIColor clearColor];
                _maxPuzzleBtn = puzzleBtn;
            } else {
                    [puzzleBtn setTitle:[NSString stringWithFormat:@"%d", puzzleValue + 1] forState:UIControlStateNormal];
                    puzzleBtn.backgroundColor = [UIColor colorWithRed:0x4A / 255.0 green:0xC2 / 255.0 blue:0xFB / 255.0 alpha:1];
                [puzzleBtn addTarget:self action:@selector(puzzleBtnAction:) forControlEvents:UIControlEventTouchUpInside];
            }
        }
    }
    
    3、滑块移动逻辑

    点击空白块周围数字块,数字块移到空白块区域(其实就是空白块和数字块交换)

    图4

    index:数字块对应位置如图4
    _difficulty : 拼图列数
    ③ 点击数字块依次判断其 是否有空白块
    ④ 找到空白块,将点击数字块与空白块位置交换,实现数字块移动效果

    以数字块3(index = 4)为例分析

    upIndex = index - _difficulty 判断是否在九宫格里&&其位置对应的值是否是8,即空白块。

    upIndex >= 0 && [self.randomNums[upIndex] intValue] == _puzzleCount - 1

    downIndex = index + _difficulty 判断是否在九宫格里&&其位置对应的值是否是8,即空白块。

    if (downIndex <= _puzzleCount - 1 && [self.randomNums[downIndex] intValue] == _puzzleCount - 1

    leftIndex = index - 1 判断是否在九宫格里&&其位置对应的值是否是8,即空白块

    index % _difficulty > 0 && [self.randomNums[leftIndex] intValue] == _puzzleCount - 1

    rightIndex = index + 1 判断是否在九宫格里&&其位置对应的值是否是8,即空白块

    index % _difficulty < _difficulty - 1 && [self.randomNums[rightIndex] intValue] == _puzzleCount - 1

    代码:

    - (void)puzzleBtnAction:(UIButton *)puzzleBtn {
        NSInteger index = puzzleBtn.tag;
        
        //上
        NSInteger upIndex = index - _difficulty;
        if (upIndex >= 0 && [self.randomNums[upIndex] intValue] == _puzzleCount - 1) {
            
            CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
            CGPoint puzzleBtnCenter = puzzleBtn.center;
            _maxPuzzleBtn.tag = index;
            puzzleBtn.tag = upIndex;
            self.randomNums[upIndex] = @([self.randomNums[index] intValue]);
            self.randomNums[index] = @(_puzzleCount - 1);
            [UIView animateWithDuration:0.35 animations:^{
                puzzleBtn.center = maxPuzzleBtnCenter;
                _maxPuzzleBtn.center = puzzleBtnCenter;
            }];
            
            [self isWin];
            
            return;
            
        }
        //下
        NSInteger downIndex = index + _difficulty;
        if (downIndex <= _puzzleCount - 1 && [self.randomNums[downIndex] intValue] == _puzzleCount - 1) {
            CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
            CGPoint puzzleBtnCenter = puzzleBtn.center;
            _maxPuzzleBtn.tag = index;
            puzzleBtn.tag = downIndex;
            self.randomNums[downIndex] = @([self.randomNums[index] intValue]);
            self.randomNums[index] = @(_puzzleCount - 1);
            [UIView animateWithDuration:0.35 animations:^{
                puzzleBtn.center = maxPuzzleBtnCenter;
                _maxPuzzleBtn.center = puzzleBtnCenter;
            }];
            
            [self isWin];
            return;
        }
        //左
        NSInteger leftIndex = index - 1;
        if (index % _difficulty > 0 && [self.randomNums[leftIndex] intValue] == _puzzleCount - 1) {
            CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
            CGPoint puzzleBtnCenter = puzzleBtn.center;
            _maxPuzzleBtn.tag = index;
            puzzleBtn.tag = leftIndex;
            self.randomNums[leftIndex] = @([self.randomNums[index] intValue]);
            self.randomNums[index] = @(_puzzleCount - 1);
            [UIView animateWithDuration:0.35 animations:^{
                puzzleBtn.center = maxPuzzleBtnCenter;
                _maxPuzzleBtn.center = puzzleBtnCenter;
            }];
            
            [self isWin];
            return;
        }
        //右
        NSInteger rightIndex = index + 1;
        if (index % _difficulty < _difficulty - 1 && [self.randomNums[rightIndex] intValue] == _puzzleCount - 1) {
            CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
            CGPoint puzzleBtnCenter = puzzleBtn.center;
            _maxPuzzleBtn.tag = index;
            puzzleBtn.tag = rightIndex;
            self.randomNums[rightIndex] = @([self.randomNums[index] intValue]);
            self.randomNums[index] = @(_puzzleCount - 1);
            [UIView animateWithDuration:0.35 animations:^{
                puzzleBtn.center = maxPuzzleBtnCenter;
                _maxPuzzleBtn.center = puzzleBtnCenter;
            }];
            
            [self isWin];
            return;
        }
        
    }
    
    *4、另一种打乱拼图的方法

    思路:将图1经过有限次数随机移动达到打乱拼图的目的,这样打乱的拼图肯定是可还原的。

    代码:

    - (NSMutableArray *)getNewAvailableRandomNums2 {
        
       NSMutableArray *randomNums = [NSMutableArray array];//随机数组 - 初始化0-n数字
        for (int i = 0; i < _puzzleCount; i++) {
            [randomNums addObject:@(i)];
        }
        
        int randCount = _puzzleCount * _puzzleCount;
        int randDirection = 0; //0 上 1 下 2 左 3 右
        BOOL aliableDirection = NO;
        int blankIndex = 8;
        int index = 0;
        while (randCount--) {
            
            aliableDirection = NO;
            randDirection = arc4random() % 4;
            while (1) {
                switch (randDirection) {
                    case 0:
                        
                        if (blankIndex / _difficulty > 0) {
                            index = blankIndex - _difficulty;
                            aliableDirection = YES;
                        }
                        break;
                       case 1:
                        
                        if (blankIndex / _difficulty < _difficulty - 1) {
                            index = blankIndex + _difficulty;
                            aliableDirection = YES;
                        }
                        break;
                    case 2:
                        
                        if (blankIndex % _difficulty > 0) {
                            index = blankIndex - 1;
                            aliableDirection = YES;
                        }
                        break;
                    case 3:
                        
                        if (blankIndex % _difficulty < _difficulty - 1) {
                            index = blankIndex + 1;
                            aliableDirection = YES;
                        }
                        break;
                    default:
                        break;
                }
                if (aliableDirection == YES) {
                    break;
                }
                randDirection = (randDirection + 1) % 4;
            }
            
            randomNums[blankIndex] = @([randomNums[index] intValue]);
            randomNums[index] = @(8);
            blankIndex = index;
            
        }
        return randomNums;
    }
    

    三、其他细节功能

    1、难度选择 3*3(低), 4*4(中), 5*5(高)
    2、自定义图片拼图(相机和相册)
    3、图片拼图提示
    4、步数统计
    5、最佳记录
    6、移动提示音设置

    具体请下载demo查看

    四、参考

    1、不可还原拼图
    2、回忆经典,讲述滑块游戏背后的数学故事
    3、吴昊品游戏核心算法 Round 17 —— 吴昊教你玩拼图游戏(15 puzzle)

    相关文章

      网友评论

      本文标题:iOS 滑块拼图游戏(Puzzle8)

      本文链接:https://www.haomeiwen.com/subject/yhsedxtx.html