game: working game, half-working database

This commit is contained in:
Mark Joshwel 2024-11-18 18:02:41 +08:00
parent a338a62f27
commit 5e1defa793
8 changed files with 150 additions and 105 deletions

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="MarkdownNoTableBorders" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View file

@ -143,7 +143,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!114 &133964671
MonoBehaviour:
m_ObjectHideFlags: 0
@ -258,7 +258,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!4 &447905427
Transform:
m_ObjectHideFlags: 0
@ -476,7 +476,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!114 &1204483825
MonoBehaviour:
m_ObjectHideFlags: 0
@ -505,37 +505,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1680304394
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1680304395}
m_Layer: 0
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1680304395
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1680304394}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@ -545,4 +514,3 @@ SceneRoots:
- {fileID: 133964672}
- {fileID: 447905427}
- {fileID: 1204483826}
- {fileID: 1680304395}

View file

@ -546,7 +546,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
}
catch (Exception e)
{
Debug.LogError(e);
Debug.LogError($"{e}\n{child.GetRawJsonValue()}");
}
}

View file

@ -3,13 +3,48 @@
public static class Colorimetry
{
/// <summary>
/// calculate a similarity percentage from a colour distance
/// </summary>
/// <param name="delta">
/// the <c>DeltaLabChE</c> object returned by <c>CalculateDistance</c>,
/// </param>
/// <param name="chromaMax">
/// the maximum chroma value to use for the similarity percentage calculation,
/// defaults to 1.0f
/// </param>
/// <param name="hueMax">
/// the maximum hue value to use for the similarity percentage calculation,
/// defaults to 1.0f
/// </param>
/// <param name="lightnessMax">
/// the maximum lightness value to use for the similarity percentage calculation,
/// defaults to 1.0f
/// </param>
/// <returns>a <c>LCh</c> struct with 0-1f values</returns>
public static LCh CalculateLChSimilarityPercentage(
DeltaLabChE delta,
double chromaMax = 1.0d,
double hueMax = 1.0d,
double lightnessMax = 1.0d)
{
// dL = [-1, 1] lightness difference (negative = template is darker)
// dC = [-inf, +inf] chroma difference (negative = template is more chromatic)
// dH = [0, +inf] hue difference (zero for grayscale or similar hues)
// dE = [0, 1] overall perceptual difference in the oklab colour space
// (but since we're using sRGB, we just use 1.0f as the max bounds)
return new LCh((float)Math.Clamp(1 - Math.Abs(delta.dL) / lightnessMax, 0, 1),
(float)Math.Clamp(1 - Math.Abs(delta.dC) / chromaMax, 0, 1),
(float)Math.Clamp(1 - delta.dh / hueMax, 0, 1));
}
/// <summary>
/// calculate a 0-100% distance/accuracy between two unity rgba colour objects
/// </summary>
/// <param name="template">the template colour to compare against</param>
/// <param name="response">the response colour to compare</param>
/// <returns>a <c>DeltaLabCHE</c> struct</returns>
public static DeltaLabCHE CalculateDistance(Color template, Color response)
/// <returns>a <c>DeltaLabChE</c> struct</returns>
public static DeltaLabChE CalculateDistance(Color template, Color response)
{
// rgb to oklab
var templateOklab = linear_srgb_to_oklab(new RGB(
@ -48,7 +83,7 @@ public static DeltaLabCHE CalculateDistance(Color template, Color response)
var deltaH = Math.Max(0d, Math.Sqrt(deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
var deltaE = Math.Sqrt(deltaL * deltaL + deltaC * deltaC + deltaH * deltaH);
return new DeltaLabCHE(deltaL, deltaA, deltaB, deltaC, deltaH, deltaE);
return new DeltaLabChE(deltaL, deltaA, deltaB, deltaC, deltaH, deltaE);
}
/// <summary>
@ -136,7 +171,6 @@ public static RGB gamut_clip_preserve_chroma(RGB rgb)
/// a and b must be normalized so a^2 + b^2 == 1
/// </summary>
// https://bottosson.github.io/posts/gamutclipping/ (MIT)
// ReSharper disable once MemberCanBePrivate.Global
public static float find_gamut_intersection(
float a,
float b,
@ -380,27 +414,27 @@ public static float compute_max_saturation(float a, float b)
}
// ReSharper disable once InconsistentNaming
public struct DeltaLabCHE
public struct DeltaLabChE
{
// ReSharper disable once InconsistentNaming
public double dL;
public readonly double dL;
// ReSharper disable once InconsistentNaming
public double da;
public readonly double da;
// ReSharper disable once InconsistentNaming
public double db;
public readonly double db;
// ReSharper disable once InconsistentNaming
public double dC;
public readonly double dC;
// ReSharper disable once InconsistentNaming
public double dH;
public readonly double dh;
// ReSharper disable once InconsistentNaming
public double dE;
public readonly double dE;
public DeltaLabCHE(
public DeltaLabChE(
// ReSharper disable once InconsistentNaming
double L,
double a,
@ -416,7 +450,7 @@ public struct DeltaLabCHE
da = a;
db = b;
dC = C;
dH = H;
dh = H;
dE = E;
}
}
@ -440,6 +474,27 @@ public Lab(float L, float a, float b)
}
}
public readonly struct LCh
{
public readonly float L;
public readonly float C;
// ReSharper disable once InconsistentNaming
public readonly float h;
public LCh(
// ReSharper disable once InconsistentNaming
float L,
// ReSharper disable once InconsistentNaming
float C,
float h)
{
this.L = L;
this.C = C;
this.h = h;
}
}
public readonly struct RGB
{
// ReSharper disable once InconsistentNaming

View file

@ -203,23 +203,47 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
var roundHueAcc = 0d;
var roundPerceivedAcc = 0d;
var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white);
foreach (var distance in playedRounds.Select(round =>
Colorimetry.CalculateDistance(round.TemplateColour, round.ResponseColour)))
var templateColour = Color.clear;
var responseColour = Color.clear;
var roundNumber = 1;
foreach (var distance in playedRounds.Take(Gameplay.RoundsPerGame).Select(round =>
Colorimetry.CalculateDistance(templateColour = round.TemplateColour,
responseColour = round.ResponseColour)))
{
roundLightnessAcc += distance.dL / maxDistance.dL;
roundChromaAcc += distance.dC / maxDistance.dC;
roundHueAcc += distance.dH / maxDistance.dH;
roundPerceivedAcc += distance.dE / maxDistance.dE;
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 showcaseTemplate = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}TemplateColour");
var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour");
var showcaseInfo = ui.UI.Q<Label>($"ShowcasePair{roundNumber}Info");
if (showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null)
{
Debug.LogError($"showcase pair {roundNumber} not found");
roundNumber++;
continue;
}
roundLightnessAcc /= playedRounds.Count;
roundChromaAcc /= playedRounds.Count;
roundHueAcc /= playedRounds.Count;
roundPerceivedAcc /= playedRounds.Count;
showcaseTemplate.style.backgroundColor = templateColour;
showcaseResponse.style.backgroundColor = responseColour;
showcaseInfo.text = $"{roundLightnessAcc:F}% {roundChromaAcc:F}% {roundHueAcc:F}% ({roundPerceivedAcc:F}%)";
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3;
roundNumber++;
}
roundLightnessAcc /= Gameplay.RoundsPerGame;
roundChromaAcc /= Gameplay.RoundsPerGame;
roundHueAcc /= Gameplay.RoundsPerGame;
roundPerceivedAcc /= Gameplay.RoundsPerGame;
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc + roundPerceivedAcc + roundPerceivedAcc) / 5;
// make comparison texts
var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") +
@ -237,7 +261,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
(float)roundPerceivedAcc);
_data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
FireLocalPlayerDataChangeCallbacks(Instance.Data);
Backend.SubmitScore(score,
submitRes =>
@ -255,7 +279,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
{
Debug.Log("couldn't calculate user rating");
TransitionToResultsView(_data.CalculateUserRating());
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
FireLocalPlayerDataChangeCallbacks(Instance.Data);
return;
}
@ -277,14 +301,14 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
void TransitionToResultsView(float rating)
{
var ratingText = rating >= 0 ? $"\nYour rating is {rating}" : "\nYour rating could not be calculated.";
var ratingText = rating >= 0 ? $"\nYour rating is {rating:F}" : "\nYour rating could not be calculated.";
// build the result text and show the results view
ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,",
$"you were {roundAcc} accurate.", "",
$"Lightness was {roundLightnessAcc}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {roundChromaAcc}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {roundHueAcc}% accurate. ({hAccDeltaText} from your average)") + ratingText;
$"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;
ui.SetDisplayState(UIManager.DisplayState.ResultsView);
}

View file

@ -9,6 +9,17 @@
/// </summary>
public class Gameplay
{
/// <summary>
/// singleton instance of the gameplay class
/// </summary>
public const int RoundsPerGame = 5;
/// <summary>
/// seconds per round
/// </summary>
// ReSharper disable once MemberCanBePrivate.Global
public const double SecondsPerRound = 15d;
/// <summary>
/// countdown text label for showing the countdown
/// </summary>
@ -44,16 +55,6 @@ public class Gameplay
/// </summary>
public int Round = -1;
/// <summary>
/// singleton instance of the gameplay class
/// </summary>
private const int RoundsPerGame = 5;
/// <summary>
/// seconds per round
/// </summary>
private const double SecondsPerRound = 15d;
/// <summary>
/// constructor for the gameplay class
/// </summary>
@ -126,20 +127,17 @@ private void StoreRoundInfo()
/// </summary>
private void GenerateNewTemplateColour()
{
var r = new Random();
// - lightness: 0.4-0.8
// - chroma: 0.0-0.2
// - lightness: 40-80
// - chroma: 0.05-0.20
// - hue: all (0-360)
var colour = Colorimetry.RawLchToColor(
Math.Clamp(r.NextDouble() * 0.4d + 0.4d, 0.4d, 0.8d),
Math.Clamp(r.NextDouble() * 0.2d, 0d, 0.2d),
Math.Clamp(r.NextDouble() * 360d, 0d, 360d)
);
_templateColour.style.backgroundColor = new StyleColor(colour);
Debug.Log($"generated new template colour {colour}");
var r = new Random();
var l = Math.Clamp(r.NextDouble() * 40d + 40d, 40d, 100d);
var c = Math.Clamp(r.NextDouble() * 0.15d + 0.05d, 0.05d, 0.20d);
var h = Math.Clamp(r.NextDouble() * 360d, 0d, 360d);
var colour = Colorimetry.RawLchToColor(l, c, h);
Debug.Log($"generated new template colour LCh({l:F}, {c:F}, {h:F}) -> {colour}");
_templateColour.style.backgroundColor = colour;
}
/// <summary>

View file

@ -254,17 +254,17 @@ public Score(Dictionary<string, object> data)
// 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 long timestamp))
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 int noOfRounds))
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not int noOfRounds)
throw new ArgumentException("noOfRounds not found or invalid");
if (!data.ContainsKey("avgLightnessAccuracy") || !(data["avgLightnessAccuracy"] is float avgLightnessAccuracy))
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 float avgChromaAccuracy))
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 float avgHueAccuracy))
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 float avgPerceivedAccuracy))
if (!data.ContainsKey("avgPerceivedAccuracy") || data["avgPerceivedAccuracy"] is not float avgPerceivedAccuracy)
throw new ArgumentException("avgPerceivedAccuracy not found or invalid");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;

View file

@ -145,19 +145,13 @@ private void RenderFromPlayerData(LocalPlayerData data)
var playerText = GameManager.Instance.Backend.IsSignedIn
? data.LastKnownUsername
: $"{data.LastKnownUsername} (Not Signed In)";
var rating = data.CalculateUserRating();
// finally, set the labels
_playerText.text = playerText;
_lightnessAccuracyText.text = $"{lightnessAcc:F}";
_chromaAccuracyText.text = $"{chromaAcc:F}";
_hueAccuracyText.text = $"{hueAcc:F}";
// and set the player rating, but after we get it from the backend
// (god I LOVE async (I am LYING out of my teeth))
GameManager.Instance.Backend.CalculateUserRating((dtr, rating) =>
{
if (dtr != Backend.DatabaseTransactionResult.Ok) return;
_ratingText.text = $"{rating:F}";
});
}
}