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

View file

@ -22,6 +22,7 @@ public enum AuthenticationResult
AlreadyAuthenticated, AlreadyAuthenticated,
NonExistentUser, NonExistentUser,
AlreadyExistingUser, AlreadyExistingUser,
UsernameAlreadyTaken,
InvalidEmail, InvalidEmail,
InvalidCredentials, InvalidCredentials,
GenericError GenericError
@ -308,6 +309,26 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
if (registerUser) 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 // register user
_auth.CreateUserWithEmailAndPasswordAsync(email, password) _auth.CreateUserWithEmailAndPasswordAsync(email, password)
.ContinueWithOnMainThread(createTask => .ContinueWithOnMainThread(createTask =>
@ -372,6 +393,7 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
break; break;
} }
}); });
});
return; return;
} }
@ -443,7 +465,11 @@ private void RetrieveUsernameWithCallback(Action<DatabaseTransactionResult, stri
return; 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; DatabaseTransactionResult result;
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)
@ -485,14 +511,34 @@ public void SignOutUser()
_auth.SignOut(); _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> /// <summary>
/// abstraction function for the user to reset their password /// abstraction function for the user to reset their password
/// </summary> /// </summary>
/// <param name="email">the forgetful user's email lol</param> /// <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> /// <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) if (resetTask.IsCompletedSuccessfully)
{ {
@ -538,7 +584,6 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
var scores = new List<LocalPlayerData.Score>(); var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children) foreach (var child in task.Result.Children)
{
try try
{ {
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>); 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()}"); Debug.LogError($"{e}\n{child.GetRawJsonValue()}");
} }
}
callback(DatabaseTransactionResult.Ok, scores); callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); 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 /// callback function that takes in a <c>DatabaseTransactionResult</c> enum and a
/// <c>List&lt;LocalPlayerData.Score&gt;</c> /// <c>List&lt;LocalPlayerData.Score&gt;</c>
/// </param> /// </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; if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
@ -587,7 +631,6 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
var scores = new List<LocalPlayerData.Score>(); var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children) foreach (var child in task.Result.Children)
{
try try
{ {
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>); 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); Debug.LogError(e);
} }
}
callback(DatabaseTransactionResult.Ok, scores); callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
@ -651,10 +693,10 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
{ {
GetRecentScores((recentRes, recentScores) => GetRecentScores((recentRes, recentScores) =>
{ {
if (recentRes == DatabaseTransactionResult.Error) if (recentRes != DatabaseTransactionResult.Ok)
{ {
Debug.Log("failed to get recent scores"); Debug.Log("failed to get recent scores");
callback(DatabaseTransactionResult.Error, 0f); callback(recentRes, 0f);
return; return;
} }
@ -664,10 +706,10 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
GetBestScores((bestRes, bestScores) => GetBestScores((bestRes, bestScores) =>
{ {
if (bestRes == DatabaseTransactionResult.Error) if (bestRes != DatabaseTransactionResult.Ok)
{ {
Debug.Log("failed to get recent scores"); Debug.Log("failed to get recent scores");
callback(DatabaseTransactionResult.Error, 0f); callback(recentRes, 0f);
return; return;
} }
@ -687,7 +729,30 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
public void UpdateUserRating( public void UpdateUserRating(
Action<DatabaseTransactionResult> callback) 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> /// <summary>
@ -729,6 +794,8 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
{ {
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)
{ {
GameManager.Instance.Data.LastKnownEmail = newValue;
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
callback(DatabaseTransactionResult.Ok); callback(DatabaseTransactionResult.Ok);
} }
else else
@ -740,12 +807,17 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
break; break;
case UserAccountDetailTargetEnum.Username: 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 => .ContinueWithOnMainThread(task =>
{ {
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)
{ {
_username = newValue; _username = newValue;
GameManager.Instance.Data.LastKnownUsername = newValue;
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
callback(DatabaseTransactionResult.Ok); callback(DatabaseTransactionResult.Ok);
} }
else else

View file

@ -62,13 +62,13 @@ public static DeltaLabChE CalculateDistance(Color template, Color response)
// ... distance between the (L, a, b) coordinates." // ... distance between the (L, a, b) coordinates."
// https://github.com/svgeesus/svgeesus.github.io/blob/master/Color/OKLab-notes.md#color-difference-metric // https://github.com/svgeesus/svgeesus.github.io/blob/master/Color/OKLab-notes.md#color-difference-metric
// ... ΔL = L1 - L2 // ... ΔL = L1 - L2
// ... C1 = √(a1² + b1²) -> chroma values // ... C1 = √(a1² + b1²)
// ... C2 = √(a2² + b2²) -> chroma values // ... C2 = √(a2² + b2²)
// ... ΔC = C1 - C2 -> chroma difference // ... ΔC = C1 - C2
// ... Δa = a1 - a2 // ... Δa = a1 - a2
// ... Δb = b1 - b2 // ... Δb = b1 - b2
// ... ΔH = √(Δa² + Δb² - ΔC²) -> hue difference // ... ΔH = √(Δa² + Δb² - ΔC²)
// ... ΔE = √(ΔL² + ΔC² + ΔH²) -> final difference // ... ΔE = √(ΔL² + ΔC² + ΔH²)
float l1, a1, b1, l2, a2, b2; float l1, a1, b1, l2, a2, b2;
(l1, a1, b1) = (templateOklab.L, templateOklab.a, templateOklab.b); (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> /// <param name="x">the linear srgb value to transform</param>
/// <returns>the non-linear srgb value</returns> /// <returns>the non-linear srgb value</returns>
// https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F (no licence specified) // 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) public static double srgb_nonlinear_transform_f(double x)
{ {
if (x >= 0.0031308d) 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> /// <param name="x">the non-linear srgb value to transform</param>
/// <returns>the linear srgb value</returns> /// <returns>the linear srgb value</returns>
// https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F (no licence specified) // 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) public static double srgb_nonlinear_transform_f_inv(double x)
{ {
if (x >= 0.04045d) 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 /// clips a colour to the sRGB gamut while preserving chroma
/// </summary> /// </summary>
// https://bottosson.github.io/posts/gamutclipping/ (MIT) // https://bottosson.github.io/posts/gamutclipping/ (MIT)
// ReSharper disable once MemberCanBePrivate.Global
public static RGB gamut_clip_preserve_chroma(RGB rgb) public static RGB gamut_clip_preserve_chroma(RGB rgb)
{ {
if (rgb is { r: < 1 and > 0, g: < 1 and > 0, b: < 1 and > 0 }) 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 /// a and b must be normalized so a^2 + b^2 == 1
/// </summary> /// </summary>
// https://bottosson.github.io/posts/gamutclipping/ (MIT) // https://bottosson.github.io/posts/gamutclipping/ (MIT)
// ReSharper disable once MemberCanBePrivate.Global
public static float find_gamut_intersection( public static float find_gamut_intersection(
float a, float a,
float b, 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) // 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) public static RGB oklab_to_linear_srgb(Lab c)
{ {
var interimL = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b; 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 historicalLightnessAcc = 0f;
var historicalChromaAcc = 0f; var historicalChromaAcc = 0f;
var historicalHueAcc = 0f; var historicalHueAcc = 0f;
var historicalRounds = 0;
foreach (var localScore in _data.RecentLocalScores) foreach (var localScore in _data.RecentLocalScores)
{ {
historicalLightnessAcc += localScore.AvgLightnessAccuracy; historicalLightnessAcc += localScore.AvgLightnessAccuracy;
historicalChromaAcc += localScore.AvgChromaAccuracy; historicalChromaAcc += localScore.AvgChromaAccuracy;
historicalHueAcc += localScore.AvgHueAccuracy; historicalHueAcc += localScore.AvgHueAccuracy;
historicalRounds += localScore.NoOfRounds;
} }
foreach (var onlineScore in _data.RecentOnlineScores) foreach (var onlineScore in _data.RecentOnlineScores)
@ -188,6 +190,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy; historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy; historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy; historicalHueAcc += onlineScore.AvgHueAccuracy;
historicalRounds += onlineScore.NoOfRounds;
} }
foreach (var onlineScore in _data.BestOnlineScores) foreach (var onlineScore in _data.BestOnlineScores)
@ -195,13 +198,18 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy; historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy; historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy; historicalHueAcc += onlineScore.AvgHueAccuracy;
historicalRounds += onlineScore.NoOfRounds;
} }
historicalLightnessAcc /= historicalRounds;
historicalChromaAcc /= historicalRounds;
historicalHueAcc /= historicalRounds;
// calculate round averages // calculate round averages
var roundLightnessAcc = 0d; var gameLightnessAcc = 0d;
var roundChromaAcc = 0d; var gameChromaAcc = 0d;
var roundHueAcc = 0d; var gameHueAcc = 0d;
var roundPerceivedAcc = 0d; var gamePerceivedAcc = 0d;
var templateColour = Color.clear; var templateColour = Color.clear;
var responseColour = Color.clear; var responseColour = Color.clear;
@ -215,54 +223,65 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
Debug.Log( Debug.Log(
$"processing round: template={templateColour}, response={responseColour} (dL%={dLCh.L}, dC%={dLCh.C}, dh%={dLCh.h}, dEok={distance.dE:F})"); $"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); var roundLightnessAcc = Math.Clamp(dLCh.L * 100d, 0d, 100d);
roundChromaAcc += Math.Clamp(dLCh.C * 100d, 0d, 100d); var roundChromaAcc = Math.Clamp(dLCh.C * 100d, 0d, 100d);
roundHueAcc += Math.Clamp(dLCh.h * 100d, 0d, 100d); var roundHueAcc = Math.Clamp(dLCh.h * 100d, 0d, 100d);
roundPerceivedAcc += Math.Clamp((100d - distance.dE) * 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 showcaseTemplate = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}TemplateColour");
var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour"); var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour");
var showcaseInfo = ui.UI.Q<Label>($"ShowcasePair{roundNumber}Info"); 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; showcaseTemplate.style.backgroundColor = templateColour;
showcaseResponse.style.backgroundColor = responseColour; showcaseResponse.style.backgroundColor = responseColour;
showcaseInfo.text = $"{roundLightnessAcc:F}% {roundChromaAcc:F}% {roundHueAcc:F}% ({roundPerceivedAcc:F}%)"; showcaseInfo.text =
$"{roundLightnessAcc:N0}% {roundChromaAcc:N0}% {roundHueAcc:N0}% ({roundPerceivedAcc:N0}%)";
roundNumber++; }
else
{
Debug.LogError($"showcase pair {roundNumber} not found");
} }
roundLightnessAcc /= Gameplay.RoundsPerGame; roundNumber++; // used for ui querying
roundChromaAcc /= Gameplay.RoundsPerGame; }
roundHueAcc /= Gameplay.RoundsPerGame;
roundPerceivedAcc /= Gameplay.RoundsPerGame;
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 // make comparison texts
var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") + var lAccDeltaText = (gameLightnessAcc > historicalLightnessAcc ? "+" : "-") +
Math.Abs(roundLightnessAcc - historicalLightnessAcc).ToString("P"); Math.Abs(gameLightnessAcc - historicalLightnessAcc).ToString("F2");
var cAccDeltaText = (roundChromaAcc > historicalChromaAcc ? "+" : "-") + var cAccDeltaText = (gameChromaAcc > historicalChromaAcc ? "+" : "-") +
Math.Abs(roundChromaAcc - historicalChromaAcc).ToString("P"); Math.Abs(gameChromaAcc - historicalChromaAcc).ToString("F2");
var hAccDeltaText = (roundHueAcc > historicalHueAcc ? "+" : "-") + var hAccDeltaText = (gameHueAcc > historicalHueAcc ? "+" : "-") +
Math.Abs(roundHueAcc - historicalHueAcc).ToString("P"); Math.Abs(gameHueAcc - historicalHueAcc).ToString("F2");
var score = new LocalPlayerData.Score(DateTime.Now, var score = new LocalPlayerData.Score(DateTime.Now,
playedRounds.Count, playedRounds.Count,
(float)roundLightnessAcc, (float)gameLightnessAcc,
(float)roundChromaAcc, (float)gameChromaAcc,
(float)roundHueAcc, (float)gameHueAcc,
(float)roundPerceivedAcc); (float)gamePerceivedAcc);
var oldRating = _data.CalculateUserRating();
_data.RegisterLocalScore(score); _data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(Instance.Data); FireLocalPlayerDataChangeCallbacks(Instance.Data);
Debug.Log("submitting score to backend");
Backend.SubmitScore(score, Backend.SubmitScore(score,
submitRes => submitRes =>
{ {
@ -301,14 +320,19 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
void TransitionToResultsView(float rating) 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 // build the result text and show the results view
ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,", ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,",
$"you were {roundAcc} accurate.", "", $"you were {gameAccuracy} accurate.", "",
$"Lightness was {roundLightnessAcc:P}% accurate. ({lAccDeltaText} from your average)", $"Lightness was {gameLightnessAcc:F2}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {roundChromaAcc:P}% accurate. ({cAccDeltaText} from your average)", $"Chroma was {gameChromaAcc:F2}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {roundHueAcc:P}% accurate. ({hAccDeltaText} from your average)") + ratingText; $"Hue was {gameHueAcc:F2}% accurate. ({hAccDeltaText} from your average)") + ratingText;
ui.SetDisplayState(UIManager.DisplayState.ResultsView); ui.SetDisplayState(UIManager.DisplayState.ResultsView);
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using UnityEngine; using UnityEngine;
public class LocalPlayerData public class LocalPlayerData
@ -16,10 +17,9 @@ public class LocalPlayerData
public const int MaxRecentLocalScores = 10; public const int MaxRecentLocalScores = 10;
/// <summary> /// <summary>
/// queue of the best online scores, /// the gamma value used in the exponential user rating calculation
/// used in user rating calculation and accuracy display stats
/// </summary> /// </summary>
public Queue<Score> BestOnlineScores = new(20); private const float ExponentialUserRatingGamma = 1.75f;
/// <summary> /// <summary>
/// last known email used /// last known email used
@ -34,13 +34,19 @@ public class LocalPlayerData
/// <summary> /// <summary>
/// queue of the 10 most recent local scores /// queue of the 10 most recent local scores
/// </summary> /// </summary>
public Queue<Score> RecentLocalScores = new(10); public readonly Queue<Score> RecentLocalScores = new(10);
/// <summary> /// <summary>
/// queue of the 10 most recent online scores, /// queue of the 10 most recent online scores,
/// used in user rating calculation and accuracy display stats /// used in user rating calculation and accuracy display stats
/// </summary> /// </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> /// <summary>
/// loads player data from player prefs and database /// 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))); 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 // load online scores
RecentOnlineScores.Clear(); RecentOnlineScores.Clear();
GameManager.Instance.Backend.GetRecentScores((_, recentOnlineScores) => GameManager.Instance.Backend.GetRecentScores((_, recentOnlineScores) =>
@ -103,7 +113,7 @@ public void LoadFromTheWorld(Action<LocalPlayerData> callback)
} }
Debug.Log( 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); callback(this);
}); });
@ -130,7 +140,7 @@ public void SaveToTheWorld()
idx++; idx++;
} }
Debug.Log("saved lpdata to playerprefs"); // online scores are already saved in the backend Debug.Log("saved lpdata to player prefs"); // online scores are already saved in the backend
} }
/// <summary> /// <summary>
@ -149,7 +159,7 @@ public void RegisterLocalScore(Score score)
/// <returns>the user rating (0-100f)</returns> /// <returns>the user rating (0-100f)</returns>
public float CalculateUserRating() 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 // where best + recent scores are averaged out
// in this case 20 best scores, and 10 recent scores are used // in this case 20 best scores, and 10 recent scores are used
@ -165,7 +175,7 @@ public float CalculateUserRating()
var scores = 0; var scores = 0;
var totalRating = 0d; var totalRating = 0d;
foreach (var score in recentScores) foreach (var score in recentScores.Take(10))
{ {
scores++; scores++;
var dL = score.AvgLightnessAccuracy; var dL = score.AvgLightnessAccuracy;
@ -174,10 +184,10 @@ public float CalculateUserRating()
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy + score.AvgChromaAccuracy * score.AvgChromaAccuracy
+ score.AvgHueAccuracy * score.AvgHueAccuracy); + 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++; scores++;
var dL = score.AvgLightnessAccuracy; var dL = score.AvgLightnessAccuracy;
@ -186,10 +196,18 @@ public float CalculateUserRating()
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy + score.AvgChromaAccuracy * score.AvgChromaAccuracy
+ score.AvgHueAccuracy * score.AvgHueAccuracy); + 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 public struct Score
@ -232,7 +250,8 @@ public struct Score
/// <param name="l">average lightness accuracy across all rounds (0-100)</param> /// <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="c">average chroma accuracy across all rounds (0-100)</param>
/// <param name="h">average hue 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, public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f,
float h = 100.0f, float e = 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) if (!data.ContainsKey("timestamp") || data["timestamp"] is not long timestamp)
throw new ArgumentException("timestamp not found or invalid"); 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"); 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"); var avgLightnessAccuracy = GetFloatyKey("avgLightnessAccuracy");
if (!data.ContainsKey("avgChromaAccuracy") || data["avgChromaAccuracy"] is not float avgChromaAccuracy) var avgChromaAccuracy = GetFloatyKey("avgChromaAccuracy");
throw new ArgumentException("avgChromaAccuracy not found or invalid"); var avgHueAccuracy = GetFloatyKey("avgHueAccuracy");
if (!data.ContainsKey("avgHueAccuracy") || data["avgHueAccuracy"] is not float avgHueAccuracy) var avgPerceivedAccuracy = GetFloatyKey("avgPerceivedAccuracy");
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");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
NoOfRounds = noOfRounds; NoOfRounds = (int)noOfRounds;
AvgLightnessAccuracy = avgLightnessAccuracy; AvgLightnessAccuracy = avgLightnessAccuracy;
AvgChromaAccuracy = avgChromaAccuracy; AvgChromaAccuracy = avgChromaAccuracy;
AvgHueAccuracy = avgHueAccuracy; AvgHueAccuracy = avgHueAccuracy;
AvgPerceivedAccuracy = avgPerceivedAccuracy; 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> /// <summary>

View file

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

View file

@ -132,7 +132,7 @@
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="ResultsView" <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" <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;"> 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" <ui:Label tabindex="-1" text="Templates" parse-escape-sequences="true"
@ -202,7 +202,7 @@
<ui:ListView name="LeaderboardListView"/> <ui:ListView name="LeaderboardListView"/>
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="AccountView" <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" <ui:Label tabindex="-1" text="You are not signed in." parse-escape-sequences="true"
display-tooltip-when-elided="true" name="AccountHeader" display-tooltip-when-elided="true" name="AccountHeader"
style="font-size: 58px; -unity-font-style: normal;"/> 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;"/> 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" <ui:Button text="Secondary Action Button →" parse-escape-sequences="true"
display-tooltip-when-elided="true" name="SecondaryActionButton" 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;"/> style="margin-top: 1%; margin-right: 1%; -unity-font-style: bold;"/>
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>