WIP saved games

main
Mathieu GROUSSEAU 1 week ago
parent 7ae16d7cc1
commit 595996f639

@ -24,6 +24,7 @@
F05DA2132E01BA270094A4A8 /* PlayerVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05DA2122E01BA270094A4A8 /* PlayerVM.swift */; };
F05DA2152E02D1850094A4A8 /* UIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05DA2142E02D1850094A4A8 /* UIUtilities.swift */; };
F05DA2172E082F5B0094A4A8 /* Owner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05DA2162E082F5B0094A4A8 /* Owner.swift */; };
F05DA2192E0843170094A4A8 /* SavedGamesVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05DA2182E0843170094A4A8 /* SavedGamesVM.swift */; };
F0F59E492DD4958800BE32D6 /* C4.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0F59E432DD492B400BE32D6 /* C4.xcframework */; };
F0F59E4A2DD4958800BE32D6 /* C4Persistance.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0F59E412DD492B400BE32D6 /* C4Persistance.xcframework */; };
F0F59E4B2DD4958800BE32D6 /* C4Players.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0F59E422DD492B400BE32D6 /* C4Players.xcframework */; };
@ -76,6 +77,7 @@
F05DA2122E01BA270094A4A8 /* PlayerVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerVM.swift; sourceTree = "<group>"; };
F05DA2142E02D1850094A4A8 /* UIUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIUtilities.swift; sourceTree = "<group>"; };
F05DA2162E082F5B0094A4A8 /* Owner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Owner.swift; sourceTree = "<group>"; };
F05DA2182E0843170094A4A8 /* SavedGamesVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedGamesVM.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>"; };
@ -198,6 +200,7 @@
F0143C2C2DF01E120086CAAA /* NewGameVM.swift */,
F0143C2E2DF96B690086CAAA /* InGameVM.swift */,
F05DA2122E01BA270094A4A8 /* PlayerVM.swift */,
F05DA2182E0843170094A4A8 /* SavedGamesVM.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -379,6 +382,7 @@
F0143C362DFA9A000086CAAA /* RulesType.swift in Sources */,
F0F59E532DDDC35100BE32D6 /* NewGameView.swift in Sources */,
F0143C2F2DF96B690086CAAA /* InGameVM.swift in Sources */,
F05DA2192E0843170094A4A8 /* SavedGamesVM.swift in Sources */,
F05DA2132E01BA270094A4A8 /* PlayerVM.swift in Sources */,
F0F59E5B2DE6F68800BE32D6 /* ScoreboardView.swift in Sources */,
F0143C2B2DF018F20086CAAA /* GameScene.swift in Sources */,

@ -18,6 +18,7 @@
}
},
"%@ savedGames.section.unfinished.entry %@" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -286,6 +287,7 @@
}
},
"inGame.ppButton.pause" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -302,6 +304,7 @@
}
},
"inGame.ppButton.resume" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -584,6 +587,7 @@
}
},
"savedGames.section.unfinished" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {

@ -13,8 +13,12 @@ class PieceNode : SKNode {
var piece: Piece = .init(withOwner: .noOne) {
didSet {
#if os(iOS)
circle.fillColor = UIColor(piece.owner.pieceColor)
circle.strokeColor = UIColor(.pieceBorder)
#else
circle.fillColor = NSColor(piece.owner.pieceColor)
#endif
circle.strokeColor = .pieceBorder
}
}
var boardPosition: (x: Int, y: Int)? = nil {

@ -37,9 +37,9 @@ struct PlayerVSPage: View {
PlayerPicker()
}
ScoreboardView("scoreboard.table.column.players") { result in
"\(result.player1) savedGames.section.finished.entry \(result.player2)"
}
// ScoreboardView("scoreboard.table.column.players") { result in
// "\(result.player1) savedGames.section.finished.entry \(result.player2)"
// }
}
}
}

@ -1,36 +1,30 @@
//
// SavedGamesView.swift
// App
//
// Created by etudiant2 on 28/05/2025.
//
import SwiftUI
struct SavedGamesView: View {
@StateObject
private var vm = SavedGamesVM()
var body: some View {
// TODO: use the same collection view with headers instead? Or use collapsible sections?
Section("savedGames.section.unfinished") {
ScoreboardView("scoreboard.table.column.players") { result in
"\(result.player1) savedGames.section.unfinished.entry \(result.player2)"
}
}
Section("savedGames.section.finished") {
ScoreboardView("scoreboard.table.column.players") { result in
"\(result.player1) savedGames.section.finished.entry \(result.player2)"
VStack {
// TODO: use the same collection view with headers instead? Or use collapsible sections?
//Section("savedGames.section.unfinished") {
// ScoreboardView("scoreboard.table.column.players") { result in
// "\(result.player1) savedGames.section.unfinished.entry \(result.player2)"
// }
//}
Section("savedGames.section.finished") {
ScoreboardView(results: self.$vm.results, "scoreboard.table.column.players") { result in
"\(result.player1) savedGames.section.finished.entry \(result.player2)"
}
}
}.onAppear {
vm.loadData()
}
}
}
struct Result: Identifiable {
let id = UUID()
let date: Date
let player1: String
let player2: String
let rules: String
init() {
}
}
#Preview {

@ -12,50 +12,49 @@ struct ScoreboardView: View {
private var horizontalSizeClass: UserInterfaceSizeClass?
private let playerRelatedColumnKey: LocalizedStringKey
private let localizedKeyProvider: (Result) -> LocalizedStringKey
private let localizedKeyProvider: (ResultVM) -> LocalizedStringKey
@State private var unsinished = [
Result(date: Date.now, player1: "P1", player2: "P2", rules: "Rule1"),
Result(date: Date.now, player1: "P2", player2: "P3", rules: "Rule2"),
Result(date: Date.now, player1: "P3", player2: "P4", rules: "Rule3"),
Result(date: Date.now, player1: "P4", player2: "P5", rules: "Rule4"),
Result(date: Date.now, player1: "P5", player2: "P1", rules: "Rule5")
];
@Binding
private var results: [ResultVM]
var body: some View {
// TODO: sort by date
if horizontalSizeClass == .compact {
List(self.unsinished) { result in
List(self.results) { result in
VStack(alignment: .center) {
Text(localizedKeyProvider(result))
HStack {
Text(result.date, style: .date)
Spacer()
Text(result.rules)
Text(result.rules.baseTranslationKey)
}.foregroundStyle(.secondary)
}
}
} else{
Table(self.unsinished) {
Table(self.results) {
TableColumn("scoreboard.table.column.date") { result in
Text(result.date, style: .date)
}
TableColumn(playerRelatedColumnKey) { result in
Text(localizedKeyProvider(result))
}
TableColumn("scoreboard.table.column.rules", value: \.rules)
TableColumn("scoreboard.table.column.rules", content: { result in
Text(result.rules.baseTranslationKey)
})
}
}
}
public init(_ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (Result) -> LocalizedStringKey) {
public init(results: Binding<[ResultVM]>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (ResultVM) -> LocalizedStringKey) {
self._results = results
self.playerRelatedColumnKey = playerRelatedColumnKey
self.localizedKeyProvider = localizedKeyProvider
}
}
#Preview {
ScoreboardView("scoreboard.table.column.players") { result in
"\(result.player1) savedGames.section.unfinished.entry \(result.player2)"
}
}
//TODO
// #Preview {
// ScoreboardView("scoreboard.table.column.players") { result in
// "\(result.player1) savedGames.section.unfinished.entry \(result.player2)"
// }
// }

@ -77,8 +77,21 @@ class IngameVM: ObservableObject {
// TODO
}
game.addGameChangedListener { game, result in
print("game changed")
// TODO
if game.players.contains(where: { $0.value is ReplayPlayer }) {
// Do not save progress of an already finished game
return
}
defer {
print("game saved")
}
if result == .notFinished {
_ = try await Persistance.saveGame(withName: "\(self.gameName).co4", andGame: self.game, withFolderName: "connect4.games")
return
}
_ = try await Persistance.saveGameResult(withName: "savedGames.json", andGame: game, andResult: result, withFolderName: "connect4.games")
}
game.addGameStartedListener { board in
print("game started")

@ -0,0 +1,73 @@
import Foundation
import Connect4Persistance
class SavedGamesVM: ObservableObject {
private var loaded: Bool = false
@Published
var results: [ResultVM] = []
init() {
}
func loadData() {
if (!loaded) {
loaded = true
Task(priority: .userInitiated) {
let results = (try await Persistance.loadGameResults(withName: "savedGames.json", withFolderName: "connect4.games")) ?? []
let vms = results.map(ResultVM.init)
DispatchQueue.main.async {
self.results = vms
}
}
}
}
}
class ResultVM: Identifiable {
var id: Date { self.date }
let date: Date
let player1: String
let player1Type: PlayerType
let player2: String
let player2Type: PlayerType
let rules: RulesType
init(from: GameResult) {
self.date = from.date
let p = from.players[0]
if p.id == .player1 {
self.player1 = p.name
self.player1Type = Self.i(data: p)
self.player2 = p.name
self.player2Type = Self.i(data: from.players[1])
} else {
self.player2 = p.name
self.player2Type = Self.i(data: p)
self.player1 = p.name
self.player1Type = Self.i(data: from.players[1])
}
self.rules = switch (from.rules.type) {
case "Connect4Rules": .Classic
case "TicTacToeRules": .TicTacToe
case "PopOutRules": .PopOut
default: fatalError("Unexpected rules type")
}
}
private static func i(data: PlayerData) -> PlayerType {
return switch (data.type) {
case "HumanPlayer": .Human
case "RandomPlayer": .AIRandom
case "FinnishHimPlayer": .AIFinnishHim
case "SimpleNegaMaxPlayer": .AISimpleNegaMax
default: fatalError("Unexpected player type")
}
}
}
Loading…
Cancel
Save