import ComposableArchitecture import SwiftUI import ComposableUndo public enum Player: Equatable { case o case x public mutating func toggle() { switch self { case .o: self = .x case .x: self = .o } } public var label: String { switch self { case .o: return "⭕️" case .x: return "❌" } } } public struct GameState: Equatable { public struct BoardState: Equatable { public var board: Three> = .empty public var currentPlayer: Player = .x } public var oPlayerName: String public var xPlayerName: String @CheckpointState public var boardState: BoardState public var board: Three> { get { boardState.board } set { boardState.board = newValue } } public var currentPlayer: Player { get { boardState.currentPlayer } set { boardState.currentPlayer = newValue } } public init(oPlayerName: String, xPlayerName: String) { self.oPlayerName = oPlayerName self.xPlayerName = xPlayerName self.boardState = .init() } public var currentPlayerName: String { switch self.currentPlayer { case .o: return self.oPlayerName case .x: return self.xPlayerName } } } public enum GameAction: Equatable, SingleCheckpointAction { case cellTapped(row: Int, column: Int) case playAgainButtonTapped case quitButtonTapped case checkpoint(CheckpointAction) } public struct GameEnvironment { public init() {} } public let gameReducer = Reducer { state, action, _ in switch action { case let .cellTapped(row, column): guard state.board[row][column] == nil, !state.board.hasWinner else { return .none } let checkpointLabel = "\(state.currentPlayer.label)'s move" state.board[row][column] = state.currentPlayer if !state.board.hasWinner { state.currentPlayer.toggle() } return .checkpoint(.register(checkpointLabel)) case .playAgainButtonTapped: state.board = .empty state.currentPlayer = .x return .checkpoint(.removeAll) case .quitButtonTapped: return .checkpoint(.detachManager) case .checkpoint: return .none } } .trackCheckpoints(of: \.$boardState) extension Three where Element == Three { public static let empty = Self( .init(nil, nil, nil), .init(nil, nil, nil), .init(nil, nil, nil) ) public var isFilled: Bool { self.allSatisfy { $0.allSatisfy { $0 != nil } } } func hasWin(_ player: Player) -> Bool { let winConditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [6, 4, 2], ] for condition in winConditions { let matches = condition .map { self[$0 % 3][$0 / 3] } let matchCount = matches .filter { $0 == player } .count if matchCount == 3 { return true } } return false } public var hasWinner: Bool { hasWin(.x) || hasWin(.o) } }