From a338a62f2733e382098f899454d1fe0ae1aaa787 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Mon, 18 Nov 2024 10:34:29 +0800 Subject: [PATCH] game: gameplay + database interim 2 --- ColourMeOKGame/Assets/Scripts/Backend.cs | 84 +++++++++++++++++-- ColourMeOKGame/Assets/Scripts/GameManager.cs | 18 +++- ColourMeOKGame/Assets/Scripts/Gameplay.cs | 38 +++++---- .../Assets/Scripts/LocalPlayerData.cs | 50 +++++++---- ColourMeOKGame/Assets/Scripts/SideViewUI.cs | 7 -- ColourMeOKGame/Assets/Scripts/UIManager.cs | 10 +++ 6 files changed, 163 insertions(+), 44 deletions(-) diff --git a/ColourMeOKGame/Assets/Scripts/Backend.cs b/ColourMeOKGame/Assets/Scripts/Backend.cs index cc99b08..3daf4e4 100644 --- a/ColourMeOKGame/Assets/Scripts/Backend.cs +++ b/ColourMeOKGame/Assets/Scripts/Backend.cs @@ -515,8 +515,44 @@ public void ForgotPassword(string email, Action callback) /// public void GetRecentScores(Action> callback) { - // TODO: implement this - callback(DatabaseTransactionResult.Error, new List(0)); + if (!Status.Equals(FirebaseConnectionStatus.Connected)) return; + + if (_user == null) + { + callback(DatabaseTransactionResult.Unauthenticated, new List(0)); + return; + } + + _db.Child("scores") + .OrderByChild("timestamp") + .LimitToLast(LocalPlayerData.MaxBestOnlineScores) + .GetValueAsync() + .ContinueWithOnMainThread(task => + { + if (!task.IsCompletedSuccessfully) + { + Debug.LogError(task.Exception); + callback(DatabaseTransactionResult.Error, new List(0)); + return; + } + + var scores = new List(); + foreach (var child in task.Result.Children) + { + try + { + var score = new LocalPlayerData.Score(child.Value as Dictionary); + scores.Add(score); + } + catch (Exception e) + { + Debug.LogError(e); + } + } + + callback(DatabaseTransactionResult.Ok, scores); + GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + }); } /// @@ -528,8 +564,44 @@ public void GetRecentScores(Action public void GetBestScores(Action> callback) { - // TODO: implement this - callback(DatabaseTransactionResult.Error, new List(0)); + if (!Status.Equals(FirebaseConnectionStatus.Connected)) return; + + if (_user == null) + { + callback(DatabaseTransactionResult.Unauthenticated, new List(0)); + return; + } + + _db.Child("scores") + .OrderByChild("avgPerceivedAccuracy") + .LimitToLast(LocalPlayerData.MaxBestOnlineScores) + .GetValueAsync() + .ContinueWithOnMainThread(task => + { + if (!task.IsCompletedSuccessfully) + { + Debug.LogError(task.Exception); + callback(DatabaseTransactionResult.Error, new List(0)); + return; + } + + var scores = new List(); + foreach (var child in task.Result.Children) + { + try + { + var score = new LocalPlayerData.Score(child.Value as Dictionary); + scores.Add(score); + } + catch (Exception e) + { + Debug.LogError(e); + } + } + + callback(DatabaseTransactionResult.Ok, scores); + GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + }); } /// @@ -588,7 +660,7 @@ public void GetBestScores(Action LocalPlayerData.MaxBestOnlineScores) recentScoreQueue.Dequeue(); + while (recentScoreQueue.Count > LocalPlayerData.MaxRecentLocalScores) recentScoreQueue.Dequeue(); GetBestScores((bestRes, bestScores) => { @@ -600,7 +672,7 @@ public void GetBestScores(Action LocalPlayerData.MaxBestOnlineScores) bestScoreQueue.Dequeue(); callback(DatabaseTransactionResult.Ok, GameManager.Instance.Data.CalculateUserRating()); diff --git a/ColourMeOKGame/Assets/Scripts/GameManager.cs b/ColourMeOKGame/Assets/Scripts/GameManager.cs index d702e8a..5c8f6d0 100644 --- a/ColourMeOKGame/Assets/Scripts/GameManager.cs +++ b/ColourMeOKGame/Assets/Scripts/GameManager.cs @@ -34,6 +34,11 @@ public class GameManager : MonoBehaviour /// public Backend Backend; + /// + /// gameplay object for handling game loop + /// + public Gameplay Gameplay; + /// /// read-only property for accessing the local player data outside this class /// @@ -121,6 +126,8 @@ private void OnEnable() ? DisplayStyle.Flex : DisplayStyle.None; }); + + Gameplay = new Gameplay(ui.UI); } /// @@ -146,7 +153,7 @@ public void RegisterOnLocalPlayerDataChangeCallback(Action call /// /// function to fire all local player data change callbacks /// - private void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data) + public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data) { Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})"); foreach (var callback in _onLocalPlayerDataChangeCallbacks) @@ -194,6 +201,7 @@ public void SignalGameEnd(List playedRounds) var roundLightnessAcc = 0d; var roundChromaAcc = 0d; var roundHueAcc = 0d; + var roundPerceivedAcc = 0d; var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white); @@ -203,11 +211,13 @@ public void SignalGameEnd(List playedRounds) roundLightnessAcc += distance.dL / maxDistance.dL; roundChromaAcc += distance.dC / maxDistance.dC; roundHueAcc += distance.dH / maxDistance.dH; + roundPerceivedAcc += distance.dE / maxDistance.dE; } roundLightnessAcc /= playedRounds.Count; roundChromaAcc /= playedRounds.Count; roundHueAcc /= playedRounds.Count; + roundPerceivedAcc /= playedRounds.Count; var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3; @@ -223,9 +233,12 @@ public void SignalGameEnd(List playedRounds) playedRounds.Count, (float)roundLightnessAcc, (float)roundChromaAcc, - (float)roundHueAcc); + (float)roundHueAcc, + (float)roundPerceivedAcc); _data.RegisterLocalScore(score); + FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + Backend.SubmitScore(score, submitRes => { @@ -242,6 +255,7 @@ public void SignalGameEnd(List playedRounds) { Debug.Log("couldn't calculate user rating"); TransitionToResultsView(_data.CalculateUserRating()); + FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); return; } diff --git a/ColourMeOKGame/Assets/Scripts/Gameplay.cs b/ColourMeOKGame/Assets/Scripts/Gameplay.cs index 522a10d..446f46a 100644 --- a/ColourMeOKGame/Assets/Scripts/Gameplay.cs +++ b/ColourMeOKGame/Assets/Scripts/Gameplay.cs @@ -42,16 +42,23 @@ public class Gameplay /// /// game round counter /// - private int _round = -1; + public int Round = -1; - public int RoundsPerGame = 5; - public double SecondsPerRound = 15d; + /// + /// singleton instance of the gameplay class + /// + private const int RoundsPerGame = 5; + + /// + /// seconds per round + /// + private const double SecondsPerRound = 15d; /// /// constructor for the gameplay class /// /// the visual element object for the ui document with the GameView - private Gameplay(VisualElement ui) + public Gameplay(VisualElement ui) { _roundText = ui.Q private void AdvanceToNextRound() { - if (_round > 0) StoreRoundInfo(); + if (Round > 0) StoreRoundInfo(); GenerateNewTemplateColour(); - if (_round < RoundsPerGame) + if (Round < RoundsPerGame) { - _round++; + Debug.Log("round advance"); + Round++; _countdownDatetime = DateTime.Now.AddSeconds(SecondsPerRound); } else { // end game - _round = -1; + Round = -1; GameManager.Instance.SignalGameEnd(_playedRounds); } } @@ -109,6 +117,7 @@ private void StoreRoundInfo() TemplateColour = _templateColour.style.backgroundColor.value, ResponseColour = _responseColour.style.backgroundColor.value }); + Debug.Log("stored round info"); } /// @@ -130,6 +139,7 @@ private void GenerateNewTemplateColour() ); _templateColour.style.backgroundColor = new StyleColor(colour); + Debug.Log($"generated new template colour {colour}"); } /// @@ -138,7 +148,7 @@ private void GenerateNewTemplateColour() private void Render() { var remaining = (_countdownDatetime - DateTime.Now).TotalSeconds; - _roundText.text = $"{_round}/{RoundsPerGame}"; + _roundText.text = $"{Round}/{RoundsPerGame}"; _countdownText.text = $"{remaining:F}"; } diff --git a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs index 21b0c6b..868b4ec 100644 --- a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs +++ b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs @@ -83,9 +83,10 @@ public void LoadFromTheWorld(Action callback) var l = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", -1f); var c = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", -1f); var h = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", -1f); + var e = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", -1f); // if any of the values are invalid, don't add the score - if (noOfRounds < 0 || l < 0 || c < 0 || h < 0) continue; + if (noOfRounds < 0 || l < 0 || c < 0 || h < 0 || e < 0) continue; RegisterLocalScore(new Score(timestamp, Math.Max(1, noOfRounds), Math.Clamp(l, 0f, 100f), Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f))); @@ -125,6 +126,7 @@ public void SaveToTheWorld() PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", score.AvgLightnessAccuracy); PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", score.AvgChromaAccuracy); PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", score.AvgHueAccuracy); + PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", score.AvgPerceivedAccuracy); idx++; } @@ -216,6 +218,11 @@ public struct Score /// average hue accuracy across all rounds (0-100) /// public readonly float AvgHueAccuracy; + + /// + /// average perceived accuracy across all rounds (0-100) + /// + public readonly float AvgPerceivedAccuracy; /// /// constructor for the score struct @@ -225,35 +232,47 @@ public struct Score /// average lightness accuracy across all rounds (0-100) /// average chroma accuracy across all rounds (0-100) /// average hue accuracy across all rounds (0-100) + /// /// average perceived accuracy across all rounds (0-100) public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f, - float h = 100.0f) + float h = 100.0f, float e = 100.0f) { Timestamp = timestamp; NoOfRounds = noOfRounds; AvgLightnessAccuracy = l; AvgChromaAccuracy = c; AvgHueAccuracy = h; + AvgPerceivedAccuracy = e; } /// /// dict-based constructor for the score struct /// /// dictionary of the score data + /// thrown if the dictionary is malformed or missing data public Score(Dictionary data) { // try to safely construct the score from a backend-provided dictionary - // for each value, if it's not found, or not a valid value, set it to a default value - Timestamp = data.ContainsKey("timestamp") && data["timestamp"] is long t - ? DateTimeOffset.FromUnixTimeSeconds(t).DateTime - : DateTime.MinValue; - NoOfRounds = data.ContainsKey("noOfRounds") && data["noOfRounds"] is int n ? n : 1; - AvgLightnessAccuracy = data.ContainsKey("avgLightnessAccuracy") && data["avgLightnessAccuracy"] is float l - ? l - : 100.0f; - AvgChromaAccuracy = data.ContainsKey("avgChromaAccuracy") && data["avgChromaAccuracy"] is float c - ? c - : 100.0f; - AvgHueAccuracy = data.ContainsKey("avgHueAccuracy") && data["avgHueAccuracy"] is float h ? h : 100.0f; + // for each value, if it's not found, or not a valid value, throw an exception + + if (!data.ContainsKey("timestamp") || !(data["timestamp"] is long timestamp)) + throw new ArgumentException("timestamp not found or invalid"); + if (!data.ContainsKey("noOfRounds") || !(data["noOfRounds"] is int noOfRounds)) + throw new ArgumentException("noOfRounds not found or invalid"); + if (!data.ContainsKey("avgLightnessAccuracy") || !(data["avgLightnessAccuracy"] is float avgLightnessAccuracy)) + throw new ArgumentException("avgLightnessAccuracy not found or invalid"); + if (!data.ContainsKey("avgChromaAccuracy") || !(data["avgChromaAccuracy"] is float avgChromaAccuracy)) + throw new ArgumentException("avgChromaAccuracy not found or invalid"); + if (!data.ContainsKey("avgHueAccuracy") || !(data["avgHueAccuracy"] is float avgHueAccuracy)) + throw new ArgumentException("avgHueAccuracy not found or invalid"); + if (!data.ContainsKey("avgPerceivedAccuracy") || !(data["avgPerceivedAccuracy"] is float avgPerceivedAccuracy)) + throw new ArgumentException("avgPerceivedAccuracy not found or invalid"); + + Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + NoOfRounds = noOfRounds; + AvgLightnessAccuracy = avgLightnessAccuracy; + AvgChromaAccuracy = avgChromaAccuracy; + AvgHueAccuracy = avgHueAccuracy; + AvgPerceivedAccuracy = avgPerceivedAccuracy; } /// @@ -268,7 +287,8 @@ public Score(Dictionary data) { "noOfRounds", NoOfRounds }, { "avgLightnessAccuracy", AvgLightnessAccuracy }, { "avgChromaAccuracy", AvgChromaAccuracy }, - { "avgHueAccuracy", AvgHueAccuracy } + { "avgHueAccuracy", AvgHueAccuracy }, + { "avgPerceivedAccuracy", AvgPerceivedAccuracy } }; } } diff --git a/ColourMeOKGame/Assets/Scripts/SideViewUI.cs b/ColourMeOKGame/Assets/Scripts/SideViewUI.cs index b6813d5..698440d 100644 --- a/ColourMeOKGame/Assets/Scripts/SideViewUI.cs +++ b/ColourMeOKGame/Assets/Scripts/SideViewUI.cs @@ -16,11 +16,6 @@ public class SideViewUI : MonoBehaviour /// private Label _chromaAccuracyText; - /// - /// connection status label for showing the connection status - /// - private Label _connectionStatusLabel; - /// /// text label for showing the player's stable-ish hue accuracy /// @@ -67,8 +62,6 @@ private void OnEnable() _accountButton = ui.Q