Compare commits
No commits in common. 'main' and 'Players' have entirely different histories.
@ -1,126 +0,0 @@
|
|||||||
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 {
|
public struct Piece: Equatable, Sendable {
|
||||||
public let owner: Player
|
public let type: PieceType
|
||||||
public var type: PieceType { owner.piece_type }
|
|
||||||
|
|
||||||
public init(owner: Player) {
|
// Required for public visibility
|
||||||
self.owner = owner
|
public init(type: PieceType) {
|
||||||
|
self.type = type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
public class RandomPlayer : Player {
|
public class RandomPlayer : Player {
|
||||||
public override var type: PlayerType { .AIRandom }
|
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> Move.Action {
|
||||||
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) async -> Move.Action {
|
|
||||||
moves[Int.random(in: moves.indices)]
|
moves[Int.random(in: moves.indices)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
/.build
|
|
||||||
/Packages
|
|
||||||
xcuserdata/
|
|
||||||
DerivedData/
|
|
||||||
.swiftpm/configuration/registries.json
|
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
||||||
.netrc
|
|
@ -1,24 +0,0 @@
|
|||||||
// 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"]),
|
|
||||||
]
|
|
||||||
)
|
|
@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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 { }
|
|
@ -1,131 +0,0 @@
|
|||||||
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 })!)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
Reference in new issue