[swift实战入门]手把手教你编写2048(三)

上篇地址:swift实战入门之手把手教你编写2048(二)
github地址:https://github.com/scarlettbai/2048.git

今天给大家带来2048最后一篇,之前已经实现了向游戏区域中随机插入数字块,接下来要做的,就是当我们滑动屏幕时移动及合并数字块以及插入一个新的数字块。本篇的难点就是移动时的算法问题,首先来给大家讲一下算法。

2048的算法实现其实很简单,假如我们当前数字格的格式如下:

| |4| | |
| | |4| |
|2| |2|2|
|2| | | |

如果用户选择向上滑动,那么这里我们算法里要做的是,先取出第一列的4个格存为一个数组,对应坐标为[(0,1),(0,2),(0,3),(0,4)],其中对应的值为| | |2|2|,首先对数组进行去除空操作,去除之后数据为:[(0,3),(0,4)],对应值为|2|2|,之后再进行合并操作,合并时我们可以取到数组中原来两个2的坐标以及最终坐标,那么此时我们只要更新存储当前数字块状态的数组以及数字块视图,将之前两个2的地方置空,并在(0,1)处插入一个4即可,之后再继续遍历下一列做同样的操作即可。

这里用户一共有4个操作,上下左右,分别取出对应的行列一行(列)一行(列)的进行处理即可。那么接下来看代码:

首先我们定义几个枚举:

//用户操作---上下左右
enum MoveDirection {
    case UP,DOWN,LEFT,RIGHT
}
//用于存放数字块的移动状态,是否需要移动以及两个一块合并并移动等,关键数据是数组中位置以及最新的数字块的值
enum TileAction{
    case NOACTION(source : Int , value : Int)
    case MOVE(source : Int , value : Int)
    case SINGLECOMBINE(source : Int , value : Int)
    case DOUBLECOMBINE(firstSource : Int , secondSource : Int , value : Int)

    func getValue() -> Int {
        switch self {
        case let .NOACTION(_, value) : return value
        case let .MOVE(_, value) : return value
        case let .SINGLECOMBINE(_, value) : return value
        case let .DOUBLECOMBINE(_, _, value) : return value
        }
    }

    func getSource() -> Int {
        switch self {
        case let .NOACTION(source , _) : return source
        case let .MOVE(source , _) : return source
        case let .SINGLECOMBINE(source , _) : return source
        case let .DOUBLECOMBINE(source , _ , _) : return source
        }
    }
}
//最终的移动数据封装,标注了所有需移动的块的原位置及新位置,以及块的最新值
enum MoveOrder{
    case SINGLEMOVEORDER(source : Int , destination : Int , value : Int , merged : Bool)
    case DOUBLEMOVEORDER(firstSource : Int , secondSource : Int , destination : Int , value : Int)
}

接下来就看具体算法:

func merge(group : [TileEnum]) -> [MoveOrder] {
    return convert(collapse(condense(group)))
}

//去除空   如:| | |2|2|去掉空为:|2|2| | |
func condense(group : [TileEnum]) -> [TileAction] {
    var buffer = [TileAction]()
    for (index , tile) in group.enumerate(){
        switch tile {
        //如果buffer的大小和当前group的下标一致,则表示当前数字块不需要移动
        //如|2| |2| |,第一次时buffer大小和index都是0,不需要移动
        //下一个2时,buffer大小为1,groupindex为2,则需要移动了
        case let .Tile(value) where buffer.count == index :
            buffer.append(TileAction.NOACTION(source: index, value: value))
        case let .Tile(value) :
            buffer.append(TileAction.MOVE(source: index, value: value))
        default:
            break
        }
    }
    return buffer
}

//合并相同的    如:|2| | 2|2|合并为:|4|2| | |
func collapse(group : [TileAction]) -> [TileAction] {

    var tokenBuffer = [TileAction]()
    //是否跳过下一个,如果把下一个块合并过来,则下一个数字块应该跳过
    var skipNext = false
    for (idx, token) in group.enumerate() {
        if skipNext {
            skipNext = false
            continue
        }
        switch token {
        //当前块和下一个块的值相同且当前块不需要移动,那么需要将下一个块合并到当前块来
        case let .NOACTION(s, v)
            where (idx < group.count-1
                && v == group[idx+1].getValue()
                && GameModle.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s)):
            let next = group[idx+1]
            let nv = v + group[idx+1].getValue()
            skipNext = true
            tokenBuffer.append(TileAction.SINGLECOMBINE(source: next.getSource(), value: nv))
        //当前块和下一个块的值相同,且两个块都需要移动,则将两个块移动到新的位置
        case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()):
            let next = group[idx+1]
            let nv = t.getValue() + group[idx+1].getValue()
            skipNext = true
            tokenBuffer.append(TileAction.DOUBLECOMBINE(firstSource: t.getSource(), secondSource: next.getSource(), value: nv))
        //上一步判定不需要移动,但是之前的块有合并过,所以需要移动
        case let .NOACTION(s, v) where !GameModle.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s):
            tokenBuffer.append(TileAction.MOVE(source: s, value: v))
        //上一步判定不需要移动,且之前的块也没有合并,则不需要移动
        case let .NOACTION(s, v):
            tokenBuffer.append(TileAction.NOACTION(source: s, value: v))
        //上一步判定需要移动且不符合上面的条件的,则继续保持移动
        case let .MOVE(s, v):
            tokenBuffer.append(TileAction.MOVE(source: s, value: v))
        default:
            break
        }
    }
    return tokenBuffer
}

class func quiescentTileStillQuiescent(inputPosition: Int, outputLength: Int, originalPosition: Int) -> Bool {
    return (inputPosition == outputLength) && (originalPosition == inputPosition)
}

//转换为MOVEORDER便于后续处理
func convert(group : [TileAction]) -> [MoveOrder] {
    var buffer = [MoveOrder]()
    for (idx , tileAction) in group.enumerate() {
        switch tileAction {
        case let .MOVE(s, v) :
            //单纯的将一个块由s位置移动到idx位置,新值为v
            buffer.append(MoveOrder.SINGLEMOVEORDER(source: s, destination: idx, value: v, merged: false))
        case let .SINGLECOMBINE(s, v) :
            //将一个块由s位置移动到idx位置,且idx位置有数字块,俩数字块进行合并,新值为v
            buffer.append(MoveOrder.SINGLEMOVEORDER(source: s, destination: idx, value: v, merged: true))
        case let .DOUBLECOMBINE(s, d, v) :
            //将s和d两个数字块移动到idx位置并进行合并,新值为v
            buffer.append(MoveOrder.DOUBLEMOVEORDER(firstSource: s, secondSource: d, destination: idx, value: v))
        default:
            break
        }
    }
    return buffer
}

上面代码里注释已经很详细了,这里再简单说下,**condense方法的作用就是去除空的数字块,入参就是一列的四个数字块,里面是定义了一个TileAction数组buffer,之后判断入参中不为空的则加入buffer中,其中只是做了判断数字块是否需要移动。collapse方法就是合并操作**,其实只是记录一个合并状态,如果不需要合并的就还是只判断是否需要移动,convert中则将collapse中返回的结果进行包装,表明具体的移动前和移动后的位置,以及新的值和是否需要合并。

这里算法的具体实现就做完了,下面来看下具体调用:

//提供给主控制器调用,入参为移动方向和一个需要一个是否移动过的Bool值为入参的闭包    
func queenMove(direction : MoveDirection , completion : (Bool) -> ()){
    let changed = performMove(direction)
    completion(changed)

}
//移动实现
func performMove(direction : MoveDirection) -> Bool {
    //根据上下左右返回每列(行)的四个块的坐标
    let getMoveQueen : (Int) -> [(Int , Int)] = { (idx : Int) -> [(Int , Int)] in
        var buffer = Array<(Int , Int)>(count : self.dimension , repeatedValue : (0, 0))
        for i in 0..<self.dimension {
            switch direction {
            case .UP : buffer[i] = (idx, i)
            case .DOWN : buffer[i] = (idx, self.dimension - i - 1)
            case .LEFT : buffer[i] = (i, idx)
            case .RIGHT : buffer[i] = (self.dimension - i - 1, idx)
            }
        }
        return buffer
    }
    var movedFlag = false
    //逐列(行)进行处理
    for i in 0..<self.dimension {
        //获取当前列(行)的4个坐标
        let moveQueen = getMoveQueen(i)
        //从gamebord中取出当前4个坐标中的值存为数组
        let tiles = moveQueen.map({ (c : (Int, Int)) -> TileEnum in
            let (source , value) = c
            return self.gamebord[source , value]
        })
        //调用算法
        let moveOrders = merge(tiles)
        movedFlag = moveOrders.count > 0 ? true : movedFlag
        //对算法返回结果进行具体处理.1:更新gamebord中的数据,2:更新视图中的数字块
        for order in moveOrders {
            switch order {
            //单个移动或合并的
            case let .SINGLEMOVEORDER(s, d, v, m):
                let (sx, sy) = moveQueen[s]
                let (dx, dy) = moveQueen[d]
                if m {
                    self.score += v
                }
                //将原位置置空,新位置设置为新的值
                gamebord[sx , sy] = TileEnum.Empty
                gamebord[dx , dy] = TileEnum.Tile(v)
                //TODO 调用游戏视图更新视图中的数字块   
                delegate.moveOneTile((sx, sy), to: (dx, dy), value: v)
            //两个进行合并的
            case let .DOUBLEMOVEORDER(fs , ts , d , v):
                let (fsx , fsy) = moveQueen[fs]
                let (tsx , tsy) = moveQueen[ts]
                let (dx , dy) = moveQueen[d]
                self.score += v
                //将原位置置空,新位置设置为新的值
                gamebord[fsx , fsy] = TileEnum.Empty
                gamebord[tsx , tsy] = TileEnum.Empty
                gamebord[dx , dy] = TileEnum.Tile(v)
                //TODO 调用游戏视图更新视图中的数字块   
                delegate.moveTwoTiles((moveQueen[fs], moveQueen[ts]), to: moveQueen[d], value: v)

            }
        }
    }
    return movedFlag
}

可以看到,上面调用我们之前写的算法,以及将gamebord中存储内容更新了(gamebord存储的是当前各个位置的数字块状态,前两篇有介绍),接下来需要更新游戏视图中的数字块,接下来在GamebordView.swift中添加如下代码:

//从from位置移动一个块到to位置,并赋予新的值value
func moveOneTiles(from : (Int , Int)  , to : (Int , Int) , value : Int) {
    let (fx , fy) = from
    let (tx , ty) = to
    let fromKey = NSIndexPath(forRow: fx , inSection: fy)
    let toKey = NSIndexPath(forRow: tx, inSection: ty)
    //取出from位置和to位置的数字块
    guard let tile = tiles[fromKey] else{
        assert(false, "not exists tile")
    }
    let endTile = tiles[toKey]
    //将from位置的数字块的位置定到to位置
    var changeFrame = tile.frame
    changeFrame.origin.x = tilePadding + CGFloat(tx)*(tilePadding + tileWidth)
    changeFrame.origin.y = tilePadding + CGFloat(ty)*(tilePadding + tileWidth)

    tiles.removeValueForKey(fromKey)
    tiles[toKey] = tile

    // 动画以及给新位置的数字块赋值
    let shouldPop = endTile != nil
    UIView.animateWithDuration(perSquareSlideDuration,
                               delay: 0.0,
                               options: UIViewAnimationOptions.BeginFromCurrentState,
                               animations: {
                                tile.frame = changeFrame
        },
                               completion: { (finished: Bool) -> Void in
                                //对新位置的数字块赋值
                                tile.value = value
                                endTile?.removeFromSuperview()
                                if !shouldPop || !finished {
                                    return
                                }
                                tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tileMergeStartScale, self.tileMergeStartScale))
                                UIView.animateWithDuration(self.tileMergeExpandTime,
                                    animations: {
                                        tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale))
                                    },
                                    completion: { finished in
                                        UIView.animateWithDuration(self.tileMergeContractTime) {
                                            tile.layer.setAffineTransform(CGAffineTransformIdentity)
                                        }
                                })
    })
}
//将from里两个位置的数字块移动到to位置,并赋予新的值,原理同上
func moveTwoTiles(from: ((Int, Int), (Int, Int)), to: (Int, Int), value: Int) {
    assert(positionIsValid(from.0) && positionIsValid(from.1) && positionIsValid(to))
    let (fromRowA, fromColA) = from.0
    let (fromRowB, fromColB) = from.1
    let (toRow, toCol) = to
    let fromKeyA = NSIndexPath(forRow: fromRowA, inSection: fromColA)
    let fromKeyB = NSIndexPath(forRow: fromRowB, inSection: fromColB)
    let toKey = NSIndexPath(forRow: toRow, inSection: toCol)

    guard let tileA = tiles[fromKeyA] else {
        assert(false, "placeholder error")
    }
    guard let tileB = tiles[fromKeyB] else {
        assert(false, "placeholder error")
    }

    var finalFrame = tileA.frame
    finalFrame.origin.x = tilePadding + CGFloat(toRow)*(tileWidth + tilePadding)
    finalFrame.origin.y = tilePadding + CGFloat(toCol)*(tileWidth + tilePadding)

    let oldTile = tiles[toKey]  
    oldTile?.removeFromSuperview()
    tiles.removeValueForKey(fromKeyA)
    tiles.removeValueForKey(fromKeyB)
    tiles[toKey] = tileA

    UIView.animateWithDuration(perSquareSlideDuration,
                               delay: 0.0,
                               options: UIViewAnimationOptions.BeginFromCurrentState,
                               animations: {
                                tileA.frame = finalFrame
                                tileB.frame = finalFrame
        },
                               completion: { finished in
                                //赋值
                                tileA.value = value
                                tileB.removeFromSuperview()
                                if !finished {
                                    return
                                }
                                tileA.layer.setAffineTransform(CGAffineTransformMakeScale(self.tileMergeStartScale, self.tileMergeStartScale))
                                UIView.animateWithDuration(self.tileMergeExpandTime,
                                    animations: {
                                        tileA.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale))
                                    },
                                    completion: { finished in
                                        UIView.animateWithDuration(self.tileMergeContractTime) {
                                            tileA.layer.setAffineTransform(CGAffineTransformIdentity)
                                        }
                                })
    })
}

func positionIsValid(pos: (Int, Int)) -> Bool {
    let (x, y) = pos
    return (x >= 0 && x < dimension && y >= 0 && y < dimension)
}

上面方法更新了游戏视图中的数字块状态。那么接下来我们在主控制器中调用queenMove就可以运行游戏看移动效果了,在NumbertailGameController.swift的NumbertailGameController类中添加如下代码:

//注册监听器,监听当前视图里的手指滑动操作,上下左右分别对应下面的四个方法
func setupSwipeConttoller() {
    let upSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.upCommand(_:)))
    upSwipe.numberOfTouchesRequired = 1
    upSwipe.direction = UISwipeGestureRecognizerDirection.Up
    view.addGestureRecognizer(upSwipe)

    let downSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.downCommand(_:)))
    downSwipe.numberOfTouchesRequired = 1
    downSwipe.direction = UISwipeGestureRecognizerDirection.Down
    view.addGestureRecognizer(downSwipe)

    let leftSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.leftCommand(_:)))
    leftSwipe.numberOfTouchesRequired = 1
    leftSwipe.direction = UISwipeGestureRecognizerDirection.Left
    view.addGestureRecognizer(leftSwipe)

    let rightSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.rightCommand(_:)))
    rightSwipe.numberOfTouchesRequired = 1
    rightSwipe.direction = UISwipeGestureRecognizerDirection.Right
    view.addGestureRecognizer(rightSwipe)
}
//向上滑动的方法,调用queenMove,传入MoveDirection.UP
func upCommand(r : UIGestureRecognizer) {
    let m = gameModle!
    m.queenMove(MoveDirection.UP , completion: { (changed : Bool) -> () in
        if  changed {
            self.followUp()
        }
    })
}
//向下滑动的方法,调用queenMove,传入MoveDirection.DOWN
func downCommand(r : UIGestureRecognizer) {
    let m = gameModle!
    m.queenMove(MoveDirection.DOWN , completion: { (changed : Bool) -> () in
        if  changed {
            self.followUp()
        }
    })
}
//向左滑动的方法,调用queenMove,传入MoveDirection.LEFT
func leftCommand(r : UIGestureRecognizer) {
    let m = gameModle!
    m.queenMove(MoveDirection.LEFT , completion: { (changed : Bool) -> () in
        if  changed {
            self.followUp()
        }
    })
}
//向右滑动的方法,调用queenMove,传入MoveDirection.RIGHT
func rightCommand(r : UIGestureRecognizer) {
    let m = gameModle!
    m.queenMove(MoveDirection.RIGHT , completion: { (changed : Bool) -> () in
        if  changed {
            self.followUp()
        }
    })
}
//移动之后需要判断用户的输赢情况,如果赢了则弹框提示,给一个重玩和取消按钮
func followUp() {
    assert(gameModle != nil)
    let m = gameModle!
    let (userWon, _) = m.userHasWon()
    if userWon {
        let winAlertView = UIAlertController(title: "結果", message: "你贏了", preferredStyle: UIAlertControllerStyle.Alert)
        let resetAction = UIAlertAction(title: "重置", style: UIAlertActionStyle.Default, handler: {(u : UIAlertAction) -> () in
            self.reset()
        })
        winAlertView.addAction(resetAction)
        let cancleAction = UIAlertAction(title: "取消", style: UIAlertActionStyle.Default, handler: nil)
        winAlertView.addAction(cancleAction)
        self.presentViewController(winAlertView, animated: true, completion: nil)
        return
    }
    //如果没有赢则需要插入一个新的数字块
    let randomVal = Int(arc4random_uniform(10))
    m.insertRandomPositoinTile(randomVal == 1 ? 4 : 2)
    //插入数字块后判断是否输了,输了则弹框提示
    if m.userHasLost() {
        NSLog("You lost...")
        let lostAlertView = UIAlertController(title: "結果", message: "你輸了", preferredStyle: UIAlertControllerStyle.Alert)
        let resetAction = UIAlertAction(title: "重置", style: UIAlertActionStyle.Default, handler: {(u : UIAlertAction) -> () in
            self.reset()
        })
        lostAlertView.addAction(resetAction)
        let cancleAction = UIAlertAction(title: "取消", style: UIAlertActionStyle.Default, handler: nil)
        lostAlertView.addAction(cancleAction)
        self.presentViewController(lostAlertView, animated: true, completion: nil)
    }
}

上面代码中的userHasLost和userHasWon方法需要在GameModel中进行判断,这里是通过gameModle进行调用的,接下来看下具体的判断代码:

//如果gamebord中有超过我们定的最大分数threshold的,则用户赢了
func userHasWon() -> (Bool, (Int, Int)?) {
    for i in 0..<dimension {
        for j in 0..<dimension {
            if case let .Tile(v) = gamebord[i, j] where v >= threshold {
                return (true, (i, j))
            }
        }
    }
    return (false, nil)
}
//当前gamebord已经满了且两两间的值都不同,则用户输了    
func userHasLost() -> Bool {
    guard getEmptyPosition().isEmpty else {
        return false
    }
    for i in 0..<dimension {
        for j in 0..<dimension {
            switch gamebord[i, j] {
            case .Empty:
                assert(false, "Gameboard reported itself as full, but we still found an empty tile. This is a logic error.")
            case let .Tile(v):
                if tileBelowHasSameValue((i, j), v) || tileToRightHasSameValue((i, j), v) {
                    return false
                }
            }
        }
    }
    return true
}

func tileBelowHasSameValue(location: (Int, Int), _ value: Int) -> Bool {
    let (x, y) = location
    guard y != dimension - 1 else {
        return false
    }
    if case let .Tile(v) = gamebord[x, y+1] {
        return v == value
    }
    return false
}

func tileToRightHasSameValue(location: (Int, Int), _ value: Int) -> Bool {
    let (x, y) = location
    guard x != dimension - 1 else {
        return false
    }
    if case let .Tile(v) = gamebord[x+1, y] {
        return v == value
    }
    return false
}

接下来将之前的setupSwipeConttoller方法放入游戏初始化代码中则可以运行游戏了,在NumbertailGameController类的init方法中添加调用:

init(dimension d : Int , threshold t : Int) {
    //此处省略之前代码
    setupSwipeConttoller()
}

接下来就可以运行游戏了,其他的都是些边边角角的优化了,reset方法什么的,大家可以在github中把代码下下来看就行,这里就不多做介绍了。

这里再讲一点就是之前说的将面板中的数字换成文字,其实很简单,就在TileView中定义一个字典Dictionary<Int,String>,放如值如[2:”我”,4:”的”],在给数字块赋值的时候根据原本的值取出对应的文字赋到数字块上即可。


我的博客:blog.scarlettbai.com
我的微信公众号:读书健身编程

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值