diff --git a/ColourMeOKGame/Assets/Scripts/Backend.cs b/ColourMeOKGame/Assets/Scripts/Backend.cs index 1d3bef8..170f5c1 100644 --- a/ColourMeOKGame/Assets/Scripts/Backend.cs +++ b/ColourMeOKGame/Assets/Scripts/Backend.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Firebase; using Firebase.Auth; @@ -584,9 +585,13 @@ public void GetRecentScores(Action { @@ -597,20 +602,23 @@ public void GetRecentScores(Action(); - 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}\n{child.GetRawJsonValue()}"); - } + // then sort them by timestamp + try + { + var sortedScores = task.Result.Children.Select( + score => new LocalPlayerData.Score(score.Value as Dictionary) + ) + .OrderByDescending(score => score.Timestamp) + .Take(LocalPlayerData.MaxRecentScores); - callback(TransactionResult.Ok, scores); - GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + callback(TransactionResult.Ok, sortedScores.ToList()); + GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + } + catch (Exception e) + { + Debug.LogError($"error while sorting scores by timestamp: {e}"); + callback(TransactionResult.Error, new List(0)); + } }); } @@ -631,9 +639,40 @@ private void GetBestScores(Action return; } + // old code + // _db.Child("scores") + // .OrderByChild("avgPerceivedAccuracy") + // .LimitToLast(LocalPlayerData.MaxBestScores) + // .GetValueAsync() + // .ContinueWithOnMainThread(task => + // { + // if (!task.IsCompletedSuccessfully) + // { + // Debug.LogError(task.Exception); + // callback(TransactionResult.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(TransactionResult.Ok, scores); + // GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + // }); + + // firstly, get the user's scores _db.Child("scores") - .OrderByChild("avgPerceivedAccuracy") - .LimitToLast(LocalPlayerData.MaxBestScores) + .OrderByChild("userId") + .EqualTo(_user.UserId) .GetValueAsync() .ContinueWithOnMainThread(task => { @@ -644,20 +683,27 @@ private void GetBestScores(Action 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); - } + // then sort them by how good they are + // (dL + dC + dh + de) / 4d + try + { + var sortedScores = task.Result.Children.Select( + score => new LocalPlayerData.Score(score.Value as Dictionary) + ) + .OrderByDescending(score => + (score.AvgLightnessAccuracy + score.AvgChromaAccuracy + score.AvgHueAccuracy + + score.AvgPerceivedAccuracy) / 4d + ) + .Take(LocalPlayerData.MaxBestScores); - callback(TransactionResult.Ok, scores); - GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + callback(TransactionResult.Ok, sortedScores.ToList()); + GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); + } + catch (Exception e) + { + Debug.LogError($"error while sorting scores by timestamp: {e}"); + callback(TransactionResult.Error, new List(0)); + } }); } @@ -680,7 +726,7 @@ private void GetBestScores(Action _db.Child("scores") .Push() - .SetValueAsync(score.ToDictionary()) + .SetValueAsync(score.ToDictionary(_user.UserId)) .ContinueWithOnMainThread(task => { if (task.IsCompletedSuccessfully) @@ -784,6 +830,8 @@ private void GetBestScores(Action public void GetLeaderboard( Action> callback) { + Debug.Log("getting leaderboard"); + _db.Child("users") .OrderByChild("rating") .LimitToLast(LeaderboardUI.MaxEntries) @@ -796,7 +844,7 @@ private void GetBestScores(Action callback(TransactionResult.Error, new List(0)); return; } - + var entries = new List(); foreach (var child in task.Result.Children) try diff --git a/ColourMeOKGame/Assets/Scripts/GameManager.cs b/ColourMeOKGame/Assets/Scripts/GameManager.cs index 8fca922..2ca9446 100644 --- a/ColourMeOKGame/Assets/Scripts/GameManager.cs +++ b/ColourMeOKGame/Assets/Scripts/GameManager.cs @@ -210,8 +210,9 @@ public void SignalGameEnd(List playedRounds) historicalChromaAcc /= historicalGames; historicalHueAcc /= historicalGames; historicalPerceivedAcc /= historicalGames; - - Debug.Log($"historical averages: L={historicalLightnessAcc:F2}, C={historicalChromaAcc:F2}, h={historicalHueAcc:F2}, dE={historicalPerceivedAcc:F2}"); + + Debug.Log( + $"historical averages: L={historicalLightnessAcc:F2}, C={historicalChromaAcc:F2}, h={historicalHueAcc:F2}, dE={historicalPerceivedAcc:F2}"); // calculate round averages var gameLightnessAcc = 0d; diff --git a/ColourMeOKGame/Assets/Scripts/Gameplay.cs b/ColourMeOKGame/Assets/Scripts/Gameplay.cs index 41badde..16dad5c 100644 --- a/ColourMeOKGame/Assets/Scripts/Gameplay.cs +++ b/ColourMeOKGame/Assets/Scripts/Gameplay.cs @@ -73,6 +73,7 @@ public Gameplay(VisualElement ui) public void StartGame() { Round = 0; + _playedRounds.Clear(); AdvanceToNextRound(); } diff --git a/ColourMeOKGame/Assets/Scripts/LeaderboardUI.cs b/ColourMeOKGame/Assets/Scripts/LeaderboardUI.cs index 38cfb48..6f39ec5 100644 --- a/ColourMeOKGame/Assets/Scripts/LeaderboardUI.cs +++ b/ColourMeOKGame/Assets/Scripts/LeaderboardUI.cs @@ -28,7 +28,7 @@ public class LeaderboardUI : MonoBehaviour /// reference to the leaderboard scroll view /// private ScrollView _leaderboardScrollView; - + /// /// register callbacks /// @@ -38,7 +38,7 @@ private void OnEnable() { if (newState == UIManager.DisplayState.LeaderboardView) LoadLeaderboardData(); }); - + _leaderboardScrollView = UIManager.Instance.UI.Q("LeaderboardListContent"); if (_leaderboardScrollView == null) throw new NullReferenceException("leaderboard scroll view not found in the UI"); @@ -75,7 +75,10 @@ private void LoadLeaderboardData() /// render leaderboard data in the UI /// /// message to display in the leaderboard in lieu of actual data - /// thrown when the leaderboard scroll view is missing or when the leaderboard entry + /// + /// thrown when the leaderboard scroll view is missing or when the leaderboard + /// entry + /// private void RenderLeaderboardData(string message = "") { _leaderboardScrollView.Clear(); @@ -88,7 +91,8 @@ private void RenderLeaderboardData(string message = "") } Debug.Log($"rendering {_leaderboardData.Count} leaderboard entries"); - foreach (var (entry, index) in _leaderboardData.Take(MaxEntries).Reverse().Select((entry, index) => (entry, index))) + foreach (var (entry, index) in _leaderboardData.Take(MaxEntries).Reverse() + .Select((entry, index) => (entry, index))) _leaderboardScrollView.Add(BuildEntryElement((index + 1).ToString(), entry.Username, $"{entry.Rating:F3}")); } @@ -133,7 +137,7 @@ public LeaderboardEntry(Dictionary data) { if (!data.ContainsKey("username") || data["username"] is not string username) throw new ArgumentException("data['username'] not found or invalid"); - + Username = username; Rating = LocalPlayerData.GetFloatyKey(data, "rating"); } diff --git a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs index 18bb905..ca2b3d9 100644 --- a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs +++ b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs @@ -186,29 +186,21 @@ public float CalculateUserRating() var recentScores = RecentOnlineScores.Count > 0 ? RecentOnlineScores : RecentLocalScores; var bestScores = BestOnlineScores; - var scores = 0; var totalRating = 0d; - foreach (var score in recentScores.Take(10)) - { + foreach (var score in recentScores.Take(MaxRecentScores)) totalRating += (score.AvgLightnessAccuracy + score.AvgChromaAccuracy + score.AvgHueAccuracy + score.AvgPerceivedAccuracy) / 4d; - scores++; - } - foreach (var score in bestScores.Take(20)) - { + foreach (var score in bestScores.Take(MaxBestScores)) totalRating += (score.AvgLightnessAccuracy + score.AvgChromaAccuracy + score.AvgHueAccuracy + score.AvgPerceivedAccuracy) / 4d; - scores++; - } - var rating = UserRatingScalingF(totalRating /= Math.Max(1, scores)); + var rating = UserRatingScalingF(totalRating /= MaxRecentScores + MaxBestScores); Debug.Log($"locally calculated user rating: lin: {totalRating} -> exp: {rating}"); return (float)rating; } - /// /// safely get a float value from a dictionary @@ -217,7 +209,7 @@ public float CalculateUserRating() /// the key to get the value from /// the float value /// thrown if the key is not found, or the value is not a valid float - public static float GetFloatyKey(Dictionary data, string key) + public static float GetFloatyKey(Dictionary data, string key) { if (!data.TryGetValue(key, out var possibleFloat)) throw new ArgumentException($"{key} not found"); return possibleFloat switch @@ -292,9 +284,9 @@ public Score(Dictionary data) // for each value, if it's not found, or not a valid value, throw an exception if (!data.ContainsKey("timestamp") || data["timestamp"] is not long timestamp) - throw new ArgumentException("timestamp not found or invalid"); + throw new ArgumentException("data['timestamp'] not found or invalid"); if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds) - throw new ArgumentException("noOfRounds not found or invalid"); + throw new ArgumentException("data['noOfRounds'] not found or invalid"); var avgLightnessAccuracy = GetFloatyKey(data, "avgLightnessAccuracy"); var avgChromaAccuracy = GetFloatyKey(data, "avgChromaAccuracy"); @@ -325,5 +317,23 @@ public Score(Dictionary data) { "avgPerceivedAccuracy", AvgPerceivedAccuracy } }; } + + /// + /// converts the score struct to a dictionary safe for the backend + /// + /// + public Dictionary ToDictionary(string userId) + { + return new Dictionary + { + { "userId", userId }, + { "timestamp", new DateTimeOffset(Timestamp).ToUnixTimeSeconds() }, + { "noOfRounds", NoOfRounds }, + { "avgLightnessAccuracy", AvgLightnessAccuracy }, + { "avgChromaAccuracy", AvgChromaAccuracy }, + { "avgHueAccuracy", AvgHueAccuracy }, + { "avgPerceivedAccuracy", AvgPerceivedAccuracy } + }; + } } } \ No newline at end of file diff --git a/ColourMeOKGame/Assets/Scripts/UIManager.cs b/ColourMeOKGame/Assets/Scripts/UIManager.cs index 1ae5f82..063e6cb 100644 --- a/ColourMeOKGame/Assets/Scripts/UIManager.cs +++ b/ColourMeOKGame/Assets/Scripts/UIManager.cs @@ -94,7 +94,7 @@ public void RegisterOnDisplayStateChangeCallback(Action("AccountSection").style.display = DisplayStyle.Flex; UI.Q("ConnectionStatus").style.display = DisplayStyle.Flex; } - + FireOnDisplayStateChange(state, newDisplayState); state = newDisplayState; } diff --git a/ColourMeOKGame/Assets/UI/GameUI.uss b/ColourMeOKGame/Assets/UI/GameUI.uss index 791e902..1626948 100644 --- a/ColourMeOKGame/Assets/UI/GameUI.uss +++ b/ColourMeOKGame/Assets/UI/GameUI.uss @@ -127,8 +127,7 @@ #AccountButtons > Button { #LeaderboardEntry Label, #LeaderboardEntryReference Label, -#LeaderboardEntryHeader Label -{ +#LeaderboardEntryHeader Label { margin: 0; padding: 0; font-size: 26px; @@ -149,7 +148,6 @@ #LeaderboardEntryHeader { flex-grow: 0; } -#LeaderboardEntryHeader -{ +#LeaderboardEntryHeader { border-top-width: 1px; } diff --git a/ColourMeOKGame/Assets/UI/GameUI.uxml b/ColourMeOKGame/Assets/UI/GameUI.uxml index 437aded..b3a07c0 100644 --- a/ColourMeOKGame/Assets/UI/GameUI.uxml +++ b/ColourMeOKGame/Assets/UI/GameUI.uxml @@ -1,149 +1,271 @@ - -