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

@ -3,92 +3,93 @@ import Foundation
import Model
import CustomTypes
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")
enum EnumRules: String, CaseIterable {
case FourInARow = "Four in a row"
case TicTacToe = "Tic Tac Toe"
}
func enumPrompt<E: RawRepresentable<String> & CaseIterable>(prompt: String) -> E where E.AllCases.Index == Int {
print("\(prompt):")
E.allCases.enumerated().forEach { (index, variant) in
print("\t\(index + 1). \(variant.rawValue)")
}
guard let id = Int(input), id >= 1, id <= allowed_moves.count else {
fatalError("Invalid move id: \(input)")
guard let rawInput = readLine(strippingNewline: true),
let index = Int(rawInput), index >= 1, index <= E.allCases.count
else {
fatalError("Invalid variant number")
}
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")
return E.allCases[index - 1]
}
var board = rules.createBoard()
let gameType: EnumRules = enumPrompt(prompt: "Choose a game type")
guard rules.isValid(board: board) else {
fatalError("Board is invalid")
enum PlayerType: String, CaseIterable {
case AIRandom = "(AI) Random"
case Human = "Player"
}
var state = rules.gameState(board: board, last_turn: nil)
print(state)
func createPlayer(type: PlayerType, playing pieces: PieceType, named name: String) -> Player {
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:")
for move in rules.validMoves(board: board) {
print(move)
let players: [Player] = PieceType.allCases.map { type in
let ptype: PlayerType = enumPrompt(prompt: "Player playing pieces \(type)")
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):")
for move in rules.validMoves(board: board, for_player: ai) {
print(move)
let rules: Rules = switch gameType {
case .FourInARow: FourInARowRules(players: players)!
case .TicTacToe: TicTacToeRules(players: players)!
}
print(board)
var game = Game(players: players, rules: rules)
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, _):
board[at] = Piece(type: move.player.piece_type)
case .Occupied:
fatalError("Not supported")
}
}
game.boardChanged.add { board in
print(board)
state = rules.gameState(board: board, last_turn: turn)
}
if case .Win(let winner, _) = state {
print("\(winner.name) is the winner")
} else {
print("Draw.")
game.moveIsInvalid.add { move in
print("\(move.player.name), this move is invalid according to the rules.")
}
game.gameStateChanged.add { state in
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
extension PieceType: CustomStringConvertible {
public var description: String {
switch self {
case .A:
"🔴"
case .B:
"🟡"
}
}
}
extension Board: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
extension Board {
public func display(winCells: [Coords]) -> String {
var str = String()
for row in 0..<self.rows {
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"
@ -25,6 +23,12 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
return str
}
}
extension Board: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
self.display(winCells: [])
}
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]"

@ -9,10 +9,13 @@ final class CustomTypesTests: XCTestCase {
return
}
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 playerA = Player(name: "Dummy A", piece_type: .A)
let playerB = Player(name: "Dummy B", 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

@ -1,7 +1,7 @@
// public typealias Coords = (col: Int, row: Int)
public struct Coords: Equatable {
var col: Int
var row: Int
public var col: Int
public var row: Int
public init(_ col: Int, _ row: Int) {
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 let type: PieceType
public struct Piece: Equatable {
public let owner: Player
public var type: PieceType { owner.piece_type }
// Required for public visibility
public init(type: PieceType) {
self.type = type
public init(owner: Player) {
self.owner = owner
}
}

@ -11,6 +11,11 @@ public class Player : Equatable {
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 {
// TODO: name equality or reference equality?
lhs === rhs

@ -112,7 +112,7 @@ public struct FourInARowRules: Rules {
if cells.count >= minimum_aligned {
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 {
case Playing(turn: Player)
case Win(winner: Player, cells: [Coords])
case Win(winner: Player, board: Board, cells: [Coords])
case Draw
}

@ -2,7 +2,13 @@ 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]) {
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.rows = rows
self.minAligned = minAligned

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

@ -2,6 +2,9 @@ import XCTest
@testable import Model
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!
override func setUp() {
@ -15,9 +18,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(type: .A)
Piece(owner: Self.playerA)
} else {
Piece(type: .B)
Piece(owner: Self.playerB)
}
}
}

@ -2,6 +2,9 @@ import XCTest
@testable import Model
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 board: Board!
private var players: [Player]!
@ -58,7 +61,7 @@ final class FourInARowRulesTests: XCTestCase {
func testOnMoveDone() {
let coord = Coords(1, board.rows - 1)
board[coord] = Piece(type: .A)
board[coord] = Piece(owner: Self.playerA)
XCTAssertEqual(
rules.gameState(board: board, last_turn: players.first!),
@ -67,37 +70,37 @@ final class FourInARowRulesTests: XCTestCase {
}
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[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(owner: Self.playerB)
board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(owner: Self.playerB)
board[2, 1] = Piece(owner: Self.playerA)
board[0, 2] = Piece(owner: Self.playerA)
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))
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)
board[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(owner: Self.playerA) //
board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(owner: Self.playerB)
board[2, 1] = Piece(owner: Self.playerA)
board[0, 2] = Piece(owner: Self.playerA)
board[1, 2] = Piece(owner: Self.playerA)
board[2, 2] = Piece(owner: Self.playerB)
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)])
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
// 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)
board[0, 0] = Piece(owner: Self.playerA)
board[1, 0] = Piece(owner: Self.playerA)
board[2, 0] = Piece(owner: Self.playerA)
board[0, 1] = Piece(owner: Self.playerB)
board[1, 1] = Piece(owner: Self.playerA)
board[2, 1] = Piece(owner: Self.playerB)
board[0, 2] = Piece(owner: Self.playerB)
board[1, 2] = Piece(owner: Self.playerB)
board[2, 2] = Piece(owner: Self.playerA)
self._testCountMaxRow(coords: coords, expected: expected)

Loading…
Cancel
Save