Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building the game “2048” in Swift

Realm
July 24, 2014

Building the game “2048” in Swift

As given by Austin Zheng at the Swift Language User Group

Watch the video: http://realm.io/news/swift-2048/

Realm

July 24, 2014
Tweet

More Decks by Realm

Other Decks in Technology

Transcript

  1. Who am I? • My name is Austin Zheng •

    I work at LinkedIn as an iOS developer • Before that I wrote firmware for an embedded systems startup in Redwood City
  2. What Is 2048? • swift-2048 is a port of an

    Objective- C version of 2048 I wrote… • …which was a clone of the web game “2048” by Gabriele Cirulli • In turn based on iOS game “Threes” by Asher Vollmer • Slide in a direction to combine like tiles • Make a ‘2048’ tile, or fill up the board and lose
  3. Architecture (very high level) View Game Logic (Model) View Controller

    Actions View Commands (forwarded to view) Backing Store
  4. Backing Store (Old) @interface F3HGameModel () @property (nonatomic, strong) NSMutableArray

    *gameState; @property (nonatomic) NSUInteger dimension; //... @end
  5. Backing Store struct SquareGameboard<T> { var boardArray: [T]; let dimension:

    Int init(dimension d: Int, initialValue: T) { dimension = d boardArray = [T](count:d*d, repeatedValue:initialValue) } subscript(row: Int, col: Int) -> T { get { return boardArray[row*dimension + col] } set { boardArray[row*dimension + col] = newValue } } }
  6. Structs • Like classes, they can have properties and methods.

    • Unlike classes, structs can’t inherit from other structs. • Unlike classes, structs are value types
  7. Generics struct SquareGameboard<T> { let dimension: Int var boardArray: [T]

    init(dimension d: Int, initialValue: T) { dimension = d boardArray = [T](count:d*d, repeatedValue:initialValue) } }
  8. Subscripts subscript(row: Int, col: Int) -> T { get {

    return boardArray[row*dimension + col] } set { boardArray[row*dimension + col] = newValue } } gameboard[x, y] = TileObject.Empty let someTile = gameboard[x, y]
  9. TileModel (Old) // This is an Objective-C class which represents

    a tile @interface F3HTileModel : NSObject @property (nonatomic) BOOL empty; @property (nonatomic) NSUInteger value; @end
  10. TileObject enum TileObject { case Empty case Tile(value: Int) }

    let anEmptyTile = TileObject.Empty let eightTile = TileObject.Tile(value: 8) let anotherTile = TileObject.Tile(value: 2)
  11. Swift Enums • They can do everything C or Objective-C

    enums can… • They can also do everything structs in Swift can do - methods and properties… • Optionally, you can have an enum value store associated data. (variants, tagged unions, sum types, case classes)
  12. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  13. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  14. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  15. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  16. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  17. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  18. A single row… • Condense the row to remove any

    space - some might move, some might stay still • Collapse two adjacent tiles of equal value into a single tile with double the value • Convert our intermediate representation into ‘Actions’ that the view layer can easily act upon
  19. Tracking changes? • We want to know when a tile

    is moved, when it stays still, when it’s combined • Let’s posit an ActionToken • An ActionToken lives in an array. Its position in the array is the final position of the tile it represents • An ActionToken also tracks the state of the tile or tiles that undertook the action it describes
  20. ActionToken (old) typedef enum { F3HMergeTileModeNoAction = 0, F3HMergeTileModeMove, F3HMergeTileModeSingleCombine,

    F3HMergeTileModeDoubleCombine } F3HMergeTileMode; @interface F3HMergeTile : NSObject @property (nonatomic) F3HMergeTileMode mode; @property (nonatomic) NSInteger originalIndexA; @property (nonatomic) NSInteger originalIndexB; @property (nonatomic) NSInteger value; + (instancetype)mergeTile; @end
  21. ActionToken enum ActionToken { case NoAction(source: Int, value: Int) case

    Move(source: Int, value: Int) case SingleCombine(source: Int, value: Int) case DoubleCombine(source: Int, second: Int, value: Int) }
  22. func condense(group: [TileObject]) -> [ActionToken] { var tokenBuffer = [ActionToken]()

    for (idx, tile) in enumerate(group) { switch tile { case let .Tile(value) where tokenBuffer.count == idx: tokenBuffer.append(ActionToken.NoAction(source: idx, value: value)) case let .Tile(value): tokenBuffer.append(ActionToken.Move(source: idx, value: value)) default: break } } return tokenBuffer; }
  23. func condense(group: [TileObject]) -> [ActionToken] { var tokenBuffer = [ActionToken]()

    for (idx, tile) in enumerate(group) { switch tile { case let .Tile(value) where tokenBuffer.count == idx: tokenBuffer.append(ActionToken.NoAction(source: idx, value: value)) case let .Tile(value): tokenBuffer.append(ActionToken.Move(source: idx, value: value)) default: break } } return tokenBuffer; }
  24. Swift ‘switch’ • At its most basic, works like the

    C or Objective-C switch statement • But it can do far more! • One example: take the values out of an enum • Cases can be qualified by ‘where’ clauses • Has to be comprehensive, and no default fallthrough
  25. func collapse(group: [ActionToken]) -> [ActionToken] { func quiescentTileStillQuiescent(inputPosition: Int, outputLength:

    Int, originalPosition: Int) -> Bool { return (inputPosition == outputLength) && (originalPosition == inputPosition) } var tokenBuffer = [ActionToken]() var skipNext = false for (idx, token) in enumerate(group) { if skipNext { skipNext = false continue } switch token { case .SingleCombine: assert(false, "Cannot have single combine token in input") case .DoubleCombine: assert(false, "Cannot have double combine token in input") case let .NoAction(s, v) where (idx < group.count-1 && v == group[idx+1].getValue() && quiescentTileStillQuiescent(idx, tokenBuffer.count, s)): let nv = v + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.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(ActionToken.DoubleCombine(source: t.getSource(), second: next.getSource(), value: nv)) case let .NoAction(s, v) where !quiescentTileStillQuiescent(idx, tokenBuffer.count, s): tokenBuffer.append(ActionToken.Move(source: s, value: v)) case let .NoAction(s, v): tokenBuffer.append(ActionToken.NoAction(source: s, value: v)) case let .Move(s, v): tokenBuffer.append(ActionToken.Move(source: s, value: v)) default: break } } return tokenBuffer }
  26. Game Logic • Convert - create ‘move orders’ for the

    view enum MoveOrder { case SingleMoveOrder(source: Int, destination: Int, value: Int, wasMerge: Bool) case DoubleMoveOrder(firstSource: Int, secondSource: Int, destination: Int, value: Int) }
  27. func convert(group: [ActionToken]) -> [MoveOrder] { var moveBuffer = [MoveOrder]()

    for (idx, t) in enumerate(group) { switch t { case let .Move(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: false)) case let .SingleCombine(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: true)) case let .DoubleCombine(s1, s2, v): moveBuffer.append(MoveOrder.DoubleMoveOrder(firstSource: s1, secondSource: s2, destination: idx, value: v)) default: break } } return moveBuffer }
  28. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  29. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  30. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  31. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  32. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  33. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  34. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  35. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  36. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }
  37. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }
  38. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }