This repository has been archived on 2024-11-20. You can view files and clone it, but cannot push or open issues or pull requests.
colourmeok/ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs

339 lines
14 KiB
C#
Raw Permalink Normal View History

2024-11-17 19:10:01 +08:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
2024-11-17 19:10:01 +08:00
using UnityEngine;
2024-11-19 14:38:40 +08:00
/// <summary>
/// local player data structure/class
/// </summary>
2024-11-17 19:10:01 +08:00
public class LocalPlayerData
{
2024-11-18 09:16:03 +08:00
/// <summary>
/// maximum number of the best online scores to keep track of
/// </summary>
2024-11-19 03:59:29 +08:00
public const int MaxBestScores = 10;
2024-11-18 09:16:03 +08:00
/// <summary>
/// maximum number of recent local scores to keep track of
/// </summary>
2024-11-19 03:59:29 +08:00
public const int MaxRecentScores = 10;
2024-11-17 22:05:04 +08:00
/// <summary>
/// the gamma value used in the exponential user rating calculation
2024-11-17 22:05:04 +08:00
/// </summary>
2024-11-19 03:59:29 +08:00
private const float ExponentialUserRatingGamma = 2f;
2024-11-17 22:05:04 +08:00
2024-11-17 19:10:01 +08:00
/// <summary>
/// queue of the best online scores,
/// used in user rating calculation and accuracy display stats
2024-11-17 19:10:01 +08:00
/// </summary>
public readonly Queue<Score> BestOnlineScores = new(20);
2024-11-17 22:05:04 +08:00
2024-11-17 19:10:01 +08:00
/// <summary>
/// queue of the 10 most recent local scores
/// </summary>
public readonly Queue<Score> RecentLocalScores = new(10);
2024-11-17 22:05:04 +08:00
2024-11-17 19:10:01 +08:00
/// <summary>
/// queue of the 10 most recent online scores,
/// used in user rating calculation and accuracy display stats
/// </summary>
public readonly Queue<Score> RecentOnlineScores = new(10);
/// <summary>
/// last known email used
/// </summary>
public string LastKnownEmail = "";
/// <summary>
/// last known username used
/// </summary>
public string LastKnownUsername = "Guest";
2024-11-17 19:10:01 +08:00
/// <summary>
/// loads player data from player prefs and database
/// </summary>
public void LoadFromTheWorld(Action<LocalPlayerData> callback)
2024-11-17 19:10:01 +08:00
{
// load user data, possibly from the backend
var possibleUser = GameManager.Instance.Backend.GetUser();
var currentKnownEmail = string.Empty;
var currentKnownUsername = string.Empty;
if (possibleUser != null)
{
currentKnownEmail = possibleUser.Email;
currentKnownUsername = GameManager.Instance.Backend.GetUsername();
}
2024-11-17 22:05:04 +08:00
2024-11-17 19:10:01 +08:00
var lastStoredEmail = PlayerPrefs.GetString("LastKnownEmail", "");
var lastStoredUsername = PlayerPrefs.GetString("LastKnownUsername", "Guest");
LastKnownEmail = string.IsNullOrEmpty(currentKnownEmail) ? lastStoredEmail : currentKnownEmail;
LastKnownUsername = string.IsNullOrEmpty(currentKnownUsername) ? lastStoredUsername : currentKnownUsername;
// load local scores
RecentLocalScores.Clear();
for (var idx = 0; idx < 10; idx++)
{
var timestampRaw = PlayerPrefs.GetString($"RecentLocalScores_{idx}_Timestamp", "");
if (timestampRaw == "") continue;
var timestamp = DateTime.TryParseExact(timestampRaw,
"s",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var t)
? t
: DateTime.MinValue;
if (timestamp == DateTime.MinValue) continue;
var noOfRounds = PlayerPrefs.GetInt($"RecentLocalScores_{idx}_NoOfRounds", -1);
var l = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", -1f);
var c = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", -1f);
var h = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", -1f);
2024-11-18 10:34:29 +08:00
var e = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", -1f);
2024-11-17 19:10:01 +08:00
// if any of the values are invalid, don't add the score
2024-11-18 10:34:29 +08:00
if (noOfRounds < 0 || l < 0 || c < 0 || h < 0 || e < 0) continue;
2024-11-17 21:32:14 +08:00
2024-11-18 09:16:03 +08:00
RegisterLocalScore(new Score(timestamp, Math.Max(1, noOfRounds), Math.Clamp(l, 0f, 100f),
Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f)));
}
2024-11-17 22:05:04 +08:00
Debug.Log(
$"loaded lpdata from the local world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
callback(this);
2024-11-17 19:10:01 +08:00
// load online scores
RecentOnlineScores.Clear();
2024-11-17 22:05:04 +08:00
GameManager.Instance.Backend.GetRecentScores((_, recentOnlineScores) =>
2024-11-17 19:10:01 +08:00
{
foreach (var onlineScore in recentOnlineScores)
{
if (RecentOnlineScores.Count > 10) RecentOnlineScores.Dequeue();
RecentOnlineScores.Enqueue(onlineScore);
}
2024-11-17 22:31:03 +08:00
Debug.Log(
$"loaded lpdata from the online world (now {LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
2024-11-18 09:16:03 +08:00
callback(this);
2024-11-17 22:05:04 +08:00
});
2024-11-17 19:10:01 +08:00
}
/// <summary>
/// saves player data to player prefs
/// </summary>
public void SaveToTheWorld()
{
PlayerPrefs.SetString("LastKnownEmail", LastKnownEmail);
PlayerPrefs.SetString("LastKnownUsername", LastKnownUsername);
var idx = 0;
foreach (var score in RecentLocalScores)
{
PlayerPrefs.SetString($"RecentLocalScores_{idx}_Timestamp",
score.Timestamp.ToString("s", CultureInfo.InvariantCulture));
PlayerPrefs.SetInt($"RecentLocalScores_{idx}_NoOfRounds", score.NoOfRounds);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", score.AvgLightnessAccuracy);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", score.AvgChromaAccuracy);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", score.AvgHueAccuracy);
2024-11-18 10:34:29 +08:00
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", score.AvgPerceivedAccuracy);
2024-11-17 19:10:01 +08:00
idx++;
}
Debug.Log("saved lpdata to player prefs"); // online scores are already saved in the backend
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
/// <summary>
/// registers a score to the player's local data
/// </summary>
/// <param name="score">the score to register</param>
public void RegisterLocalScore(Score score)
{
2024-11-19 03:59:29 +08:00
while (RecentLocalScores.Count >= MaxRecentScores) RecentLocalScores.Dequeue();
2024-11-17 22:05:04 +08:00
RecentLocalScores.Enqueue(score);
}
2024-11-19 03:59:29 +08:00
/// <summary>
/// scaling function for user ratings
2024-11-19 03:59:29 +08:00
/// </summary>
/// <returns>the scaled user rating <c>double</c></returns>
2024-11-19 03:59:29 +08:00
public static double UserRatingScalingF(double rating)
{
var rawUserRating = Math.Clamp(rating, 0d, 100d);
var exponentialRating = 100d * Math.Pow(rawUserRating / 100d, ExponentialUserRatingGamma);
return Math.Clamp(exponentialRating, 0d, 100d);
}
2024-11-18 09:16:03 +08:00
/// <summary>
/// calculates the user rating based on whatever local data is available
/// </summary>
/// <returns>the user rating (0-100f)</returns>
public float CalculateUserRating()
{
// user rating is like CHUNITHMs rating system
2024-11-18 09:16:03 +08:00
// where best + recent scores are averaged out
// in this case 20 best scores, and 10 recent scores are used
// ensure the scores don't exceed their arbitrary limits
2024-11-19 03:59:29 +08:00
while (RecentOnlineScores.Count > MaxRecentScores) RecentOnlineScores.Dequeue();
while (RecentLocalScores.Count > MaxRecentScores) RecentLocalScores.Dequeue();
while (BestOnlineScores.Count > MaxBestScores) BestOnlineScores.Dequeue();
2024-11-18 09:16:03 +08:00
// if online scores are available, use them
var recentScores = RecentOnlineScores.Count > 0 ? RecentOnlineScores : RecentLocalScores;
var bestScores = BestOnlineScores;
var totalRating = 0d;
2024-11-19 22:13:53 +08:00
foreach (var score in recentScores.Take(MaxRecentScores))
totalRating += (score.AvgLightnessAccuracy + score.AvgChromaAccuracy + score.AvgHueAccuracy +
score.AvgPerceivedAccuracy) / 4d;
2024-11-18 09:16:03 +08:00
2024-11-19 22:13:53 +08:00
foreach (var score in bestScores.Take(MaxBestScores))
totalRating += (score.AvgLightnessAccuracy + score.AvgChromaAccuracy + score.AvgHueAccuracy +
score.AvgPerceivedAccuracy) / 4d;
2024-11-19 22:13:53 +08:00
var rating = UserRatingScalingF(totalRating /= MaxRecentScores + MaxBestScores);
2024-11-19 03:59:29 +08:00
Debug.Log($"locally calculated user rating: lin: {totalRating} -> exp: {rating}");
2024-11-19 03:59:29 +08:00
return (float)rating;
2024-11-18 09:16:03 +08:00
}
2024-11-19 21:03:04 +08:00
/// <summary>
/// safely get a float value from a dictionary
/// </summary>
/// <param name="data">the dictionary to get the value from</param>
/// <param name="key">the key to get the value from</param>
/// <returns>the float value</returns>
/// <exception cref="ArgumentException">thrown if the key is not found, or the value is not a valid float</exception>
2024-11-19 22:13:53 +08:00
public static float GetFloatyKey(Dictionary<string, object> data, string key)
2024-11-19 21:03:04 +08:00
{
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($"data['{key}'] not a valid float")
};
}
2024-11-18 09:16:03 +08:00
2024-11-17 19:10:01 +08:00
public struct Score
{
/// <summary>
/// timestamp of the score
/// </summary>
public DateTime Timestamp;
/// <summary>
/// number of rounds played (0-100)
/// </summary>
2024-11-18 09:16:03 +08:00
public readonly int NoOfRounds;
2024-11-17 19:10:01 +08:00
/// <summary>
/// average lightness accuracy across all rounds (0-100)
/// </summary>
2024-11-18 09:16:03 +08:00
public readonly float AvgLightnessAccuracy;
2024-11-17 19:10:01 +08:00
/// <summary>
/// average chroma accuracy across all rounds (0-100)
/// </summary>
2024-11-18 09:16:03 +08:00
public readonly float AvgChromaAccuracy;
2024-11-17 19:10:01 +08:00
/// <summary>
/// average hue accuracy across all rounds (0-100)
/// </summary>
2024-11-18 09:16:03 +08:00
public readonly float AvgHueAccuracy;
2024-11-18 10:34:29 +08:00
/// <summary>
/// average perceived accuracy across all rounds (0-100)
/// </summary>
public readonly float AvgPerceivedAccuracy;
2024-11-17 19:10:01 +08:00
/// <summary>
/// constructor for the score struct
/// </summary>
/// <param name="timestamp">timestamp of the score</param>
/// <param name="noOfRounds">number of rounds played (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="h">average hue accuracy across all rounds (0-100)</param>
/// ///
/// <param name="e">average perceived accuracy across all rounds (0-100)</param>
2024-11-17 19:10:01 +08:00
public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f,
2024-11-18 10:34:29 +08:00
float h = 100.0f, float e = 100.0f)
2024-11-17 19:10:01 +08:00
{
Timestamp = timestamp;
NoOfRounds = noOfRounds;
AvgLightnessAccuracy = l;
AvgChromaAccuracy = c;
AvgHueAccuracy = h;
2024-11-18 10:34:29 +08:00
AvgPerceivedAccuracy = e;
2024-11-17 19:10:01 +08:00
}
2024-11-18 09:16:03 +08:00
/// <summary>
/// dict-based constructor for the score struct
/// </summary>
/// <param name="data">dictionary of the score data</param>
2024-11-18 10:34:29 +08:00
/// <exception cref="ArgumentException">thrown if the dictionary is malformed or missing data</exception>
2024-11-18 09:16:03 +08:00
public Score(Dictionary<string, object> data)
{
// try to safely construct the score from a backend-provided dictionary
2024-11-18 10:34:29 +08:00
// 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)
2024-11-19 22:13:53 +08:00
throw new ArgumentException("data['timestamp'] not found or invalid");
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds)
2024-11-19 22:13:53 +08:00
throw new ArgumentException("data['noOfRounds'] not found or invalid");
2024-11-19 21:03:04 +08:00
var avgLightnessAccuracy = GetFloatyKey(data, "avgLightnessAccuracy");
var avgChromaAccuracy = GetFloatyKey(data, "avgChromaAccuracy");
var avgHueAccuracy = GetFloatyKey(data, "avgHueAccuracy");
var avgPerceivedAccuracy = GetFloatyKey(data, "avgPerceivedAccuracy");
2024-11-18 10:34:29 +08:00
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
NoOfRounds = (int)noOfRounds;
2024-11-18 10:34:29 +08:00
AvgLightnessAccuracy = avgLightnessAccuracy;
AvgChromaAccuracy = avgChromaAccuracy;
AvgHueAccuracy = avgHueAccuracy;
AvgPerceivedAccuracy = avgPerceivedAccuracy;
2024-11-18 09:16:03 +08:00
}
/// <summary>
/// converts the score struct to a dictionary safe for the backend
/// </summary>
/// <returns></returns>
public Dictionary<string, object> ToDictionary()
{
return new Dictionary<string, object>
{
{ "timestamp", new DateTimeOffset(Timestamp).ToUnixTimeSeconds() },
{ "noOfRounds", NoOfRounds },
{ "avgLightnessAccuracy", AvgLightnessAccuracy },
{ "avgChromaAccuracy", AvgChromaAccuracy },
2024-11-18 10:34:29 +08:00
{ "avgHueAccuracy", AvgHueAccuracy },
{ "avgPerceivedAccuracy", AvgPerceivedAccuracy }
2024-11-18 09:16:03 +08:00
};
}
2024-11-19 22:13:53 +08:00
/// <summary>
/// converts the score struct to a dictionary safe for the backend
/// </summary>
/// <returns></returns>
public Dictionary<string, object> ToDictionary(string userId)
{
return new Dictionary<string, object>
{
{ "userId", userId },
{ "timestamp", new DateTimeOffset(Timestamp).ToUnixTimeSeconds() },
{ "noOfRounds", NoOfRounds },
{ "avgLightnessAccuracy", AvgLightnessAccuracy },
{ "avgChromaAccuracy", AvgChromaAccuracy },
{ "avgHueAccuracy", AvgHueAccuracy },
{ "avgPerceivedAccuracy", AvgPerceivedAccuracy }
};
}
2024-11-17 19:10:01 +08:00
}
}