Compare commits

...

7 Commits

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
282549B82D3278F9008D2C3B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282549B72D3278F9008D2C3B /* main.swift */; };
282549C02D327907008D2C3B /* Model in Frameworks */ = {isa = PBXBuildFile; productRef = 282549BF2D327907008D2C3B /* Model */; };
EC6173932D57473D00B099CD /* Persistance in Frameworks */ = {isa = PBXBuildFile; productRef = EC6173922D57473D00B099CD /* Persistance */; };
EC9914352D40233000C0459C /* CustomTypes in Frameworks */ = {isa = PBXBuildFile; productRef = EC9914342D40233000C0459C /* CustomTypes */; };
/* End PBXBuildFile section */
@ -34,6 +35,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EC6173932D57473D00B099CD /* Persistance in Frameworks */,
282549C02D327907008D2C3B /* Model in Frameworks */,
EC9914352D40233000C0459C /* CustomTypes in Frameworks */,
);
@ -93,6 +95,7 @@
packageProductDependencies = (
282549BF2D327907008D2C3B /* Model */,
EC9914342D40233000C0459C /* CustomTypes */,
EC6173922D57473D00B099CD /* Persistance */,
);
productName = CLI;
productReference = 282549B42D3278F9008D2C3B /* CLI */;
@ -124,6 +127,7 @@
mainGroup = 282549AB2D3278F9008D2C3B;
packageReferences = (
EC9914332D40233000C0459C /* XCLocalSwiftPackageReference "../CustomTypes" */,
EC6173912D57473D00B099CD /* XCLocalSwiftPackageReference "../Persistance" */,
);
productRefGroup = 282549B52D3278F9008D2C3B /* Products */;
projectDirPath = "";
@ -307,6 +311,10 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
EC6173912D57473D00B099CD /* XCLocalSwiftPackageReference "../Persistance" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../Persistance;
};
EC9914332D40233000C0459C /* XCLocalSwiftPackageReference "../CustomTypes" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../CustomTypes;
@ -318,6 +326,10 @@
isa = XCSwiftPackageProductDependency;
productName = Model;
};
EC6173922D57473D00B099CD /* Persistance */ = {
isa = XCSwiftPackageProductDependency;
productName = Persistance;
};
EC9914342D40233000C0459C /* CustomTypes */ = {
isa = XCSwiftPackageProductDependency;
productName = CustomTypes;

@ -2,93 +2,133 @@ import Foundation
import Model
import CustomTypes
import Persistance
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")
var game: Game? = nil
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) else { exit(EXIT_SUCCESS) }
guard 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()
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)
print("All moves:")
for move in rules.validMoves(board: board) {
print(move)
@MainActor
func playerPrompt(allowed_moves: [Move.Action], board: Board) async -> Move.Action {
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)")
}
if let rawInput = readLine(strippingNewline: true) {
guard let index = Int(rawInput), index >= 1, index <= allowed_moves.count
else {
fatalError("Invalid move number")
}
return allowed_moves[index - 1]
} else {
// dump game to disk
do {
try await Storage.save(game: game!, finished: false)
} catch {
fatalError("Failed to save game: \(error)")
}
exit(EXIT_SUCCESS)
}
}
print("Moves for \(PieceType.A):")
for move in rules.validMoves(board: board, for_player: ai) {
print(move)
func createPlayer(type: PlayerType, playing pieces: PieceType, named name: String) -> Player {
switch type {
case .Human:
HumanPlayer(name: name, piece_type: pieces, callback: playerPrompt)
case .AIRandom:
RandomPlayer(name: name, piece_type: pieces)
}
}
print(board)
while case .Playing(let turn) = state {
let action = turn.chooseMove(
allowed_moves: rules.validMoves(board: board, for_player: turn),
board: board
)
if let unfinished = try await Storage.loadUnfinishedGame(callbackFactory: { name, type in playerPrompt }) {
print("There was an unfinished game. Resume? [Y/n]")
let move = Move(player: turn, action: action)
guard rules.isValid(board: board, move: move) else {
fatalError("Move of \(turn) is invalid!")
guard let answer = readLine(strippingNewline: true) else {
exit(EXIT_SUCCESS)
}
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")
}
switch answer.lowercased() {
case "", "y", "yes":
game = unfinished
default:
break;
}
print(board)
try await Storage.clearUnfinished()
}
if game == nil {
let gameType: EnumRules = enumPrompt(prompt: "Choose a game type")
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)
}
let rules: any Rules = switch gameType {
case .FourInARow: FourInARowRules(players: players)!
case .TicTacToe: TicTacToeRules(players: players)!
}
state = rules.gameState(board: board, last_turn: turn)
game = Game(players: players, rules: rules)
}
if case .Win(let winner, _) = state {
print("\(winner.name) is the winner")
} else {
print("Draw.")
game!.boardChanged.add { board in
print(board)
}
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.")
case .Invalid:
fatalError("Game is invalid!")
}
}
await game!.play()
exit(EXIT_SUCCESS)
try await Storage.save(game: game!, finished: true)

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Persistance">
</FileRef>
<FileRef
location = "group:CustomTypes">
</FileRef>

@ -1,23 +1,33 @@
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()
let rows = Self.numbers(size: self.rows)
let columns = Self.numbers(size: self.columns)
for row in 0..<self.rows {
if row == 0 {
str += String(repeating: " ", count: rows.size)
str += columns.nums.joined()
str += "\n"
}
str += rows.nums[row]
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 += String(repeating: " ", count: columns.size - 2) + char
}
str += "\n"
@ -26,6 +36,18 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
return str
}
private static func numbers(size: Int) -> (nums: [String], size: Int) {
let elems = Array((1...size).map { String($0) })
let size = max(2, elems.lazy.map { $0.count }.max() ?? 0)
return (nums: elems.map { String(repeating: " ", count: size - $0.count) + $0 }, size: size )
}
}
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,13 +9,16 @@ 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
XCTAssertEqual(text, "⚪⚪⚪⚪\n⚪🔴🔴⚪\n⚪🟡🔴⚪\n⚪⚪⚪⚪\n")
XCTAssertEqual(text, " 1 2 3 4\n 1⚪⚪⚪⚪\n 2⚪🔴🔴⚪\n 3⚪🟡🔴⚪\n 4⚪⚪⚪⚪\n")
}
}

@ -42,10 +42,17 @@ public struct Board: Equatable {
}
}
public func forEach(_ consumer: (Piece?) -> Void) {
for column in grid {
for piece in column {
consumer(piece)
/// Iterate in the same order as coords
public var cells: any Sequence<Piece?> {
// self.grid.lazy.flatMap { (row: [Piece?]) -> LazySequence<[Piece?]> in row.lazy }
self.coords.map { self[$0] }
}
/// Iterate row by row, from 0 to width
public var coords: any Sequence<Coords> {
(0..<self.rows).lazy.flatMap { (row: Int) in
(0..<self.columns).lazy.map { (column: Int) in
Coords(column, row)
}
}
}
@ -53,7 +60,7 @@ public struct Board: Equatable {
public func countPieces(filter: (Piece) -> Bool = { piece in true }) -> Int {
var count = 0
self.forEach {
self.cells.forEach {
if let piece = $0 {
if filter(piece) {
count += 1

@ -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
@ -12,6 +12,10 @@ public struct Coords: Equatable {
self.init(pair.0, pair.1)
}
public func toIndex(width: Int) -> Int {
col + row * width
}
/* static func +(lhs: Self, rhs: Self) -> Self {
Self(lhs.col + rhs.col, lhs.row + rhs.row)
}

@ -0,0 +1,126 @@
public typealias BoardChangedListener = (Board) -> Void
public typealias GameStateChangedListener = (GameState) -> Void
public class Game: Equatable {
public var boardChanged = Event<Board>()
public var gameStateChanged = Event<GameState>()
public var moveIsInvalid = Event<Move>()
public let players: [Player]
public let rules: any Rules
/// Has to be public for serialization purposes
public var board: Board
/// Has to be public for serialization purposes
public var state: GameState
public init(players: [Player], rules: any Rules) {
self.players = players
self.rules = rules
// Initialize state
self.board = rules.createBoard()
self.state = rules.gameState(board: board, last_turn: nil)
}
public init?(players: [Player], rules: any Rules, board: Board, state: GameState?) {
guard rules.isValid(board: board) else { return nil }
self.state = state ?? rules.gameState(board: board, last_turn: nil)
self.players = players
self.rules = rules
self.board = board
}
@MainActor
public func play() async {
switch state {
case .Playing:
break
default:
return
}
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 = await 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)
}
public static func == (lhs: Game, rhs: Game) -> Bool {
lhs.players == rhs.players
&& RulesUtils.equals(lhs: lhs.rules, rhs: rhs.rules)
&& lhs.board == rhs.board
&& lhs.state == rhs.state
}
}
@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: [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
}
}

@ -1,7 +1,9 @@
public typealias MoveCallback = ([Move.Action], Board) -> Move.Action
public typealias MoveCallback = ([Move.Action], Board) async -> Move.Action
public class HumanPlayer : Player {
private let callback: MoveCallback
public override var type: PlayerType { .Human }
public init(name: String, piece_type: PieceType, callback: @escaping MoveCallback) {
self.callback = callback
@ -9,7 +11,7 @@ public class HumanPlayer : Player {
super.init(name: name, piece_type: piece_type)
}
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action {
callback(moves, board)
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) async -> Move.Action {
await callback(moves, board)
}
}

@ -1,18 +1,29 @@
public class Player : Equatable {
public let name: String
public let piece_type: PieceType
public var type: PlayerType { fatalError("abstract property not implemented") }
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 {
public func chooseMove(allowed_moves moves: [Move.Action], board: Board) async -> Move.Action {
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
lhs === rhs || (lhs.type == rhs.type && lhs.name == rhs.name && lhs.piece_type == rhs.piece_type)
}
}
public enum PlayerType: CaseIterable {
case Human
case AIRandom
}

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

@ -11,7 +11,9 @@ public struct FourInARowRules: Rules {
// private(set) public var history: [Move] = []
private let columns: Int, rows: Int, minAligned: Int
public let columns: Int, rows: Int, minAligned: Int
public var type: RulesTypes = .FourInARow
public init?(players: [Player]) {
self.init(columns: Self.COLUMNS_DEFAULT, rows: Self.ROWS_DEFAULT, players: players)
@ -99,21 +101,18 @@ public struct FourInARowRules: Rules {
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 current in board.coords {
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)
// 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 cells.count >= minimum_aligned {
let player = players.first(where: { $0.piece_type == piece.type })!
return GameState.Win(winner: player, board: board, cells: cells)
}
}
}
@ -126,41 +125,6 @@ public struct FourInARowRules: Rules {
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 {

@ -1,7 +1,5 @@
public protocol Rules {
// var state: GameState { get }
// var history: [Move] { get }
public protocol Rules: Equatable {
var type: RulesTypes { get }
func createBoard() -> Board
@ -14,14 +12,33 @@ public protocol Rules {
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 struct RulesUtils {
public static func equals(lhs: any Rules, rhs: any Rules) -> Bool {
let t1 = lhs.type
let t2 = rhs.type
if t1 != t2 { return false }
return switch t1 {
case .FourInARow:
(lhs as! FourInARowRules) == (rhs as! FourInARowRules)
case .TicTacToe:
(lhs as! TicTacToeRules) == (rhs as! TicTacToeRules)
}
}
}
public enum GameState: Equatable {
case Playing(turn: Player)
case Win(winner: Player, cells: [Coords])
case Win(winner: Player, board: Board, cells: [Coords])
case Draw
/// In rare cases, the state may be invalid because the game was incorrectly reconstructed (serialization, ...)
case Invalid
}
public enum RulesTypes: CaseIterable {
case FourInARow, TicTacToe
}

@ -1,8 +1,16 @@
public struct TicTacToeRules: Rules {
private let players: [Player]
private let columns: Int, rows: Int, minAligned: Int
public let columns: Int, rows: Int, minAligned: Int
public init(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) {
public var type: RulesTypes = .TicTacToe
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
@ -45,11 +53,9 @@ public struct TicTacToeRules: Rules {
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)))
}
board.coords.forEach { coords in
if board[coords] == nil {
moves.append(.InsertAt(where: coords))
}
}
@ -59,24 +65,4 @@ public struct TicTacToeRules: Rules {
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)
}
} */
}

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

@ -9,7 +9,7 @@ final class HumanPlayerTests: XCTestCase {
XCTAssertEqual(p.piece_type, .B)
}
func testChoose() {
func testChoose() async {
let board = Board(columns: 3, rows: 3)!
let moves: [Move.Action] = [.InsertAt(where: Coords(1, 2)), .InsertOnSide(side: .Left, offset: 1)]
@ -22,6 +22,7 @@ final class HumanPlayerTests: XCTestCase {
return .InsertOnSide(side: .Bottom, offset: 99)
})
XCTAssertEqual(player.chooseMove(allowed_moves: moves, board: board), .InsertOnSide(side: .Bottom, offset: 99))
let result = await player.chooseMove(allowed_moves: moves, board: board)
XCTAssertEqual(result, .InsertOnSide(side: .Bottom, offset: 99))
}
}

@ -11,7 +11,7 @@ final class RandomPlayerTests: XCTestCase {
}
}
func testChoose() {
func testChoose() async {
let board = Board(columns: 3, rows: 3)!
let moves: [Move.Action] = [.InsertAt(where: Coords(1, 2)), .InsertOnSide(side: .Left, offset: 1)]
@ -19,7 +19,7 @@ final class RandomPlayerTests: XCTestCase {
// +5 test quality credits
for _ in 1...10 {
let choosen = player.chooseMove(allowed_moves: moves, board: board)
let choosen = await player.chooseMove(allowed_moves: moves, board: board)
XCTAssertNotNil(moves.firstIndex(of: choosen))
}

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

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

@ -0,0 +1,24 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Persistance",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Persistance",
targets: ["Persistance"]),
],
dependencies: [
.package(path: "../Model")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Persistance",
dependencies: ["Model"]),
]
)

@ -0,0 +1,53 @@
import Model
extension Board: Codable {
enum CodingKeys: CodingKey {
case width, height, content
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let width = try container.decode(Int.self, forKey: .width)
let height = try container.decode(Int.self, forKey: .height)
let pieces = try container.decode([Piece?].self, forKey: .content)
if pieces.count != width * height {
throw DeError.InvalidData
}
guard var zelf = Self.init(columns: width, rows: height) else {
throw DeError.InitializationFailed
}
zelf.coords.forEach { coord in
zelf[coord] = pieces[coord.toIndex(width: width)]
}
self = zelf
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.columns, forKey: .width)
try container.encode(self.rows, forKey: .height)
// Array is flattened because Board inner cell data structure is private and only accessible via a sequence
try container.encode(Array(self.cells), forKey: .content)
}
}
extension Coords: Codable {
public init(from decoder: any Decoder) throws {
var container = try decoder.unkeyedContainer()
self.init(try container.decode(Int.self), try container.decode(Int.self))
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(self.col)
try container.encode(self.row)
}
}

@ -0,0 +1,25 @@
internal struct CodableEnum<E: CaseIterable & CodingKey>: Codable {
public let variant: E
init(of variant: E) {
self.variant = variant
}
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let name = try container.decode(String.self)
guard let variant = E.allCases.first(where: { variant in
variant.stringValue == name
}) else {
throw DeError.InvalidVariant
}
self.variant = variant
}
func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.variant.stringValue)
}
}

@ -0,0 +1,109 @@
import Model
/// This struct is a workaround as it seems codeable required initializer to be required, which is not possible from an extension
public struct CodableGameWrapper: Codable {
public let game: Game
public init(of game: Game) {
self.game = game
}
enum CodingKeys: CodingKey {
case players, rules, board, state
}
public init(from decoder: any Decoder) throws {
guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext
else { throw DeError.CodableContext }
let container = try decoder.container(keyedBy: CodingKeys.self)
context.players = (try container.decode([CodablePlayerWrapper].self, forKey: .players)).map { $0.player }
context.board = try container.decode(Board.self, forKey: .board)
let rules = (try container.decode(CodableRulesWrapper.self, forKey: .rules)).rules
let state = try container.decode(GameState.self, forKey: .state)
guard let game = Game.init(players: context.players!, rules: rules, board: context.board!, state: state)
else { throw DeError.InitializationFailed }
self.game = game
}
public func encode(to encoder: any Encoder) throws {
guard let context = encoder.userInfo[.gameDecodingContext] as? CodableContext
else { throw DeError.CodableContext }
var container = encoder.container(keyedBy: CodingKeys.self)
context.players = self.game.players
context.board = self.game.board
try container.encode(self.game.players.map { CodablePlayerWrapper(of: $0) }, forKey: .players)
try container.encode(self.game.board, forKey: .board)
try container.encode(CodableRulesWrapper(of: self.game.rules), forKey: .rules)
try container.encode(self.game.state, forKey: .state)
}
}
extension GameState: Codable {
enum VariantCodingKeys: CaseIterable, CodingKey {
case Playing, Win, Draw, Invalid
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self)
self = switch (try container.decode(CodableEnum<VariantCodingKeys>.self, forKey: .type)).variant {
case .Playing:
.Playing(turn: (try (try decoder.container(keyedBy: ExtraCodingKeys.self)).decode(CodablePlayerReferenceWrapper.self, forKey: .player)).player)
case .Win:
try Self.decodeWin(from: decoder)
case .Draw:
.Draw
case .Invalid:
.Invalid
}
}
private enum ExtraCodingKeys: CodingKey {
case player, winner, cells
}
private static func decodeWin(from decoder: any Decoder) throws -> Self {
guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext
else { throw DeError.InvalidVariant }
let container = try decoder.container(keyedBy: ExtraCodingKeys.self)
let winner = (try container.decode(CodablePlayerReferenceWrapper.self, forKey: .winner)).player
let cells = try container.decode([Coords].self, forKey: .cells)
return .Win(winner: winner, board: context.board!, cells: cells)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: EnumCommonCodingKeys.self)
var container2 = encoder.container(keyedBy: ExtraCodingKeys.self)
switch self {
case .Playing(let turn):
try container.encode(CodableEnum(of: VariantCodingKeys.Playing), forKey: .type)
try container2.encode(CodablePlayerReferenceWrapper(of: turn), forKey: .player)
case .Win(let winner, _, let cells):
try container.encode(CodableEnum(of: VariantCodingKeys.Win), forKey: .type)
try container2.encode(CodablePlayerReferenceWrapper(of: winner), forKey: .winner)
try container2.encode(cells, forKey: .cells)
case .Draw:
try container.encode(CodableEnum(of: VariantCodingKeys.Draw), forKey: .type)
case .Invalid:
try container.encode(CodableEnum(of: VariantCodingKeys.Invalid), forKey: .type)
}
}
}

@ -0,0 +1,27 @@
import Foundation
import Model
public enum DeError: Error {
case CodableContext
case InvalidVariant
case InvalidData
case InitializationFailed
}
extension CodingUserInfoKey {
public static let gameDecodingContext = CodingUserInfoKey(rawValue: "gameDecodingContext")!
}
public class CodableContext {
internal var callbackFactory: ((_ name: String, _ type: PieceType) -> MoveCallback)?
internal var players: [Player]? = nil
internal var board: Board? = nil
public init(creatingCallbacks callbackFactory: ((_ name: String, _ type: PieceType) -> MoveCallback)? = nil) {
self.callbackFactory = callbackFactory
}
}
enum EnumCommonCodingKeys: CodingKey {
case type
}

@ -0,0 +1,14 @@
import Model
extension Piece: Codable {
public init(from decoder: any Decoder) throws {
let player = (try CodablePlayerReferenceWrapper.init(from: decoder)).player
self = Self.init(owner: player)
}
public func encode(to encoder: any Encoder) throws {
try CodablePlayerReferenceWrapper(of: self.owner).encode(to: encoder)
}
}
extension PieceType: CodingKey { }

@ -0,0 +1,131 @@
import Model
public struct CodablePlayerWrapper: Codable {
public let player: Player
public init(of player: Player) {
self.player = player
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self)
self.player = try (try container.decode(PlayerType.self, forKey: .type)).decodePlayer(from: decoder)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: EnumCommonCodingKeys.self)
let type = self.player.type
try container.encode(type, forKey: .type)
switch type {
case .Human:
try CodableHumanPlayerWrapper(of: self.player as! HumanPlayer).encode(to: encoder)
case .AIRandom:
try CodableRandomAIPlayerWrapper(of: self.player as! RandomPlayer).encode(to: encoder)
}
}
}
extension PlayerType: CodingKey, Codable {
public init(from decoder: any Decoder) throws {
self = (try CodableEnum<Self>(from: decoder)).variant
}
public func encode(to encoder: any Encoder) throws {
try CodableEnum(of: self).encode(to: encoder)
}
public func decodePlayer(from decoder: any Decoder) throws -> Player {
switch self {
case .Human: (try CodableHumanPlayerWrapper(from: decoder)).player
case .AIRandom: (try CodableRandomAIPlayerWrapper(from: decoder)).player
}
}
}
private enum CommonPlayerCodingKeys: CodingKey {
case name, pieces
}
private struct CodableHumanPlayerWrapper: Codable {
let player: HumanPlayer
init(of player: HumanPlayer) {
self.player = player
}
init(from decoder: any Decoder) throws {
guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext
else { throw DeError.CodableContext }
let container = try decoder.container(keyedBy: CommonPlayerCodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let pieces = try container.decode(CodableEnum<PieceType>.self, forKey: .pieces)
self.init(of: HumanPlayer(name: name, piece_type: pieces.variant, callback: context.callbackFactory!(name, pieces.variant)))
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CommonPlayerCodingKeys.self)
try container.encode(self.player.name, forKey: .name)
try container.encode(CodableEnum(of: self.player.piece_type), forKey: .pieces)
}
}
private struct CodableRandomAIPlayerWrapper: Codable {
let player: RandomPlayer
init(of player: RandomPlayer) {
self.player = player
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CommonPlayerCodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let pieces = try container.decode(CodableEnum<PieceType>.self, forKey: .pieces)
self.init(of: RandomPlayer(name: name, piece_type: pieces.variant))
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CommonPlayerCodingKeys.self)
try container.encode(self.player.name, forKey: .name)
try container.encode(CodableEnum(of: self.player.piece_type), forKey: .pieces)
}
}
public struct CodablePlayerReferenceWrapper: Codable {
public let player: Player
public init(of player: Player) {
self.player = player
}
public init(from decoder: any Decoder) throws {
guard
let context = decoder.userInfo[.gameDecodingContext] as? CodableContext,
let players = context.players
else { throw DeError.CodableContext }
let container = try decoder.singleValueContainer()
let id = try container.decode(Int.self)
guard id < players.count else { throw DeError.InvalidData }
self.player = players[id]
}
public func encode(to encoder: any Encoder) throws {
guard
let context = encoder.userInfo[.gameDecodingContext] as? CodableContext,
let players = context.players
else { throw DeError.CodableContext }
var container = encoder.singleValueContainer()
try container.encode(players.firstIndex(where: { self.player === $0 })!)
}
}

@ -0,0 +1,95 @@
import Model
public struct CodableRulesWrapper: Codable {
public let rules: any Rules
public init(of rules: any Rules) {
self.rules = rules
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: EnumCommonCodingKeys.self)
self.rules = try (try container.decode(RulesTypes.self, forKey: .type)).decodeRules(from: decoder)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: EnumCommonCodingKeys.self)
let type = self.rules.type
try container.encode(type, forKey: .type)
try (self.rules as! Encodable).encode(to: encoder)
}
}
extension RulesTypes: CodingKey, Codable {
public init(from decoder: any Decoder) throws {
self = (try CodableEnum<Self>(from: decoder)).variant
}
public func encode(to encoder: any Encoder) throws {
try CodableEnum(of: self).encode(to: encoder)
}
public func decodeRules(from decoder: any Decoder) throws -> any Rules {
switch self {
case .FourInARow: try FourInARowRules(from: decoder)
case .TicTacToe: try TicTacToeRules(from: decoder)
}
}
}
private enum CommonRulesCodingKeys: CodingKey {
case minAligned
}
extension FourInARowRules: Codable {
public init(from decoder: any Decoder) throws {
guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext else {
throw DeError.CodableContext
}
let container = try decoder.container(keyedBy: CommonRulesCodingKeys.self)
guard let zelf = Self.init(
columns: context.board!.columns,
rows: context.board!.rows,
minAligned: try container.decode(Int.self, forKey: .minAligned),
players: context.players!
) else {
throw DeError.InitializationFailed
}
self = zelf
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CommonRulesCodingKeys.self)
try container.encode(self.minAligned, forKey: .minAligned)
}
}
extension TicTacToeRules: Codable {
public init(from decoder: any Decoder) throws {
guard let context = decoder.userInfo[.gameDecodingContext] as? CodableContext else {
throw DeError.CodableContext
}
let container = try decoder.container(keyedBy: CommonRulesCodingKeys.self)
guard let zelf = Self.init(
columns: context.board!.columns,
rows: context.board!.rows,
minAligned: try container.decode(Int.self, forKey: .minAligned),
players: context.players!
) else {
throw DeError.InitializationFailed
}
self = zelf
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CommonRulesCodingKeys.self)
try container.encode(self.minAligned, forKey: .minAligned)
}
}

@ -0,0 +1,61 @@
import Foundation
import Model
@available(macOS 13.0, *)
public struct Storage {
private static let gameDirectory = try! URL(for: .applicationDirectory, in: .userDomainMask).appending(path: "Connect4")
private static let unfinishedGame = gameDirectory.appending(path: "unfinished.json")
private static let finishedGameDirectory = gameDirectory.appending(path: "finished")
/* func hasUnfinishedGame() -> Bool {
return FileManager.default.fileExists(atPath: unfinishedGameDirectory.path)
} */
public static func loadUnfinishedGame(callbackFactory: @escaping (_ name: String, _ type: PieceType) -> MoveCallback) async throws -> Game? {
// Will read the file asynchronously
let content = await Task {
if FileManager.default.fileExists(atPath: unfinishedGame.path) {
return FileManager.default.contents(atPath: unfinishedGame.path)
} else {
return nil
}
}.value
guard let content = content else { return nil }
let decoder = JSONDecoder()
decoder.userInfo[.gameDecodingContext] = CodableContext(creatingCallbacks: callbackFactory)
return (try decoder.decode(CodableGameWrapper.self, from: content)).game
}
public static func save(game: Game, finished: Bool) async throws {
let (directory, file) = if finished {
(
finishedGameDirectory.path,
finishedGameDirectory
.appending(path: "\(Date())")
.appendingPathExtension(".json")
)
} else {
(gameDirectory.path, unfinishedGame)
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.userInfo[.gameDecodingContext] = CodableContext()
let data = try encoder.encode(CodableGameWrapper(of: game))
// Does Swift have true async IO natively???
try await Task {
try FileManager.default.createDirectory(atPath: directory, withIntermediateDirectories: true)
try data.write(to: file)
}.value
}
public static func clearUnfinished() async throws {
try await Task {
try FileManager.default.removeItem(at: unfinishedGame)
}.value
}
}
Loading…
Cancel
Save