Solving the 15-puzzle in Swift: A Look at Algorithms and Speed

79fe3c13c618a61329298bdd6a86ec42?s=47 Bas Broek
August 29, 2019

Solving the 15-puzzle in Swift: A Look at Algorithms and Speed

Algorithms and optimization can sound daunting, but are a really interesting programming problem. In this talk, we'll be looking at writing a solver for a puzzle, improving it along the way by making it more performant.

79fe3c13c618a61329298bdd6a86ec42?s=128

Bas Broek

August 29, 2019
Tweet

Transcript

  1. 2.

    SOLVING THE 15 PUZZLE IN SWIFT A Look at Algorithms

    and Speed A Look at Algorithms and Performance 2 — @basthomas
  2. 4.

    | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11|12| |13|14|15| | 4 — @basthomas
  3. 5.

    | 1| 6| 2| 3| | |10| 7| 4| |

    5| 9|11| 8| |13|14|15|12| 5 — @basthomas
  4. 6.

    | 1| 6| 2| 3| | 5|10| 7| 4| |

    | 9|11| 8| |13|14|15|12| 6 — @basthomas
  5. 7.

    | 1| 6| 2| 3| | 5|10| 7| 4| |

    9| |11| 8| |13|14|15|12| 7 — @basthomas
  6. 8.

    | 1| 6| 2| 3| | 5| | 7| 4|

    | 9|10|11| 8| |13|14|15|12| 8 — @basthomas
  7. 9.

    | 1| | 2| 3| | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 9 — @basthomas
  8. 10.

    | 1| 2| | 3| | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 10 — @basthomas
  9. 11.

    | 1| 2| 3| | | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 11 — @basthomas
  10. 12.

    | 1| 2| 3| 4| | 5| 6| 7| |

    | 9|10|11| 8| |13|14|15|12| 12 — @basthomas
  11. 13.

    | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11| | |13|14|15|12| 13 — @basthomas
  12. 14.

    | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11|12| |13|14|15| | 14 — @basthomas
  13. 23.

    struct Solution<Problem> { struct Step<T> { let step: T }

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

    let solution = Solution<Int>( steps: Solution.Step(step: 1), Solution.Step(step: 2), Solution.Step(step:

    3) ) print(solution.input) // Step<Int>(step: 1) print(solution.output) // Step<Int>(step: 3) 24 — @basthomas
  15. 26.

    struct Solution<Problem>: Collection { var startIndex: Int { return steps.startIndex

    } var endIndex: Int { return steps.endIndex } subscript(i: Int) -> Step<Problem> { return steps[i] } func index(after i: Int) -> Int { return steps.index(after: i) } } 26 — @basthomas
  16. 30.

    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<Board> } 30 — @basthomas
  17. 31.

    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
  18. 32.

    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
  19. 36.

    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
  20. 38.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  21. 39.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  22. 40.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  23. 41.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  24. 42.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  25. 43.

    init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  26. 47.
  27. 51.

    @discardableResult mutating func solve() -> Solution<Board> { var boards: [Board]

    = [] repeat { next() boards.append(self) } while isSolved == false return Solution(steps: boards.map(Solution.Step.init)) } 46 — @basthomas
  28. 52.

    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
  29. 53.

    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
  30. 54.

    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
  31. 55.

    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
  32. 56.

    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
  33. 57.

    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
  34. 58.

    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
  35. 59.

    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
  36. 60.

    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
  37. 61.

    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
  38. 62.

    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
  39. 63.

    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
  40. 64.

    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
  41. 65.

    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
  42. 66.

    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
  43. 67.

    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
  44. 71.

    BRANCH... AND BOUND ▸ What are the options? ▸ Is

    there a best option? 51 — @basthomas
  45. 72.

    BRANCH... AND BOUND ▸ What are the options? ▸ Is

    there a best option? ▸ Do it again 51 — @basthomas
  46. 73.

    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
  47. 75.

    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
  48. 76.

    let amountOfValidPositionsList = boardOptions.map { $0.validPositions } let largestAmountOfValidPositions =

    amountOfValidPositionsList.max() precondition( largestAmountOfValidPositions != nil, "No maximum valid positions found in \(boardOptions)" ) 55 — @basthomas
  49. 77.

    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
  50. 79.

    | 1| 2| 3| 4| | 5|10| 6|11| | 9|14|

    8| 7| |13| |15|12| 58 — @basthomas
  51. 87.

    VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down)

    > 9 ▸ 15 (left) > 7 60 — @basthomas
  52. 88.

    VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down)

    > 9 ▸ 15 (left) > 7 ▸ ... and repeat 60 — @basthomas
  53. 94.