serde.rs joy and pain back again

Persistance
Mathieu GROUSSEAU 2 months ago
parent 70d4fcd33e
commit 8770d1f423

@ -2,6 +2,7 @@ import Foundation
import Model
import CustomTypes
import Persistance
enum EnumRules: String, CaseIterable {
case FourInARow = "Four in a row"
@ -89,7 +90,17 @@ game.gameStateChanged.add { state in
print(board.display(winCells: cells))
case .Draw:
print("Its a daw.")
case .Invalid:
fatalError("Game is invalid!")
}
}
game.play()
var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.userInfo[.gameDecodingContext] = CodableContext()
let data = try encoder.encode(CodableGameWrapper(of: game))
print(String(data: data, encoding: .utf8)!)

@ -42,10 +42,29 @@ public struct Board: Equatable {
}
}
public var cells: any Sequence<Piece?> {
self.grid.lazy.flatMap { (row: [Piece?]) -> LazySequence<[Piece?]> in row.lazy }
}
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)
}
}
}
@available(*, deprecated, renamed: "cells", message: "Use cells sequence instead")
public func forEach(_ consumer: (Piece?) -> Void) {
for column in grid {
for piece in column {
consumer(piece)
self.cells.forEach(consumer)
}
@available(*, deprecated, renamed: "cells", message: "Use cells sequence instead")
public mutating func forEach(mutating consumer: (Coords, inout Piece?) -> Void) {
for column in 0..<self.columns {
for row in 0..<self.rows {
let coords = Coords(column, row)
consumer(coords, &self[coords])
}
}
}
@ -53,7 +72,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

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

@ -9,14 +9,38 @@ public class Game {
public let players: [Player]
public let rules: 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: 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: 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
}
public func play() {
var board = rules.createBoard()
var state = rules.gameState(board: board, last_turn: nil)
switch state {
case .Playing:
break
default:
return
}
boardChanged(board)
gameStateChanged(state)

@ -2,6 +2,8 @@ public typealias MoveCallback = ([Move.Action], Board) -> 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

@ -1,6 +1,8 @@
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
@ -21,3 +23,8 @@ public class Player : Equatable {
lhs === rhs
}
}
public enum PlayerType: CaseIterable {
case Human
case AIRandom
}

@ -1,4 +1,5 @@
public class RandomPlayer : Player {
public override var type: PlayerType { .AIRandom }
public override func chooseMove(allowed_moves moves: [Move.Action], board: Board) -> 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)

@ -1,7 +1,5 @@
public protocol Rules {
// var state: GameState { get }
// var history: [Move] { get }
var type: RulesTypes { get }
func createBoard() -> Board
@ -14,14 +12,17 @@ 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 enum GameState: Equatable {
case Playing(turn: Player)
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,6 +1,8 @@
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 var type: RulesTypes = .TicTacToe
public init?(players: [Player]) {
self.init(columns: 3, rows: 3, players: players)

@ -18,6 +18,7 @@ let package = Package(
// 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"),
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 CodablePlayerWrapper(from: decoder)).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 +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: Rules
public init(of rules: 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 -> 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)
}
}
Loading…
Cancel
Save