game: gameplay + database interim 2

This commit is contained in:
Mark Joshwel 2024-11-18 10:34:29 +08:00
parent cc79a5b1a0
commit a338a62f27
6 changed files with 163 additions and 44 deletions

View file

@ -515,8 +515,44 @@ public void ForgotPassword(string email, Action<bool> callback)
/// </param> /// </param>
public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback) public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
{ {
// TODO: implement this if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
if (_user == null)
{
callback(DatabaseTransactionResult.Unauthenticated, new List<LocalPlayerData.Score>(0));
return;
}
_db.Child("scores")
.OrderByChild("timestamp")
.LimitToLast(LocalPlayerData.MaxBestOnlineScores)
.GetValueAsync()
.ContinueWithOnMainThread(task =>
{
if (!task.IsCompletedSuccessfully)
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0)); callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0));
return;
}
var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children)
{
try
{
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>);
scores.Add(score);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
});
} }
/// <summary> /// <summary>
@ -528,8 +564,44 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// </param> /// </param>
public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback) public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
{ {
// TODO: implement this if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
if (_user == null)
{
callback(DatabaseTransactionResult.Unauthenticated, new List<LocalPlayerData.Score>(0));
return;
}
_db.Child("scores")
.OrderByChild("avgPerceivedAccuracy")
.LimitToLast(LocalPlayerData.MaxBestOnlineScores)
.GetValueAsync()
.ContinueWithOnMainThread(task =>
{
if (!task.IsCompletedSuccessfully)
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0)); callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0));
return;
}
var scores = new List<LocalPlayerData.Score>();
foreach (var child in task.Result.Children)
{
try
{
var score = new LocalPlayerData.Score(child.Value as Dictionary<string, object>);
scores.Add(score);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
callback(DatabaseTransactionResult.Ok, scores);
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
});
} }
/// <summary> /// <summary>
@ -588,7 +660,7 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
var recentScoreQueue = GameManager.Instance.Data.RecentOnlineScores; var recentScoreQueue = GameManager.Instance.Data.RecentOnlineScores;
foreach (var score in recentScores) recentScoreQueue.Enqueue(score); foreach (var score in recentScores) recentScoreQueue.Enqueue(score);
while (recentScoreQueue.Count > LocalPlayerData.MaxBestOnlineScores) recentScoreQueue.Dequeue(); while (recentScoreQueue.Count > LocalPlayerData.MaxRecentLocalScores) recentScoreQueue.Dequeue();
GetBestScores((bestRes, bestScores) => GetBestScores((bestRes, bestScores) =>
{ {
@ -600,7 +672,7 @@ public void GetBestScores(Action<DatabaseTransactionResult, List<LocalPlayerData
} }
var bestScoreQueue = GameManager.Instance.Data.BestOnlineScores; var bestScoreQueue = GameManager.Instance.Data.BestOnlineScores;
foreach (var score in recentScores) bestScoreQueue.Enqueue(score); foreach (var score in bestScores) bestScoreQueue.Enqueue(score);
while (bestScoreQueue.Count > LocalPlayerData.MaxBestOnlineScores) bestScoreQueue.Dequeue(); while (bestScoreQueue.Count > LocalPlayerData.MaxBestOnlineScores) bestScoreQueue.Dequeue();
callback(DatabaseTransactionResult.Ok, GameManager.Instance.Data.CalculateUserRating()); callback(DatabaseTransactionResult.Ok, GameManager.Instance.Data.CalculateUserRating());

View file

@ -34,6 +34,11 @@ public class GameManager : MonoBehaviour
/// </summary> /// </summary>
public Backend Backend; public Backend Backend;
/// <summary>
/// gameplay object for handling game loop
/// </summary>
public Gameplay Gameplay;
/// <summary> /// <summary>
/// read-only property for accessing the local player data outside this class /// read-only property for accessing the local player data outside this class
/// </summary> /// </summary>
@ -121,6 +126,8 @@ private void OnEnable()
? DisplayStyle.Flex ? DisplayStyle.Flex
: DisplayStyle.None; : DisplayStyle.None;
}); });
Gameplay = new Gameplay(ui.UI);
} }
/// <summary> /// <summary>
@ -146,7 +153,7 @@ public void RegisterOnLocalPlayerDataChangeCallback(Action<LocalPlayerData> call
/// <summary> /// <summary>
/// function to fire all local player data change callbacks /// function to fire all local player data change callbacks
/// </summary> /// </summary>
private void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data) public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
{ {
Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})"); Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
foreach (var callback in _onLocalPlayerDataChangeCallbacks) foreach (var callback in _onLocalPlayerDataChangeCallbacks)
@ -194,6 +201,7 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
var roundLightnessAcc = 0d; var roundLightnessAcc = 0d;
var roundChromaAcc = 0d; var roundChromaAcc = 0d;
var roundHueAcc = 0d; var roundHueAcc = 0d;
var roundPerceivedAcc = 0d;
var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white); var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white);
@ -203,11 +211,13 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
roundLightnessAcc += distance.dL / maxDistance.dL; roundLightnessAcc += distance.dL / maxDistance.dL;
roundChromaAcc += distance.dC / maxDistance.dC; roundChromaAcc += distance.dC / maxDistance.dC;
roundHueAcc += distance.dH / maxDistance.dH; roundHueAcc += distance.dH / maxDistance.dH;
roundPerceivedAcc += distance.dE / maxDistance.dE;
} }
roundLightnessAcc /= playedRounds.Count; roundLightnessAcc /= playedRounds.Count;
roundChromaAcc /= playedRounds.Count; roundChromaAcc /= playedRounds.Count;
roundHueAcc /= playedRounds.Count; roundHueAcc /= playedRounds.Count;
roundPerceivedAcc /= playedRounds.Count;
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3; var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3;
@ -223,9 +233,12 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
playedRounds.Count, playedRounds.Count,
(float)roundLightnessAcc, (float)roundLightnessAcc,
(float)roundChromaAcc, (float)roundChromaAcc,
(float)roundHueAcc); (float)roundHueAcc,
(float)roundPerceivedAcc);
_data.RegisterLocalScore(score); _data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
Backend.SubmitScore(score, Backend.SubmitScore(score,
submitRes => submitRes =>
{ {
@ -242,6 +255,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);
return; return;
} }

View file

@ -42,16 +42,23 @@ public class Gameplay
/// <summary> /// <summary>
/// game round counter /// game round counter
/// </summary> /// </summary>
private int _round = -1; public int Round = -1;
public int RoundsPerGame = 5; /// <summary>
public double SecondsPerRound = 15d; /// 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>
/// <param name="ui">the visual element object for the ui document with the GameView</param> /// <param name="ui">the visual element object for the ui document with the GameView</param>
private Gameplay(VisualElement ui) public Gameplay(VisualElement ui)
{ {
_roundText = ui.Q<Label>("RoundText"); _roundText = ui.Q<Label>("RoundText");
_countdownText = ui.Q<Label>("TimeText"); _countdownText = ui.Q<Label>("TimeText");
@ -62,18 +69,18 @@ private Gameplay(VisualElement ui)
/// <summary> /// <summary>
/// function for starting the game /// function for starting the game
/// </summary> /// </summary>
private void StartGame() public void StartGame()
{ {
_round = 0; Round = 0;
AdvanceToNextRound(); AdvanceToNextRound();
} }
/// <summary> /// <summary>
/// called once per frame /// (to be) called once per frame
/// </summary> /// </summary>
private void Update() public void Update()
{ {
if (_round < 1) return; if (Round < 1) return;
if (_countdownDatetime < DateTime.Now) AdvanceToNextRound(); if (_countdownDatetime < DateTime.Now) AdvanceToNextRound();
Render(); Render();
} }
@ -83,18 +90,19 @@ private void Update()
/// </summary> /// </summary>
private void AdvanceToNextRound() private void AdvanceToNextRound()
{ {
if (_round > 0) StoreRoundInfo(); if (Round > 0) StoreRoundInfo();
GenerateNewTemplateColour(); GenerateNewTemplateColour();
if (_round < RoundsPerGame) if (Round < RoundsPerGame)
{ {
_round++; Debug.Log("round advance");
Round++;
_countdownDatetime = DateTime.Now.AddSeconds(SecondsPerRound); _countdownDatetime = DateTime.Now.AddSeconds(SecondsPerRound);
} }
else else
{ {
// end game // end game
_round = -1; Round = -1;
GameManager.Instance.SignalGameEnd(_playedRounds); GameManager.Instance.SignalGameEnd(_playedRounds);
} }
} }
@ -109,6 +117,7 @@ private void StoreRoundInfo()
TemplateColour = _templateColour.style.backgroundColor.value, TemplateColour = _templateColour.style.backgroundColor.value,
ResponseColour = _responseColour.style.backgroundColor.value ResponseColour = _responseColour.style.backgroundColor.value
}); });
Debug.Log("stored round info");
} }
/// <summary> /// <summary>
@ -130,6 +139,7 @@ private void GenerateNewTemplateColour()
); );
_templateColour.style.backgroundColor = new StyleColor(colour); _templateColour.style.backgroundColor = new StyleColor(colour);
Debug.Log($"generated new template colour {colour}");
} }
/// <summary> /// <summary>
@ -138,7 +148,7 @@ private void GenerateNewTemplateColour()
private void Render() private void Render()
{ {
var remaining = (_countdownDatetime - DateTime.Now).TotalSeconds; var remaining = (_countdownDatetime - DateTime.Now).TotalSeconds;
_roundText.text = $"{_round}/{RoundsPerGame}"; _roundText.text = $"{Round}/{RoundsPerGame}";
_countdownText.text = $"{remaining:F}"; _countdownText.text = $"{remaining:F}";
} }

View file

@ -83,9 +83,10 @@ public void LoadFromTheWorld(Action<LocalPlayerData> callback)
var l = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", -1f); var l = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", -1f);
var c = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", -1f); var c = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", -1f);
var h = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", -1f); var h = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", -1f);
var e = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", -1f);
// if any of the values are invalid, don't add the score // if any of the values are invalid, don't add the score
if (noOfRounds < 0 || l < 0 || c < 0 || h < 0) continue; if (noOfRounds < 0 || l < 0 || c < 0 || h < 0 || e < 0) continue;
RegisterLocalScore(new Score(timestamp, Math.Max(1, noOfRounds), Math.Clamp(l, 0f, 100f), RegisterLocalScore(new Score(timestamp, Math.Max(1, noOfRounds), Math.Clamp(l, 0f, 100f),
Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f))); Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f)));
@ -125,6 +126,7 @@ public void SaveToTheWorld()
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", score.AvgLightnessAccuracy); PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", score.AvgLightnessAccuracy);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", score.AvgChromaAccuracy); PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", score.AvgChromaAccuracy);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", score.AvgHueAccuracy); PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", score.AvgHueAccuracy);
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgPerceivedAccuracy", score.AvgPerceivedAccuracy);
idx++; idx++;
} }
@ -217,6 +219,11 @@ public struct Score
/// </summary> /// </summary>
public readonly float AvgHueAccuracy; public readonly float AvgHueAccuracy;
/// <summary>
/// average perceived accuracy across all rounds (0-100)
/// </summary>
public readonly float AvgPerceivedAccuracy;
/// <summary> /// <summary>
/// constructor for the score struct /// constructor for the score struct
/// </summary> /// </summary>
@ -225,35 +232,47 @@ public struct Score
/// <param name="l">average lightness accuracy across all rounds (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="c">average chroma accuracy across all rounds (0-100)</param>
/// <param name="h">average hue 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>
public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f, public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f,
float h = 100.0f) float h = 100.0f, float e = 100.0f)
{ {
Timestamp = timestamp; Timestamp = timestamp;
NoOfRounds = noOfRounds; NoOfRounds = noOfRounds;
AvgLightnessAccuracy = l; AvgLightnessAccuracy = l;
AvgChromaAccuracy = c; AvgChromaAccuracy = c;
AvgHueAccuracy = h; AvgHueAccuracy = h;
AvgPerceivedAccuracy = e;
} }
/// <summary> /// <summary>
/// dict-based constructor for the score struct /// dict-based constructor for the score struct
/// </summary> /// </summary>
/// <param name="data">dictionary of the score data</param> /// <param name="data">dictionary of the score data</param>
/// <exception cref="ArgumentException">thrown if the dictionary is malformed or missing data</exception>
public Score(Dictionary<string, object> data) 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, set it to a default value // for each value, if it's not found, or not a valid value, throw an exception
Timestamp = data.ContainsKey("timestamp") && data["timestamp"] is long t
? DateTimeOffset.FromUnixTimeSeconds(t).DateTime if (!data.ContainsKey("timestamp") || !(data["timestamp"] is long timestamp))
: DateTime.MinValue; throw new ArgumentException("timestamp not found or invalid");
NoOfRounds = data.ContainsKey("noOfRounds") && data["noOfRounds"] is int n ? n : 1; if (!data.ContainsKey("noOfRounds") || !(data["noOfRounds"] is int noOfRounds))
AvgLightnessAccuracy = data.ContainsKey("avgLightnessAccuracy") && data["avgLightnessAccuracy"] is float l throw new ArgumentException("noOfRounds not found or invalid");
? l if (!data.ContainsKey("avgLightnessAccuracy") || !(data["avgLightnessAccuracy"] is float avgLightnessAccuracy))
: 100.0f; throw new ArgumentException("avgLightnessAccuracy not found or invalid");
AvgChromaAccuracy = data.ContainsKey("avgChromaAccuracy") && data["avgChromaAccuracy"] is float c if (!data.ContainsKey("avgChromaAccuracy") || !(data["avgChromaAccuracy"] is float avgChromaAccuracy))
? c throw new ArgumentException("avgChromaAccuracy not found or invalid");
: 100.0f; if (!data.ContainsKey("avgHueAccuracy") || !(data["avgHueAccuracy"] is float avgHueAccuracy))
AvgHueAccuracy = data.ContainsKey("avgHueAccuracy") && data["avgHueAccuracy"] is float h ? h : 100.0f; throw new ArgumentException("avgHueAccuracy not found or invalid");
if (!data.ContainsKey("avgPerceivedAccuracy") || !(data["avgPerceivedAccuracy"] is float avgPerceivedAccuracy))
throw new ArgumentException("avgPerceivedAccuracy not found or invalid");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
NoOfRounds = noOfRounds;
AvgLightnessAccuracy = avgLightnessAccuracy;
AvgChromaAccuracy = avgChromaAccuracy;
AvgHueAccuracy = avgHueAccuracy;
AvgPerceivedAccuracy = avgPerceivedAccuracy;
} }
/// <summary> /// <summary>
@ -268,7 +287,8 @@ public Score(Dictionary<string, object> data)
{ "noOfRounds", NoOfRounds }, { "noOfRounds", NoOfRounds },
{ "avgLightnessAccuracy", AvgLightnessAccuracy }, { "avgLightnessAccuracy", AvgLightnessAccuracy },
{ "avgChromaAccuracy", AvgChromaAccuracy }, { "avgChromaAccuracy", AvgChromaAccuracy },
{ "avgHueAccuracy", AvgHueAccuracy } { "avgHueAccuracy", AvgHueAccuracy },
{ "avgPerceivedAccuracy", AvgPerceivedAccuracy }
}; };
} }
} }

View file

@ -16,11 +16,6 @@ public class SideViewUI : MonoBehaviour
/// </summary> /// </summary>
private Label _chromaAccuracyText; private Label _chromaAccuracyText;
/// <summary>
/// connection status label for showing the connection status
/// </summary>
private Label _connectionStatusLabel;
/// <summary> /// <summary>
/// text label for showing the player's stable-ish hue accuracy /// text label for showing the player's stable-ish hue accuracy
/// </summary> /// </summary>
@ -67,8 +62,6 @@ private void OnEnable()
_accountButton = ui.Q<Button>("AccountButton"); _accountButton = ui.Q<Button>("AccountButton");
_accountButton.clicked += OnAccountButtonClicked; _accountButton.clicked += OnAccountButtonClicked;
_connectionStatusLabel = ui.Q<Label>("ConnectionStatusLabel");
_playerText = ui.Q<Label>("PlayerText"); _playerText = ui.Q<Label>("PlayerText");
_ratingText = ui.Q<Label>("RatingText"); _ratingText = ui.Q<Label>("RatingText");

View file

@ -74,6 +74,13 @@ private void OnEnable()
UI = GetComponent<UIDocument>().rootVisualElement; UI = GetComponent<UIDocument>().rootVisualElement;
} }
private void Update()
{
var gameplay = GameManager.Instance.Gameplay;
if (gameplay == null) return;
if (gameplay.Round >= 1) gameplay.Update();
}
/// <summary> /// <summary>
/// function to show a menu based on the enum passed, /// function to show a menu based on the enum passed,
/// and any other necessary actions /// and any other necessary actions
@ -134,6 +141,9 @@ public void SetDisplayState(DisplayState newDisplayState)
UI.Q<Button>("AccountButton").style.display = DisplayStyle.None; UI.Q<Button>("AccountButton").style.display = DisplayStyle.None;
UI.Q<VisualElement>("AccountSection").style.display = DisplayStyle.Flex; UI.Q<VisualElement>("AccountSection").style.display = DisplayStyle.Flex;
UI.Q<VisualElement>("ConnectionStatus").style.display = DisplayStyle.None; UI.Q<VisualElement>("ConnectionStatus").style.display = DisplayStyle.None;
// start the game
GameManager.Instance.Gameplay.StartGame();
} }
else else
{ {