@ -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)
|
||||
}
|
||||
} */
|
||||
}
|
@ -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…
Reference in new issue