上篇地址: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
我的微信公衆號:讀書健身編程