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.. 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..= threshold {
return (true, (i, j))
}
}
}
return (false, nil)
}
//當前gamebord已經滿了且兩兩間的值都不同,則用戶輸了
func userHasLost() -> Bool {
guard getEmptyPosition().isEmpty else {
return false
}
for i in 0.. 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
,放如值如[2:”我”,4:”的”],在給數字塊賦值的時候根據原本的值取出對應的文字賦到數字塊上即可。