Resumable games (& route fix)

main
Mathieu GROUSSEAU 4 days ago
parent 94dc844905
commit 5d8cefb39d

@ -83,6 +83,22 @@
}
}
},
"generic.game.launch" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Play"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jouer"
}
}
}
},
"generic.player.type.aiFinnishHim" : {
"extractionState" : "manual",
"localizations" : {
@ -498,23 +514,6 @@
}
}
},
"newGame.play" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Play"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jouer"
}
}
}
},
"newGame.player.name" : {
"extractionState" : "manual",
"localizations" : {

@ -77,10 +77,7 @@ struct IngameView: View {
}
}
init(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) {
guard let vm = IngameVM(settings: settings, player1: player1, player2: player2)
else { fatalError("TODO: how to handle game setup failure") }
init(vm: IngameVM) {
self._vm = StateObject(wrappedValue: vm)
}
}
@ -155,5 +152,5 @@ private struct PlayerView: View {
}
#Preview {
IngameView(settings: NewGameVM(), player1: PlayerSettingsVM(type: .Human), player2: PlayerSettingsVM(type: .AISimpleNegaMax))
IngameView(vm: IngameVM(settings: NewGameVM(), player1: PlayerSettingsVM(type: .Human), player2: PlayerSettingsVM(type: .AISimpleNegaMax)))
}

@ -1,6 +1,9 @@
import SwiftUI
struct MainMenuView: View {
@State
private var currentGame: IngameVM? = nil
var body: some View {
VStack(spacing: 20) {
Spacer()
@ -13,16 +16,25 @@ struct MainMenuView: View {
Spacer()
NavigationLink(destination: NewGameView().navigationTitle("newGame.title")) {
NavigationLink(destination: NewGameView(currentGame: $currentGame).navigationTitle("newGame.title")) {
Label("mainMenu.button.newGame", systemImage: "play")
}
NavigationLink(destination: SavedGamesView().navigationTitle("savedGames.title")) {
NavigationLink(destination: SavedGamesView(currentGame: $currentGame).navigationTitle("savedGames.title")) {
Label("mainMenu.button.scoreboard", systemImage: "rosette")
}
Spacer()
}
}.navigationDestination(isPresented: self.$currentGame.map(get: { $0 != nil }, setWithContext: {
game, presented in
if presented {
game
} else {
nil
}
}), destination: {
NavigationLazyView(IngameView(vm: currentGame!))
})
}
}

@ -0,0 +1,13 @@
import Foundation
import SwiftUI
// https://stackoverflow.com/questions/57594159/swiftui-navigationlink-loads-destination-view-immediately-without-clicking/61234030#61234030
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}

@ -9,6 +9,9 @@ import SwiftUI
import PhotosUI
struct NewGameView: View {
@Binding
var currentGame: IngameVM?
@StateObject
private var vm: NewGameVM = NewGameVM()
@StateObject
@ -47,27 +50,14 @@ struct NewGameView: View {
//
// }
}
}.toolbar {
NavigationLink {
NavigationLazyView(IngameView(settings: vm, player1: p1, player2: p2))
} label: {
Label("newGame.play", systemImage: "play")
}
Button("generic.game.launch", systemImage: "play") {
currentGame = IngameVM(settings: vm, player1: p1, player2: p2)
}.buttonStyle(.borderedProminent)
}
}
}
// https://stackoverflow.com/questions/57594159/swiftui-navigationlink-loads-destination-view-immediately-without-clicking/61234030#61234030
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
private struct PlayerSectionView: View {
private let sectionLabel: LocalizedStringKey
private let pieceColor: Color
@ -135,5 +125,5 @@ extension RulesType {
}
#Preview {
NewGameView()
NewGameView(currentGame: .constant(nil))
}

@ -3,12 +3,15 @@ import SwiftUI
struct SavedGamesView: View {
@StateObject
private var vm = SavedGamesVM()
@Binding
var currentGame: IngameVM?
var body: some View {
TabView {
VStack {
Section("savedGames.section.unfinished") {
ScoreboardView(results: self.$vm.unfinished, "scoreboard.table.column.players") {
ScoreboardView(results: self.$vm.unfinished, currentGame: $currentGame, "scoreboard.table.column.players") {
result in
let n1 = if result.player1Name != nil {
Text(result.player1Name!)
@ -28,7 +31,7 @@ struct SavedGamesView: View {
VStack {
Section("savedGames.section.finished") {
ScoreboardView(results: self.$vm.finished, "scoreboard.table.column.players") {
ScoreboardView(results: self.$vm.finished, currentGame: $currentGame, "scoreboard.table.column.players") {
result in
let n1 = if result.player1Name != nil {
Text(result.player1Name!)
@ -60,12 +63,8 @@ struct SavedGamesView: View {
// //}
// }
}
init() {
}
}
#Preview {
SavedGamesView()
SavedGamesView(currentGame: .constant(nil))
}

@ -1,10 +1,3 @@
//
// ScoreboardView.swift
// App
//
// Created by etudiant2 on 28/05/2025.
//
import SwiftUI
struct ScoreboardView: View {
@ -14,13 +7,19 @@ struct ScoreboardView: View {
private let playerRelatedColumnKey: LocalizedStringKey
private let localizedKeyProvider: (GameEntryVM) -> LocalizedStringKey
@State
private var selection: GameEntryVM.ID? = nil
@Binding
private var results: [GameEntryVM]
@Binding
private var currentGame: IngameVM?
var body: some View {
if horizontalSizeClass == .compact {
List(self.results) { result in
VStack(alignment: .center) {
let entry = VStack(alignment: .center) {
Text(localizedKeyProvider(result))
HStack {
Text("generic.datetime \(Text(result.date, style: .date)) \(Text(result.date, style: .time))")
@ -28,9 +27,19 @@ struct ScoreboardView: View {
Text(result.rules.baseTranslationKey)
}.foregroundStyle(.secondary)
}
if let game = result.game {
Button {
currentGame = IngameVM(game: game)
} label: {
entry
}.buttonRepeatBehavior(.disabled)
} else {
entry
}
}
} else{
Table(self.results) {
} else {
Table(self.results, selection: $selection) {
TableColumn("scoreboard.table.column.date") { result in
Text("generic.datetime \(Text(result.date, style: .date)) \(Text(result.date, style: .time))")
}
@ -40,18 +49,25 @@ struct ScoreboardView: View {
TableColumn("scoreboard.table.column.rules", content: { result in
Text(result.rules.baseTranslationKey)
})
}.contextMenu(forSelectionType: GameEntryVM.ID.self) { _ in } primaryAction: { items in
guard !items.isEmpty,
let selection,
let game = results.first(where: { $0.date == selection && $0.game != nil })?.game
else { return }
currentGame = IngameVM(game: game)
}
}
}
public init(results: Binding<[GameEntryVM]>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (GameEntryVM) -> LocalizedStringKey) {
public init(results: Binding<[GameEntryVM]>, currentGame: Binding<IngameVM?>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (GameEntryVM) -> LocalizedStringKey) {
self._results = results
self._currentGame = currentGame
self.playerRelatedColumnKey = playerRelatedColumnKey
self.localizedKeyProvider = localizedKeyProvider
}
}
//TODO
// #Preview {
// ScoreboardView("scoreboard.table.column.players") { result in
// "\(result.player1) savedGames.section.unfinished.entry \(result.player2)"

@ -23,7 +23,7 @@ extension Binding {
func map<T>(_ get: @escaping (Value) -> T) -> Binding<T> {
Binding<T>(
get: { get(self.wrappedValue) },
set: { _ in fatalError("Not implemented") }
set: { _ in fatalError("Not supported") }
)
}
@ -36,4 +36,14 @@ extension Binding {
set: { self.wrappedValue = set($0) }
)
}
func map<T>(
get: @escaping (Value) -> T,
setWithContext set: @escaping (Value, T) -> Value
) -> Binding<T> {
Binding<T>(
get: { get(self.wrappedValue) },
set: { self.wrappedValue = set(self.wrappedValue, $0) }
)
}
}

@ -40,30 +40,16 @@ class IngameVM: ObservableObject {
var result: Connect4Core.Result = .notFinished
private var running: Bool = false
init?(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) {
init(game: Game) {
let fmt = DateFormatter()
fmt.locale = Locale(identifier: "EN") // No "root" locale?
self.gameName = fmt.string(from: Date.now)
let rulesInit: (Int, Int, Int) -> (any Rules)? = switch (settings.rulesType) {
case .Classic: Connect4Rules.init
case .TicTacToe: TicTacToeRules.init
case .PopOut: PopOutRules.init
}
guard let rules = rulesInit(
Int(settings.height),
Int(settings.width),
Int(settings.alignedTokens)
) else { return nil }
guard let player1 = Self.playerOf(settings: player1, id: .player1) else { return nil }
guard let player2 = Self.playerOf(settings: player2, id: .player2) else { return nil }
self.game = try! Game(withRules: rules, andPlayer1: player1, andPlayer2: player2)
self.game = game
self.player1 = PlayerVM(inner: player1)
self.player2 = PlayerVM(inner: player2)
self.player1 = PlayerVM(inner: game.players[.player1]!)
self.player2 = PlayerVM(inner: game.players[.player2]!)
self.currentBoard = self.game.board
@ -117,6 +103,26 @@ class IngameVM: ObservableObject {
}
}
}
convenience init(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) {
let rulesInit: (Int, Int, Int) -> (any Rules)? = switch (settings.rulesType) {
case .Classic: Connect4Rules.init
case .TicTacToe: TicTacToeRules.init
case .PopOut: PopOutRules.init
}
let rules = rulesInit(
Int(settings.height),
Int(settings.width),
Int(settings.alignedTokens)
)!
let player1 = Self.playerOf(settings: player1, id: .player1)!
let player2 = Self.playerOf(settings: player2, id: .player2)!
let game = try! Game(withRules: rules, andPlayer1: player1, andPlayer2: player2)
self.init(game: game)
}
public func start() {
if running { return }

Loading…
Cancel
Save