Scoreboard view

main
Mathieu GROUSSEAU 5 days ago
parent 9690a2dcb1
commit 4b2052e2c0

@ -35,6 +35,8 @@
F08534D02E094F830002A3A0 /* C4Rules.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0F59E442DD492B400BE32D6 /* C4Rules.xcframework */; };
F08534D12E094F830002A3A0 /* C4Rules.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F0F59E442DD492B400BE32D6 /* C4Rules.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
F08534D32E0951BD0002A3A0 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F08534D22E0951BD0002A3A0 /* Images.xcassets */; };
F08534D52E0A857D0002A3A0 /* Persistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08534D42E0A857D0002A3A0 /* Persistance.swift */; };
F08534D72E0A88240002A3A0 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08534D62E0A88240002A3A0 /* String.swift */; };
F0F59E4F2DD4996F00BE32D6 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F0F59E4E2DD4996F00BE32D6 /* Localizable.xcstrings */; };
F0F59E512DD49C2800BE32D6 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0F59E502DD49C2800BE32D6 /* Colors.xcassets */; };
F0F59E532DDDC35100BE32D6 /* NewGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F59E522DDDC35100BE32D6 /* NewGameView.swift */; };
@ -104,6 +106,8 @@
F08534C32E094F4E0002A3A0 /* xcframeworks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = xcframeworks; path = ../precompiled/xcframeworks; sourceTree = "<group>"; };
F08534C82E094F7A0002A3A0 /* C4Core.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = C4Core.xcframework; path = ../precompiled/xcframeworks/C4Core.xcframework; sourceTree = "<group>"; };
F08534D22E0951BD0002A3A0 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
F08534D42E0A857D0002A3A0 /* Persistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistance.swift; sourceTree = "<group>"; };
F08534D62E0A88240002A3A0 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
F0F59E412DD492B400BE32D6 /* C4Persistance.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = C4Persistance.xcframework; path = ../precompiled/xcframeworks/C4Persistance.xcframework; sourceTree = "<group>"; };
F0F59E422DD492B400BE32D6 /* C4Players.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = C4Players.xcframework; path = ../precompiled/xcframeworks/C4Players.xcframework; sourceTree = "<group>"; };
F0F59E432DD492B400BE32D6 /* C4.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = C4.xcframework; path = ../precompiled/xcframeworks/C4.xcframework; sourceTree = "<group>"; };
@ -250,6 +254,8 @@
F0143C332DF987490086CAAA /* PlayerType.swift */,
F0143C352DFA9A000086CAAA /* RulesType.swift */,
F05DA2162E082F5B0094A4A8 /* Owner.swift */,
F08534D42E0A857D0002A3A0 /* Persistance.swift */,
F08534D62E0A88240002A3A0 /* String.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -406,6 +412,7 @@
files = (
F05DA2112E002AA00094A4A8 /* BoardNode.swift in Sources */,
F0143C312DF9813C0086CAAA /* PieceNode.swift in Sources */,
F08534D72E0A88240002A3A0 /* String.swift in Sources */,
F05DA2152E02D1850094A4A8 /* UIUtilities.swift in Sources */,
F0F59E592DE6EB2A00BE32D6 /* PlayerVSPage.swift in Sources */,
F001A04E2DD48FAB00809561 /* MainMenuView.swift in Sources */,
@ -420,6 +427,7 @@
F0143C2B2DF018F20086CAAA /* GameScene.swift in Sources */,
F0F59E572DE6D6E600BE32D6 /* SavedGamesView.swift in Sources */,
F05DA2172E082F5B0094A4A8 /* Owner.swift in Sources */,
F08534D52E0A857D0002A3A0 /* Persistance.swift in Sources */,
F0143C342DF987490086CAAA /* PlayerType.swift in Sources */,
F0F59E552DDDED1D00BE32D6 /* IngameView.swift in Sources */,
);

@ -0,0 +1,7 @@
import Connect4Persistance
extension Persistance {
static let unfinishedGameFileName: String = "lastUnfinished.co4"
static let finishedGameFileName: String = "savedGames.json"
static let saveDirectory: String = "connect4.games"
}

@ -1,7 +1,21 @@
import Foundation
import Connect4Core
import Connect4Players
enum PlayerType: CaseIterable, Identifiable {
var id: Self { self }
case Human, AIRandom, AIFinnishHim, AISimpleNegaMax
}
extension Player {
var type: PlayerType {
switch (self) {
case is HumanPlayer: .Human
case is RandomPlayer: .AIRandom
case is FinnishHimPlayer: .AIFinnishHim
case is SimpleNegaMaxPlayer: .AISimpleNegaMax
default: fatalError("Unexpected player type")
}
}
}

@ -1,5 +1,19 @@
import Connect4Core
import Connect4Rules
enum RulesType: CaseIterable, Identifiable {
var id: Self { self }
case Classic, TicTacToe, PopOut
}
extension Rules {
var type: RulesType {
switch (self) {
case is Connect4Rules: .Classic
case is TicTacToeRules: .TicTacToe
case is PopOutRules: .PopOut
default: fatalError("Unexpected rules type")
}
}
}

@ -0,0 +1,11 @@
import Foundation
extension String {
var nilIfEmpty: Self? {
if self.isEmpty {
nil
} else {
self
}
}
}

@ -3,20 +3,20 @@ import SwiftUI
struct SavedGamesView: View {
@StateObject
private var vm = SavedGamesVM()
var body: some View {
TabView {
/* VStack {
VStack {
Section("savedGames.section.unfinished") {
ScoreboardView(results: self.$vm.results, "scoreboard.table.column.players") {
ScoreboardView(results: self.$vm.unfinished, "scoreboard.table.column.players") {
result in
let n1 = if result.player1 != nil {
Text(result.player1!)
let n1 = if result.player1Name != nil {
Text(result.player1Name!)
} else {
Text(LocalizedStringKey(result.player1Type.baseTranslationKey))
}
let n2 = if result.player2 != nil {
Text(result.player2!)
let n2 = if result.player2Name != nil {
Text(result.player2Name!)
} else {
Text(LocalizedStringKey(result.player2Type.baseTranslationKey))
}
@ -24,19 +24,19 @@ struct SavedGamesView: View {
return LocalizedStringKey("\(n1) savedGames.section.unfinished.entry \(n2)")
}
}
} */
}
VStack {
Section("savedGames.section.finished") {
ScoreboardView(results: self.$vm.results, "scoreboard.table.column.players") {
ScoreboardView(results: self.$vm.finished, "scoreboard.table.column.players") {
result in
let n1 = if result.player1 != nil {
Text(result.player1!)
let n1 = if result.player1Name != nil {
Text(result.player1Name!)
} else {
Text(LocalizedStringKey(result.player1Type.baseTranslationKey))
}
let n2 = if result.player2 != nil {
Text(result.player2!)
let n2 = if result.player2Name != nil {
Text(result.player2Name!)
} else {
Text(LocalizedStringKey(result.player2Type.baseTranslationKey))
}

@ -12,13 +12,12 @@ struct ScoreboardView: View {
private var horizontalSizeClass: UserInterfaceSizeClass?
private let playerRelatedColumnKey: LocalizedStringKey
private let localizedKeyProvider: (ResultVM) -> LocalizedStringKey
private let localizedKeyProvider: (GameEntryVM) -> LocalizedStringKey
@Binding
private var results: [ResultVM]
private var results: [GameEntryVM]
var body: some View {
// TODO: sort by date
if horizontalSizeClass == .compact {
List(self.results) { result in
VStack(alignment: .center) {
@ -33,7 +32,7 @@ struct ScoreboardView: View {
} else{
Table(self.results) {
TableColumn("scoreboard.table.column.date") { result in
Text(result.date, style: .date)
Text("generic.datetime \(Text(result.date, style: .date)) \(Text(result.date, style: .time))")
}
TableColumn(playerRelatedColumnKey) { result in
Text(localizedKeyProvider(result))
@ -45,7 +44,7 @@ struct ScoreboardView: View {
}
}
public init(results: Binding<[ResultVM]>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (ResultVM) -> LocalizedStringKey) {
public init(results: Binding<[GameEntryVM]>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (GameEntryVM) -> LocalizedStringKey) {
self._results = results
self.playerRelatedColumnKey = playerRelatedColumnKey
self.localizedKeyProvider = localizedKeyProvider

@ -97,11 +97,25 @@ class IngameVM: ObservableObject {
}
if result == .notFinished {
_ = try await Persistance.saveGame(withName: "lastUnfinished.co4", andGame: self.game, withFolderName: "connect4.games")
_ = try await Persistance.saveGame(
withName: Persistance.unfinishedGameFileName,
andGame: self.game,
withFolderName: Persistance.saveDirectory
)
return
}
_ = try await Persistance.saveGameResult(withName: "savedGames.json", andGame: game, andResult: result, withFolderName: "connect4.games")
try await Persistance.deleteGame(
withName: Persistance.unfinishedGameFileName,
withFolderName: Persistance.saveDirectory
)
_ = try await Persistance.saveGameResult(
withName: Persistance.finishedGameFileName,
andGame: game,
andResult: result,
withFolderName: Persistance.saveDirectory
)
}
game.addGameStartedListener { board in
print("game started")

@ -1,33 +1,59 @@
import Foundation
import Connect4Core
import Connect4Persistance
class SavedGamesVM: ObservableObject {
@Published
var results: [ResultVM] = []
var unfinished: [GameEntryVM] = []
@Published
var finished: [GameEntryVM] = []
init() {
Task(priority: .userInitiated) {
let results = (try await Persistance.loadGameResults(withName: "savedGames.json", withFolderName: "connect4.games")) ?? []
let vms = results.sorted(by: { $0.date > $1.date }).map(ResultVM.init)
let sortedU: [GameEntryVM]
let sortedF: [GameEntryVM]
var temp: [GameEntryVM]
do {
if let game = try await Persistance.loadGame(withName: Persistance.unfinishedGameFileName, withFolderName: Persistance.saveDirectory) {
temp = [GameEntryVM(fromGame: game)]
} else {
temp = []
}
sortedU = temp.sorted(by: { $0.date > $1.date })
} catch {
sortedU = []
}
do {
let results = (try await Persistance.loadGameResults(withName: Persistance.finishedGameFileName, withFolderName: Persistance.saveDirectory)) ?? []
temp = results.map(GameEntryVM.init)
sortedF = temp.sorted(by: { $0.date > $1.date })
} catch {
sortedF = []
}
DispatchQueue.main.async {
self.results = vms
self.unfinished = sortedU
self.finished = sortedF
}
}
}
}
class ResultVM: Identifiable {
class GameEntryVM: Identifiable {
var id: Date { self.date }
let game: Game?
let date: Date
let player1: String?
let player1Name: String?
let player1Type: PlayerType
let player2: String?
let player2Name: String?
let player2Type: PlayerType
let rules: RulesType
init(from: GameResult) {
init(fromResult from: GameResult) {
self.date = from.date
let (p1, p2) = if from.players[0].id == .player1 {
(from.players[0], from.players[1])
@ -36,17 +62,9 @@ class ResultVM: Identifiable {
}
self.player1Type = Self.i(data: p1)
self.player1 = if p1.name.isEmpty {
nil
} else {
p1.name
}
self.player1Name = p1.name.nilIfEmpty
self.player2Type = Self.i(data: p2)
self.player2 = if p2.name.isEmpty {
nil
} else {
p2.name
}
self.player2Name = p2.name.nilIfEmpty
self.rules = switch (from.rules.type) {
case "Connect4Rules": .Classic
@ -54,6 +72,23 @@ class ResultVM: Identifiable {
case "PopOutRules": .PopOut
default: fatalError("Unexpected rules type")
}
self.game = nil
}
init(fromGame from: Game) {
self.game = from;
self.date = Date.now
let p1 = from.players[.player1]!
let p2 = from.players[.player2]!
self.player1Name = p1.name.nilIfEmpty
self.player1Type = p1.type
self.player2Name = p2.name.nilIfEmpty
self.player2Type = p2.type
self.rules = from.rules.type
}
private static func i(data: PlayerData) -> PlayerType {

Loading…
Cancel
Save