From e49690cde62af4538ac481cee56ba74f81899a04 Mon Sep 17 00:00:00 2001 From: Mathieu GROUSSEAU Date: Thu, 23 Jan 2025 15:21:53 +0100 Subject: [PATCH] WIP Rules by guessing how it is expected to work --- Model/Sources/Model/Board.swift | 2 +- .../Model/Rules/ConnectFourRules.swift | 145 ++++++++++++++++++ Model/Sources/Model/Rules/Move.swift | 9 ++ Model/Sources/Model/Rules/Rules.swift | 23 +++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 Model/Sources/Model/Rules/ConnectFourRules.swift create mode 100644 Model/Sources/Model/Rules/Move.swift create mode 100644 Model/Sources/Model/Rules/Rules.swift diff --git a/Model/Sources/Model/Board.swift b/Model/Sources/Model/Board.swift index f9c6155..3ade19a 100644 --- a/Model/Sources/Model/Board.swift +++ b/Model/Sources/Model/Board.swift @@ -16,7 +16,7 @@ public struct Board { self.grid = grid } - private func isInBounds(_ pos: Coords) -> Bool { + public func isInBounds(_ pos: Coords) -> Bool { pos.col >= 0 && pos.col < self.columns && pos.row >= 0 && pos.row < self.rows } diff --git a/Model/Sources/Model/Rules/ConnectFourRules.swift b/Model/Sources/Model/Rules/ConnectFourRules.swift new file mode 100644 index 0000000..e13beac --- /dev/null +++ b/Model/Sources/Model/Rules/ConnectFourRules.swift @@ -0,0 +1,145 @@ +public struct FourInARowRules: Rules { + private(set) public var state: GameState = .Playing(turn: .A) + + private(set) public var history: [Move] = [] + + private let columns: Int, rows: Int, minAligned: Int + + init?(columns: Int, rows: Int, minAligned: Int = 4) { + guard columns >= 3, rows >= 3 else { + return nil + } + + self.columns = columns + self.rows = rows + self.minAligned = minAligned + } + + public func createBoard() -> Board { + Board(columns: self.columns, rows: self.rows)! + } + + public func isValid(board: Board, move: Move) -> Bool { + guard self.isValid(board: board) else { return false } + + switch self.state { + case .Playing(let turn) where turn != move.player: + fallthrough + case .Finished(_): + return false + default: + break + } + + if case .InsertOnSide(.Top, let offset) = move.action, offset >= 0 && offset < self.columns { + return board.fallCoordinates( + initialCoords: Coords(offset, 0), + direction: .Bottom + ) != .Occupied + } + + return false + } + + public func isValid(board: Board) -> Bool { + for c in 0.. [Move] { + return [ Player.A, Player.B ].flatMap({ + player in self.validMoves(board: board, for_player: player).map({ + action in Move(player: .A, action: action) + }) + }) + } + + public func validMoves(board: Board, for_player player: Player) -> [Move.Action] { + var moves: [Move.Action] = []; + + for c in 0.. Void { + self.history.append(move) + + switch move.action { + case .InsertOnSide(side: .Top, let offset): + let initCoords = board.getInsertionCoordinates(from: .Top, offset: offset) + let pieceCoords = switch board.fallCoordinates( + initialCoords: initCoords, + direction: .Bottom + ) { + case .Occupied: + initCoords + case .Piece(_, let touched): + touched + + default: + fatalError("Illegal move \(move.action)") + } + + if countMaxRow(center: pieceCoords, board: board) >= self.minAligned { + self.state = .Finished(winner: move.player) + } else if board.countPieces() == board.columns * board.rows { + self.state = .Finished(winner: nil) + } else { + let next: Player = switch move.player { + case .A: .B + case .B: .A + } + self.state = .Playing(turn: next) + } + default: + fatalError("Illegal move \(move.action)") + } + } + + private func countMaxRow(center: Coords, board: Board) -> Int { + guard let of = board[center]?.owner else { return 0 } + + var maxLength = 0 + // For each "axis" (described as one direction) + for dir: (dc: Int, dr: Int) in [(1, 0), (0, 1), (1, 1)] { + var length = 1 + + // Run in the two opposite directions of the axis to sum the length + for (dc, dr) in [(dir.dc, dir.dr), (-dir.dc, -dir.dr)] { + var pos = center + + while true { + pos = Coords(pos.col + dc, pos.row + dr) + if board.isInBounds(pos) && board[pos]?.owner != of { break } + length += 1 + } + } + + maxLength = max(maxLength, length) + } + + return maxLength + } +} diff --git a/Model/Sources/Model/Rules/Move.swift b/Model/Sources/Model/Rules/Move.swift new file mode 100644 index 0000000..d25c197 --- /dev/null +++ b/Model/Sources/Model/Rules/Move.swift @@ -0,0 +1,9 @@ +public struct Move { + let player: Player + let action: Action + + public enum Action { + case InsertOnSide(side: Direction, offset: Int) + case InsertAt(where: Coords) + } +} diff --git a/Model/Sources/Model/Rules/Rules.swift b/Model/Sources/Model/Rules/Rules.swift new file mode 100644 index 0000000..5efb519 --- /dev/null +++ b/Model/Sources/Model/Rules/Rules.swift @@ -0,0 +1,23 @@ +public protocol Rules { + var state: GameState { get } + + var history: [Move] { get } + + mutating func createBoard() -> Board + + func isValid(board: Board) -> Bool + + func isValid(board: Board, move: Move) -> Bool + + func validMoves(board: Board) -> [Move] + + func validMoves(board: Board, for_player player: Player) -> [Move.Action] + + mutating func onMoveDone(move: Move, board: Board) -> Void +} + +public enum GameState { + case Playing(turn: Player) + + case Finished(winner: Player?) +}