Sample/SwiftUI-MVVM/TicTacToe/Sources/Game/GameViewModel.swift (182 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
import Combine
protocol GameViewModelProtocol: ObservableObject {
var boardColors: [[Color]] { get }
var alertMessage: String? { get set }
var selection: String? { get set }
func placeCurrentPlayerMark(at row: Int, col: Int)
func scoreTapped()
func reset()
}
private enum Player: Int {
case player1 = 1
case player2
var color: Color {
switch self {
case .player1:
return Color.red
case .player2:
return Color.blue
}
}
}
final class GameViewModel: GameViewModelProtocol {
private let mutableScoreStream: MutableScoreStream
private let playersStream: PlayersStream
@Published var boardColors: [[Color]] = Board.initialColors
@Published var alertMessage: String? = nil
@Published var selection: String? = nil
private var currentPlayer = Player.player1
private var cancellables = Set<AnyCancellable>()
init(
mutableScoreStream: MutableScoreStream,
playersStream: PlayersStream
) {
self.mutableScoreStream = mutableScoreStream
self.playersStream = playersStream
}
func placeCurrentPlayerMark(at row: Int, col: Int) {
guard boardColors[row][col] == .white else { return }
let currentPlayer = getAndFlipCurrentPlayer()
boardColors[row][col] = currentPlayer.color
let endGame = checkEndGame()
if endGame.didEnd {
if let winner = endGame.winner {
performOnPlayerNames { [weak self] (player1Name: String, player2Name: String) in
let winnerName = winner == .player1 ? player1Name : player2Name
let loserName = winner != .player1 ? player1Name : player2Name
self?.announce(winner)
self?.mutableScoreStream.updateScore(withWinner: winnerName, loser: loserName)
}
} else {
announceDraw()
mutableScoreStream.updateDraw()
}
}
}
func reset() {
boardColors = Board.initialColors
alertMessage = nil
}
func scoreTapped() {
selection = Screen.score.rawValue
}
private func announce(_ winner: Player) {
performOnPlayerNames { [weak self] (player1Name: String, player2Name: String) in
let winnerName: String
switch winner {
case .player1:
winnerName = player1Name
case .player2:
winnerName = player2Name
}
self?.alertMessage = "\(winnerName) Won!"
}
}
private func announceDraw() {
alertMessage = "It's a Tie"
}
private func performOnPlayerNames(with handler: @escaping (String, String) -> Void) {
playersStream.names
.prefix(1)
.flatMap { (names: (String, String)?) -> AnyPublisher<(String, String), Never> in
if let names = names {
return Just(names).eraseToAnyPublisher()
} else {
return Empty<(String, String), Never>(completeImmediately: false).eraseToAnyPublisher()
}
}
.sink { (player1Name: String, player2Name: String) in
handler(player1Name, player2Name)
}
.store(in: &cancellables)
}
private func getAndFlipCurrentPlayer() -> Player {
let currentPlayer = self.currentPlayer
self.currentPlayer = currentPlayer == .player1 ? .player2 : .player1
return currentPlayer
}
private func checkEndGame() -> (winner: Player?, didEnd: Bool) {
let winner = checkWinner()
if let winner = winner {
return (winner, true)
}
let isDraw = checkDraw()
if isDraw {
return (nil, true)
}
return (nil, false)
}
private func checkDraw() -> Bool {
for row in 0 ..< 3 {
for col in 0 ..< 3 {
if boardColors[row][col] == .white {
return false
}
}
}
return true
}
private func checkWinner() -> Player? {
// Rows
for row in 0..<3 {
if let winner = boardColors[row].winner() {
return winner
}
}
// Cols
let transposedBoardColors = boardColors.transposed()
for col in 0..<3 {
if let winner = transposedBoardColors[col].winner() {
return winner
}
}
// Diagonals
let p11 = boardColors[1][1]
guard p11 != .white else { return nil }
let p00 = boardColors[0][0]
let p22 = boardColors[2][2]
let primaryDiagonal = [p00, p11, p22]
if let winner = primaryDiagonal.winner() {
return winner
}
let p02 = boardColors[0][2]
let p20 = boardColors[2][0]
let secondaryDiagonal = [p02, p11, p20]
if let winner = secondaryDiagonal.winner() {
return winner
}
return nil
}
}
struct Board {
static let initialColors: [[Color]] = Array(
repeating: Array(
repeating: Color.white,
count: 3
),
count: 3
)
}
private extension Array where Element == Color {
func allRed() -> Bool {
return self == Array(repeating: .red, count: self.count)
}
func allBlue() -> Bool {
return self == Array(repeating: .blue, count: self.count)
}
func winner() -> Player? {
if self.allRed() {
return .player1
} else if self.allBlue() {
return .player2
}
return nil
}
}
extension Collection where Self.Iterator.Element: RandomAccessCollection {
// PRECONDITION: `self` must be rectangular, i.e. every row has equal size.
func transposed() -> [[Self.Iterator.Element.Iterator.Element]] {
guard let firstRow = self.first else { return [] }
return firstRow.indices.map { index in
self.map{ $0[index] }
}
}
}