changeset 3:4dbe0cd9dadc

first game prototype
author Dennis C. M. <dennis@denniscm.com>
date Thu, 22 Sep 2022 10:42:39 +0200
parents 5b7c89bd45c3
children de54f05adb78
files GeoQuiz.xcodeproj/project.pbxproj GeoQuiz/GuessTheCapitalView.swift GeoQuiz/GuessTheFlagView.swift GeoQuiz/Helpers/GameAlertsModifier.swift GeoQuiz/Helpers/GameToolbar.swift GeoQuiz/Logic/Game.swift GeoQuiz/Logic/GameProtocol.swift GeoQuiz/Logic/GuessTheCapital.swift GeoQuiz/Logic/GuessTheFlag.swift GeoQuiz/Tests/Animation.swift GeoQuiz/Tests/ClassTest.swift GeoQuiz/Tests/ProtocolTest.swift GeoQuiz/Tests/Protocols.swift
diffstat 13 files changed, 487 insertions(+), 304 deletions(-) [+]
line wrap: on
line diff
--- a/GeoQuiz.xcodeproj/project.pbxproj	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz.xcodeproj/project.pbxproj	Thu Sep 22 10:42:39 2022 +0200
@@ -8,13 +8,18 @@
 
 /* Begin PBXBuildFile section */
 		95030CEA28D1BA4D001AA3A1 /* AnswerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95030CE928D1BA4D001AA3A1 /* AnswerButton.swift */; };
+		950C59F528DADA10007C8504 /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950C59F428DADA10007C8504 /* Animation.swift */; };
+		950C59F928DB181C007C8504 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950C59F828DB181C007C8504 /* Protocols.swift */; };
+		950C59FB28DB1F73007C8504 /* ProtocolTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950C59FA28DB1F73007C8504 /* ProtocolTest.swift */; };
+		950C59FD28DB1F79007C8504 /* ClassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950C59FC28DB1F79007C8504 /* ClassTest.swift */; };
 		951B630228D1A87C004F9877 /* GuessTheCapitalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951B630128D1A87C004F9877 /* GuessTheCapitalView.swift */; };
 		951D197328D485E000671FAD /* CustomColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D197228D485E000671FAD /* CustomColors.swift */; };
+		952E41E928DC521200198643 /* GameAlertsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952E41E828DC521200198643 /* GameAlertsModifier.swift */; };
 		9539829328C51EDE00B70973 /* GeoQuizApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9539829228C51EDE00B70973 /* GeoQuizApp.swift */; };
 		9539829728C51EDF00B70973 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9539829628C51EDF00B70973 /* Assets.xcassets */; };
 		9539829A28C51EDF00B70973 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9539829928C51EDF00B70973 /* Preview Assets.xcassets */; };
 		955A658128D703EB00CEEC6D /* GameToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658028D703EB00CEEC6D /* GameToolbar.swift */; };
-		955A658328D733E400CEEC6D /* GameProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658228D733E400CEEC6D /* GameProtocol.swift */; };
+		955A658328D733E400CEEC6D /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658228D733E400CEEC6D /* Game.swift */; };
 		955A658528D73EC600CEEC6D /* GuessTheCapital.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658428D73EC600CEEC6D /* GuessTheCapital.swift */; };
 		955A65A928D7815E00CEEC6D /* Haptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A65A828D7815E00CEEC6D /* Haptics.swift */; };
 		956273EA28CB2E98008DC094 /* FlagImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956273E928CB2E98008DC094 /* FlagImage.swift */; };
@@ -42,14 +47,19 @@
 
 /* Begin PBXFileReference section */
 		95030CE928D1BA4D001AA3A1 /* AnswerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnswerButton.swift; sourceTree = "<group>"; };
+		950C59F428DADA10007C8504 /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = "<group>"; };
+		950C59F828DB181C007C8504 /* Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protocols.swift; sourceTree = "<group>"; };
+		950C59FA28DB1F73007C8504 /* ProtocolTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolTest.swift; sourceTree = "<group>"; };
+		950C59FC28DB1F79007C8504 /* ClassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTest.swift; sourceTree = "<group>"; };
 		951B630128D1A87C004F9877 /* GuessTheCapitalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCapitalView.swift; sourceTree = "<group>"; };
 		951D197228D485E000671FAD /* CustomColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColors.swift; sourceTree = "<group>"; };
+		952E41E828DC521200198643 /* GameAlertsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameAlertsModifier.swift; sourceTree = "<group>"; };
 		9539828F28C51EDE00B70973 /* GeoQuiz.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoQuiz.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		9539829228C51EDE00B70973 /* GeoQuizApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoQuizApp.swift; sourceTree = "<group>"; };
 		9539829628C51EDF00B70973 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		9539829928C51EDF00B70973 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
 		955A658028D703EB00CEEC6D /* GameToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameToolbar.swift; sourceTree = "<group>"; };
-		955A658228D733E400CEEC6D /* GameProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameProtocol.swift; sourceTree = "<group>"; };
+		955A658228D733E400CEEC6D /* Game.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = "<group>"; };
 		955A658428D73EC600CEEC6D /* GuessTheCapital.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCapital.swift; sourceTree = "<group>"; };
 		955A65A828D7815E00CEEC6D /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.swift; sourceTree = "<group>"; };
 		956273E928CB2E98008DC094 /* FlagImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagImage.swift; sourceTree = "<group>"; };
@@ -92,15 +102,26 @@
 				95C430F628D092FC00480D23 /* GameSelection.swift */,
 				95C430F828D0A8E500480D23 /* CustomGradients.swift */,
 				951D197228D485E000671FAD /* CustomColors.swift */,
-				955A658228D733E400CEEC6D /* GameProtocol.swift */,
 				95AE8D5628C8750E0067F219 /* Load.swift */,
 				955A65A828D7815E00CEEC6D /* Haptics.swift */,
+				955A658228D733E400CEEC6D /* Game.swift */,
 				95FA409B28D9881100129B60 /* GuessTheFlag.swift */,
 				955A658428D73EC600CEEC6D /* GuessTheCapital.swift */,
 			);
 			path = Logic;
 			sourceTree = "<group>";
 		};
+		950C59F328DADA01007C8504 /* Tests */ = {
+			isa = PBXGroup;
+			children = (
+				950C59F428DADA10007C8504 /* Animation.swift */,
+				950C59F828DB181C007C8504 /* Protocols.swift */,
+				950C59FA28DB1F73007C8504 /* ProtocolTest.swift */,
+				950C59FC28DB1F79007C8504 /* ClassTest.swift */,
+			);
+			path = Tests;
+			sourceTree = "<group>";
+		};
 		9520ABBA28C86D0300A3D4D7 /* Data */ = {
 			isa = PBXGroup;
 			children = (
@@ -141,6 +162,7 @@
 				959D414728C87EA600BAAC14 /* Helpers */,
 				959D414528C87E8D00BAAC14 /* Models */,
 				95030CE728D1B60F001AA3A1 /* Logic */,
+				950C59F328DADA01007C8504 /* Tests */,
 				9520ABBA28C86D0300A3D4D7 /* Data */,
 				9539829628C51EDF00B70973 /* Assets.xcassets */,
 				9539829828C51EDF00B70973 /* Preview Content */,
@@ -176,6 +198,7 @@
 				956273E928CB2E98008DC094 /* FlagImage.swift */,
 				95030CE928D1BA4D001AA3A1 /* AnswerButton.swift */,
 				955A658028D703EB00CEEC6D /* GameToolbar.swift */,
+				952E41E828DC521200198643 /* GameAlertsModifier.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -257,15 +280,19 @@
 			buildActionMask = 2147483647;
 			files = (
 				955A65A928D7815E00CEEC6D /* Haptics.swift in Sources */,
+				952E41E928DC521200198643 /* GameAlertsModifier.swift in Sources */,
 				959D415F28C8861F00BAAC14 /* CountryPopulation.swift in Sources */,
-				955A658328D733E400CEEC6D /* GameProtocol.swift in Sources */,
+				955A658328D733E400CEEC6D /* Game.swift in Sources */,
 				95C4315628C64A8C00212131 /* ContentView.swift in Sources */,
 				95C4315928C6500000212131 /* GameButton.swift in Sources */,
+				950C59F928DB181C007C8504 /* Protocols.swift in Sources */,
 				956273EA28CB2E98008DC094 /* FlagImage.swift in Sources */,
+				950C59FB28DB1F73007C8504 /* ProtocolTest.swift in Sources */,
 				959D414928C87ED900BAAC14 /* CityPopulation.swift in Sources */,
 				955A658528D73EC600CEEC6D /* GuessTheCapital.swift in Sources */,
 				959D257028C7251F00C55A5E /* BuyLivesModal.swift in Sources */,
 				959D415928C8854800BAAC14 /* CountryCities.swift in Sources */,
+				950C59F528DADA10007C8504 /* Animation.swift in Sources */,
 				951B630228D1A87C004F9877 /* GuessTheCapitalView.swift in Sources */,
 				959D415728C8851900BAAC14 /* CountryCapitals.swift in Sources */,
 				9539829328C51EDE00B70973 /* GeoQuizApp.swift in Sources */,
@@ -276,6 +303,7 @@
 				959D415D28C885ED00BAAC14 /* CountryFlags.swift in Sources */,
 				959D415B28C885C700BAAC14 /* CountryCurrencies.swift in Sources */,
 				95C430F728D092FC00480D23 /* GameSelection.swift in Sources */,
+				950C59FD28DB1F79007C8504 /* ClassTest.swift in Sources */,
 				951D197328D485E000671FAD /* CustomColors.swift in Sources */,
 				95C430F928D0A8E500480D23 /* CustomGradients.swift in Sources */,
 				95FA409A28D9876B00129B60 /* GuessTheFlagView.swift in Sources */,
--- a/GeoQuiz/GuessTheCapitalView.swift	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz/GuessTheCapitalView.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -11,13 +11,13 @@
     @Binding var gameName: GameName?
     @StateObject var game = GuessTheCapital()
     
-    var flagSymbol: String {
-        if let countryAsked = game.countries[game.countryNameAsked] {
-            return countryAsked.flagSymbol
-        } else {
-            fatalError("Couldn't find \(game.countryNameAsked) in countries")
-        }
-    }
+//    var flagSymbol: String {
+//        if let countryAsked = game.countries[game.countryNameAsked] {
+//            return countryAsked.flagSymbol
+//        } else {
+//            fatalError("Couldn't find \(game.countryNameAsked) in countries")
+//        }
+//    }
     
     var body: some View {
         ZStack {
@@ -26,44 +26,44 @@
             
             GeometryReader { geo in
                     VStack(spacing: 20) {
-                        GameToolbar(
-                            userScore: $game.userScore,
-                            userLives: $game.userLives,
-                            gameName: $gameName,
-                            showingBuyLivesView: $game.showingBuyLivesView
-                        )
+//                        GameToolbar(
+//                            userScore: $game.userScore,
+//                            userLives: $game.userLives,
+//                            gameName: $gameName,
+//                            showingBuyLivesView: $game.showingBuyLivesView
+//                        )
 
                         Spacer()
                         
-                        FlagImage(flagSymbol: flagSymbol, cornerRadius: 20)
-                            .shadow(radius: 10)
-                            .frame(height: geo.size.height * 0.15)
+//                        FlagImage(flagSymbol: flagSymbol, cornerRadius: 20)
+//                            .shadow(radius: 10)
+//                            .frame(height: geo.size.height * 0.15)
 
                         Spacer()
 
                         HStack {
-                            VStack(alignment: .leading, spacing: 10) {
-                                Text("Question \(game.questionCounter) of \(game.countries.count)")
-                                    .font(.title3)
-
-                                Text("What is the capital of \(game.countryNameAsked)?")
-                                    .font(.title)
-                                    .fontWeight(.semibold)
-                            }
-                            .foregroundColor(.white)
+//                            VStack(alignment: .leading, spacing: 10) {
+//                                Text("Question \(game.questionCounter) of \(game.countries.count)")
+//                                    .font(.title3)
+//
+//                                Text("What is the capital of \(game.countryNameAsked)?")
+//                                    .font(.title)
+//                                    .fontWeight(.semibold)
+//                            }
+//                            .foregroundColor(.white)
 
                             Spacer()
                         }
 
                         VStack {
-                            ForEach(Array(game.userChoices.values), id: \.self) { country in
-                                Button {
-                                    game.answered(userChoice: country.capitalName)
-                                } label: {
-                                    AnswerButton(optionName: country.capitalName, color: .secondary)
-                                        .frame(height: geo.size.height * 0.08)
-                                }
-                            }
+//                            ForEach(Array(game.userChoices.values), id: \.self) { country in
+//                                Button {
+//                                    game.answered(userChoice: country.capitalName)
+//                                } label: {
+//                                    AnswerButton(optionName: country.capitalName, color: .secondary)
+//                                        .frame(height: geo.size.height * 0.08)
+//                                }
+//                            }
                         }
                     }
                     .padding()
@@ -71,29 +71,29 @@
         }
         .navigationBarHidden(true)
         
-        .sheet(isPresented: $game.showingBuyLivesView) {
-            BuyLivesModal()
-        }
-
-        .alert(game.alertTitle, isPresented: $game.showingNoLivesAlert) {
-            Button("Buy lives") { game.showingBuyLivesView = true }
-            Button("Exit", role: .destructive) { gameName = nil }
-            Button("Cancel", role: .cancel) { }
-        } message: {
-            Text(game.alertMessage)
-        }
-        
-        .alert(game.alertTitle, isPresented: $game.showingWrongAnswerAlert) {
-            Button("Continue", role: .cancel) { game.askQuestion() }
-        } message: {
-            Text(game.alertMessage)
-        }
-        
-        .alert(game.alertTitle, isPresented: $game.showingEndGameAlert) {
-            Button("Exit", role: .cancel) { gameName = nil }
-        } message: {
-            Text(game.alertMessage)
-        }
+//        .sheet(isPresented: $game.showingBuyLivesView) {
+//            BuyLivesModal()
+//        }
+//
+//        .alert(game.alertTitle, isPresented: $game.showingNoLivesAlert) {
+//            Button("Buy lives") { game.showingBuyLivesView = true }
+//            Button("Exit", role: .destructive) { gameName = nil }
+//            Button("Cancel", role: .cancel) { }
+//        } message: {
+//            Text(game.alertMessage)
+//        }
+//        
+//        .alert(game.alertTitle, isPresented: $game.showingWrongAnswerAlert) {
+//            Button("Continue", role: .cancel) { game.askQuestion() }
+//        } message: {
+//            Text(game.alertMessage)
+//        }
+//        
+//        .alert(game.alertTitle, isPresented: $game.showingEndGameAlert) {
+//            Button("Exit", role: .cancel) { gameName = nil }
+//        } message: {
+//            Text(game.alertMessage)
+//        }
     }
 }
 
--- a/GeoQuiz/GuessTheFlagView.swift	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz/GuessTheFlagView.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -16,21 +16,16 @@
             LinearGradient(gradient: .main, startPoint: .top, endPoint: .bottom)
                 .ignoresSafeArea()
             
-            GeometryReader{ geo in
+            GeometryReader { geo in
                 VStack(spacing: 20) {
-                    GameToolbar(
-                        userScore: $game.userScore,
-                        userLives: $game.userLives,
-                        gameName: $gameName,
-                        showingBuyLivesView: $game.showingBuyLivesView
-                    )
+                    GameToolbar(gameName: $gameName, game: game)
                     
                     HStack {
                         VStack(alignment: .leading, spacing: 10) {
-                            Text("Question \(game.questionCounter) of \(game.countries.count)")
+                            Text("Question \(game.questionCounter) of \(game.data.count)")
                                 .font(.title3)
 
-                            Text("What is the flag of \(game.countryNameAsked)?")
+                            Text("What is the flag of \(game.correctAnswer.key)?")
                                 .font(.title)
                                 .fontWeight(.semibold)
                         }
@@ -41,11 +36,11 @@
                     
                     Spacer()
                     
-                    ForEach(Array(game.userChoices.values), id: \.self) { flagSymbol in
+                    ForEach(Array(game.userChoices.keys), id: \.self) { countryName in
                         Button {
-                            game.answered(userChoice: flagSymbol)
+                            game.answer((key: countryName, value: game.data[countryName]!))
                         } label: {
-                            FlagImage(flagSymbol: flagSymbol, cornerRadius: 20)
+                            FlagImage(flagSymbol: game.data[countryName]!, cornerRadius: 20)
                                 .shadow(radius: 10)
                                 .frame(height: geo.size.height * 0.15)
                         }
@@ -58,30 +53,10 @@
             }
         }
         .navigationBarHidden(true)
-        
+        .modifier(GameAlertsModifier(game: game, gameName: $gameName))
         .sheet(isPresented: $game.showingBuyLivesView) {
             BuyLivesModal()
         }
-
-        .alert(game.alertTitle, isPresented: $game.showingNoLivesAlert) {
-            Button("Buy lives") { game.showingBuyLivesView = true }
-            Button("Exit", role: .destructive) { gameName = nil }
-            Button("Cancel", role: .cancel) { }
-        } message: {
-            Text(game.alertMessage)
-        }
-        
-        .alert(game.alertTitle, isPresented: $game.showingWrongAnswerAlert) {
-            Button("Continue", role: .cancel) { game.askQuestion() }
-        } message: {
-            Text(game.alertMessage)
-        }
-        
-        .alert(game.alertTitle, isPresented: $game.showingEndGameAlert) {
-            Button("Exit", role: .cancel) { gameName = nil }
-        } message: {
-            Text(game.alertMessage)
-        }
     }
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Helpers/GameAlertsModifier.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,36 @@
+//
+//  GameAlertsModifier.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 22/9/22.
+//
+
+import SwiftUI
+
+struct GameAlertsModifier<T: Game>: ViewModifier {
+    @ObservedObject var game: T
+    @Binding var gameName: GameName?
+    
+    func body(content: Content) -> some View {
+        content
+            .alert(game.alertTitle, isPresented: $game.showingWrongAnswerAlert) {
+                Button("Continue", role: .cancel) { game.askQuestion() }
+            } message: {
+                Text(game.alertMessage)
+            }
+        
+            .alert(game.alertTitle, isPresented: $game.showingNoLivesAlert) {
+                Button("Buy lives") { game.showingBuyLivesView = true }
+                Button("Exit", role: .destructive) { gameName = nil }
+                Button("Cancel", role: .cancel) { }
+            } message: {
+                Text(game.alertMessage)
+            }
+            
+            .alert(game.alertTitle, isPresented: $game.showingEndGameAlert) {
+                Button("Exit", role: .cancel) { gameName = nil }
+            } message: {
+                Text(game.alertMessage)
+            }
+    }
+}
--- a/GeoQuiz/Helpers/GameToolbar.swift	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz/Helpers/GameToolbar.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -7,11 +7,10 @@
 
 import SwiftUI
 
-struct GameToolbar: View {
-    @Binding var userScore: Int
-    @Binding var userLives: Int
+struct GameToolbar<T: Game>: View {
     @Binding var gameName: GameName?
-    @Binding var showingBuyLivesView: Bool
+    @ObservedObject var game: T
+    
     
     var body: some View {
         HStack(spacing: 0) {
@@ -32,7 +31,7 @@
             .frame(maxWidth: .infinity, alignment: .leading)
             
             Group {
-                Text("\(userScore)")
+                Text("\(game.userScore)")
                     .padding()
                     .background(
                         Circle()
@@ -41,21 +40,23 @@
             }
             .foregroundColor(.white)
             .font(.title2)
+            .scaleEffect(game.scoreScaleAmount)
             .frame(maxWidth: .infinity, alignment: .center)
             
             Group {
                 Button {
-                    showingBuyLivesView = true
+                    game.showingBuyLivesView = true
                 } label: {
                     HStack {
                         Image(systemName: "heart.fill")
-                        Text("\(userLives)")
+                        Text("\(game.userLives)")
                     }
                     .padding(10)
                     .background(
                         Capsule()
                             .strokeBorder(lineWidth: 2)
                     )
+                    .scaleEffect(game.livesScaleAmount)
                 }
             }
             .foregroundColor(.white)
@@ -74,9 +75,8 @@
             GeometryReader { geo in
                 VStack {
                     GameToolbar(
-                        userScore: .constant(0),
-                        userLives: .constant(6),
-                        gameName: .constant(.guessTheFlag), showingBuyLivesView: .constant(false)
+                        gameName: .constant(GameName.guessTheFlag),
+                        game: GuessTheFlag()
                     )
                     
                     Spacer()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Logic/Game.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,121 @@
+//
+//  GameProtocol.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 18/9/22.
+//
+
+import Foundation
+import SwiftUI
+
+protocol Game: ObservableObject {
+    
+    // Define generic type
+    associatedtype T: Equatable
+    
+    // Game
+    var data: [String: T] { get set}
+    var dataAsked: [String] { get set }
+    var correctAnswer: (key: String, value: T) { get set }
+    
+    // User
+    var userChoices: [String: T] { get set }
+    var userScore: Int { get set }
+    var userLives: Int { get set }
+    
+    // Alerts
+    var alertTitle: String { get set }
+    var alertMessage: String { get set }
+    var showingNoLivesAlert: Bool { get set }
+    var showingEndGameAlert: Bool { get set }
+    var showingWrongAnswerAlert: Bool { get set }
+    
+    // Animations
+    var scoreScaleAmount: Double { get set }
+    var livesScaleAmount: Double { get set }
+    
+    // Modal views
+    var showingBuyLivesView: Bool { get set }
+}
+
+extension Game {
+    var questionCounter: Int {
+       dataAsked.count
+    }
+    
+    func askQuestion() {
+        guard questionCounter < data.count else {
+            alertTitle = "Amazing!"
+            alertMessage = "You've completed the game."
+            showingEndGameAlert = true
+            
+            return
+        }
+        
+        // Get random choices
+        var userChoices = [String: T]()
+        
+        while userChoices.count < 2 {
+            if let choice = data.randomElement() {
+                userChoices[choice.key] = choice.value
+            } else {
+                fatalError("Couldn't get a random value from data")
+            }
+        }
+        
+        // Get question asked (correct answer)
+        let correctAnswer = data.first(where: {
+            !userChoices.keys.contains($0.key) && !dataAsked.contains($0.key)
+        })
+        
+        // Unwrap optional
+        if let correctAnswer = correctAnswer {
+            userChoices[correctAnswer.key] = correctAnswer.value
+            dataAsked.append(correctAnswer.key)
+            self.correctAnswer = correctAnswer
+        } else {
+            fatalError("Couldn't unwrap optional value")
+        }
+        
+        self.userChoices = userChoices
+    }
+    
+    func answer(_ choice: (key: String, value: T)) {
+        guard userLives > 0 else {
+            alertTitle = "Not enough lives!"
+            alertMessage = "Please buy more lives to keep playing"
+            showingNoLivesAlert = true
+
+            return
+        }
+        
+        if correctAnswer == choice {
+            hapticSuccess()
+            userScore += 1
+
+            withAnimation(.easeIn(duration: 0.5)) {
+                scoreScaleAmount += 1
+            }
+            
+            askQuestion()
+        } else {
+            hapticError()
+            userLives -= 1
+
+            withAnimation(.easeIn(duration: 0.5)) {
+                livesScaleAmount += 1
+            }
+
+            alertTitle = "Wrong!"
+            alertMessage = "You have \(userLives) lives left"
+            showingWrongAnswerAlert = true
+        }
+        
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [self] in
+            withAnimation(.easeIn(duration: 0.5)) {
+                scoreScaleAmount = 1
+                livesScaleAmount = 1
+            }
+        }
+    }
+}
--- a/GeoQuiz/Logic/GameProtocol.swift	Tue Sep 20 11:54:06 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-//
-//  GameProtocol.swift
-//  GeoQuiz
-//
-//  Created by Dennis Concepción Martín on 18/9/22.
-//
-
-import Foundation
-
-protocol Game {
-    var userScore: Int { get }
-    var userLives: Int { get }
-    var questionCounter: Int { get }
-    var alertTitle: String { get }
-    var alertMessage: String { get }
-    var showingBuyLivesView: Bool { get set }
-    var showingNoLivesAlert: Bool { get set }
-    var showingWrongAnswerAlert: Bool { get set }
-    var showingEndGameAlert: Bool { get set }
-    
-    func askQuestion()
-    func answered(userChoice: String)
-    
-}
--- a/GeoQuiz/Logic/GuessTheCapital.swift	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz/Logic/GuessTheCapital.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -7,109 +7,111 @@
 
 import Foundation
 
-class GuessTheCapital: Game, ObservableObject {
-    
-    struct Country: Hashable {
-        let capitalName: String
-        let flagSymbol: String
-    }
-    
-    let countries: [String: Country]
-    var countriesAsked = [String: Country]()
-    
-    @Published var userScore = 0
-    @Published var userLives = 3
-    @Published var questionCounter = 0
-    @Published var alertTitle = ""
-    @Published var alertMessage = ""
-    @Published var showingBuyLivesView = false
-    @Published var showingNoLivesAlert = false
-    @Published var showingWrongAnswerAlert = false
-    @Published var showingEndGameAlert = false
-    
-    @Published var userChoices = [String: Country]()
-    @Published var countryNameAsked = ""
-    
-    init() {
-        let flags: CountryFlags = load("CountryFlags.json")
-        let capitals: CountryCapitals = load("CountryCapitals.json")
-        
-        var countries = [String: Country]()
-        
-        for country in capitals.countries {
-            let countryName = country.key
-            let capitalName = country.value
-            
-            if let flagSymbol = flags.countries[countryName] {
-                countries[country.key] = Country(capitalName: capitalName, flagSymbol: flagSymbol)
-            } else {
-                fatalError()
-            }
-        }
-        
-        self.countries = countries
-        askQuestion()
-    }
+class GuessTheCapital: ObservableObject {
     
-    func askQuestion() {
-        guard questionCounter < countries.count else {
-            self.alertTitle = "Amazing!"
-            self.alertMessage = "You've completed the game."
-            self.showingEndGameAlert = true
-            
-            return
-        }
-        
-        var userChoices = [String: Country]()
-        
-        while userChoices.count < 2 {
-            if let country = countries.randomElement() {
-                userChoices[country.key] = country.value
-            } else {
-                fatalError("Couldn't get a random country")
-            }
-        }
-        
-        let countryAsked = countries.first(where: {
-            !userChoices.keys.contains($0.key) &&
-            !countriesAsked.keys.contains($0.key)
-        })
-        
-        if let countryAsked = countryAsked {
-            userChoices[countryAsked.key] = countryAsked.value
-            self.countriesAsked[countryAsked.key] = countryAsked.value
-            self.countryNameAsked = countryAsked.key
-        } else {
-            fatalError("Couldn't get countryAsked")
-        }
-        
-        self.userChoices = userChoices
-        self.questionCounter += 1
-    }
-    
-    func answered(userChoice userCapitalNameGuess: String) {
-        guard let correctCountry = countries[countryNameAsked] else {
-            fatalError("Couln't find \(countryNameAsked) in countries dictionary")
-        }
-        
-        guard userLives > 0 else {
-            self.alertTitle = "Not enough lives!"
-            self.alertMessage = "Please buy more lives to keep playing"
-            self.showingNoLivesAlert = true
-
-            return
-        }
-        
-        if correctCountry.capitalName == userCapitalNameGuess {
-            hapticSuccess()
-            self.userScore += 1
-            askQuestion()
-        } else {
-            hapticError()
-            self.userLives -= 1
-            self.alertTitle = "Wrong!"
-            self.alertMessage = "The capital of \(countryNameAsked) is \(correctCountry.capitalName). You have \(userLives) lives left"
-            self.showingWrongAnswerAlert = true
-        }
-    }
+//    struct Country: Hashable {
+//        let capitalName: String
+//        let flagSymbol: String
+//    }
+//
+//    let countries: [String: Country]
+//    var countriesAsked = [String: Country]()
+//
+//    @Published var userScore = 0
+//    @Published var userLives = 3
+//    @Published var questionCounter = 0
+//    @Published var alertTitle = ""
+//    @Published var alertMessage = ""
+//    @Published var scoreScaleAmount = 1.0
+//    @Published var livesScaleAmount = 1.0
+//    @Published var showingBuyLivesView = false
+//    @Published var showingNoLivesAlert = false
+//    @Published var showingWrongAnswerAlert = false
+//    @Published var showingEndGameAlert = false
+//
+//    @Published var userChoices = [String: Country]()
+//    @Published var countryNameAsked = ""
+//
+//    init() {
+//        let flags: CountryFlags = load("CountryFlags.json")
+//        let capitals: CountryCapitals = load("CountryCapitals.json")
+//
+//        var countries = [String: Country]()
+//
+//        for country in capitals.countries {
+//            let countryName = country.key
+//            let capitalName = country.value
+//
+//            if let flagSymbol = flags.countries[countryName] {
+//                countries[country.key] = Country(capitalName: capitalName, flagSymbol: flagSymbol)
+//            } else {
+//                fatalError()
+//            }
+//        }
+//
+//        self.countries = countries
+//        askQuestion()
+//    }
+//
+//    func askQuestion() {
+//        guard questionCounter < countries.count else {
+//            self.alertTitle = "Amazing!"
+//            self.alertMessage = "You've completed the game."
+//            self.showingEndGameAlert = true
+//
+//            return
+//        }
+//
+//        var userChoices = [String: Country]()
+//
+//        while userChoices.count < 2 {
+//            if let country = countries.randomElement() {
+//                userChoices[country.key] = country.value
+//            } else {
+//                fatalError("Couldn't get a random country")
+//            }
+//        }
+//
+//        let countryAsked = countries.first(where: {
+//            !userChoices.keys.contains($0.key) &&
+//            !countriesAsked.keys.contains($0.key)
+//        })
+//
+//        if let countryAsked = countryAsked {
+//            userChoices[countryAsked.key] = countryAsked.value
+//            self.countriesAsked[countryAsked.key] = countryAsked.value
+//            self.countryNameAsked = countryAsked.key
+//        } else {
+//            fatalError("Couldn't get countryAsked")
+//        }
+//
+//        self.userChoices = userChoices
+//        self.questionCounter += 1
+//    }
+//
+//    func answered(userChoice userCapitalNameGuess: String) {
+//        guard let correctCountry = countries[countryNameAsked] else {
+//            fatalError("Couln't find \(countryNameAsked) in countries dictionary")
+//        }
+//
+//        guard userLives > 0 else {
+//            self.alertTitle = "Not enough lives!"
+//            self.alertMessage = "Please buy more lives to keep playing"
+//            self.showingNoLivesAlert = true
+//
+//            return
+//        }
+//
+//        if correctCountry.capitalName == userCapitalNameGuess {
+//            hapticSuccess()
+//            self.userScore += 1
+//            askQuestion()
+//        } else {
+//            hapticError()
+//            self.userLives -= 1
+//            self.alertTitle = "Wrong!"
+//            self.alertMessage = "The capital of \(countryNameAsked) is \(correctCountry.capitalName). You have \(userLives) lives left"
+//            self.showingWrongAnswerAlert = true
+//        }
+//    }
 }
--- a/GeoQuiz/Logic/GuessTheFlag.swift	Tue Sep 20 11:54:06 2022 +0200
+++ b/GeoQuiz/Logic/GuessTheFlag.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -6,89 +6,39 @@
 //
 
 import Foundation
+import SwiftUI
 
-class GuessTheFlag: Game, ObservableObject {
-    let countries: [String: String]
-    var countriesAsked = [String: String]()
+class GuessTheFlag: Game, ObservableObject {    
+
+    // Define type of generics
+    var data: [String: String]
+    var dataAsked = [String]()
     
+    // Data
+    @Published var correctAnswer = (key: String(), value: String())
+    
+    // User
+    @Published var userChoices = [String: String]()
     @Published var userScore = 0
     @Published var userLives = 3
-    @Published var questionCounter = 0
-    @Published var alertTitle = ""
-    @Published var alertMessage = ""
-    @Published var showingBuyLivesView = false
+    
+    // Alerts
+    @Published var alertTitle = String()
+    @Published var alertMessage = String()
     @Published var showingNoLivesAlert = false
+    @Published var showingEndGameAlert = false
     @Published var showingWrongAnswerAlert = false
-    @Published var showingEndGameAlert = false
     
-    @Published var userChoices = [String: String]()
-    @Published var countryNameAsked = ""
+    // Animations
+    @Published var scoreScaleAmount = 1.0
+    @Published var livesScaleAmount = 1.0
+    
+    // Modal views
+    @Published var showingBuyLivesView = false
     
     init() {
         let flags: CountryFlags = load("CountryFlags.json")
-        self.countries = flags.countries
+        data = flags.countries
         askQuestion()
     }
-    
-    func askQuestion() {
-        guard questionCounter < countries.count else {
-            self.alertTitle = "Amazing!"
-            self.alertMessage = "You've completed the game."
-            self.showingEndGameAlert = true
-            
-            return
-        }
-        
-        var userChoices = [String: String]()
-        
-        while userChoices.count < 2 {
-            if let country = countries.randomElement() {
-                userChoices[country.key] = country.value
-            } else {
-                fatalError("Couldn't get a random country")
-            }
-        }
-        
-        let countryAsked = countries.first(where: {
-            !userChoices.keys.contains($0.key) &&
-            !countriesAsked.keys.contains($0.key)
-        })
-        
-        if let countryAsked = countryAsked {
-            userChoices[countryAsked.key] = countryAsked.value
-            self.countriesAsked[countryAsked.key] = countryAsked.value
-            self.countryNameAsked = countryAsked.key
-        } else {
-            fatalError("Couldn't get countryAsked")
-        }
-        
-        self.userChoices = userChoices
-        self.questionCounter += 1
-    }
-    
-    func answered(userChoice userFlagSymbolGuess: String) {
-        guard let correctFlagSymbolAnswer = countries[countryNameAsked] else {
-            fatalError("Couln't find \(countryNameAsked) in countries dictionary")
-        }
-        
-        guard userLives > 0 else {
-            self.alertTitle = "Not enough lives!"
-            self.alertMessage = "Please buy more lives to keep playing"
-            self.showingNoLivesAlert = true
-
-            return
-        }
-        
-        if correctFlagSymbolAnswer == userFlagSymbolGuess {
-            hapticSuccess()
-            self.userScore += 1
-            askQuestion()
-        } else {
-            hapticError()
-            self.userLives -= 1
-            self.alertTitle = "Wrong!"
-            self.alertMessage = "That's not the flag of \(countryNameAsked). You have \(userLives) lives left"
-            self.showingWrongAnswerAlert = true
-        }
-    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Tests/Animation.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,40 @@
+//
+//  Animation.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 21/9/22.
+//
+
+import SwiftUI
+
+struct Animation: View {
+    @State private var amount = 1.0
+    
+    var body: some View {
+        Button {
+            withAnimation(.easeIn(duration: 0.5)) {
+                amount += 1
+            }
+            
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                withAnimation(.easeIn(duration: 0.5)) {
+                    amount = 1
+                }
+            }
+        } label: {
+            Circle()
+                .overlay(
+                    Text("Button")
+                        .foregroundColor(.white)
+                )
+        }
+        .frame(height: 100)
+        .scaleEffect(amount)
+    }
+}
+
+struct Animation_Previews: PreviewProvider {
+    static var previews: some View {
+        Animation()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Tests/ClassTest.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,8 @@
+//
+//  ClassTest.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 21/9/22.
+//
+
+import Foundation
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Tests/ProtocolTest.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,23 @@
+//
+//  ProtocolTest.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 21/9/22.
+//
+
+import Foundation
+
+//protocol MyProtocol {
+//    associatedtype T
+//    
+//    var myVar: T { get set }
+//    func sayHello()
+//}
+//
+//extension MyProtocol {
+//    func sayHello() {
+//        print("Hello")
+//    }
+//}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Tests/Protocols.swift	Thu Sep 22 10:42:39 2022 +0200
@@ -0,0 +1,24 @@
+//
+//  Protocols.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 21/9/22.
+//
+
+import Foundation
+
+//protocol MyProtocol {
+//    associatedtype T
+//    
+//    var myVar: T { get set }
+//}
+//
+//class MyClass {
+//    typealias T = String
+//    
+//    let myVar: T
+//    
+//    init() {
+//        myVar = "Hello, world!"
+//    }
+//}