KEMBAR78
Swift, via "swift-2048" | PDF
Swift
(via swift-2048)
Austin Zheng
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
What Is 2048?
• iOS clone of web game “2048” by
Gabriele Cirulli
• In turn based on iOS game “Threes”
by Asher Vollmer
• Slide in a direction to combine like
tiles
• 2+2 = 4, 4+4 = 8
• Make a ‘2048’ tile, or fill up the
board and lose
(2048 demo)
Architecture
(very high level)
View
Game Logic
(Model)
View Controller
Actions
View Commands
(forwarded to view)
Backing
Store
Backing Store
Backing Store (Old)
@interface F3HGameModel ()
@property (nonatomic, strong) NSMutableArray *gameState;
@property (nonatomic) NSUInteger dimension;
//...
@end
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
}
}
}
Structs
• Like classes, they can have properties and
methods.
• Unlike classes, structs can’t inherit from other
structs.
• Unlike classes, structs are value types
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)
}
}
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]
What, exactly, are we
storing?
TileModel (Old)
// This is an Objective-C class which represents a tile
@interface F3HTileModel : NSObject
@property (nonatomic) BOOL empty;
@property (nonatomic) NSUInteger value;
@end
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)
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)
Game Logic
22
2
22
2
2
22
2
22
2
2
22
2
22
2
2
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)
// ...
}
// ...
}
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)
// ...
}
// ...
}
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)
// ...
}
// ...
}
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)
// ...
}
// ...
}
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)
// ...
}
// ...
}
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)
// ...
}
// ...
}
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
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
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
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)
}
Game Logic
• Condense - remove spaces between tiles
42
2 4
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;
}
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;
}
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
Game Logic
• Collapse - perform necessary merges
4 84
4 2 82
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
}
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)
}
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
}
Views
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)
})
})
}
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)
})
})
}
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)
})
})
}
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)
})
})
}
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]; }];
}
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]; }];
}
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]; }];
}
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]; }];
}
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()
}
})
}
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()
}
})
}
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()
}
})
}
Questions?
Swift, via "swift-2048"

Swift, via "swift-2048"

  • 1.
  • 2.
    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
  • 3.
    What Is 2048? •iOS clone of web game “2048” by Gabriele Cirulli • In turn based on iOS game “Threes” by Asher Vollmer • Slide in a direction to combine like tiles • 2+2 = 4, 4+4 = 8 • Make a ‘2048’ tile, or fill up the board and lose
  • 4.
  • 5.
    Architecture (very high level) View GameLogic (Model) View Controller Actions View Commands (forwarded to view) Backing Store
  • 6.
  • 7.
    Backing Store (Old) @interfaceF3HGameModel () @property (nonatomic, strong) NSMutableArray *gameState; @property (nonatomic) NSUInteger dimension; //... @end
  • 8.
    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 } } }
  • 9.
    Structs • Like classes,they can have properties and methods. • Unlike classes, structs can’t inherit from other structs. • Unlike classes, structs are value types
  • 10.
    Generics struct SquareGameboard<T> { letdimension: Int var boardArray: [T] ! init(dimension d: Int, initialValue: T) { dimension = d boardArray = [T](count:d*d, repeatedValue:initialValue) } }
  • 11.
    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]
  • 12.
    What, exactly, arewe storing?
  • 13.
    TileModel (Old) // Thisis an Objective-C class which represents a tile @interface F3HTileModel : NSObject @property (nonatomic) BOOL empty; @property (nonatomic) NSUInteger value; @end
  • 14.
    TileObject enum TileObject { caseEmpty case Tile(value: Int) } ! let anEmptyTile = TileObject.Empty let eightTile = TileObject.Tile(value: 8) let anotherTile = TileObject.Tile(value: 2)
  • 15.
    Swift Enums • Theycan 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)
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
    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) // ... } // ... }
  • 21.
    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) // ... } // ... }
  • 22.
    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) // ... } // ... }
  • 23.
    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) // ... } // ... }
  • 24.
    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) // ... } // ... }
  • 25.
    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) // ... } // ... }
  • 26.
    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
  • 27.
    Tracking changes? • Wewant 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
  • 28.
    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
  • 29.
    ActionToken enum ActionToken { caseNoAction(source: Int, value: Int) case Move(source: Int, value: Int) case SingleCombine(source: Int, value: Int) case DoubleCombine(source: Int, second: Int, value: Int) }
  • 30.
    Game Logic • Condense- remove spaces between tiles 42 2 4
  • 31.
    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; }
  • 32.
    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; }
  • 33.
    Swift ‘switch’ • Atits 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
  • 34.
    Game Logic • Collapse- perform necessary merges 4 84 4 2 82
  • 35.
    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 }
  • 36.
    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) }
  • 37.
    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 }
  • 38.
  • 39.
    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) }) }) }
  • 40.
    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) }) }) }
  • 41.
    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) }) }) }
  • 42.
    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) }) }) }
  • 43.
    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]; }]; }
  • 44.
    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]; }]; }
  • 45.
    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]; }]; }
  • 46.
    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]; }]; }
  • 47.
    Selectors in Swift letupSwipe = 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() } }) }
  • 48.
    Selectors in Swift letupSwipe = 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() } }) }
  • 49.
    Selectors in Swift letupSwipe = 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() } }) }
  • 50.