Skip to content

Commit

Permalink
Added not very smart computer player via Repository.
Browse files Browse the repository at this point in the history
  • Loading branch information
richy486 committed Mar 23, 2024
1 parent 06c4e38 commit 5012dbc
Show file tree
Hide file tree
Showing 14 changed files with 312 additions and 109 deletions.
16 changes: 16 additions & 0 deletions MoreChess.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -84,6 +88,10 @@
B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pieces.swift; sourceTree = "<group>"; };
B9FAC91E2BAF0D6A002A20F7 /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B9FAC9232BAF1E9F002A20F7 /* Board.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Board.swift; sourceTree = "<group>"; };
B9FAC9292BAF29DF002A20F7 /* Players.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Players.swift; sourceTree = "<group>"; };
B9FAC92B2BAF3102002A20F7 /* MoveValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidator.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -205,6 +213,7 @@
B9ABF7A42BAEF04C00907C61 /* DataAccess */ = {
isa = PBXGroup;
children = (
B9FAC9212BAF1DCD002A20F7 /* GameRepository.swift */,
);
path = DataAccess;
sourceTree = "<group>";
Expand All @@ -215,6 +224,8 @@
B94B61AF2B461D3200C998F3 /* Piece.swift */,
B9FAC91A2BAF0CA7002A20F7 /* Pieces.swift */,
B9FAC91E2BAF0D6A002A20F7 /* Player.swift */,
B9FAC9292BAF29DF002A20F7 /* Players.swift */,
B9FAC9232BAF1E9F002A20F7 /* Board.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -243,6 +254,7 @@
children = (
B94B61F42B68F8D700C998F3 /* PieceGenerator.swift */,
B94B61B22B461DD200C998F3 /* GridCoordinate.swift */,
B9FAC92B2BAF3102002A20F7 /* MoveValidator.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 │
│ └─────────────────────────┘
```
72 changes: 30 additions & 42 deletions Shared/Business/Interactors/PositioningInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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.
Expand All @@ -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
Expand Down
50 changes: 43 additions & 7 deletions Shared/Business/State/GameState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
16 changes: 16 additions & 0 deletions Shared/Business/State/SessionState.swift
Original file line number Diff line number Diff line change
@@ -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

}
}
}
90 changes: 90 additions & 0 deletions Shared/DataAccess/GameRepository.swift
Original file line number Diff line number Diff line change
@@ -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..<columnCount).randomElement() ?? 0
} else if move.column == -Int.max {
clampedColumn = ((0..<columnCount).randomElement() ?? 0) * -1
} else {
clampedColumn = move.column
}
let clampedRow: Int
if move.row == Int.max {
clampedRow = (0..<rowCount).randomElement() ?? 0
} else if move.row == -Int.max {
clampedRow = ((0..<rowCount).randomElement() ?? 0) * -1
} else {
clampedRow = move.row
}
let clampedMove = GridCoordinate(column: clampedColumn, row: clampedRow)

// Get target position
guard let targetPosition = MoveValidator.targetGridFrom(dragIndex: randomPiecePosition,
gridOffset: clampedMove,
board: board,
columnCount: columnCount,
rowCount: rowCount) else {
// Invalid move
// TODO: Must return a valid move, try again? filter valid targets?
print("Generated invalid move")
return board
}


// Update board
var updatedBoard = board
updatedBoard[targetPosition.row][targetPosition.column] = piece
updatedBoard[randomPiecePosition.row][randomPiecePosition.column] = nil

return updatedBoard
}
}

Loading

0 comments on commit 5012dbc

Please sign in to comment.