diff --git a/ColourMeOKGame/Assets/Scripts/AccountUI.cs b/ColourMeOKGame/Assets/Scripts/AccountUI.cs
index 98a9e85..1c68236 100644
--- a/ColourMeOKGame/Assets/Scripts/AccountUI.cs
+++ b/ColourMeOKGame/Assets/Scripts/AccountUI.cs
@@ -67,6 +67,12 @@ public class AccountUI : MonoBehaviour
///
private Button _secondaryActionButton;
+ ///
+ /// either 'delete local data' or 'delete account'
+ /// (in order of 'initial', and 'post' states, is hidden in 'after' state)
+ ///
+ private Button _tertiaryActionButton;
+
///
/// username text field
///
@@ -108,6 +114,9 @@ public class AccountUI : MonoBehaviour
_secondaryActionButton = ui.Q("SecondaryActionButton");
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
+ _tertiaryActionButton = ui.Q("TertiaryActionButton");
+ _tertiaryActionButton.clicked += OnTertiaryActionButtonClick;
+
TransitionStateTo(State.NotSignedIn);
if (state == State.UnassociatedState) throw new Exception("unreachable state");
@@ -120,7 +129,7 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
_ => "An error occurred updating the username. Please try again."
};
});
-
- // TODO: update lpdata
}
///
@@ -409,8 +421,6 @@ public class AccountUI : MonoBehaviour
_ => "An error occurred updating the email. Please try again."
};
});
-
- // TODO: update lpdata
}
///
@@ -513,7 +523,8 @@ public class AccountUI : MonoBehaviour
case State.UnassociatedState:
default:
- throw new ArgumentOutOfRangeException();
+ Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
+ break;
}
}
@@ -537,7 +548,7 @@ public class AccountUI : MonoBehaviour
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 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
_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 @@ public class AccountUI : MonoBehaviour
/// populate the fields with the given username and email,
/// used as a callback to when local player data is changed
///
- 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}";
}
///
diff --git a/ColourMeOKGame/Assets/Scripts/Backend.cs b/ColourMeOKGame/Assets/Scripts/Backend.cs
index b9a558f..a1443a4 100644
--- a/ColourMeOKGame/Assets/Scripts/Backend.cs
+++ b/ColourMeOKGame/Assets/Scripts/Backend.cs
@@ -22,6 +22,7 @@ public class Backend
AlreadyAuthenticated,
NonExistentUser,
AlreadyExistingUser,
+ UsernameAlreadyTaken,
InvalidEmail,
InvalidCredentials,
GenericError
@@ -308,69 +309,90 @@ public class Backend
if (registerUser)
{
- // register user
- _auth.CreateUserWithEmailAndPasswordAsync(email, password)
- .ContinueWithOnMainThread(createTask =>
+ // check if the username is already taken
+ _db.Child("users")
+ .OrderByChild("username")
+ .EqualTo(registeringUsername)
+ .GetValueAsync()
+ .ContinueWithOnMainThread(task =>
{
- if (createTask.IsCompletedSuccessfully)
- {
- // store username
- _db.Child("users")
- .Child(_user.UserId)
- .Child("username")
- .SetValueAsync(registeringUsername)
- .ContinueWithOnMainThread(setUsernameTask =>
- {
- if (setUsernameTask.IsCompletedSuccessfully)
- {
- _username = registeringUsername;
- callback(AuthenticationResult.Ok);
- }
- else
- {
- Debug.LogError(setUsernameTask.Exception);
- callback(AuthenticationResult.GenericError);
- }
- });
- return;
- }
-
- if (createTask.Exception?.InnerException == null)
+ if (task.Exception != null)
{
+ Debug.LogError(task.Exception);
callback(AuthenticationResult.GenericError);
return;
}
- var error = (AuthError)((FirebaseException)createTask.Exception.InnerException).ErrorCode;
-
- // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
- switch (error)
+ if (!task.IsCompletedSuccessfully || task.Result.ChildrenCount > 0)
{
- case AuthError.UserNotFound:
- callback(AuthenticationResult.NonExistentUser);
- return;
-
- case AuthError.InvalidEmail:
- callback(AuthenticationResult.InvalidEmail);
- return;
-
- case AuthError.WeakPassword:
- case AuthError.InvalidCredential:
- callback(AuthenticationResult.InvalidCredentials);
- return;
-
- case AuthError.AccountExistsWithDifferentCredentials:
- case AuthError.EmailAlreadyInUse:
- callback(AuthenticationResult.AlreadyExistingUser);
- return;
-
- case AuthError.Failure:
- default:
- Debug.LogError(error);
- Debug.LogError(createTask.Exception);
- callback(AuthenticationResult.GenericError);
- break;
+ callback(AuthenticationResult.UsernameAlreadyTaken);
+ return;
}
+
+ // register user
+ _auth.CreateUserWithEmailAndPasswordAsync(email, password)
+ .ContinueWithOnMainThread(createTask =>
+ {
+ if (createTask.IsCompletedSuccessfully)
+ {
+ // store username
+ _db.Child("users")
+ .Child(_user.UserId)
+ .Child("username")
+ .SetValueAsync(registeringUsername)
+ .ContinueWithOnMainThread(setUsernameTask =>
+ {
+ if (setUsernameTask.IsCompletedSuccessfully)
+ {
+ _username = registeringUsername;
+ callback(AuthenticationResult.Ok);
+ }
+ else
+ {
+ Debug.LogError(setUsernameTask.Exception);
+ callback(AuthenticationResult.GenericError);
+ }
+ });
+ return;
+ }
+
+ if (createTask.Exception?.InnerException == null)
+ {
+ callback(AuthenticationResult.GenericError);
+ return;
+ }
+
+ var error = (AuthError)((FirebaseException)createTask.Exception.InnerException).ErrorCode;
+
+ // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
+ switch (error)
+ {
+ case AuthError.UserNotFound:
+ callback(AuthenticationResult.NonExistentUser);
+ return;
+
+ case AuthError.InvalidEmail:
+ callback(AuthenticationResult.InvalidEmail);
+ return;
+
+ case AuthError.WeakPassword:
+ case AuthError.InvalidCredential:
+ callback(AuthenticationResult.InvalidCredentials);
+ return;
+
+ case AuthError.AccountExistsWithDifferentCredentials:
+ case AuthError.EmailAlreadyInUse:
+ callback(AuthenticationResult.AlreadyExistingUser);
+ return;
+
+ case AuthError.Failure:
+ default:
+ Debug.LogError(error);
+ Debug.LogError(createTask.Exception);
+ callback(AuthenticationResult.GenericError);
+ break;
+ }
+ });
});
return;
}
@@ -443,24 +465,28 @@ public class Backend
return;
}
- _db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWithOnMainThread(task =>
- {
- DatabaseTransactionResult result;
- if (task.IsCompletedSuccessfully)
+ _db.Child("users")
+ .Child(_user.UserId)
+ .Child("username")
+ .GetValueAsync()
+ .ContinueWithOnMainThread(task =>
{
- result = DatabaseTransactionResult.Ok;
- _username = task.Result.Value.ToString();
- Debug.Log($"our username is {_username}");
- }
- else
- {
- result = DatabaseTransactionResult.Error;
- _username = "Unknown";
- Debug.LogError("failed to get username");
- }
+ DatabaseTransactionResult result;
+ if (task.IsCompletedSuccessfully)
+ {
+ result = DatabaseTransactionResult.Ok;
+ _username = task.Result.Value.ToString();
+ Debug.Log($"our username is {_username}");
+ }
+ else
+ {
+ result = DatabaseTransactionResult.Error;
+ _username = "Unknown";
+ Debug.LogError("failed to get username");
+ }
- callback(result, _username);
- });
+ callback(result, _username);
+ });
}
///
@@ -485,25 +511,45 @@ public class Backend
_auth.SignOut();
}
+ ///
+ /// abstraction function to delete the user
+ ///
+ public void DeleteUser()
+ {
+ _user.DeleteAsync().ContinueWithOnMainThread(task =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ Debug.Log("user deleted");
+ SignOutUser();
+ }
+ else
+ {
+ Debug.LogError(task.Exception);
+ }
+ });
+ }
+
///
/// abstraction function for the user to reset their password
///
/// the forgetful user's email lol
/// callback function to be invoked after the password reset email is sent
- public void ForgotPassword(string email, Action callback)
+ public void ResetUserPassword(string email, Action callback)
{
- _auth.SendPasswordResetEmailAsync(email).ContinueWithOnMainThread(resetTask =>
- {
- if (resetTask.IsCompletedSuccessfully)
+ _auth.SendPasswordResetEmailAsync(email)
+ .ContinueWithOnMainThread(resetTask =>
{
- callback(true);
- }
- else
- {
- Debug.LogError(resetTask.Exception);
- callback(false);
- }
- });
+ if (resetTask.IsCompletedSuccessfully)
+ {
+ callback(true);
+ }
+ else
+ {
+ Debug.LogError(resetTask.Exception);
+ callback(false);
+ }
+ });
}
///
@@ -522,7 +568,7 @@ public class Backend
callback(DatabaseTransactionResult.Unauthenticated, new List(0));
return;
}
-
+
_db.Child("scores")
.OrderByChild("timestamp")
.LimitToLast(LocalPlayerData.MaxBestOnlineScores)
@@ -535,10 +581,9 @@ public class Backend
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);
@@ -548,7 +593,6 @@ public class Backend
{
Debug.LogError($"{e}\n{child.GetRawJsonValue()}");
}
- }
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
@@ -562,7 +606,7 @@ public class Backend
/// callback function that takes in a DatabaseTransactionResult enum and a
/// List<LocalPlayerData.Score>
///
- public void GetBestScores(Action> callback)
+ private void GetBestScores(Action> callback)
{
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
@@ -571,7 +615,7 @@ public class Backend
callback(DatabaseTransactionResult.Unauthenticated, new List(0));
return;
}
-
+
_db.Child("scores")
.OrderByChild("avgPerceivedAccuracy")
.LimitToLast(LocalPlayerData.MaxBestOnlineScores)
@@ -584,10 +628,9 @@ public class Backend
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);
@@ -597,7 +640,6 @@ public class Backend
{
Debug.LogError(e);
}
- }
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
@@ -651,10 +693,10 @@ public class Backend
{
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 class Backend
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 class Backend
public void UpdateUserRating(
Action 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);
+ }
+ });
}
///
@@ -729,6 +794,8 @@ public class Backend
{
if (task.IsCompletedSuccessfully)
{
+ GameManager.Instance.Data.LastKnownEmail = newValue;
+ GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
callback(DatabaseTransactionResult.Ok);
}
else
@@ -740,12 +807,17 @@ public class Backend
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
diff --git a/ColourMeOKGame/Assets/Scripts/Colorimetry.cs b/ColourMeOKGame/Assets/Scripts/Colorimetry.cs
index fd8ad44..f6c585e 100644
--- a/ColourMeOKGame/Assets/Scripts/Colorimetry.cs
+++ b/ColourMeOKGame/Assets/Scripts/Colorimetry.cs
@@ -59,16 +59,16 @@ public static class Colorimetry
// https://en.wikipedia.org/wiki/Oklab_color_space#Color_differences
// ... "The perceptual color difference in Oklab is calculated as the Euclidean
- // ... 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
// ... Δ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 class Colorimetry
/// the linear srgb value to transform
/// the non-linear srgb value
// 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 class Colorimetry
/// the non-linear srgb value to transform
/// the linear srgb value
// 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 class Colorimetry
/// clips a colour to the sRGB gamut while preserving chroma
///
// 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 class Colorimetry
/// a and b must be normalized so a^2 + b^2 == 1
///
// 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 class Colorimetry
}
// 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;
diff --git a/ColourMeOKGame/Assets/Scripts/GameManager.cs b/ColourMeOKGame/Assets/Scripts/GameManager.cs
index 74cf866..8ce1299 100644
--- a/ColourMeOKGame/Assets/Scripts/GameManager.cs
+++ b/ColourMeOKGame/Assets/Scripts/GameManager.cs
@@ -175,12 +175,14 @@ public class GameManager : MonoBehaviour
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 class GameManager : MonoBehaviour
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy;
+ historicalRounds += onlineScore.NoOfRounds;
}
foreach (var onlineScore in _data.BestOnlineScores)
@@ -195,14 +198,19 @@ public class GameManager : MonoBehaviour
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;
var roundNumber = 1;
@@ -211,58 +219,69 @@ public class GameManager : MonoBehaviour
responseColour = round.ResponseColour)))
{
var dLCh = Colorimetry.CalculateLChSimilarityPercentage(distance);
-
+
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($"ShowcasePair{roundNumber}TemplateColour");
var showcaseResponse = ui.UI.Q($"ShowcasePair{roundNumber}ResponseColour");
var showcaseInfo = ui.UI.Q($"ShowcasePair{roundNumber}Info");
- if (showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null)
+ if (!(showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null))
+ {
+ showcaseTemplate.style.backgroundColor = templateColour;
+ showcaseResponse.style.backgroundColor = responseColour;
+ showcaseInfo.text =
+ $"{roundLightnessAcc:N0}% {roundChromaAcc:N0}% {roundHueAcc:N0}% ({roundPerceivedAcc:N0}%)";
+ }
+ else
{
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++;
+ roundNumber++; // used for ui querying
}
- roundLightnessAcc /= Gameplay.RoundsPerGame;
- roundChromaAcc /= Gameplay.RoundsPerGame;
- roundHueAcc /= Gameplay.RoundsPerGame;
- roundPerceivedAcc /= Gameplay.RoundsPerGame;
+ gameLightnessAcc /= Gameplay.RoundsPerGame;
+ gameChromaAcc /= Gameplay.RoundsPerGame;
+ gameHueAcc /= Gameplay.RoundsPerGame;
+ gamePerceivedAcc /= Gameplay.RoundsPerGame;
- var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc + roundPerceivedAcc + roundPerceivedAcc) / 5;
+ // 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 class GameManager : MonoBehaviour
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("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);
}
diff --git a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs
index 7ffa0a5..783bd60 100644
--- a/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs
+++ b/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using UnityEngine;
public class LocalPlayerData
@@ -14,12 +15,11 @@ public class LocalPlayerData
/// maximum number of recent local scores to keep track of
///
public const int MaxRecentLocalScores = 10;
-
+
///
- /// 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
///
- public Queue BestOnlineScores = new(20);
+ private const float ExponentialUserRatingGamma = 1.75f;
///
/// last known email used
@@ -34,13 +34,19 @@ public class LocalPlayerData
///
/// queue of the 10 most recent local scores
///
- public Queue RecentLocalScores = new(10);
+ public readonly Queue RecentLocalScores = new(10);
///
/// queue of the 10 most recent online scores,
/// used in user rating calculation and accuracy display stats
///
- public Queue RecentOnlineScores = new(10);
+ public readonly Queue RecentOnlineScores = new(10);
+
+ ///
+ /// queue of the best online scores,
+ /// used in user rating calculation and accuracy display stats
+ ///
+ public readonly Queue BestOnlineScores = new(20);
///
/// loads player data from player prefs and database
@@ -92,6 +98,10 @@ public class LocalPlayerData
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 class LocalPlayerData
}
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);
});
@@ -130,7 +140,7 @@ public class LocalPlayerData
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
}
///
@@ -149,7 +159,7 @@ public class LocalPlayerData
/// the user rating (0-100f)
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 class LocalPlayerData
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 class LocalPlayerData
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 class LocalPlayerData
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
@@ -218,7 +236,7 @@ public class LocalPlayerData
/// average hue accuracy across all rounds (0-100)
///
public readonly float AvgHueAccuracy;
-
+
///
/// average perceived accuracy across all rounds (0-100)
///
@@ -232,7 +250,8 @@ public class LocalPlayerData
/// 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)
+ /// ///
+ /// 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 e = 100.0f)
{
@@ -253,26 +272,36 @@ public class LocalPlayerData
{
// try to safely construct the score from a backend-provided dictionary
// 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");
- 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")
+ };
+ }
}
///
diff --git a/ColourMeOKGame/Assets/Scripts/SideViewUI.cs b/ColourMeOKGame/Assets/Scripts/SideViewUI.cs
index 9f53ffd..5375a03 100644
--- a/ColourMeOKGame/Assets/Scripts/SideViewUI.cs
+++ b/ColourMeOKGame/Assets/Scripts/SideViewUI.cs
@@ -105,6 +105,8 @@ public class SideViewUI : MonoBehaviour
///
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 @@ public class SideViewUI : MonoBehaviour
// 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}";
}
}
\ No newline at end of file
diff --git a/ColourMeOKGame/Assets/UI/GameUI.uxml b/ColourMeOKGame/Assets/UI/GameUI.uxml
index 307d0f3..420f5f3 100644
--- a/ColourMeOKGame/Assets/UI/GameUI.uxml
+++ b/ColourMeOKGame/Assets/UI/GameUI.uxml
@@ -132,7 +132,7 @@
+ 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;">
+ 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;">
@@ -236,6 +236,9 @@
style="-unity-text-align: middle-center; margin-bottom: 1%; margin-right: 1%; margin-top: 1%; -unity-font-style: bold;"/>
+