diff --git a/MoreChess.xcodeproj/project.pbxproj b/MoreChess.xcodeproj/project.pbxproj index a251207..b396bfc 100644 --- a/MoreChess.xcodeproj/project.pbxproj +++ b/MoreChess.xcodeproj/project.pbxproj @@ -37,6 +37,10 @@ B9FAC91B2BAF0CA7002A20F7 /* Pieces.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */; }; B9FAC91D2BAF0D15002A20F7 /* Pieces.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */; }; B9FAC91F2BAF0D6A002A20F7 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC91E2BAF0D6A002A20F7 /* Player.swift */; }; + B9FAC9222BAF1DCD002A20F7 /* GameRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC9212BAF1DCD002A20F7 /* GameRepository.swift */; }; + B9FAC9242BAF1E9F002A20F7 /* Board.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC9232BAF1E9F002A20F7 /* Board.swift */; }; + B9FAC92A2BAF29DF002A20F7 /* Players.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC9292BAF29DF002A20F7 /* Players.swift */; }; + B9FAC92C2BAF3102002A20F7 /* MoveValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FAC92B2BAF3102002A20F7 /* MoveValidator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -84,6 +88,10 @@ B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pieces.swift; sourceTree = ""; }; B9FAC91E2BAF0D6A002A20F7 /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; B9FAC9202BAF14CB002A20F7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; + B9FAC9212BAF1DCD002A20F7 /* GameRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRepository.swift; sourceTree = ""; }; + B9FAC9232BAF1E9F002A20F7 /* Board.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Board.swift; sourceTree = ""; }; + B9FAC9292BAF29DF002A20F7 /* Players.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Players.swift; sourceTree = ""; }; + B9FAC92B2BAF3102002A20F7 /* MoveValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -205,6 +213,7 @@ B9ABF7A42BAEF04C00907C61 /* DataAccess */ = { isa = PBXGroup; children = ( + B9FAC9212BAF1DCD002A20F7 /* GameRepository.swift */, ); path = DataAccess; sourceTree = ""; @@ -215,6 +224,8 @@ B94B61AF2B461D3200C998F3 /* Piece.swift */, B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */, B9FAC91E2BAF0D6A002A20F7 /* Player.swift */, + B9FAC9292BAF29DF002A20F7 /* Players.swift */, + B9FAC9232BAF1E9F002A20F7 /* Board.swift */, ); path = Models; sourceTree = ""; @@ -243,6 +254,7 @@ children = ( B94B61F42B68F8D700C998F3 /* PieceGenerator.swift */, B94B61B22B461DD200C998F3 /* GridCoordinate.swift */, + B9FAC92B2BAF3102002A20F7 /* MoveValidator.swift */, ); path = Utils; sourceTree = ""; @@ -406,15 +418,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B9FAC92A2BAF29DF002A20F7 /* Players.swift in Sources */, B9ABF7AB2BAEF1EB00907C61 /* BoardView.swift in Sources */, B9FAC9172BAEFFAC002A20F7 /* LayoutState.swift in Sources */, B9FAC9112BAEF492002A20F7 /* PositioningState.swift in Sources */, B94B61B32B461DD200C998F3 /* GridCoordinate.swift in Sources */, + B9FAC9242BAF1E9F002A20F7 /* Board.swift in Sources */, B95CE5392AD4CB5E00978591 /* ContentView.swift in Sources */, + B9FAC92C2BAF3102002A20F7 /* MoveValidator.swift in Sources */, B95CE5372AD4CB5E00978591 /* MoreChessApp.swift in Sources */, B9ABF7A72BAEF11E00907C61 /* AppState.swift in Sources */, B90F55AC2BA6289800874F00 /* PieceView.swift in Sources */, B94B61B02B461D3200C998F3 /* Piece.swift in Sources */, + B9FAC9222BAF1DCD002A20F7 /* GameRepository.swift in Sources */, B90F55A92BA61F6E00874F00 /* MovesView.swift in Sources */, B9ABF7A92BAEF18A00907C61 /* GameState.swift in Sources */, B94B61F52B68F8D700C998F3 /* PieceGenerator.swift in Sources */, diff --git a/README.md b/README.md index 6766baf..de09e3b 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,28 @@ Chess with randomly generated variant pieces. Using a Clean Architecture system where updates to the board are down from the interactor to a state that is read from the view ``` - ┌────────────┐ -┌─────► Board View │ -│ └────┬───────┘ -│ │ -│ │ Drag Offset -│ │ -│ │ ┌────────────────────────┐ -│ └─► Positioning Interactor │ -│ └───────────┬────────────┘ -│ │ -│ ┌────────▼──────────┐ -│ │ Positioning State │ -│ └────────┬──────────┘ -│ │ -└────────────────────────┘ + ┌────────────┐ +┌─────► Board View │ +│ └────┬───────┘ +│ │ +│ │ Drag Offset +│ │ ┌────────────────────────┐ Move ┌─────────────────┐ +│ │ │ ├──────────► │ +│ └─► │ │ │ +│ │ Positioning Interactor │ │ Game Repository │ +│ │ │ Board │ │ +│ │ ◄──────────┤ │ +│ └────┬─────────────────┬─┘ └─────────────────┘ +│ │ │ +│ │ Move positions │ +│ │ │ +│ ┌──────▼────────────┐ │ +│◄─────────┤ Positioning State │ │ Board update +│ └───────────────────┘ │ +│ │ +│ │ +│ ┌───────────────────▼─────┐ +│◄──────────────┤ Game State │ +│ └─────────────────────────┘ + ``` diff --git a/Shared/Business/Interactors/PositioningInteractor.swift b/Shared/Business/Interactors/PositioningInteractor.swift index 392711b..de8d5e7 100644 --- a/Shared/Business/Interactors/PositioningInteractor.swift +++ b/Shared/Business/Interactors/PositioningInteractor.swift @@ -11,9 +11,15 @@ import Foundation struct PositioningInteractor { let appState: AppState + let gameRepository = GameRepository() func update(dragOffset: CGSize, from fromGridPosition: GridCoordinate) { + // Is the current player local? + guard appState.gameState.currentTurn.local else { + return + } + // Is there a piece at this grid position? guard let piece = appState.gameState.board[fromGridPosition.row][fromGridPosition.column] else { return @@ -32,8 +38,12 @@ struct PositioningInteractor { let gridOffset = gridOffsetFrom(offset: dragOffset) - appState.positioningState.targetGrid = targetGridFrom(dragIndex: fromGridPosition, - gridOffset: gridOffset) + appState.positioningState.targetGrid = + MoveValidator.targetGridFrom(dragIndex: fromGridPosition, + gridOffset: gridOffset, + board: appState.gameState.board, + columnCount: appState.gameState.columnCount, + rowCount: appState.gameState.rowCount) } func endDrag() { @@ -45,7 +55,24 @@ struct PositioningInteractor { // Move the piece if there is a valid target and swap turn. if let targetGrid = appState.positioningState.targetGrid { movePiece(from: selectedGridPosition, to: targetGrid) - appState.gameState.currentTurn = appState.gameState.currentTurn.opponent + appState.gameState.currentTurn = appState.gameState.currentOpponent + // TODO: Check for win state + + // Fetch the next board + // TODO: Make this work for two non-local players. + if appState.gameState.currentTurn.local == false { + Task { + appState.gameState.board = await gameRepository + .fetchBoard(currentPlayer: appState.gameState.currentTurn, + opponent: appState.gameState.currentOpponent, + currentBoard: appState.gameState.board, + columnCount: appState.gameState.columnCount, + rowCount: appState.gameState.rowCount) + // Switch back + appState.gameState.currentTurn = appState.gameState.currentOpponent + // TODO: Check for win state + } + } } // Reset positioning values. @@ -61,45 +88,6 @@ struct PositioningInteractor { return GridCoordinate(column: x, row: y) } - private func targetGridFrom(dragIndex: GridCoordinate?, gridOffset: GridCoordinate) -> GridCoordinate? { - guard let dragIndex, let piece = appState.gameState.board[dragIndex.row][dragIndex.column] else { - return nil - } - - let targetPosition = GridCoordinate(column: dragIndex.column + gridOffset.column, row: dragIndex.row + gridOffset.row) - - // Inside board - guard targetPosition.column >= 0 && targetPosition.column < appState.gameState.columnCount && - targetPosition.row >= 0 && targetPosition.row < appState.gameState.rowCount else { - return nil - } - - // Validate position - // TODO: Maybe replace this with a filter so we can use a guard. - for validMove in piece.validMoves { - // Check for not `moveDown`. - let validMoveRow = piece.player.movingDown ? validMove.row : validMove.row * -1 - let validMoveColumn = piece.player.movingDown ? validMove.column : validMove.column * -1 - - // If there are two `Int.max` their absolute values must be equal. - if abs(validMoveRow) == Int.max && abs(validMoveColumn) == Int.max - && abs(gridOffset.row) != abs(gridOffset.column) { - continue - } - - if (gridOffset.column == validMoveColumn || (gridOffset.column > 0 && validMoveColumn == Int.max) || (gridOffset.column < 0 && validMoveColumn == -Int.max)) - && (gridOffset.row == validMoveRow || (gridOffset.row > 0 && validMoveRow == Int.max) || (gridOffset.row < 0 && validMoveRow == -Int.max) ) { - - // TODO: Is target empty space or opponent's space? - - // Found a valid target - return targetPosition - } - } - - return nil - } - private func movePiece(from: GridCoordinate, to: GridCoordinate) { let piece = appState.gameState.board[from.row][from.column] appState.gameState.board[to.row][to.column] = piece diff --git a/Shared/Business/State/GameState.swift b/Shared/Business/State/GameState.swift index 4fbafc1..5c70bfc 100644 --- a/Shared/Business/State/GameState.swift +++ b/Shared/Business/State/GameState.swift @@ -8,26 +8,62 @@ import Foundation struct GameState { - // This data structure is (row, column), most of the other code is (column, row) because that is (x, y). - var board: [[Piece?]] = [[]] - var currentTurn: Player = .one - + // MARK: Board + + // This data structure is (row, column), most of the other code is (column, row) because that is + // (x, y). + var board: Board = [[]] var rowCount: Int { return board.count } + // calculate columns from minimum grid positions in the rows so there are no positions without an + // array value. var columnCount: Int { return board.reduce(999, { partialResult, row in return min(partialResult, row.count) }) } + // MARK: Current Player + + // TODO: too much calculation and errors here, refactor + var players: [Player] = [ + Players.one(local: true), + Players.two(local: false) + ] + private var currentTurnIndex: Int + var currentTurn: Player { + get { + return players[currentTurnIndex] + } + set { + guard let index = players.firstIndex(of: newValue) else { + fatalError("no index of player") + } + currentTurnIndex = index + } + + } + var currentOpponent: Player { + switch currentTurnIndex { + case 0: + return players[1] + case 1: + return players[0] + default: + fatalError("currentTurnIndex out of range") + } + } + init() { + currentTurnIndex = 0 + // Init board - let initialBoard: [[Piece?]] = [ - [PieceGenerator.randomPiece(forPlayer: .two, horizontalSize: 5, verticalSize: 5), Pieces.🐴(.two), PieceGenerator.randomPiece(forPlayer: .two, horizontalSize: 5, verticalSize: 5), Pieces.👉(.two), Pieces.🏰(.two)], + let initialBoard: Board = [ + [PieceGenerator.randomPiece(forPlayer: players[1], horizontalSize: 5, verticalSize: 5), Pieces.🐴(players[1]), PieceGenerator.randomPiece(forPlayer: players[1], horizontalSize: 5, verticalSize: 5), Pieces.👉(players[1]), Pieces.🏰(players[1])], [nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil], - [Pieces.🐴(.one), Pieces.🥷(.one), Pieces.🤴(.one), Pieces.👸(.one), PieceGenerator.randomPiece(forPlayer: .one, horizontalSize: 5, verticalSize: 5)], + [Pieces.🐴(players[0]), Pieces.🥷(players[0]), Pieces.🤴(players[0]), Pieces.👸(players[0]), PieceGenerator.randomPiece(forPlayer: players[0], horizontalSize: 5, verticalSize: 5)], ] let allBoard = initialBoard.joined().compactMap { $0 } diff --git a/Shared/Business/State/SessionState.swift b/Shared/Business/State/SessionState.swift new file mode 100644 index 0000000..bf7171f --- /dev/null +++ b/Shared/Business/State/SessionState.swift @@ -0,0 +1,16 @@ +// +// SessionState.swift +// MoreChess (iOS) +// +// Created by Richard Adem on 3/23/24. +// + +import Foundation + +struct SessionState { + var isPlayerLocal: (Player) -> Bool { + { player in + + } + } +} diff --git a/Shared/DataAccess/GameRepository.swift b/Shared/DataAccess/GameRepository.swift new file mode 100644 index 0000000..6c0af01 --- /dev/null +++ b/Shared/DataAccess/GameRepository.swift @@ -0,0 +1,90 @@ +// +// GameRepository.swift +// MoreChess (iOS) +// +// Created by Richard Adem on 3/23/24. +// + +import Foundation + +// TODO: "Facade" this with a protocol so data can be mocked. + +struct GameRepository { + func fetchBoard(currentPlayer: Player, + opponent: Player, + currentBoard board: Board, + columnCount: Int, + rowCount: Int) async -> Board { + // Debug sleep + try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) + + // Get all current piece positions + let allPositions: [GridCoordinate] = board.enumerated().reduce([], { partialResult, value in + let rowIndex = value.offset + let row = value.element + let rowPositions: [GridCoordinate] = Array(row.enumerated()).compactMap { (columnIndex, piece) in + guard let piece else { + return nil + } + guard piece.player == currentPlayer else { + return nil + } + return GridCoordinate(column: columnIndex, row: rowIndex) + } + return partialResult + rowPositions + }) + + // Get a random piece + guard let randomPiecePosition = allPositions.randomElement() else { + fatalError("Can't get random piece positions") + } + guard let piece = board[randomPiecePosition.row][randomPiecePosition.column] else { + fatalError("Can't get random piece") + } + + // Get a random valid move + guard let move = piece.validMoves.randomElement() else { + fatalError("Can't get random move") + } + + // If move contains infinity pick a random number + let clampedColumn: Int + if move.column == Int.max { + clampedColumn = (0.. Player { + return Player(name: "One 🔵", + color: Color(hue: 0.55, saturation: 0.75, brightness: 1), + movingDown: false, + local: local) + } + static func two(local: Bool) -> Player { + return Player(name: "Two 🟡", + color: Color(hue: 0.15, saturation: 0.75, brightness: 1), + movingDown: true, + local: local) + } +} diff --git a/Shared/Presentation/ContentView.swift b/Shared/Presentation/ContentView.swift index df1b2ed..61846a4 100644 --- a/Shared/Presentation/ContentView.swift +++ b/Shared/Presentation/ContentView.swift @@ -14,6 +14,7 @@ struct ContentView: View { VStack { Text("Current turn: \(appState.gameState.currentTurn.name)") + Text("Playing state: \(appState.gameState.currentTurn.local ? "📱" : "⏱️")") Spacer() diff --git a/Shared/Presentation/MovesView.swift b/Shared/Presentation/MovesView.swift index bd66d91..fa990a2 100644 --- a/Shared/Presentation/MovesView.swift +++ b/Shared/Presentation/MovesView.swift @@ -17,9 +17,9 @@ struct MovesView: View { #Preview { MovesView(pieceList: [ - Pieces.🐴(.one), - Pieces.🥷(.one), - Pieces.🤴(.one), - Pieces.👸(.one), +// Pieces.🐴(.one), +// Pieces.🥷(.one), +// Pieces.🤴(.one), +// Pieces.👸(.one), ]) } diff --git a/Shared/Presentation/PieceView.swift b/Shared/Presentation/PieceView.swift index 04d4480..be917f4 100644 --- a/Shared/Presentation/PieceView.swift +++ b/Shared/Presentation/PieceView.swift @@ -17,5 +17,5 @@ struct PieceView: View { } #Preview { - PieceView(piece: Pieces.🐴(.one)) + PieceView(piece: Pieces.🐴(Players.one(local: true))) } diff --git a/Shared/Utils/MoveValidator.swift b/Shared/Utils/MoveValidator.swift new file mode 100644 index 0000000..0cba92e --- /dev/null +++ b/Shared/Utils/MoveValidator.swift @@ -0,0 +1,49 @@ +// +// MoveValidator.swift +// MoreChess (iOS) +// +// Created by Richard Adem on 3/23/24. +// + +import Foundation + +struct MoveValidator { + static func targetGridFrom(dragIndex: GridCoordinate?, gridOffset: GridCoordinate, board: Board, columnCount: Int, rowCount: Int) -> GridCoordinate? { + guard let dragIndex, let piece = board[dragIndex.row][dragIndex.column] else { + return nil + } + + let targetPosition = GridCoordinate(column: dragIndex.column + gridOffset.column, row: dragIndex.row + gridOffset.row) + + // Inside board + guard targetPosition.column >= 0 && targetPosition.column < columnCount && + targetPosition.row >= 0 && targetPosition.row < rowCount else { + return nil + } + + // Validate position + // TODO: Maybe replace this with a filter so we can use a guard. + for validMove in piece.validMoves { + // Check for not `moveDown`. + let validMoveRow = piece.player.movingDown ? validMove.row : validMove.row * -1 + let validMoveColumn = piece.player.movingDown ? validMove.column : validMove.column * -1 + + // If there are two `Int.max` their absolute values must be equal. + if abs(validMoveRow) == Int.max && abs(validMoveColumn) == Int.max + && abs(gridOffset.row) != abs(gridOffset.column) { + continue + } + + if (gridOffset.column == validMoveColumn || (gridOffset.column > 0 && validMoveColumn == Int.max) || (gridOffset.column < 0 && validMoveColumn == -Int.max)) + && (gridOffset.row == validMoveRow || (gridOffset.row > 0 && validMoveRow == Int.max) || (gridOffset.row < 0 && validMoveRow == -Int.max) ) { + + // TODO: Is target empty space or opponent's space? + + // Found a valid target + return targetPosition + } + } + + return nil + } +}