Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
|
baf8fb6d0c | 1 week ago |
|
2305ba43ad | 1 week ago |
|
1c45aa447d | 2 weeks ago |
|
2f7ff9bbdc | 2 weeks ago |
|
8770d1f423 | 2 weeks ago |
|
70d4fcd33e | 2 weeks ago |
|
38aa08ba5a | 2 weeks ago |
@ -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,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)]
|
||||
}
|
||||
}
|
||||
|
@ -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…
Reference in new issue