Players, Rules refactor, unhappy code noises

pull/3/head
Mathieu GROUSSEAU 3 weeks ago
parent f1fab2ec5a
commit f66d5fe3f3

@ -3,7 +3,35 @@ import Foundation
import Model
import CustomTypes
var rules = FourInARowRules()
let ai = RandomPlayer(name: "Le robot a mythos", piece_type: .A)
let cli = HumanPlayer(name: "Humain", piece_type: .B, callback: {
allowed_moves, board in
print("Available moves:")
for i in allowed_moves.indices {
print("\(i + 1)\t\(allowed_moves[i])")
}
print(">")
guard let input = readLine(strippingNewline: true) else {
fatalError("Invalid input")
}
guard let id = Int(input), id >= 1, id <= allowed_moves.count else {
fatalError("Invalid move id: \(input)")
}
return allowed_moves[id - 1]
})
let players = [ cli, ai ]
guard var rules = FourInARowRules(
columns: FourInARowRules.COLUMNS_DEFAULT,
rows: FourInARowRules.ROWS_DEFAULT,
players: players
) else {
fatalError("Rules settings are invalid")
}
var board = rules.createBoard()
@ -11,7 +39,8 @@ guard rules.isValid(board: board) else {
fatalError("Board is invalid")
}
print(rules.state)
var state = rules.gameState(board: board, last_turn: nil)
print(state)
print("All moves:")
for move in rules.validMoves(board: board) {
@ -19,52 +48,47 @@ for move in rules.validMoves(board: board) {
}
print("Moves for \(PieceType.A):")
for move in rules.validMoves(board: board, for_player: .A) {
for move in rules.validMoves(board: board, for_player: ai) {
print(move)
}
print(board)
while case .Playing(let turn) = rules.state {
let moves = rules.validMoves(board: board, for_player: turn)
let action = moves[Int.random(in: 0..<moves.count)]
guard case .InsertOnSide(let side, let offset) = action else {
fatalError("Unexpected move!")
}
while case .Playing(let turn) = state {
let action = turn.chooseMove(
allowed_moves: rules.validMoves(board: board, for_player: turn),
board: board
)
let move = Move(player: turn, action: action)
guard rules.isValid(board: board, move: move) else {
fatalError("Move is in fact invalid???")
fatalError("Move of \(turn) is invalid!")
}
switch board.fallCoordinates(
initialCoords: board.getInsertionCoordinates(from: side, offset: offset),
direction: .Bottom
) {
case .Border(let at), .Piece(let at, _):
board[at] = Piece(type: turn)
case .Occupied:
fatalError("Occupied???")
switch move.action {
case .InsertAt(let coords):
board[coords] = Piece(type: move.player.piece_type)
case .InsertOnSide(let side, let offset):
switch board.fallCoordinates(
initialCoords: board.getInsertionCoordinates(from: side, offset: offset),
direction: side.opposite
) {
case .Border(let at), .Piece(let at, _):
board[at] = Piece(type: move.player.piece_type)
case .Occupied:
fatalError("Not supported")
}
}
rules.onMoveDone(move: move, board: board)
print(board)
state = rules.gameState(board: board, last_turn: turn)
}
if case .Finished(let winner) = rules.state {
if let winner = winner {
print("\(winner) is the winner")
} else {
print("Draw.")
}
if case .Win(let winner, _) = state {
print("\(winner.name) is the winner")
} else {
fatalError()
}
print("Moves were:")
for move in rules.history {
print("\(move.player) did \(move.action)")
print("Draw.")
}
exit(EXIT_SUCCESS)

@ -1,5 +1,5 @@
public struct Board {
private var grid: [[Piece?]]
public struct Board: Equatable {
private var grid: [[Piece?]]
public var columns: Int { return grid.count }
public var rows: Int { return grid.first!.count }
@ -123,4 +123,8 @@ public struct Board {
}
// TODO push
public static func == (lhs: Board, rhs: Board) -> Bool {
lhs.grid == rhs.grid
}
}

@ -11,4 +11,12 @@ public struct Coords: Equatable {
public init(pair: (Int, Int)) {
self.init(pair.0, pair.1)
}
/* static func +(lhs: Self, rhs: Self) -> Self {
Self(lhs.col + rhs.col, lhs.row + rhs.row)
}
static func -(lhs: Self, rhs: Self) -> Self {
Self(lhs.col - rhs.col, lhs.row - rhs.row)
} */
}

@ -1,7 +1,7 @@
public enum Direction: CaseIterable {
case Top, Left, Bottom, Right
var opposite: Self {
public var opposite: Self {
switch self {
case .Top:
.Bottom

@ -1,4 +1,4 @@
public struct Piece : Sendable {
public struct Piece: Equatable, Sendable {
public let type: PieceType
// Required for public visibility

@ -1,3 +1,3 @@
public enum PieceType: CaseIterable, Sendable {
public enum PieceType: Equatable, CaseIterable, Sendable {
case A, B
}

@ -3,10 +3,10 @@ public typealias MoveCallback = ([Move.Action], Board) -> Move.Action
public class HumanPlayer : Player {
private let callback: MoveCallback
public init(name: String, callback: @escaping MoveCallback) {
public init(name: String, piece_type: PieceType, callback: @escaping MoveCallback) {
self.callback = callback
super.init(name: name)
super.init(name: name, piece_type: piece_type)
}
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action {

@ -1,11 +1,18 @@
public class Player {
public class Player : Equatable {
public let name: String
public let piece_type: PieceType
init(name: String) {
public init(name: String, piece_type: PieceType) {
self.name = name
self.piece_type = piece_type
}
public func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action {
fatalError("abstract method not implemented")
}
public static func == (lhs: Player, rhs: Player) -> Bool {
// TODO: name equality or reference equality?
lhs === rhs
}
}

@ -3,23 +3,31 @@ public struct FourInARowRules: Rules {
public static let COLUMNS_DEFAULT: Int = 7
public static let ROWS_MIN: Int = 3
public static let ROWS_DEFAULT: Int = 6
private let players: [Player]
// internal for unit testing purposes
internal(set) public var state: GameState = .Playing(turn: .A)
// // internal for unit testing purposes
// internal(set) public var state: GameState
private(set) public var history: [Move] = []
// private(set) public var history: [Move] = []
private let columns: Int, rows: Int, minAligned: Int
public init() {
self.init(columns: Self.COLUMNS_DEFAULT, rows: Self.ROWS_DEFAULT)!
public init?(players: [Player]) {
self.init(columns: Self.COLUMNS_DEFAULT, rows: Self.ROWS_DEFAULT, players: players)
}
public init?(columns: Int, rows: Int, minAligned: Int = 4) {
guard columns >= Self.COLUMNS_MIN, rows >= Self.ROWS_MIN, minAligned > 1 else {
public init?(columns: Int, rows: Int, minAligned: Int = 4, players: [Player]) {
guard
columns >= Self.COLUMNS_MIN,
rows >= Self.ROWS_MIN,
minAligned > 1,
players.count >= 2
else {
return nil
}
self.players = players
self.columns = columns
self.rows = rows
self.minAligned = minAligned
@ -32,8 +40,6 @@ public struct FourInARowRules: Rules {
public func isValid(board: Board, move: Move) -> Bool {
guard self.isValid(board: board) else { return false }
guard case .Playing(let turn) = state, turn == move.player else { return false }
if case .InsertOnSide(.Top, let offset) = move.action, offset >= 0 && offset < self.columns {
return board.fallCoordinates(
initialCoords: Coords(offset, 0),
@ -63,14 +69,14 @@ public struct FourInARowRules: Rules {
}
public func validMoves(board: Board) -> [Move] {
return PieceType.allCases.flatMap({
return self.players.flatMap({
player in self.validMoves(board: board, for_player: player).map({
action in Move(player: player, action: action)
})
})
}
public func validMoves(board: Board, for_player player: PieceType) -> [Move.Action] {
public func validMoves(board: Board, for_player player: Player) -> [Move.Action] {
var moves: [Move.Action] = [];
for c in 0..<board.columns {
@ -85,9 +91,44 @@ public struct FourInARowRules: Rules {
return moves
}
public func gameState(board: Board, last_turn: Player?) -> GameState {
Self.rowOfLengthWin(board: board, last_turn: last_turn, minimum_aligned: self.minAligned, players: self.players)
}
internal static func rowOfLengthWin(board: Board, last_turn: Player?, minimum_aligned: Int, players: [Player]) -> GameState {
var occupied = 0
for column in 0..<board.columns {
for row in 0..<board.rows {
let current = Coords(column, row)
guard let piece = board[current] else { continue }
occupied += 1
// For each "axis" (described as one direction)
for dir: (Int, Int) in [(1, 0), (0, 1), (1, 1), (-1, 1)] {
let cells = Self.row(center: current, board: board, dir: dir)
if cells.count >= minimum_aligned {
let player = players.first(where: { $0.piece_type == piece.type })!
return GameState.Win(winner: player, cells: cells)
}
}
}
}
if occupied == board.columns * board.rows {
return .Draw
} else if let last_turn = last_turn {
return .Playing(turn: players[(players.firstIndex(of: last_turn)! + 1) % players.count])
} else {
return .Playing(turn: players.first!)
}
}
public mutating func onMoveDone(move: Move, board: Board) -> Void {
self.history.append(move)
/* public mutating func onMoveDone(move: Move, board: Board) -> Void {
// self.history.append(move)
switch move.action {
case .InsertOnSide(side: .Top, let offset):
@ -119,30 +160,35 @@ public struct FourInARowRules: Rules {
default:
fatalError("Illegal move \(move.action)")
}
}
} */
@available(*, deprecated, message: "Old Rules design remnents")
internal static func countMaxRow(center: Coords, board: Board) -> Int {
guard let of = board[center]?.type 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), (-1, 1)] {
var length = 1
maxLength = max(maxLength, row(center: center, board: board, dir: dir).count)
}
return maxLength
}
private static func row(center: Coords, board: Board, dir: (dc: Int, dr: Int)) -> [Coords] {
guard let of = board[center]?.type else { return [] }
var cells = [center]
// Run in the two opposite directions of the axis
for (dc, dr) in [(dir.dc, dir.dr), (-dir.dc, -dir.dr)] {
var pos = center
// 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]?.type != of { break }
length += 1
}
while true {
pos = Coords(pos.col + dc, pos.row + dr)
if !board.isInBounds(pos) || board[pos]?.type != of { break }
cells.append(pos)
}
maxLength = max(maxLength, length)
}
return maxLength
return cells
}
}

@ -1,8 +1,8 @@
public struct Move: Equatable {
public let player: PieceType
public let player: Player
public let action: Action
public init(player: PieceType, action: Action) {
public init(player: Player, action: Action) {
self.player = player
self.action = action
}

@ -1,9 +1,9 @@
public protocol Rules {
var state: GameState { get }
// var state: GameState { get }
var history: [Move] { get }
// var history: [Move] { get }
mutating func createBoard() -> Board
func createBoard() -> Board
func isValid(board: Board) -> Bool
@ -11,13 +11,17 @@ public protocol Rules {
func validMoves(board: Board) -> [Move]
func validMoves(board: Board, for_player player: PieceType) -> [Move.Action]
func validMoves(board: Board, for_player player: Player) -> [Move.Action]
mutating func onMoveDone(move: Move, board: Board) -> Void
func gameState(board: Board, last_turn: Player?) -> GameState
// mutating func onMoveDone(move: Move, board: Board) -> Void
}
public enum GameState: Equatable {
case Playing(turn: PieceType)
case Playing(turn: Player)
case Finished(winner: PieceType?)
case Win(winner: Player, cells: [Coords])
case Draw
}

@ -1,14 +1,12 @@
public struct TicTacToeRules: Rules {
public var state: GameState = .Playing(turn: .A)
public var history: [Move] = []
private let players: [Player]
private let columns: Int, rows: Int, minAligned: Int
public init(columns: Int, rows: Int, minAligned: Int = 3) {
public init(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) {
self.columns = columns
self.rows = rows
self.minAligned = minAligned
self.players = players
}
public func createBoard() -> Board {
@ -23,8 +21,6 @@ public struct TicTacToeRules: Rules {
public func isValid(board: Board, move: Move) -> Bool {
guard self.isValid(board: board) else { return false }
guard case .Playing(let turn) = state, turn == move.player else { return false }
if case .InsertAt(let coord) = move.action, board.isInBounds(coord) {
return true
}
@ -33,9 +29,9 @@ public struct TicTacToeRules: Rules {
}
public func validMoves(board: Board) -> [Move] {
let player = Self.nextPlayer(board: board)
return self.validMoves(board: board, for_player: player).map { Move(player: player, action: $0) }
return self.players.flatMap { player in
self.validMoves(board: board, for_player: player).map { Move(player: player, action: $0) }
}
}
private static func nextPlayer(board: Board) -> PieceType {
@ -46,7 +42,7 @@ public struct TicTacToeRules: Rules {
}
}
public func validMoves(board: Board, for_player player: PieceType) -> [Move.Action] {
public func validMoves(board: Board, for_player player: Player) -> [Move.Action] {
var moves: [Move.Action] = []
for col in 0..<board.columns {
@ -60,7 +56,11 @@ public struct TicTacToeRules: Rules {
return moves
}
public mutating func onMoveDone(move: Move, board: Board) {
public func gameState(board: Board, last_turn: Player?) -> GameState {
FourInARowRules.rowOfLengthWin(board: board, last_turn: last_turn, minimum_aligned: self.minAligned, players: self.players)
}
/* public mutating func onMoveDone(move: Move, board: Board) {
self.history.append(move)
guard case .InsertAt(let coords) = move.action else {
@ -78,5 +78,5 @@ public struct TicTacToeRules: Rules {
}
self.state = .Playing(turn: next)
}
}
} */
}

@ -0,0 +1,27 @@
import XCTest
@testable import Model
final class HumanPlayerTests: XCTestCase {
func testInit() {
let p = HumanPlayer(name: "the name", piece_type: .B, callback: { _, _ in fatalError("NOOP") })
XCTAssertEqual(p.name, "the name")
XCTAssertEqual(p.piece_type, .B)
}
func testChoose() {
let board = Board(columns: 3, rows: 3)!
let moves: [Move.Action] = [.InsertAt(where: Coords(1, 2)), .InsertOnSide(side: .Left, offset: 1)]
let player = HumanPlayer(name: "name", piece_type: .A, callback: {
moves2, board2 in
XCTAssertEqual(moves2, moves)
XCTAssertEqual(board2, board)
return .InsertOnSide(side: .Bottom, offset: 99)
})
XCTAssertEqual(player.chooseMove(allowed_moves: moves, board: board), .InsertOnSide(side: .Bottom, offset: 99))
}
}

@ -0,0 +1,27 @@
import XCTest
@testable import Model
final class RandomPlayerTests: XCTestCase {
func testInit() {
for t in [PieceType.A, .B] {
let p = RandomPlayer(name: "the name", piece_type: t)
XCTAssertEqual(p.name, "the name")
XCTAssertEqual(p.piece_type, t)
}
}
func testChoose() {
let board = Board(columns: 3, rows: 3)!
let moves: [Move.Action] = [.InsertAt(where: Coords(1, 2)), .InsertOnSide(side: .Left, offset: 1)]
let player = RandomPlayer(name: "name", piece_type: .A)
// +5 test quality credits
for _ in 1...10 {
let choosen = player.chooseMove(allowed_moves: moves, board: board)
XCTAssertNotNil(moves.firstIndex(of: choosen))
}
}
}

@ -4,11 +4,17 @@ import XCTest
final class FourInARowRulesTests: XCTestCase {
private var rules: FourInARowRules!
private var board: Board!
private var players: [Player]!
override func setUpWithError() throws {
try super.setUpWithError()
guard let rules = FourInARowRules(columns: 3, rows: 3, minAligned: 3) else {
self.players = [
Player(name: "A", piece_type: .A),
Player(name: "B", piece_type: .B)
]
guard let rules = FourInARowRules(columns: 3, rows: 3, minAligned: 3, players: players) else {
XCTFail()
return
}
@ -17,27 +23,25 @@ final class FourInARowRulesTests: XCTestCase {
self.board = self.rules.createBoard()
}
func testEmptyHistory() {
XCTAssertTrue(self.rules.history.isEmpty)
}
func testBoardIsValid() {
XCTAssertTrue(self.rules.isValid(board: self.board))
}
func testState() {
XCTAssertEqual(self.rules.state, GameState.Playing(turn: .A))
func testInitialState() {
XCTAssertEqual(
self.rules.gameState(board: board, last_turn: nil),
GameState.Playing(turn: self.players.first!)
)
}
func testMovesAreValid() {
for move in self.rules.validMoves(board: self.board) {
rules.state = .Playing(turn: move.player)
XCTAssertTrue(self.rules.isValid(board: board, move: move), "\(move)")
}
}
func testMovesOfPlayerAreValid() throws {
for player in PieceType.allCases {
for player in self.players {
try self.setUpWithError()
try self._testMovesOfPlayerAreValid(player: player)
@ -46,9 +50,8 @@ final class FourInARowRulesTests: XCTestCase {
}
}
private func _testMovesOfPlayerAreValid(player: PieceType) throws {
private func _testMovesOfPlayerAreValid(player: Player) throws {
for action in self.rules.validMoves(board: self.board, for_player: player) {
rules.state = .Playing(turn: player)
XCTAssertTrue(self.rules.isValid(board: board, move: Move(player: player, action: action)), "\(action)")
}
}
@ -56,11 +59,11 @@ final class FourInARowRulesTests: XCTestCase {
func testOnMoveDone() {
let coord = Coords(1, board.rows - 1)
board[coord] = Piece(type: .A)
let move = Move(player: .A, action: .InsertOnSide(side: .Top, offset: 1))
self.rules.onMoveDone(move: move, board: self.board)
XCTAssertEqual(self.rules.state, GameState.Playing(turn: .B))
XCTAssertEqual(self.rules.history.last, move)
XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!),
GameState.Playing(turn: players.last!)
)
}
func testOnMoveDoneDraw() {
@ -76,12 +79,7 @@ final class FourInARowRulesTests: XCTestCase {
board[2, 2] = Piece(type: .B)
XCTAssertTrue(rules.isValid(board: board))
let move = Move(player: .B, action: .InsertOnSide(side: .Top, offset: 2))
self.rules.onMoveDone(move: move, board: board)
XCTAssertEqual(rules.state, GameState.Finished(winner: nil))
XCTAssertEqual(rules.gameState(board: board, last_turn: players.last!), GameState.Draw)
}
func testOnMoveDoneWin() {
@ -96,12 +94,11 @@ final class FourInARowRulesTests: XCTestCase {
board[2, 2] = Piece(type: .B)
XCTAssertTrue(rules.isValid(board: board))
let move = Move(player: .A, action: .InsertOnSide(side: .Top, offset: 2))
self.rules.onMoveDone(move: move, board: board)
XCTAssertEqual(rules.state, GameState.Finished(winner: .A))
XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!),
GameState.Win(winner: players.first!, cells: [Coords(0, 0), Coords(1, 0), Coords(2, 0)])
)
}
func testCountMaxRow() throws {

Loading…
Cancel
Save