Game part-4
Mathieu GROUSSEAU 2 weeks ago
parent f66d5fe3f3
commit 38aa08ba5a

@ -3,92 +3,93 @@ import Foundation
import Model import Model
import CustomTypes import CustomTypes
let ai = RandomPlayer(name: "Le robot a mythos", piece_type: .A) enum EnumRules: String, CaseIterable {
let cli = HumanPlayer(name: "Humain", piece_type: .B, callback: { case FourInARow = "Four in a row"
allowed_moves, board in case TicTacToe = "Tic Tac Toe"
print("Available moves:") }
for i in allowed_moves.indices {
print("\(i + 1)\t\(allowed_moves[i])") func enumPrompt<E: RawRepresentable<String> & CaseIterable>(prompt: String) -> E where E.AllCases.Index == Int {
} print("\(prompt):")
print(">") E.allCases.enumerated().forEach { (index, variant) in
print("\t\(index + 1). \(variant.rawValue)")
guard let input = readLine(strippingNewline: true) else {
fatalError("Invalid input")
} }
guard let id = Int(input), id >= 1, id <= allowed_moves.count else { guard let rawInput = readLine(strippingNewline: true),
fatalError("Invalid move id: \(input)") let index = Int(rawInput), index >= 1, index <= E.allCases.count
else {
fatalError("Invalid variant number")
} }
return allowed_moves[id - 1] return E.allCases[index - 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() let gameType: EnumRules = enumPrompt(prompt: "Choose a game type")
guard rules.isValid(board: board) else { enum PlayerType: String, CaseIterable {
fatalError("Board is invalid") case AIRandom = "(AI) Random"
case Human = "Player"
} }
var state = rules.gameState(board: board, last_turn: nil) func createPlayer(type: PlayerType, playing pieces: PieceType, named name: String) -> Player {
print(state) switch type {
case .Human:
HumanPlayer(name: name, piece_type: pieces, callback: {
allowed_moves, board in
for (i, action) in allowed_moves.enumerated() {
let text = switch action {
case .InsertAt(let at): "Place piece at \(at.col):\(at.row)"
case .InsertOnSide(let side, let offset): "Fall piece from \(side), offset \(offset)"
}
print("\(i + 1). \(text)")
}
guard let rawInput = readLine(strippingNewline: true),
let index = Int(rawInput), index >= 1, index <= allowed_moves.count
else {
fatalError("Invalid move number")
}
return allowed_moves[index - 1]
})
case .AIRandom:
RandomPlayer(name: name, piece_type: pieces)
}
}
print("All moves:") let players: [Player] = PieceType.allCases.map { type in
for move in rules.validMoves(board: board) { let ptype: PlayerType = enumPrompt(prompt: "Player playing pieces \(type)")
print(move)
print("Enter player name:")
guard let playerName = readLine(strippingNewline: true) else {
fatalError("Player name unspecified")
}
return createPlayer(type: ptype, playing: type, named: playerName)
} }
print("Moves for \(PieceType.A):") let rules: Rules = switch gameType {
for move in rules.validMoves(board: board, for_player: ai) { case .FourInARow: FourInARowRules(players: players)!
print(move) case .TicTacToe: TicTacToeRules(players: players)!
} }
print(board) var game = Game(players: players, rules: rules)
while case .Playing(let turn) = state { game.boardChanged.add { board in
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, _):
board[at] = Piece(type: move.player.piece_type)
case .Occupied:
fatalError("Not supported")
}
}
print(board) print(board)
state = rules.gameState(board: board, last_turn: turn)
} }
game.moveIsInvalid.add { move in
if case .Win(let winner, _) = state { print("\(move.player.name), this move is invalid according to the rules.")
print("\(winner.name) is the winner") }
} else { game.gameStateChanged.add { state in
print("Draw.") switch state {
case .Playing(let player):
print("\(player.name) turn.")
case .Win(let winner, let board, let cells):
print("\(winner.name) is the winner")
print(board.display(winCells: cells))
case .Draw:
print("Its a daw.")
}
} }
exit(EXIT_SUCCESS) game.play()

@ -1,23 +1,21 @@
import Model import Model
extension PieceType: CustomStringConvertible { extension Board {
public var description: String { public func display(winCells: [Coords]) -> String {
switch self {
case .A:
"🔴"
case .B:
"🟡"
}
}
}
extension Board: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
var str = String() var str = String()
for row in 0..<self.rows { for row in 0..<self.rows {
for col in 0..<self.columns { for col in 0..<self.columns {
str += self[col, row]?.type.description ?? "" let coord = Coords(col, row)
let char = switch (self[coord]?.type, winCells.contains(coord)) {
case (.A, false): "🔴"
case (.A, true): "🟥"
case (.B, false): "🟡"
case (.B, true): "🟨"
default: ""
}
str += char
} }
str += "\n" str += "\n"
@ -25,6 +23,12 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
return str return str
} }
}
extension Board: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
self.display(winCells: [])
}
public var debugDescription: String { public var debugDescription: String {
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]" 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,13 @@ final class CustomTypesTests: XCTestCase {
return return
} }
board[1, 1] = Piece(type: .A) let playerA = Player(name: "Dummy A", piece_type: .A)
board[2, 1] = Piece(type: .A) let playerB = Player(name: "Dummy B", piece_type: .B)
board[2, 2] = Piece(type: .A)
board[1, 2] = Piece(type: .B) board[1, 1] = Piece(owner: playerA)
board[2, 1] = Piece(owner: playerA)
board[2, 2] = Piece(owner: playerA)
board[1, 2] = Piece(owner: playerB)
let text: String = board.description let text: String = board.description

@ -1,7 +1,7 @@
// public typealias Coords = (col: Int, row: Int) // public typealias Coords = (col: Int, row: Int)
public struct Coords: Equatable { public struct Coords: Equatable {
var col: Int public var col: Int
var row: Int public var row: Int
public init(_ col: Int, _ row: Int) { public init(_ col: Int, _ row: Int) {
self.col = col self.col = col

@ -0,0 +1,96 @@
public typealias BoardChangedListener = (Board) -> Void
public typealias GameStateChangedListener = (GameState) -> Void
public class Game {
public var boardChanged = Event<Board>()
public var gameStateChanged = Event<GameState>()
public var moveIsInvalid = Event<Move>()
public let players: [Player]
public let rules: Rules
public init(players: [Player], rules: Rules) {
self.players = players
self.rules = rules
}
public func play() {
var board = rules.createBoard()
var state = rules.gameState(board: board, last_turn: nil)
boardChanged(board)
gameStateChanged(state)
while case .Playing(let player_turn) = state {
let valid_moves = rules.validMoves(board: board, for_player: player_turn)
if valid_moves.isEmpty {
player_turn.skipMove(board: board)
} else {
while true {
let choosen_action = player_turn.chooseMove(allowed_moves: valid_moves, board: board)
let move = Move(player: player_turn, action: choosen_action)
guard rules.isValid(board: board, move: move) else {
moveIsInvalid(move)
continue
}
self.performMove(move: move, on_board: &board)
precondition(rules.isValid(board: board), "move that was valid made the board invalid")
boardChanged(board)
break
}
}
state = rules.gameState(board: board, last_turn: player_turn)
gameStateChanged(state)
}
// Finished
}
private func performMove(move: Move, on_board board: inout Board) {
let finalCoords = switch move.action {
case .InsertAt(let at): at
case .InsertOnSide(let side, let offset):
{
// TODO: with this, I am unsure how other moves on sides should handle pushing, popping, ...
let insertCoords = board.getInsertionCoordinates(from: side, offset: offset)
return switch board.fallCoordinates(initialCoords: insertCoords, direction: side.opposite) {
case .Border(let at), .Piece(let at, _):
at
case .Occupied:
// NOTE: assure the move is indeed legal and replace the current piece
insertCoords
}
}()
}
board[finalCoords] = Piece(owner: move.player)
}
}
@dynamicCallable
public struct Event<Param> {
private var listeners: [(Param) -> Void] = []
public init() {}
public mutating func add(_ listener: @escaping (Param) -> Void) {
self.listeners.append(listener)
}
// func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count }
func dynamicallyCall(withArguments args: [Param]) -> Void {
for param in args {
for listener in self.listeners {
listener(param)
}
}
}
}

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

@ -11,6 +11,11 @@ public class Player : Equatable {
fatalError("abstract method not implemented") fatalError("abstract method not implemented")
} }
/// Called when there is no possible move for this player's turn
public func skipMove(board: Board) -> Void {
// NO-OP
}
public static func == (lhs: Player, rhs: Player) -> Bool { public static func == (lhs: Player, rhs: Player) -> Bool {
// TODO: name equality or reference equality? // TODO: name equality or reference equality?
lhs === rhs lhs === rhs

@ -112,7 +112,7 @@ public struct FourInARowRules: Rules {
if cells.count >= minimum_aligned { if cells.count >= minimum_aligned {
let player = players.first(where: { $0.piece_type == piece.type })! let player = players.first(where: { $0.piece_type == piece.type })!
return GameState.Win(winner: player, cells: cells) return GameState.Win(winner: player, board: board, cells: cells)
} }
} }
} }

@ -21,7 +21,7 @@ public protocol Rules {
public enum GameState: Equatable { public enum GameState: Equatable {
case Playing(turn: Player) case Playing(turn: Player)
case Win(winner: Player, cells: [Coords]) case Win(winner: Player, board: Board, cells: [Coords])
case Draw case Draw
} }

@ -2,7 +2,13 @@ public struct TicTacToeRules: Rules {
private let players: [Player] private let players: [Player]
private let columns: Int, rows: Int, minAligned: Int private let columns: Int, rows: Int, minAligned: Int
public init(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) { public init?(players: [Player]) {
self.init(columns: 3, rows: 3, players: players)
}
public init?(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) {
guard players.count >= 2 else { return nil }
self.columns = columns self.columns = columns
self.rows = rows self.rows = rows
self.minAligned = minAligned self.minAligned = minAligned

@ -2,6 +2,9 @@ import XCTest
@testable import Model @testable import Model
final class EmptyBoardTests: XCTestCase { final class EmptyBoardTests: XCTestCase {
private static let playerA = Player(name: "dummy A", piece_type: .A)
private static let playerB = Player(name: "dummy B", piece_type: .B)
private var board: Board! private var board: Board!
override func setUpWithError() throws { override func setUpWithError() throws {
@ -35,7 +38,7 @@ final class EmptyBoardTests: XCTestCase {
for p: PieceType in [.A, .B] { for p: PieceType in [.A, .B] {
try self.setUpWithError() try self.setUpWithError()
try self._testCustomSubscript(at: Coords(c, r), player: p) try self._testCustomSubscript(at: Coords(c, r), type: p)
try self.tearDownWithError() try self.tearDownWithError()
} }
@ -43,16 +46,17 @@ final class EmptyBoardTests: XCTestCase {
} }
} }
private func _testCustomSubscript(at: Coords, player: PieceType) throws { private func _testCustomSubscript(at: Coords, type: PieceType) throws {
board[at] = Piece(type: player) let player = Player(name: "dummy", piece_type: type)
board[at] = Piece(owner: player)
XCTAssertEqual(board[at]?.type, player) XCTAssertEqual(board[at]?.owner, player)
} }
func testCounts() throws { func testCounts() throws {
board[1, 2] = Piece(type: .B) board[1, 2] = Piece(owner: Self.playerB)
board[0, 2] = Piece(type: .A) board[0, 2] = Piece(owner: Self.playerA)
board[1, 1] = Piece(type: .A) board[1, 1] = Piece(owner: Self.playerA)
XCTAssertEqual(board.countPieces { piece in piece.type == .A }, 2) XCTAssertEqual(board.countPieces { piece in piece.type == .A }, 2)
XCTAssertEqual(board.countPieces { piece in piece.type == .B }, 1) XCTAssertEqual(board.countPieces { piece in piece.type == .B }, 1)
@ -129,9 +133,9 @@ final class EmptyBoardTests: XCTestCase {
] { ] {
// ensure it works with any player // ensure it works with any player
board[Coords(pair: pos)] = if (pos.0 + pos.1) & 1 == 1 { board[Coords(pair: pos)] = if (pos.0 + pos.1) & 1 == 1 {
Piece(type: .A) Piece(owner: Self.playerA)
} else { } else {
Piece(type: .B) Piece(owner: Self.playerB)
} }
} }

@ -2,6 +2,9 @@ import XCTest
@testable import Model @testable import Model
final class FilledBoardTests: XCTestCase { final class FilledBoardTests: XCTestCase {
private static let playerA = Player(name: "dummy A", piece_type: .A)
private static let playerB = Player(name: "dummy B", piece_type: .B)
private var board: Board! private var board: Board!
override func setUp() { override func setUp() {
@ -15,9 +18,9 @@ final class FilledBoardTests: XCTestCase {
for row in 0..<board.rows { for row in 0..<board.rows {
for col in 0..<board.columns { for col in 0..<board.columns {
board[col, row] = if (row & 1) == 1 { board[col, row] = if (row & 1) == 1 {
Piece(type: .A) Piece(owner: Self.playerA)
} else { } else {
Piece(type: .B) Piece(owner: Self.playerB)
} }
} }
} }

@ -2,6 +2,9 @@ import XCTest
@testable import Model @testable import Model
final class FourInARowRulesTests: XCTestCase { final class FourInARowRulesTests: XCTestCase {
private static let playerA = Player(name: "dummy A", piece_type: .A)
private static let playerB = Player(name: "dummy B", piece_type: .B)
private var rules: FourInARowRules! private var rules: FourInARowRules!
private var board: Board! private var board: Board!
private var players: [Player]! private var players: [Player]!
@ -58,7 +61,7 @@ final class FourInARowRulesTests: XCTestCase {
func testOnMoveDone() { func testOnMoveDone() {
let coord = Coords(1, board.rows - 1) let coord = Coords(1, board.rows - 1)
board[coord] = Piece(type: .A) board[coord] = Piece(owner: Self.playerA)
XCTAssertEqual( XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!), rules.gameState(board: board, last_turn: players.first!),
@ -67,37 +70,37 @@ final class FourInARowRulesTests: XCTestCase {
} }
func testOnMoveDoneDraw() { func testOnMoveDoneDraw() {
board[0, 0] = Piece(type: .A) board[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(type: .A) board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(type: .B) board[2, 0] = Piece(owner: Self.playerB)
board[0, 1] = Piece(type: .B) board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(type: .B) board[1, 1] = Piece(owner: Self.playerB)
board[2, 1] = Piece(type: .A) board[2, 1] = Piece(owner: Self.playerA)
board[0, 2] = Piece(type: .A) board[0, 2] = Piece(owner: Self.playerA)
board[1, 2] = Piece(type: .A) board[1, 2] = Piece(owner: Self.playerA)
board[2, 2] = Piece(type: .B) board[2, 2] = Piece(owner: Self.playerB)
XCTAssertTrue(rules.isValid(board: board)) XCTAssertTrue(rules.isValid(board: board))
XCTAssertEqual(rules.gameState(board: board, last_turn: players.last!), GameState.Draw) XCTAssertEqual(rules.gameState(board: board, last_turn: players.last!), GameState.Draw)
} }
func testOnMoveDoneWin() { func testOnMoveDoneWin() {
board[0, 0] = Piece(type: .A) board[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(type: .A) board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(type: .A) // board[2, 0] = Piece(owner: Self.playerA) //
board[0, 1] = Piece(type: .B) board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(type: .B) board[1, 1] = Piece(owner: Self.playerB)
board[2, 1] = Piece(type: .A) board[2, 1] = Piece(owner: Self.playerA)
board[0, 2] = Piece(type: .A) board[0, 2] = Piece(owner: Self.playerA)
board[1, 2] = Piece(type: .A) board[1, 2] = Piece(owner: Self.playerA)
board[2, 2] = Piece(type: .B) board[2, 2] = Piece(owner: Self.playerB)
XCTAssertTrue(rules.isValid(board: board)) XCTAssertTrue(rules.isValid(board: board))
XCTAssertEqual( XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!), rules.gameState(board: board, last_turn: players.first!),
GameState.Win(winner: players.first!, cells: [Coords(0, 0), Coords(1, 0), Coords(2, 0)]) GameState.Win(winner: players.first!, board: board, cells: [Coords(0, 0), Coords(1, 0), Coords(2, 0)])
) )
} }
@ -115,15 +118,15 @@ final class FourInARowRulesTests: XCTestCase {
// AAA // AAA
// BAB // BAB
// BBA // BBA
board[0, 0] = Piece(type: .A) board[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(type: .A) board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(type: .A) board[2, 0] = Piece(owner: Self.playerA)
board[0, 1] = Piece(type: .B) board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(type: .A) board[1, 1] = Piece(owner: Self.playerA)
board[2, 1] = Piece(type: .B) board[2, 1] = Piece(owner: Self.playerB)
board[0, 2] = Piece(type: .B) board[0, 2] = Piece(owner: Self.playerB)
board[1, 2] = Piece(type: .B) board[1, 2] = Piece(owner: Self.playerB)
board[2, 2] = Piece(type: .A) board[2, 2] = Piece(owner: Self.playerA)
self._testCountMaxRow(coords: coords, expected: expected) self._testCountMaxRow(coords: coords, expected: expected)

Loading…
Cancel
Save