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_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!114 &133964671 --- !u!114 &133964671
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -258,7 +258,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!4 &447905427 --- !u!4 &447905427
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -476,7 +476,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!114 &1204483825 --- !u!114 &1204483825
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -505,37 +505,6 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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 --- !u!1660057539 &9223372036854775807
SceneRoots: SceneRoots:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -545,4 +514,3 @@ SceneRoots:
- {fileID: 133964672} - {fileID: 133964672}
- {fileID: 447905427} - {fileID: 447905427}
- {fileID: 1204483826} - {fileID: 1204483826}
- {fileID: 1680304395}

View file

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

View file

@ -3,13 +3,48 @@
public static class Colorimetry 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> /// <summary>
/// calculate a 0-100% distance/accuracy between two unity rgba colour objects /// calculate a 0-100% distance/accuracy between two unity rgba colour objects
/// </summary> /// </summary>
/// <param name="template">the template colour to compare against</param> /// <param name="template">the template colour to compare against</param>
/// <param name="response">the response colour to compare</param> /// <param name="response">the response colour to compare</param>
/// <returns>a <c>DeltaLabCHE</c> struct</returns> /// <returns>a <c>DeltaLabChE</c> struct</returns>
public static DeltaLabCHE CalculateDistance(Color template, Color response) public static DeltaLabChE CalculateDistance(Color template, Color response)
{ {
// rgb to oklab // rgb to oklab
var templateOklab = linear_srgb_to_oklab(new RGB( 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 deltaH = Math.Max(0d, Math.Sqrt(deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
var deltaE = Math.Sqrt(deltaL * deltaL + deltaC * deltaC + deltaH * deltaH); 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> /// <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 /// 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,
@ -380,27 +414,27 @@ public static float compute_max_saturation(float a, float b)
} }
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public struct DeltaLabCHE public struct DeltaLabChE
{ {
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double dL; public readonly double dL;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double da; public readonly double da;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double db; public readonly double db;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double dC; public readonly double dC;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double dH; public readonly double dh;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public double dE; public readonly double dE;
public DeltaLabCHE( public DeltaLabChE(
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
double L, double L,
double a, double a,
@ -416,7 +450,7 @@ public struct DeltaLabCHE
da = a; da = a;
db = b; db = b;
dC = C; dC = C;
dH = H; dh = H;
dE = E; 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 public readonly struct RGB
{ {
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming

View file

@ -202,24 +202,48 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
var roundChromaAcc = 0d; var roundChromaAcc = 0d;
var roundHueAcc = 0d; var roundHueAcc = 0d;
var roundPerceivedAcc = 0d; var roundPerceivedAcc = 0d;
var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white); var templateColour = Color.clear;
var responseColour = Color.clear;
foreach (var distance in playedRounds.Select(round => var roundNumber = 1;
Colorimetry.CalculateDistance(round.TemplateColour, round.ResponseColour))) foreach (var distance in playedRounds.Take(Gameplay.RoundsPerGame).Select(round =>
Colorimetry.CalculateDistance(templateColour = round.TemplateColour,
responseColour = round.ResponseColour)))
{ {
roundLightnessAcc += distance.dL / maxDistance.dL; var dLCh = Colorimetry.CalculateLChSimilarityPercentage(distance);
roundChromaAcc += distance.dC / maxDistance.dC;
roundHueAcc += distance.dH / maxDistance.dH; Debug.Log(
roundPerceivedAcc += distance.dE / maxDistance.dE; $"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;
}
showcaseTemplate.style.backgroundColor = templateColour;
showcaseResponse.style.backgroundColor = responseColour;
showcaseInfo.text = $"{roundLightnessAcc:F}% {roundChromaAcc:F}% {roundHueAcc:F}% ({roundPerceivedAcc:F}%)";
roundNumber++;
} }
roundLightnessAcc /= playedRounds.Count; roundLightnessAcc /= Gameplay.RoundsPerGame;
roundChromaAcc /= playedRounds.Count; roundChromaAcc /= Gameplay.RoundsPerGame;
roundHueAcc /= playedRounds.Count; roundHueAcc /= Gameplay.RoundsPerGame;
roundPerceivedAcc /= playedRounds.Count; roundPerceivedAcc /= Gameplay.RoundsPerGame;
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3; var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc + roundPerceivedAcc + roundPerceivedAcc) / 5;
// make comparison texts // make comparison texts
var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") + var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") +
@ -237,7 +261,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
(float)roundPerceivedAcc); (float)roundPerceivedAcc);
_data.RegisterLocalScore(score); _data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); FireLocalPlayerDataChangeCallbacks(Instance.Data);
Backend.SubmitScore(score, Backend.SubmitScore(score,
submitRes => submitRes =>
@ -255,7 +279,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
{ {
Debug.Log("couldn't calculate user rating"); Debug.Log("couldn't calculate user rating");
TransitionToResultsView(_data.CalculateUserRating()); TransitionToResultsView(_data.CalculateUserRating());
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data); FireLocalPlayerDataChangeCallbacks(Instance.Data);
return; return;
} }
@ -277,14 +301,14 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
void TransitionToResultsView(float rating) 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 // 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 {roundAcc} accurate.", "",
$"Lightness was {roundLightnessAcc}% accurate. ({lAccDeltaText} from your average)", $"Lightness was {roundLightnessAcc:P}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {roundChromaAcc}% accurate. ({cAccDeltaText} from your average)", $"Chroma was {roundChromaAcc:P}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {roundHueAcc}% accurate. ({hAccDeltaText} from your average)") + ratingText; $"Hue was {roundHueAcc:P}% accurate. ({hAccDeltaText} from your average)") + ratingText;
ui.SetDisplayState(UIManager.DisplayState.ResultsView); ui.SetDisplayState(UIManager.DisplayState.ResultsView);
} }

View file

@ -9,6 +9,17 @@
/// </summary> /// </summary>
public class Gameplay 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> /// <summary>
/// countdown text label for showing the countdown /// countdown text label for showing the countdown
/// </summary> /// </summary>
@ -44,16 +55,6 @@ public class Gameplay
/// </summary> /// </summary>
public int Round = -1; 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> /// <summary>
/// constructor for the gameplay class /// constructor for the gameplay class
/// </summary> /// </summary>
@ -126,20 +127,17 @@ private void StoreRoundInfo()
/// </summary> /// </summary>
private void GenerateNewTemplateColour() private void GenerateNewTemplateColour()
{ {
var r = new Random(); // - lightness: 40-80
// - chroma: 0.05-0.20
// - lightness: 0.4-0.8
// - chroma: 0.0-0.2
// - hue: all (0-360) // - hue: all (0-360)
var colour = Colorimetry.RawLchToColor( var r = new Random();
Math.Clamp(r.NextDouble() * 0.4d + 0.4d, 0.4d, 0.8d), var l = Math.Clamp(r.NextDouble() * 40d + 40d, 40d, 100d);
Math.Clamp(r.NextDouble() * 0.2d, 0d, 0.2d), var c = Math.Clamp(r.NextDouble() * 0.15d + 0.05d, 0.05d, 0.20d);
Math.Clamp(r.NextDouble() * 360d, 0d, 360d) 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 = new StyleColor(colour); _templateColour.style.backgroundColor = colour;
Debug.Log($"generated new template colour {colour}");
} }
/// <summary> /// <summary>

View file

@ -254,17 +254,17 @@ public Score(Dictionary<string, object> data)
// try to safely construct the score from a backend-provided dictionary // 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 // 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"); 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"); 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"); 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"); 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"); 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"); throw new ArgumentException("avgPerceivedAccuracy not found or invalid");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;

View file

@ -145,19 +145,13 @@ private void RenderFromPlayerData(LocalPlayerData data)
var playerText = GameManager.Instance.Backend.IsSignedIn var playerText = GameManager.Instance.Backend.IsSignedIn
? data.LastKnownUsername ? data.LastKnownUsername
: $"{data.LastKnownUsername} (Not Signed In)"; : $"{data.LastKnownUsername} (Not Signed In)";
var rating = data.CalculateUserRating();
// finally, set the labels // finally, set the labels
_playerText.text = playerText; _playerText.text = playerText;
_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}";
// 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}";
});
} }
} }