game: gameplay loop works but inaccurate result calcs

This commit is contained in:
Mark Joshwel 2024-11-19 01:36:19 +08:00
parent 5e1defa793
commit e1aee5d946
7 changed files with 360 additions and 185 deletions

View file

@ -67,6 +67,12 @@ public class AccountUI : MonoBehaviour
/// </summary>
private Button _secondaryActionButton;
/// <summary>
/// either 'delete local data' or 'delete account'
/// (in order of 'initial', and 'post' states, is hidden in 'after' state)
/// </summary>
private Button _tertiaryActionButton;
/// <summary>
/// username text field
/// </summary>
@ -108,6 +114,9 @@ private void OnEnable()
_secondaryActionButton = ui.Q<Button>("SecondaryActionButton");
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
_tertiaryActionButton = ui.Q<Button>("TertiaryActionButton");
_tertiaryActionButton.clicked += OnTertiaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
if (state == State.UnassociatedState) throw new Exception("unreachable state");
@ -120,7 +129,7 @@ private void OnEnable()
_usernameField.value = username;
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
});
GameManager.Instance.RegisterOnLocalPlayerDataChangeCallback(PopulateFields);
GameManager.Instance.RegisterOnLocalPlayerDataChangeCallback(OnLocalPlayerDataChangeCallback);
}
private void TransitionStateTo(State newState, bool keepAccompanyingText = false)
@ -157,9 +166,11 @@ private void TransitionStateTo(State newState, bool keepAccompanyingText = false
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
_tertiaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Continue \u2192";
_secondaryActionButton.text = "Forgot Password \u2192";
_tertiaryActionButton.text = "Delete Local Data \u2192";
break;
case State.AfterContinue:
@ -171,6 +182,7 @@ private void TransitionStateTo(State newState, bool keepAccompanyingText = false
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
_tertiaryActionButton.style.display = DisplayStyle.None;
_primaryActionButton.text = "Retry Log In \u2192";
_secondaryActionButton.text = "Create an Account \u2192";
@ -188,8 +200,10 @@ private void TransitionStateTo(State newState, bool keepAccompanyingText = false
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.None;
_tertiaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Sign Out \u2192";
_tertiaryActionButton.text = "Delete Account \u2192";
break;
case State.UnassociatedState:
@ -375,8 +389,6 @@ private void OnUsernameUpdateButtonClick()
_ => "An error occurred updating the username. Please try again."
};
});
// TODO: update lpdata
}
/// <summary>
@ -409,8 +421,6 @@ private void OnEmailUpdateButtonClick()
_ => "An error occurred updating the email. Please try again."
};
});
// TODO: update lpdata
}
/// <summary>
@ -513,7 +523,8 @@ private void OnPrimaryActionButtonClick()
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
@ -537,7 +548,7 @@ private void OnSecondaryActionButtonClick()
return;
}
GameManager.Instance.Backend.ForgotPassword(_emailField.value, result =>
GameManager.Instance.Backend.ResetUserPassword(_emailField.value, result =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text =
@ -591,6 +602,11 @@ private void OnSecondaryActionButtonClick()
_accompanyingText.text = "Invalid credentials. Please try again.";
break;
case Backend.AuthenticationResult.UsernameAlreadyTaken:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Username already taken. Please try another.";
break;
case Backend.AuthenticationResult.NonExistentUser:
case Backend.AuthenticationResult.AlreadyExistingUser:
case Backend.AuthenticationResult.GenericError:
@ -605,10 +621,33 @@ private void OnSecondaryActionButtonClick()
_usernameField.value);
break;
case State.SignedIn:
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
private void OnTertiaryActionButtonClick()
{
switch (state)
{
// delete local data
case State.NotSignedIn:
PlayerPrefs.DeleteAll();
GameManager.Instance.Data.LoadFromTheWorld(GameManager.Instance.FireLocalPlayerDataChangeCallbacks);
break;
// delete user account
case State.SignedIn:
GameManager.Instance.Backend.DeleteUser();
break;
case State.AfterContinue:
case State.UnassociatedState:
default:
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
@ -616,12 +655,13 @@ private void OnSecondaryActionButtonClick()
/// populate the fields with the given username and email,
/// used as a callback to when local player data is changed
/// </summary>
public void PopulateFields(LocalPlayerData data)
private void OnLocalPlayerDataChangeCallback(LocalPlayerData data)
{
Debug.Log(
$"populating AccountView fields with lkUsername={data.LastKnownUsername} and lkEmail={data.LastKnownEmail}");
$"updating AccountView ui with lkUsername={data.LastKnownUsername} and lkEmail={data.LastKnownEmail}");
_usernameField.value = data.LastKnownUsername;
_emailField.value = data.LastKnownEmail;
_header.text = $"Signed in as {data.LastKnownUsername}";
}
/// <summary>

View file

@ -22,6 +22,7 @@ public enum AuthenticationResult
AlreadyAuthenticated,
NonExistentUser,
AlreadyExistingUser,
UsernameAlreadyTaken,
InvalidEmail,
InvalidCredentials,
GenericError
@ -308,6 +309,26 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
if (registerUser)
{
// check if the username is already taken
_db.Child("users")
.OrderByChild("username")
.EqualTo(registeringUsername)
.GetValueAsync()
.ContinueWithOnMainThread(task =>
{
if (task.Exception != null)
{
Debug.LogError(task.Exception);
callback(AuthenticationResult.GenericError);
return;
}
if (!task.IsCompletedSuccessfully || task.Result.ChildrenCount > 0)
{
callback(AuthenticationResult.UsernameAlreadyTaken);
return;
}
// register user
_auth.CreateUserWithEmailAndPasswordAsync(email, password)
.ContinueWithOnMainThread(createTask =>
@ -372,6 +393,7 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
break;
}
});
});
return;
}
@ -443,7 +465,11 @@ private void RetrieveUsernameWithCallback(Action<DatabaseTransactionResult, stri
return;
}
_db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWithOnMainThread(task =>
_db.Child("users")
.Child(_user.UserId)
.Child("username")
.GetValueAsync()
.ContinueWithOnMainThread(task =>
{
DatabaseTransactionResult result;
if (task.IsCompletedSuccessfully)
@ -485,14 +511,34 @@ public void SignOutUser()
_auth.SignOut();
}
/// <summary>
/// abstraction function to delete the user
/// </summary>
public void DeleteUser()
{
_user.DeleteAsync().ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
Debug.Log("user deleted");
SignOutUser();
}
else
{
Debug.LogError(task.Exception);
}
});
}
/// <summary>
/// abstraction function for the user to reset their password
/// </summary>
/// <param name="email">the forgetful user's email lol</param>
/// <param name="callback">callback function to be invoked after the password reset email is sent</param>
public void ForgotPassword(string email, Action<bool> callback)
public void ResetUserPassword(string email, Action<bool> callback)
{
_auth.SendPasswordResetEmailAsync(email).ContinueWithOnMainThread(resetTask =>
_auth.SendPasswordResetEmailAsync(email)
.ContinueWithOnMainThread(resetTask =>
{
if (resetTask.IsCompletedSuccessfully)
{
@ -538,7 +584,6 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children)
{
try
{
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>);
@ -548,7 +593,6 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
{
Debug.LogError($"{e}\n{child.GetRawJsonValue()}");
}
}
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
@ -562,7 +606,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// callback function that takes in a <c>DatabaseTransactionResult</c> enum and a
/// <c>List&lt;LocalPlayerData.Score&gt;</c>
/// </param>
public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
private void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
{
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
@ -587,7 +631,6 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children)
{
try
{
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>);
@ -597,7 +640,6 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
{
Debug.LogError(e);
}
}
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
@ -651,10 +693,10 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
{
GetRecentScores((recentRes, recentScores) =>
{
if (recentRes == DatabaseTransactionResult.Error)
if (recentRes != DatabaseTransactionResult.Ok)
{
Debug.Log("failed to get recent scores");
callback(DatabaseTransactionResult.Error, 0f);
callback(recentRes, 0f);
return;
}
@ -664,10 +706,10 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
GetBestScores((bestRes, bestScores) =>
{
if (bestRes == DatabaseTransactionResult.Error)
if (bestRes != DatabaseTransactionResult.Ok)
{
Debug.Log("failed to get recent scores");
callback(DatabaseTransactionResult.Error, 0f);
callback(recentRes, 0f);
return;
}
@ -687,7 +729,30 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
public void UpdateUserRating(
Action<DatabaseTransactionResult> callback)
{
throw new NotImplementedException();
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
if (_user == null)
{
callback(DatabaseTransactionResult.Unauthenticated);
return;
}
_db.Child("users")
.Child(_user.UserId)
.Child("rating")
.SetValueAsync(GameManager.Instance.Data.CalculateUserRating())
.ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
callback(DatabaseTransactionResult.Ok);
}
else
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error);
}
});
}
/// <summary>
@ -729,6 +794,8 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
{
if (task.IsCompletedSuccessfully)
{
GameManager.Instance.Data.LastKnownEmail = newValue;
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
callback(DatabaseTransactionResult.Ok);
}
else
@ -740,12 +807,17 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
break;
case UserAccountDetailTargetEnum.Username:
_db.Child("users").Child(_user.UserId).Child("username").SetValueAsync(newValue)
_db.Child("users")
.Child(_user.UserId)
.Child("username")
.SetValueAsync(newValue)
.ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
_username = newValue;
GameManager.Instance.Data.LastKnownUsername = newValue;
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
callback(DatabaseTransactionResult.Ok);
}
else

View file

@ -62,13 +62,13 @@ public static DeltaLabChE CalculateDistance(Color template, Color response)
// ... distance between the (L, a, b) coordinates."
// https://github.com/svgeesus/svgeesus.github.io/blob/master/Color/OKLab-notes.md#color-difference-metric
// ... ΔL = L1 - L2
// ... C1 = √(a1² + b1²) -> chroma values
// ... C2 = √(a2² + b2²) -> chroma values
// ... ΔC = C1 - C2 -> chroma difference
// ... C1 = √(a1² + b1²)
// ... C2 = √(a2² + b2²)
// ... ΔC = C1 - C2
// ... Δa = a1 - a2
// ... Δb = b1 - b2
// ... ΔH = √(Δa² + Δb² - ΔC²) -> hue difference
// ... ΔE = √(ΔL² + ΔC² + ΔH²) -> final difference
// ... ΔH = √(Δa² + Δb² - ΔC²)
// ... ΔE = √(ΔL² + ΔC² + ΔH²)
float l1, a1, b1, l2, a2, b2;
(l1, a1, b1) = (templateOklab.L, templateOklab.a, templateOklab.b);
@ -117,6 +117,7 @@ public static Color RawLchToColor(double lightness, double chroma, double hue)
/// <param name="x">the linear srgb value to transform</param>
/// <returns>the non-linear srgb value</returns>
// https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F (no licence specified)
// ReSharper disable once MemberCanBePrivate.Global
public static double srgb_nonlinear_transform_f(double x)
{
if (x >= 0.0031308d)
@ -130,6 +131,7 @@ public static double srgb_nonlinear_transform_f(double x)
/// <param name="x">the non-linear srgb value to transform</param>
/// <returns>the linear srgb value</returns>
// https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F (no licence specified)
// ReSharper disable once MemberCanBePrivate.Global
public static double srgb_nonlinear_transform_f_inv(double x)
{
if (x >= 0.04045d)
@ -141,6 +143,7 @@ public static double srgb_nonlinear_transform_f_inv(double x)
/// clips a colour to the sRGB gamut while preserving chroma
/// </summary>
// https://bottosson.github.io/posts/gamutclipping/ (MIT)
// ReSharper disable once MemberCanBePrivate.Global
public static RGB gamut_clip_preserve_chroma(RGB rgb)
{
if (rgb is { r: < 1 and > 0, g: < 1 and > 0, b: < 1 and > 0 })
@ -171,6 +174,7 @@ public static RGB gamut_clip_preserve_chroma(RGB rgb)
/// a and b must be normalized so a^2 + b^2 == 1
/// </summary>
// https://bottosson.github.io/posts/gamutclipping/ (MIT)
// ReSharper disable once MemberCanBePrivate.Global
public static float find_gamut_intersection(
float a,
float b,
@ -307,6 +311,7 @@ public static Lab linear_srgb_to_oklab(RGB c)
}
// https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab (public domain)
// ReSharper disable once MemberCanBePrivate.Global
public static RGB oklab_to_linear_srgb(Lab c)
{
var interimL = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b;

View file

@ -175,12 +175,14 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
var historicalLightnessAcc = 0f;
var historicalChromaAcc = 0f;
var historicalHueAcc = 0f;
var historicalRounds = 0;
foreach (var localScore in _data.RecentLocalScores)
{
historicalLightnessAcc += localScore.AvgLightnessAccuracy;
historicalChromaAcc += localScore.AvgChromaAccuracy;
historicalHueAcc += localScore.AvgHueAccuracy;
historicalRounds += localScore.NoOfRounds;
}
foreach (var onlineScore in _data.RecentOnlineScores)
@ -188,6 +190,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy;
historicalRounds += onlineScore.NoOfRounds;
}
foreach (var onlineScore in _data.BestOnlineScores)
@ -195,13 +198,18 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy;
historicalRounds += onlineScore.NoOfRounds;
}
historicalLightnessAcc /= historicalRounds;
historicalChromaAcc /= historicalRounds;
historicalHueAcc /= historicalRounds;
// calculate round averages
var roundLightnessAcc = 0d;
var roundChromaAcc = 0d;
var roundHueAcc = 0d;
var roundPerceivedAcc = 0d;
var gameLightnessAcc = 0d;
var gameChromaAcc = 0d;
var gameHueAcc = 0d;
var gamePerceivedAcc = 0d;
var templateColour = Color.clear;
var responseColour = Color.clear;
@ -215,54 +223,65 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
Debug.Log(
$"processing round: template={templateColour}, response={responseColour} (dL%={dLCh.L}, dC%={dLCh.C}, dh%={dLCh.h}, dEok={distance.dE:F})");
roundLightnessAcc += Math.Clamp(dLCh.L * 100d, 0d, 100d);
roundChromaAcc += Math.Clamp(dLCh.C * 100d, 0d, 100d);
roundHueAcc += Math.Clamp(dLCh.h * 100d, 0d, 100d);
roundPerceivedAcc += Math.Clamp((100d - distance.dE) * 100d, 0d, 100d);
var roundLightnessAcc = Math.Clamp(dLCh.L * 100d, 0d, 100d);
var roundChromaAcc = Math.Clamp(dLCh.C * 100d, 0d, 100d);
var roundHueAcc = Math.Clamp(dLCh.h * 100d, 0d, 100d);
var roundPerceivedAcc = Math.Clamp((100d - distance.dE) * 100d, 0d, 100d);
gameLightnessAcc += roundLightnessAcc;
gameChromaAcc += roundChromaAcc;
gameHueAcc += roundHueAcc;
gamePerceivedAcc += roundPerceivedAcc;
var showcaseTemplate = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}TemplateColour");
var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour");
var showcaseInfo = ui.UI.Q<Label>($"ShowcasePair{roundNumber}Info");
if (showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null)
if (!(showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null))
{
Debug.LogError($"showcase pair {roundNumber} not found");
roundNumber++;
continue;
}
showcaseTemplate.style.backgroundColor = templateColour;
showcaseResponse.style.backgroundColor = responseColour;
showcaseInfo.text = $"{roundLightnessAcc:F}% {roundChromaAcc:F}% {roundHueAcc:F}% ({roundPerceivedAcc:F}%)";
roundNumber++;
showcaseInfo.text =
$"{roundLightnessAcc:N0}% {roundChromaAcc:N0}% {roundHueAcc:N0}% ({roundPerceivedAcc:N0}%)";
}
else
{
Debug.LogError($"showcase pair {roundNumber} not found");
}
roundLightnessAcc /= Gameplay.RoundsPerGame;
roundChromaAcc /= Gameplay.RoundsPerGame;
roundHueAcc /= Gameplay.RoundsPerGame;
roundPerceivedAcc /= Gameplay.RoundsPerGame;
roundNumber++; // used for ui querying
}
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc + roundPerceivedAcc + roundPerceivedAcc) / 5;
gameLightnessAcc /= Gameplay.RoundsPerGame;
gameChromaAcc /= Gameplay.RoundsPerGame;
gameHueAcc /= Gameplay.RoundsPerGame;
gamePerceivedAcc /= Gameplay.RoundsPerGame;
// NOTE: this is NOT equiv to user rating, this is just a per-game accuracy score
// all that math is done in LocalPlayerData.CalculateUserRating
var gameAccuracy = (gameLightnessAcc + gameChromaAcc + gameHueAcc + gamePerceivedAcc) / 4;
// make comparison texts
var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") +
Math.Abs(roundLightnessAcc - historicalLightnessAcc).ToString("P");
var cAccDeltaText = (roundChromaAcc > historicalChromaAcc ? "+" : "-") +
Math.Abs(roundChromaAcc - historicalChromaAcc).ToString("P");
var hAccDeltaText = (roundHueAcc > historicalHueAcc ? "+" : "-") +
Math.Abs(roundHueAcc - historicalHueAcc).ToString("P");
var lAccDeltaText = (gameLightnessAcc > historicalLightnessAcc ? "+" : "-") +
Math.Abs(gameLightnessAcc - historicalLightnessAcc).ToString("F2");
var cAccDeltaText = (gameChromaAcc > historicalChromaAcc ? "+" : "-") +
Math.Abs(gameChromaAcc - historicalChromaAcc).ToString("F2");
var hAccDeltaText = (gameHueAcc > historicalHueAcc ? "+" : "-") +
Math.Abs(gameHueAcc - historicalHueAcc).ToString("F2");
var score = new LocalPlayerData.Score(DateTime.Now,
playedRounds.Count,
(float)roundLightnessAcc,
(float)roundChromaAcc,
(float)roundHueAcc,
(float)roundPerceivedAcc);
(float)gameLightnessAcc,
(float)gameChromaAcc,
(float)gameHueAcc,
(float)gamePerceivedAcc);
var oldRating = _data.CalculateUserRating();
_data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(Instance.Data);
Debug.Log("submitting score to backend");
Backend.SubmitScore(score,
submitRes =>
{
@ -301,14 +320,19 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
void TransitionToResultsView(float rating)
{
var ratingText = rating >= 0 ? $"\nYour rating is {rating:F}" : "\nYour rating could not be calculated.";
Debug.Log("signal GameManager-UIManager transition to results view");
var ratingDifferenceDescriptor = oldRating > rating ? "decreased" : "increased";
var ratingText = rating >= 0
? $"\nYour rating has {ratingDifferenceDescriptor} by {Math.Abs(rating - oldRating):F2}."
: "\nYour rating could not be calculated.";
// build the result text and show the results view
ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,",
$"you were {roundAcc} accurate.", "",
$"Lightness was {roundLightnessAcc:P}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {roundChromaAcc:P}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {roundHueAcc:P}% accurate. ({hAccDeltaText} from your average)") + ratingText;
$"you were {gameAccuracy} accurate.", "",
$"Lightness was {gameLightnessAcc:F2}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {gameChromaAcc:F2}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {gameHueAcc:F2}% accurate. ({hAccDeltaText} from your average)") + ratingText;
ui.SetDisplayState(UIManager.DisplayState.ResultsView);
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEngine;
public class LocalPlayerData
@ -16,10 +17,9 @@ public class LocalPlayerData
public const int MaxRecentLocalScores = 10;
/// <summary>
/// queue of the best online scores,
/// used in user rating calculation and accuracy display stats
/// the gamma value used in the exponential user rating calculation
/// </summary>
public Queue<Score> BestOnlineScores = new(20);
private const float ExponentialUserRatingGamma = 1.75f;
/// <summary>
/// last known email used
@ -34,13 +34,19 @@ public class LocalPlayerData
/// <summary>
/// queue of the 10 most recent local scores
/// </summary>
public Queue<Score> RecentLocalScores = new(10);
public readonly Queue<Score> RecentLocalScores = new(10);
/// <summary>
/// queue of the 10 most recent online scores,
/// used in user rating calculation and accuracy display stats
/// </summary>
public Queue<Score> RecentOnlineScores = new(10);
public readonly Queue<Score> RecentOnlineScores = new(10);
/// <summary>
/// queue of the best online scores,
/// used in user rating calculation and accuracy display stats
/// </summary>
public readonly Queue<Score> BestOnlineScores = new(20);
/// <summary>
/// loads player data from player prefs and database
@ -92,6 +98,10 @@ public void LoadFromTheWorld(Action<LocalPlayerData> callback)
Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f)));
}
Debug.Log(
$"loaded lpdata from the local world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
callback(this);
// load online scores
RecentOnlineScores.Clear();
GameManager.Instance.Backend.GetRecentScores((_, recentOnlineScores) =>
@ -103,7 +113,7 @@ public void LoadFromTheWorld(Action<LocalPlayerData> callback)
}
Debug.Log(
$"loaded lpdata from the world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
$"loaded lpdata from the online world (now {LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
callback(this);
});
@ -149,7 +159,7 @@ public void RegisterLocalScore(Score score)
/// <returns>the user rating (0-100f)</returns>
public float CalculateUserRating()
{
// user rating is like CHUNITHM's rating system
// user rating is like CHUNITHMs rating system
// where best + recent scores are averaged out
// in this case 20 best scores, and 10 recent scores are used
@ -165,7 +175,7 @@ public float CalculateUserRating()
var scores = 0;
var totalRating = 0d;
foreach (var score in recentScores)
foreach (var score in recentScores.Take(10))
{
scores++;
var dL = score.AvgLightnessAccuracy;
@ -174,10 +184,10 @@ public float CalculateUserRating()
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy
+ score.AvgHueAccuracy * score.AvgHueAccuracy);
totalRating = (dL + dC + dH + dE) / 4d;
totalRating += (dL + dC + dH + dE + dE) / 5d;
}
foreach (var score in bestScores)
foreach (var score in bestScores.Take(20))
{
scores++;
var dL = score.AvgLightnessAccuracy;
@ -186,10 +196,18 @@ public float CalculateUserRating()
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy
+ score.AvgHueAccuracy * score.AvgHueAccuracy);
totalRating = (dL + dC + dH + dE) / 4d;
totalRating += (dL + dC + dH + dE + dE) / 5d;
}
return Math.Clamp((float)(totalRating / scores), 0f, 100f);
scores = Math.Max(1, scores);
totalRating /= scores;
var rawUserRating = Math.Clamp(totalRating, 0d, 100d);
var exponentialRating = 100d * Math.Pow(rawUserRating / 100d, ExponentialUserRatingGamma);
Debug.Log($"locally calculated user rating: lin: {rawUserRating} -> exp: {exponentialRating}");
return (float)exponentialRating;
}
public struct Score
@ -232,7 +250,8 @@ public struct Score
/// <param name="l">average lightness accuracy across all rounds (0-100)</param>
/// <param name="c">average chroma accuracy across all rounds (0-100)</param>
/// <param name="h">average hue accuracy across all rounds (0-100)</param>
/// /// <param name="e">average perceived accuracy across all rounds (0-100)</param>
/// ///
/// <param name="e">average perceived accuracy across all rounds (0-100)</param>
public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f,
float h = 100.0f, float e = 100.0f)
{
@ -256,23 +275,33 @@ public Score(Dictionary<string, object> data)
if (!data.ContainsKey("timestamp") || data["timestamp"] is not long timestamp)
throw new ArgumentException("timestamp not found or invalid");
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not int noOfRounds)
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds)
throw new ArgumentException("noOfRounds not found or invalid");
if (!data.ContainsKey("avgLightnessAccuracy") || data["avgLightnessAccuracy"] is not float avgLightnessAccuracy)
throw new ArgumentException("avgLightnessAccuracy not found or invalid");
if (!data.ContainsKey("avgChromaAccuracy") || data["avgChromaAccuracy"] is not float avgChromaAccuracy)
throw new ArgumentException("avgChromaAccuracy not found or invalid");
if (!data.ContainsKey("avgHueAccuracy") || data["avgHueAccuracy"] is not float avgHueAccuracy)
throw new ArgumentException("avgHueAccuracy not found or invalid");
if (!data.ContainsKey("avgPerceivedAccuracy") || data["avgPerceivedAccuracy"] is not float avgPerceivedAccuracy)
throw new ArgumentException("avgPerceivedAccuracy not found or invalid");
var avgLightnessAccuracy = GetFloatyKey("avgLightnessAccuracy");
var avgChromaAccuracy = GetFloatyKey("avgChromaAccuracy");
var avgHueAccuracy = GetFloatyKey("avgHueAccuracy");
var avgPerceivedAccuracy = GetFloatyKey("avgPerceivedAccuracy");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
NoOfRounds = noOfRounds;
NoOfRounds = (int)noOfRounds;
AvgLightnessAccuracy = avgLightnessAccuracy;
AvgChromaAccuracy = avgChromaAccuracy;
AvgHueAccuracy = avgHueAccuracy;
AvgPerceivedAccuracy = avgPerceivedAccuracy;
return;
float GetFloatyKey(string key)
{
if (!data.TryGetValue(key, out var possibleFloat)) throw new ArgumentException($"{key} not found");
return possibleFloat switch
{
double f => (float)f,
long l => l,
_ => throw new ArgumentException($"{key} not a valid float")
};
}
}
/// <summary>

View file

@ -105,6 +105,8 @@ private static void OnAccountButtonClicked()
/// </summary>
private void RenderFromPlayerData(LocalPlayerData data)
{
Debug.Log("updating SideView > AccountSection with new player data");
// calculate averages from both recent local scores and online scores
var totalLightnessAcc = 0f;
var totalChromaAcc = 0f;
@ -149,9 +151,9 @@ private void RenderFromPlayerData(LocalPlayerData data)
// finally, set the labels
_playerText.text = playerText;
_ratingText.text = $"{rating:F}";
_lightnessAccuracyText.text = $"{lightnessAcc:F}";
_chromaAccuracyText.text = $"{chromaAcc:F}";
_hueAccuracyText.text = $"{hueAcc:F}";
_ratingText.text = $"{rating:F}";
}
}

View file

@ -132,7 +132,7 @@
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="ResultsView"
style="flex-grow: 1; display: flex; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; justify-content: space-between;">
style="flex-grow: 1; display: none; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; justify-content: space-between;">
<ui:VisualElement name="ColourShowcase"
style="flex-grow: 1; background-color: rgb(255, 255, 255); border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom-right-radius: 8px; border-bottom-left-radius: 8px; padding-top: 2%; padding-right: 2%; padding-bottom: 2%; padding-left: 2%; margin-bottom: 2%; margin-top: 0; margin-right: 0; margin-left: 0;">
<ui:Label tabindex="-1" text="Templates" parse-escape-sequences="true"
@ -202,7 +202,7 @@
<ui:ListView name="LeaderboardListView"/>
</ui:VisualElement>
<ui:VisualElement name="AccountView"
style="flex-grow: 1; display: none; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; flex-direction: column; justify-content: space-between;">
style="flex-grow: 1; display: flex; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; flex-direction: column; justify-content: space-between;">
<ui:Label tabindex="-1" text="You are not signed in." parse-escape-sequences="true"
display-tooltip-when-elided="true" name="AccountHeader"
style="font-size: 58px; -unity-font-style: normal;"/>
@ -236,6 +236,9 @@
style="-unity-text-align: middle-center; margin-bottom: 1%; margin-right: 1%; margin-top: 1%; -unity-font-style: bold;"/>
<ui:Button text="Secondary Action Button →" parse-escape-sequences="true"
display-tooltip-when-elided="true" name="SecondaryActionButton"
style="-unity-text-align: middle-center; margin-bottom: 1%; margin-right: 1%; margin-top: 1%; -unity-font-style: bold;"/>
<ui:Button text="Tertiary Action Button →" parse-escape-sequences="true"
display-tooltip-when-elided="true" name="TertiaryActionButton"
style="margin-top: 1%; margin-right: 1%; -unity-font-style: bold;"/>
</ui:VisualElement>
</ui:VisualElement>