Slide 1

Slide 1 text

SOLVING THE 15 PUZZLE IN SWIFT A Look at Algorithms and Speed 1 — @basthomas

Slide 2

Slide 2 text

SOLVING THE 15 PUZZLE IN SWIFT A Look at Algorithms and Speed A Look at Algorithms and Performance 2 — @basthomas

Slide 3

Slide 3 text

WHAT IS A 15 PUZZLE? 3 — @basthomas

Slide 4

Slide 4 text

| 1| 2| 3| 4| | 5| 6| 7| 8| | 9|10|11|12| |13|14|15| | 4 — @basthomas

Slide 5

Slide 5 text

| 1| 6| 2| 3| | |10| 7| 4| | 5| 9|11| 8| |13|14|15|12| 5 — @basthomas

Slide 6

Slide 6 text

| 1| 6| 2| 3| | 5|10| 7| 4| | | 9|11| 8| |13|14|15|12| 6 — @basthomas

Slide 7

Slide 7 text

| 1| 6| 2| 3| | 5|10| 7| 4| | 9| |11| 8| |13|14|15|12| 7 — @basthomas

Slide 8

Slide 8 text

| 1| 6| 2| 3| | 5| | 7| 4| | 9|10|11| 8| |13|14|15|12| 8 — @basthomas

Slide 9

Slide 9 text

| 1| | 2| 3| | 5| 6| 7| 4| | 9|10|11| 8| |13|14|15|12| 9 — @basthomas

Slide 10

Slide 10 text

| 1| 2| | 3| | 5| 6| 7| 4| | 9|10|11| 8| |13|14|15|12| 10 — @basthomas

Slide 11

Slide 11 text

| 1| 2| 3| | | 5| 6| 7| 4| | 9|10|11| 8| |13|14|15|12| 11 — @basthomas

Slide 12

Slide 12 text

| 1| 2| 3| 4| | 5| 6| 7| | | 9|10|11| 8| |13|14|15|12| 12 — @basthomas

Slide 13

Slide 13 text

| 1| 2| 3| 4| | 5| 6| 7| 8| | 9|10|11| | |13|14|15|12| 13 — @basthomas

Slide 14

Slide 14 text

| 1| 2| 3| 4| | 5| 6| 7| 8| | 9|10|11|12| |13|14|15| | 14 — @basthomas

Slide 15

Slide 15 text

BEFORE WE START, SOME HISTORY 15 — @basthomas

Slide 16

Slide 16 text

TVOS... DOES ANYONE STILL REMEMBER THAT? (OUTSIDE OF THE US) 16 — @basthomas

Slide 17

Slide 17 text

THIS WAS 4(!) YEARS AGO 17 — @basthomas

Slide 18

Slide 18 text

18 — @basthomas

Slide 19

Slide 19 text

UICollectionView HOUSES SOME VERY, VERY COOL APIS 19 — @basthomas

Slide 20

Slide 20 text

FAST-FORWARD TO JANUARY THIS YEAR 20 — @basthomas

Slide 21

Slide 21 text

START... SOMEWHERE 21 — @basthomas

Slide 22

Slide 22 text

START... WITH THE SOLUTION 22 — @basthomas

Slide 23

Slide 23 text

struct Solution { struct Step { let step: T } let steps: [Step] var input: Step { return steps.first! } var output: Step { return steps.last! } init(steps: [Step]) { precondition(steps.count > 0, "Solution must contain at least one step.") self.steps = steps } } 23 — @basthomas

Slide 24

Slide 24 text

let solution = Solution( steps: Solution.Step(step: 1), Solution.Step(step: 2), Solution.Step(step: 3) ) print(solution.input) // Step(step: 1) print(solution.output) // Step(step: 3) 24 — @basthomas

Slide 25

Slide 25 text

COMFORMING TO Collection 25 — @basthomas

Slide 26

Slide 26 text

struct Solution: Collection { var startIndex: Int { return steps.startIndex } var endIndex: Int { return steps.endIndex } subscript(i: Int) -> Step { return steps[i] } func index(after i: Int) -> Int { return steps.index(after: i) } } 26 — @basthomas

Slide 27

Slide 27 text

for step in solution { print(step) } 27 — @basthomas

Slide 28

Slide 28 text

BUT NOW WHAT? 28 — @basthomas

Slide 29

Slide 29 text

LAYING IT (ALL) OUT 29 — @basthomas

Slide 30

Slide 30 text

struct Board { struct Position {} enum Tile { case empty, number(Int) } func next() func position(for tile: Tile) -> Position func tile(at position: Position) -> Tile func swap(_ aTile: Tile, with bTile: Tile) -> Bool func move(tile: Tile) -> Bool func shuffle(moves: Int = 50) func adjacentPositions(to position: Position) -> [Position] func solve() -> Solution } 30 — @basthomas

Slide 31

Slide 31 text

struct Position: CustomStringConvertible, Equatable { let row: Int let column: Int func isAdjacent(to position: Position, in board: Board) -> Bool { return board.adjacentPositions(to: self) .filter { $0 == position } .isEmpty == false } var description: String { return "row: \(row), column: \(column)" } } 31 — @basthomas

Slide 32

Slide 32 text

func isAdjacent(to position: Position, in board: Board) -> Bool { return board.adjacentPositions(to: self) - .filter { $0 == position } - .isEmpty == false + .first { $0 == position } != nil } 32 — @basthomas

Slide 33

Slide 33 text

IT'S MAGICAL 33 — @basthomas

Slide 34

Slide 34 text

IT'S MAGICAL THAT IT IS NOT. 34 — @basthomas

Slide 35

Slide 35 text

LET'S LOOK AT A Tile 35 — @basthomas

Slide 36

Slide 36 text

enum Tile: Equatable, ExpressibleByIntegerLiteral { case empty, number(Int) private static let emptyValue = -1 init(integerLiteral value: Int) { if value == Tile.emptyValue { self = .empty } else { self = .number(value) } } var intValue: Int { switch self { case .empty: return Tile.emptyValue case .number(let number): return number } } } 36 — @basthomas

Slide 37

Slide 37 text

NOW IT'S ALMOST TIME FOR THE REAL DEAL... 37 — @basthomas

Slide 38

Slide 38 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 39

Slide 39 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 40

Slide 40 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 41

Slide 41 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 42

Slide 42 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 43

Slide 43 text

init(rows: Int) { precondition(rows > 1, "A puzzle should at least be 2x2") var _board: [[Tile]] = [] for row in 0..

Slide 44

Slide 44 text

ALGORITHMS? 39 — @basthomas

Slide 45

Slide 45 text

SPEED? SCALE? PERFORMANCE..? 40 — @basthomas

Slide 46

Slide 46 text

WHAT IS PERFORMANCE? 41 — @basthomas

Slide 47

Slide 47 text

PERFORMANCE, NOUN per·for·mance / pərˈfɔr məns / per-fawr-muh ns a: the execution of an action b: something accomplished 42 — @basthomas

Slide 48

Slide 48 text

MAYBE IT'S NOT JUST ABOUT SPEED AND SCALE 43 — @basthomas

Slide 49

Slide 49 text

PERFORMANCE IS A MINDSET 44 — @basthomas

Slide 50

Slide 50 text

WHERE TO START? 45 — @basthomas

Slide 51

Slide 51 text

@discardableResult mutating func solve() -> Solution { var boards: [Board] = [] repeat { next() boards.append(self) } while isSolved == false return Solution(steps: boards.map(Solution.Step.init)) } 46 — @basthomas

Slide 52

Slide 52 text

private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions: [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas

Slide 53

Slide 53 text

private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions: [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas

Slide 54

Slide 54 text

private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions: [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas

Slide 55

Slide 55 text

private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions: [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas

Slide 56

Slide 56 text

private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions: [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas

Slide 57

Slide 57 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 58

Slide 58 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 59

Slide 59 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 60

Slide 60 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 61

Slide 61 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 62

Slide 62 text

for loopingPosition in positions where position != loopingPosition { switch loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas

Slide 63

Slide 63 text

mutating func shuffle(moves: Int = 50) { for _ in 1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas

Slide 64

Slide 64 text

mutating func shuffle(moves: Int = 50) { for _ in 1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas

Slide 65

Slide 65 text

mutating func shuffle(moves: Int = 50) { for _ in 1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas

Slide 66

Slide 66 text

mutating func shuffle(moves: Int = 50) { for _ in 1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas

Slide 67

Slide 67 text

mutating func shuffle(moves: Int = 50) { for _ in 1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas

Slide 68

Slide 68 text

REWIRING YOUR BRAIN 50 — @basthomas

Slide 69

Slide 69 text

BRANCH... AND BOUND 51 — @basthomas

Slide 70

Slide 70 text

BRANCH... AND BOUND ▸ What are the options? 51 — @basthomas

Slide 71

Slide 71 text

BRANCH... AND BOUND ▸ What are the options? ▸ Is there a best option? 51 — @basthomas

Slide 72

Slide 72 text

BRANCH... AND BOUND ▸ What are the options? ▸ Is there a best option? ▸ Do it again 51 — @basthomas

Slide 73

Slide 73 text

mutating func next() { let currentBoard = self.currentBoard let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) let boardOptions = adjacentToEmpty.map { position -> (board: [[Tile]], moved: Tile, validPositions: Int) in let tileToMove = tile(at: position) move(tile: tileToMove) defer { self.currentBoard = currentBoard } return (self.currentBoard, tileToMove, validPositions) }.filter { $0.moved != _previousNextTile } let amountOfValidPositionsList = boardOptions.map { $0.validPositions } let largestAmountOfValidPositions = amountOfValidPositionsList.max() precondition(largestAmountOfValidPositions != nil, "No maximum valid positions found in \(boardOptions)") let nextBestSteps = amountOfValidPositionsList.filter { $0 == largestAmountOfValidPositions } let bestStepIndex = boardOptions.firstIndex { $0.validPositions == nextBestSteps.first } precondition(bestStepIndex != nil, "Should always have an index for the next best step") let bestOption = boardOptions[bestStepIndex!] self.currentBoard = bestOption.board _previousNextTile = bestOption.moved } 52 — @basthomas

Slide 74

Slide 74 text

let currentBoard = self.currentBoard let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) 53 — @basthomas

Slide 75

Slide 75 text

let boardOptions = adjacentToEmpty.map { position -> (board: [[Tile]], moved: Tile, validPositions: Int) in let tileToMove = tile(at: position) move(tile: tileToMove) defer { self.currentBoard = currentBoard } return (self.currentBoard, tileToMove, validPositions) }.filter { $0.moved != _previousNextTile } 54 — @basthomas

Slide 76

Slide 76 text

let amountOfValidPositionsList = boardOptions.map { $0.validPositions } let largestAmountOfValidPositions = amountOfValidPositionsList.max() precondition( largestAmountOfValidPositions != nil, "No maximum valid positions found in \(boardOptions)" ) 55 — @basthomas

Slide 77

Slide 77 text

let nextBestSteps = amountOfValidPositionsList.filter { $0 == largestAmountOfValidPositions } let bestStepIndex = boardOptions.firstIndex { $0.validPositions == nextBestSteps.first } precondition(bestStepIndex != nil, "Should always have an index for the next best step") let bestOption = boardOptions[bestStepIndex!] self.currentBoard = bestOption.board _previousNextTile = bestOption.moved 56 — @basthomas

Slide 78

Slide 78 text

AN EXAMPLE 57 — @basthomas

Slide 79

Slide 79 text

| 1| 2| 3| 4| | 5|10| 6|11| | 9|14| 8| 7| |13| |15|12| 58 — @basthomas

Slide 80

Slide 80 text

ADJACENT POSITIONS 59 — @basthomas

Slide 81

Slide 81 text

ADJACENT POSITIONS ▸ 13 59 — @basthomas

Slide 82

Slide 82 text

ADJACENT POSITIONS ▸ 13 ▸ 14 59 — @basthomas

Slide 83

Slide 83 text

ADJACENT POSITIONS ▸ 13 ▸ 14 ▸ 15 59 — @basthomas

Slide 84

Slide 84 text

VALID POSITIONS 60 — @basthomas

Slide 85

Slide 85 text

VALID POSITIONS ▸ 13 (right) > 7 60 — @basthomas

Slide 86

Slide 86 text

VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down) > 9 60 — @basthomas

Slide 87

Slide 87 text

VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down) > 9 ▸ 15 (left) > 7 60 — @basthomas

Slide 88

Slide 88 text

VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down) > 9 ▸ 15 (left) > 7 ▸ ... and repeat 60 — @basthomas

Slide 89

Slide 89 text

BE YOUR OWN BEST FRIEND ... AND FAIL EARLY 61 — @basthomas

Slide 90

Slide 90 text

But this will never happen! 62 — @basthomas

Slide 91

Slide 91 text

BREAK UP THE PUZZLE... IN PIECES 63 — @basthomas

Slide 92

Slide 92 text

START SOMEWHERE 64 — @basthomas

Slide 93

Slide 93 text

MAKE YOUR "EDGE" CASES EXPLICIT LET THE COMPILER HELP 65 — @basthomas

Slide 94

Slide 94 text

SOLVING THE 15 PUZZLE IN SWIFT A Look at Algorithms and Speed 66 — @basthomas