Async Sync IO...

Persistance part-5
Mathieu GROUSSEAU 1 week ago
parent 1c45aa447d
commit 2305ba43ad

@ -4,6 +4,8 @@ import Model
import CustomTypes
import Persistance
var game: Game? = nil
enum EnumRules: String, CaseIterable {
case FourInARow = "Four in a row"
case TicTacToe = "Tic Tac Toe"
@ -15,8 +17,8 @@ func enumPrompt<E: RawRepresentable<String> & CaseIterable>(prompt: String) -> E
print("\t\(index + 1). \(variant.rawValue)")
}
guard let rawInput = readLine(strippingNewline: true),
let index = Int(rawInput), index >= 1, index <= E.allCases.count
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")
}
@ -24,14 +26,13 @@ func enumPrompt<E: RawRepresentable<String> & CaseIterable>(prompt: String) -> E
return E.allCases[index - 1]
}
let gameType: EnumRules = enumPrompt(prompt: "Choose a game type")
enum PlayerType: String, CaseIterable {
case AIRandom = "(AI) Random"
case Human = "Player"
}
func playerPrompt(allowed_moves: [Move.Action], board: Board) -> Move.Action {
@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)"
@ -40,13 +41,24 @@ func playerPrompt(allowed_moves: [Move.Action], board: Board) -> Move.Action {
print("\(i + 1). \(text)")
}
guard let rawInput = readLine(strippingNewline: true),
let index = Int(rawInput), index >= 1, index <= allowed_moves.count
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)
}
}
func createPlayer(type: PlayerType, playing pieces: PieceType, named name: String) -> Player {
@ -58,6 +70,26 @@ func createPlayer(type: PlayerType, playing pieces: PieceType, named name: Strin
}
}
if let unfinished = try await Storage.loadUnfinishedGame(callbackFactory: { name, type in playerPrompt }) {
print("There was an unfinished game. Resume? [Y/n]")
guard let answer = readLine(strippingNewline: true) else {
exit(EXIT_SUCCESS)
}
switch answer.lowercased() {
case "", "y", "yes":
game = unfinished
default:
break;
}
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)")
@ -74,15 +106,16 @@ case .FourInARow: FourInARowRules(players: players)!
case .TicTacToe: TicTacToeRules(players: players)!
}
var game = Game(players: players, rules: rules)
game = Game(players: players, rules: rules)
}
game.boardChanged.add { board in
game!.boardChanged.add { board in
print(board)
}
game.moveIsInvalid.add { move in
game!.moveIsInvalid.add { move in
print("\(move.player.name), this move is invalid according to the rules.")
}
game.gameStateChanged.add { state in
game!.gameStateChanged.add { state in
switch state {
case .Playing(let player):
print("\(player.name) turn.")
@ -96,16 +129,6 @@ game.gameStateChanged.add { state in
}
}
game.play()
var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.userInfo[.gameDecodingContext] = CodableContext()
let data = try encoder.encode(CodableGameWrapper(of: game))
var decoder = JSONDecoder()
decoder.userInfo[.gameDecodingContext] = CodableContext(creatingCallbacks: { name, type in playerPrompt })
let game2 = (try decoder.decode(CodableGameWrapper.self, from: data)).game
await game!.play()
assert(game == game2, "Games are not equals!")
try await Storage.save(game: game!, finished: true)

@ -33,7 +33,8 @@ public class Game: Equatable {
self.board = board
}
public func play() {
@MainActor
public func play() async {
switch state {
case .Playing:
break
@ -52,7 +53,7 @@ public class Game: Equatable {
player_turn.skipMove(board: board)
} else {
while true {
let choosen_action = player_turn.chooseMove(allowed_moves: valid_moves, board: board)
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 {

@ -1,4 +1,4 @@
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
@ -11,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)
}
}

@ -9,7 +9,7 @@ public class Player : Equatable {
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")
}

@ -1,6 +1,6 @@
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)]
}
}

@ -61,7 +61,7 @@ extension GameState: Codable {
self = switch (try container.decode(CodableEnum<VariantCodingKeys>.self, forKey: .type)).variant {
case .Playing:
.Playing(turn: (try CodablePlayerWrapper(from: decoder)).player)
.Playing(turn: (try (try decoder.container(keyedBy: ExtraCodingKeys.self)).decode(CodablePlayerReferenceWrapper.self, forKey: .player)).player)
case .Win:
try Self.decodeWin(from: decoder)
case .Draw:

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