2024-11-17 19:10:01 +08:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
2024-11-19 01:36:19 +08:00
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-19 08:30:45 +08:00
2024-11-17 22:05:04 +08:00
/// <summary>
2024-11-19 01:36:19 +08:00
/// 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>
2024-11-19 08:30:45 +08:00
/// queue of the best online scores,
/// used in user rating calculation and accuracy display stats
2024-11-17 19:10:01 +08:00
/// </summary>
2024-11-19 08:30:45 +08:00
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>
2024-11-19 01:36:19 +08:00
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>
2024-11-19 01:36:19 +08:00
public readonly Queue < Score > RecentOnlineScores = new ( 10 ) ;
/// <summary>
2024-11-19 08:30:45 +08:00
/// last known email used
2024-11-19 01:36:19 +08:00
/// </summary>
2024-11-19 08:30:45 +08:00
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>
2024-11-18 00:16:53 +08:00
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 21:32:26 +08:00
}
2024-11-17 22:05:04 +08:00
2024-11-19 01:36:19 +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 21:32:26 +08:00
2024-11-17 22:31:03 +08:00
Debug . Log (
2024-11-19 01:36:19 +08:00
$"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
2024-11-18 00:16:53 +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 + + ;
}
2024-11-19 01:36:19 +08:00
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>
2024-11-19 08:30:45 +08:00
/// scaling function for user ratings
2024-11-19 03:59:29 +08:00
/// </summary>
2024-11-19 08:30:45 +08:00
/// <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 ( )
{
2024-11-19 01:36:19 +08:00
// 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 ) )
2024-11-19 08:30:45 +08:00
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 ) )
2024-11-19 08:30:45 +08:00
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 08:30:45 +08:00
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-19 01:36:19 +08:00
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>
2024-11-19 01:36:19 +08:00
/// ///
/// <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
2024-11-19 01:36:19 +08:00
2024-11-18 18:02:41 +08:00
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" ) ;
2024-11-19 01:36:19 +08:00
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 01:36:19 +08:00
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-19 01:36:19 +08:00
2024-11-18 10:34:29 +08:00
Timestamp = DateTimeOffset . FromUnixTimeSeconds ( timestamp ) . DateTime ;
2024-11-19 01:36:19 +08:00
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
}
}