Players #3

Merged
mathieu.grousseau merged 7 commits from Players into main 3 weeks ago

@ -3,43 +3,92 @@ import Foundation
import Model
import CustomTypes
guard var board = Board(columns: 5, rows: 5) else {
print("Failed to create board.")
exit(EXIT_FAILURE)
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()
guard rules.isValid(board: board) else {
fatalError("Board is invalid")
}
var state = rules.gameState(board: board, last_turn: nil)
print(state)
print("All moves:")
for move in rules.validMoves(board: board) {
print(move)
}
print("Moves for \(PieceType.A):")
for move in rules.validMoves(board: board, for_player: ai) {
print(move)
}
print(board.debugDescription)
print(board)
for i in 0...2 {
for _ in 0...i {
let dropAt = board.getInsertionCoordinates(from: .Top, offset: i)
let landAt = switch board.fallCoordinates(initialCoords: dropAt, direction: .Bottom) {
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 of \(turn) is invalid!")
}
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, _):
at
board[at] = Piece(type: move.player.piece_type)
case .Occupied:
exit(EXIT_FAILURE)
fatalError("Not supported")
}
board[landAt] = Piece(owner: .A)
}
print(board)
state = rules.gameState(board: board, last_turn: turn)
}
print(board)
board[0, 2] = Piece(owner: .B)
board[2, 4] = nil
for c in 0..<board.rows {
switch board.fallCoordinates(initialCoords: board.getInsertionCoordinates(from: .Right, offset: c), direction: .Left) {
case .Border(let at):
board[at] = Piece(owner: .B)
case .Piece(let at, let touched):
(board[at], board[touched]) = (board[touched], Piece(owner: .B))
case .Occupied:
break
}
if case .Win(let winner, _) = state {
print("\(winner.name) is the winner")
} else {
print("Draw.")
}
print(board.debugDescription)
print(board)
exit(EXIT_SUCCESS)

@ -1,6 +1,6 @@
import Model
extension Player: CustomStringConvertible {
extension PieceType: CustomStringConvertible {
public var description: String {
switch self {
case .A:
@ -17,7 +17,7 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
for row in 0..<self.rows {
for col in 0..<self.columns {
str += self[col, row]?.owner.description ?? ""
str += self[col, row]?.type.description ?? ""
}
str += "\n"
@ -27,7 +27,7 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
}
public var debugDescription: String {
return "Board[\(self.columns)x\(self.rows), \(self.countPieces { p in p.owner == .A }) A and \(self.countPieces { p in p.owner == .B }) B pieces]"
return "Board[\(self.columns)x\(self.rows), \(self.countPieces { p in p.type == .A }) A and \(self.countPieces { p in p.type == .B }) B pieces]"
}
}

@ -9,10 +9,10 @@ final class CustomTypesTests: XCTestCase {
return
}
board[1, 1] = Piece(owner: .A)
board[2, 1] = Piece(owner: .A)
board[2, 2] = Piece(owner: .A)
board[1, 2] = Piece(owner: .B)
board[1, 1] = Piece(type: .A)
board[2, 1] = Piece(type: .A)
board[2, 2] = Piece(type: .A)
board[1, 2] = Piece(type: .B)
let text: String = board.description

@ -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 }
@ -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
}
@ -42,15 +42,21 @@ public struct Board {
}
}
public func forEach(_ consumer: (Piece?) -> Void) {
for column in grid {
for piece in column {
consumer(piece)
}
}
}
public func countPieces(filter: (Piece) -> Bool = { piece in true }) -> Int {
var count = 0
for column in grid {
for piece in column {
if let piece = piece {
if filter(piece) {
count += 1
}
self.forEach {
if let piece = $0 {
if filter(piece) {
count += 1
}
}
}
@ -117,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 {
public enum Direction: CaseIterable {
case Top, Left, Bottom, Right
var opposite: Self {
public var opposite: Self {
switch self {
case .Top:
.Bottom

@ -1,8 +1,8 @@
public struct Piece {
public let owner: Player
public struct Piece: Equatable, Sendable {
public let type: PieceType
// Required for public visibility
public init(owner: Player) {
self.owner = owner
public init(type: PieceType) {
self.type = type
}
}

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

@ -1,3 +0,0 @@
public enum Player {
case A, B
}

@ -0,0 +1,3 @@
public class AIPlayer : Player {
}

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

@ -0,0 +1,18 @@
public class Player : Equatable {
public let name: String
public let piece_type: PieceType
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
}
}

@ -0,0 +1,5 @@
public class RandomPlayer : Player {
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action {
moves[Int.random(in: moves.indices)]
}
}

@ -0,0 +1,194 @@
public struct FourInARowRules: Rules {
public static let COLUMNS_MIN: Int = 3
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
// private(set) public var history: [Move] = []
private let columns: Int, rows: Int, minAligned: Int
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, 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
}
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 }
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..<board.columns {
var had = false;
for r in 0..<board.rows {
switch board[c, r] != nil {
case false where had:
return false
case let has:
had = has
}
}
}
return true
}
public func validMoves(board: Board) -> [Move] {
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: Player) -> [Move.Action] {
var moves: [Move.Action] = [];
for c in 0..<board.columns {
switch board.fallCoordinates(initialCoords: Coords(c, 0), direction: .Bottom) {
case .Border, .Piece:
moves.append(.InsertOnSide(side: .Top, offset: c))
default:
break
}
}
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)
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 Self.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: PieceType = switch move.player {
case .A: .B
case .B: .A
}
self.state = .Playing(turn: next)
}
default:
fatalError("Illegal move \(move.action)")
}
} */
@available(*, deprecated, message: "Old Rules design remnents")
internal static func countMaxRow(center: Coords, board: Board) -> Int {
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)] {
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
while true {
pos = Coords(pos.col + dc, pos.row + dr)
if !board.isInBounds(pos) || board[pos]?.type != of { break }
cells.append(pos)
}
}
return cells
}
}

@ -0,0 +1,14 @@
public struct Move: Equatable {
public let player: Player
public let action: Action
public init(player: Player, action: Action) {
self.player = player
self.action = action
}
public enum Action: Equatable {
case InsertOnSide(side: Direction, offset: Int)
case InsertAt(where: Coords)
}
}

@ -0,0 +1,27 @@
public protocol Rules {
// var state: GameState { get }
// var history: [Move] { get }
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]
func gameState(board: Board, last_turn: Player?) -> GameState
// mutating func onMoveDone(move: Move, board: Board) -> Void
}
public enum GameState: Equatable {
case Playing(turn: Player)
case Win(winner: Player, cells: [Coords])
case Draw
}

@ -0,0 +1,82 @@
public struct TicTacToeRules: Rules {
private let players: [Player]
private let columns: Int, rows: Int, minAligned: Int
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 {
Board(columns: self.columns, rows: self.rows)!
}
public func isValid(board: Board) -> Bool {
abs(board.countPieces(filter: { p in p.type == .A }) -
board.countPieces(filter: { p in p.type == .B })) <= 1
}
public func isValid(board: Board, move: Move) -> Bool {
guard self.isValid(board: board) else { return false }
if case .InsertAt(let coord) = move.action, board.isInBounds(coord) {
return true
}
return false
}
public func validMoves(board: Board) -> [Move] {
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 {
if board.countPieces(filter: { p in p.type == .A }) > board.countPieces(filter: { p in p.type == .B }) {
.B
} else {
.A
}
}
public func validMoves(board: Board, for_player player: Player) -> [Move.Action] {
var moves: [Move.Action] = []
for col in 0..<board.columns {
for row in 0..<board.rows {
if board[col, row] == nil {
moves.append(.InsertAt(where: Coords(col, row)))
}
}
}
return moves
}
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 {
fatalError("Illegal move \(move.action)")
}
if FourInARowRules.countMaxRow(center: coords, 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: PieceType = switch move.player {
case .A: .B
case .B: .A
}
self.state = .Playing(turn: next)
}
} */
}

@ -5,7 +5,7 @@ final class EmptyBoardTests: XCTestCase {
private var board: Board!
override func setUpWithError() throws {
super.setUp()
try super.setUpWithError()
guard let board = Board(columns: 5, rows: 6) else {
XCTFail()
@ -32,7 +32,7 @@ final class EmptyBoardTests: XCTestCase {
func testCustomSubscript() throws {
for r in 0..<board.rows {
for c in 0..<board.columns {
for p: Player in [.A, .B] {
for p: PieceType in [.A, .B] {
try self.setUpWithError()
try self._testCustomSubscript(at: Coords(c, r), player: p)
@ -43,19 +43,19 @@ final class EmptyBoardTests: XCTestCase {
}
}
private func _testCustomSubscript(at: Coords, player: Player) throws {
board[at] = Piece(owner: player)
private func _testCustomSubscript(at: Coords, player: PieceType) throws {
board[at] = Piece(type: player)
XCTAssertEqual(board[at]?.owner, player)
XCTAssertEqual(board[at]?.type, player)
}
func testCounts() throws {
board[1, 2] = Piece(owner: .B)
board[0, 2] = Piece(owner: .A)
board[1, 1] = Piece(owner: .A)
board[1, 2] = Piece(type: .B)
board[0, 2] = Piece(type: .A)
board[1, 1] = Piece(type: .A)
XCTAssertEqual(board.countPieces { piece in piece.owner == .A }, 2)
XCTAssertEqual(board.countPieces { piece in piece.owner == .B }, 1)
XCTAssertEqual(board.countPieces { piece in piece.type == .A }, 2)
XCTAssertEqual(board.countPieces { piece in piece.type == .B }, 1)
}
func testInsertsSides() throws {
@ -129,9 +129,9 @@ final class EmptyBoardTests: XCTestCase {
] {
// ensure it works with any player
board[Coords(pair: pos)] = if (pos.0 + pos.1) & 1 == 1 {
Piece(owner: .A)
Piece(type: .A)
} else {
Piece(owner: .B)
Piece(type: .B)
}
}

@ -15,9 +15,9 @@ final class FilledBoardTests: XCTestCase {
for row in 0..<board.rows {
for col in 0..<board.columns {
board[col, row] = if (row & 1) == 1 {
Piece(owner: .A)
Piece(type: .A)
} else {
Piece(owner: .B)
Piece(type: .B)
}
}
}

@ -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))
}
}
}

@ -0,0 +1,137 @@
import XCTest
@testable import Model
final class FourInARowRulesTests: XCTestCase {
private var rules: FourInARowRules!
private var board: Board!
private var players: [Player]!
override func setUpWithError() throws {
try super.setUpWithError()
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
}
self.rules = rules
self.board = self.rules.createBoard()
}
func testBoardIsValid() {
XCTAssertTrue(self.rules.isValid(board: self.board))
}
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) {
XCTAssertTrue(self.rules.isValid(board: board, move: move), "\(move)")
}
}
func testMovesOfPlayerAreValid() throws {
for player in self.players {
try self.setUpWithError()
try self._testMovesOfPlayerAreValid(player: player)
try self.tearDownWithError()
}
}
private func _testMovesOfPlayerAreValid(player: Player) throws {
for action in self.rules.validMoves(board: self.board, for_player: player) {
XCTAssertTrue(self.rules.isValid(board: board, move: Move(player: player, action: action)), "\(action)")
}
}
func testOnMoveDone() {
let coord = Coords(1, board.rows - 1)
board[coord] = Piece(type: .A)
XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!),
GameState.Playing(turn: players.last!)
)
}
func testOnMoveDoneDraw() {
board[0, 0] = Piece(type: .A)
board[1, 0] = Piece(type: .A)
board[2, 0] = Piece(type: .B)
board[0, 1] = Piece(type: .B)
board[1, 1] = Piece(type: .B)
board[2, 1] = Piece(type: .A)
board[0, 2] = Piece(type: .A)
board[1, 2] = Piece(type: .A)
board[2, 2] = Piece(type: .B)
XCTAssertTrue(rules.isValid(board: board))
XCTAssertEqual(rules.gameState(board: board, last_turn: players.last!), GameState.Draw)
}
func testOnMoveDoneWin() {
board[0, 0] = Piece(type: .A)
board[1, 0] = Piece(type: .A)
board[2, 0] = Piece(type: .A) //
board[0, 1] = Piece(type: .B)
board[1, 1] = Piece(type: .B)
board[2, 1] = Piece(type: .A)
board[0, 2] = Piece(type: .A)
board[1, 2] = Piece(type: .A)
board[2, 2] = Piece(type: .B)
XCTAssertTrue(rules.isValid(board: board))
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 {
for (coords, expected) in [
(Coords(1, 0), 3),
(Coords(0, 1), 2),
(Coords(0, 2), 2),
(Coords(1, 1), 3),
(Coords(2, 2), 3),
(Coords(2, 1), 2),
] {
try self.setUpWithError()
// AAA
// BAB
// BBA
board[0, 0] = Piece(type: .A)
board[1, 0] = Piece(type: .A)
board[2, 0] = Piece(type: .A)
board[0, 1] = Piece(type: .B)
board[1, 1] = Piece(type: .A)
board[2, 1] = Piece(type: .B)
board[0, 2] = Piece(type: .B)
board[1, 2] = Piece(type: .B)
board[2, 2] = Piece(type: .A)
self._testCountMaxRow(coords: coords, expected: expected)
try self.tearDownWithError()
}
}
private func _testCountMaxRow(coords: Coords, expected: Int) {
XCTAssertEqual(FourInARowRules.countMaxRow(center: coords, board: board), expected, "\(coords)")
}
}
Loading…
Cancel
Save