From af7457a9d958893697128f357dc9582c0741602f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A4seToatz?= Date: Wed, 6 Nov 2024 21:46:37 +0100 Subject: [PATCH] Finished scoring system and started tests --- Memory.Cmd/{Renderer.cs => CmdGame.cs} | 58 ++++++++++++++++++++++---- Memory.Cmd/Program.cs | 24 ++++------- Memory.Data/Memory.Data.csproj | 4 ++ Memory.Data/ScoreHandler.cs | 38 +++++++++++++++++ Memory.Gui/App.xaml.cs | 8 +--- Memory.Gui/MainWindow.xaml | 19 ++++++--- Memory.Gui/MainWindow.xaml.cs | 54 +++++++++++++++++++----- Memory.Logic/Game.cs | 28 ++++++++++++- Memory.Logic/IScoreHandler.cs | 2 + Memory.Logic/Score.cs | 8 ++++ Memory.Test/GameTest.cs | 36 ++++++++++++++++ Memory.Test/Memory.Test.csproj | 28 +++++++++++++ Memory.sln | 8 +++- 13 files changed, 265 insertions(+), 50 deletions(-) rename Memory.Cmd/{Renderer.cs => CmdGame.cs} (59%) create mode 100644 Memory.Logic/Score.cs create mode 100644 Memory.Test/GameTest.cs create mode 100644 Memory.Test/Memory.Test.csproj diff --git a/Memory.Cmd/Renderer.cs b/Memory.Cmd/CmdGame.cs similarity index 59% rename from Memory.Cmd/Renderer.cs rename to Memory.Cmd/CmdGame.cs index b61a6f0..283282e 100644 --- a/Memory.Cmd/Renderer.cs +++ b/Memory.Cmd/CmdGame.cs @@ -2,7 +2,7 @@ namespace Memory.Cmd { - internal class Renderer(Game game) + internal class CmdGame(Game game) { private readonly Game game = game; @@ -26,7 +26,7 @@ namespace Memory.Cmd for (int i = 0; i < Game.GRIDSIZE; i++) { Console.CursorLeft += (cardWidth + 1) * column; - Console.CursorTop = i + ((Game.GRIDSIZE + 1) * row); + Console.CursorTop = i + ((Game.GRIDSIZE + 1) * row) + 1; for (int j = 0; j < cardWidth; j++) { if (i == 0) @@ -50,14 +50,14 @@ namespace Memory.Cmd } else if (i == Game.GRIDSIZE / 2 && j > 1 && j < cardWidth - 2) { - //if (card.Selected()) - //{ + if (card.Selected()) + { Console.Write(num[j - 2]); - //} - //else - //{ - // Console.Write('*'); - //} + } + else + { + Console.Write('*'); + } } else { @@ -71,6 +71,7 @@ namespace Memory.Cmd public void Redraw() { Console.Clear(); + Console.WriteLine($"Score: {game.Scoring.Points}"); for (int i = 0; i < game.Cards.Count; i++) { Card card = game.Cards[i]; @@ -89,5 +90,44 @@ namespace Memory.Cmd } } } + + public void WriteScores() + { + Console.WriteLine($"Your score: {game.Scoring.Points}\nTop 10 Highscores:"); + List highscores = game.ScoreHandler.GetTopScores(); + for (int i = 0; i < highscores.Count; i++) + { + Score score = highscores[i]; + Console.WriteLine($"{i+1}. {score.Name}: {score.Points}"); + } + } + + public static string GetPlayerName() + { + Console.Write("Enter your name: "); + string? name = Console.ReadLine(); + if (name == null || name.Length > 32 || name.Length < 2) + { + Console.Write("Name must be between 2 and 32 characters.\nEnter your name: "); + name = Console.ReadLine(); + } + return name!; + } + + public static bool ShouldReplay() + { + Console.Write("Game Finished. Do you want to play again? (Y/N): "); + string? answer = Console.ReadLine(); + while (answer == null || (!answer.Equals("y", StringComparison.CurrentCultureIgnoreCase) && !answer.Equals("n", StringComparison.CurrentCultureIgnoreCase))) + { + Console.Write("Invalid answer.\nDo you want to play again? (Y/N): "); + answer = Console.ReadLine(); + } + if (answer.Equals("n", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + return true; + } } } diff --git a/Memory.Cmd/Program.cs b/Memory.Cmd/Program.cs index f13692a..8b42f68 100644 --- a/Memory.Cmd/Program.cs +++ b/Memory.Cmd/Program.cs @@ -7,36 +7,28 @@ namespace Memory.Cmd { static void Main() { + string name = CmdGame.GetPlayerName(); while (true) { - Game game = new(new ScoreHandler()); - Renderer renderer = new(game); + Game game = new(new ScoreHandler(), name); + CmdGame cmdGame = new(game); while (!game.IsFinished()) { - renderer.Redraw(); + cmdGame.Redraw(); Console.Write("Enter card number: "); try { game.ClickCard(game.Cards[int.Parse(Console.ReadLine()!) - 1]); } - catch (Exception) - { - Console.Write("Invalid card number given."); - Console.ReadLine(); - } + catch (Exception) {} } Console.Clear(); - Console.Write("Game Finished. Do you want to play again? (Y/N): "); - string? answer = Console.ReadLine(); - while (answer == null || (!answer.Equals("y", StringComparison.CurrentCultureIgnoreCase) && !answer.Equals("n", StringComparison.CurrentCultureIgnoreCase))) - { - Console.Write("Invalid answer.\nDo you want to play again? (Y/N): "); - answer = Console.ReadLine(); - } - if (answer.Equals("n", StringComparison.CurrentCultureIgnoreCase)) + cmdGame.WriteScores(); + if (!CmdGame.ShouldReplay()) { break; } + } } } diff --git a/Memory.Data/Memory.Data.csproj b/Memory.Data/Memory.Data.csproj index 7507a42..02fec2b 100644 --- a/Memory.Data/Memory.Data.csproj +++ b/Memory.Data/Memory.Data.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/Memory.Data/ScoreHandler.cs b/Memory.Data/ScoreHandler.cs index 9062e5c..4115687 100644 --- a/Memory.Data/ScoreHandler.cs +++ b/Memory.Data/ScoreHandler.cs @@ -1,9 +1,47 @@ using Memory.Logic; +using System.Data.SQLite; namespace Memory.Data { public class ScoreHandler : IScoreHandler { + public const string URI = "Data Source=Scores.db;Version=3;"; + public ScoreHandler() + { + using SQLiteConnection connection = new(URI); + connection.Open(); + using SQLiteCommand command = new("CREATE TABLE IF NOT EXISTS Scores(ID INTEGER PRIMARY KEY, Name TEXT, Points INTEGER)", connection); + command.ExecuteNonQuery(); + connection.Close(); + } + + public List GetTopScores() + { + List scores = []; + using SQLiteConnection connection = new(URI); + connection.Open(); + using SQLiteCommand command = new("SELECT Name, Points FROM Scores ORDER BY Points DESC LIMIT 10", connection); + using (SQLiteDataReader reader = command.ExecuteReader()) + { + while (reader.Read()) + { + scores.Add(new(reader.GetString(0), reader.GetInt32(1))); + } + } + connection.Close(); + return scores; + } + + public void WriteScore(Score score) + { + using SQLiteConnection connection = new(URI); + connection.Open(); + using SQLiteCommand command = new("INSERT INTO Scores(Name, Points) VALUES(@Name, @Points)", connection); + command.Parameters.AddWithValue("@Name", score.Name); + command.Parameters.AddWithValue("@Points", score.Points); + command.ExecuteNonQuery(); + connection.Close(); + } } } diff --git a/Memory.Gui/App.xaml.cs b/Memory.Gui/App.xaml.cs index 3ee6ed3..f7222f6 100644 --- a/Memory.Gui/App.xaml.cs +++ b/Memory.Gui/App.xaml.cs @@ -1,14 +1,8 @@ -using System.Configuration; -using System.Data; -using System.Windows; +using System.Windows; namespace Memory.Gui { - /// - /// Interaction logic for App.xaml - /// public partial class App : Application { } - } diff --git a/Memory.Gui/MainWindow.xaml b/Memory.Gui/MainWindow.xaml index c47524f..d689092 100644 --- a/Memory.Gui/MainWindow.xaml +++ b/Memory.Gui/MainWindow.xaml @@ -11,14 +11,23 @@ ResizeMode="NoResize"> - - + + + + - diff --git a/Memory.Gui/MainWindow.xaml.cs b/Memory.Gui/MainWindow.xaml.cs index e27a2d3..28129a1 100644 --- a/Memory.Gui/MainWindow.xaml.cs +++ b/Memory.Gui/MainWindow.xaml.cs @@ -8,6 +8,9 @@ namespace Memory.Gui { public partial class MainWindow : Window { + public const int SCOREPADDING = 160; + public const int SCOREMARGIN = 30; + private Game? game; public MainWindow() @@ -18,15 +21,45 @@ namespace Memory.Gui private void StartGame(object sender, RoutedEventArgs args) { - game = new(new ScoreHandler()); - StartScreen.Visibility = Visibility.Hidden; - FinishScreen.Visibility = Visibility.Hidden; - GameScreen.Visibility = Visibility.Visible; - Redraw(); + string name = Name.Text; + if (name.Length > 32 || name.Length < 2) + { + ErrorLabel.Visibility = Visibility.Visible; + } + else + { + game = new(new ScoreHandler(), name); + ErrorLabel.Visibility = Visibility.Hidden; + StartScreen.Visibility = Visibility.Hidden; + FinishScreen.Visibility = Visibility.Hidden; + GameScreen.Visibility = Visibility.Visible; + Redraw(); + } + } + + private void DrawScores() + { + Highscores.Children.Clear(); + OwnScore.Content = $"Your score: {game!.Scoring.Points}"; + List highscores = game.ScoreHandler.GetTopScores(); + for (int i = 0; i < highscores.Count; i++) + { + Score score = highscores[i]; + Label label = new() + { + Content = $"{i+1}. {score.Name}: {score.Points}", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top, + FontSize = 30, + Margin = new Thickness(0, SCOREPADDING + SCOREMARGIN * i, 0, 0) + }; + Highscores.Children.Add(label); + } } private void FinishGame() { + DrawScores(); GameScreen.Visibility = Visibility.Hidden; FinishScreen.Visibility = Visibility.Visible; } @@ -41,7 +74,7 @@ namespace Memory.Gui { Width = new(1, GridUnitType.Star) }; - GameScreen.ColumnDefinitions.Add(colDef); + Cards.ColumnDefinitions.Add(colDef); } for (int i = 0; i < rows; i++) { @@ -49,14 +82,15 @@ namespace Memory.Gui { Height = new(1, GridUnitType.Star) }; - GameScreen.RowDefinitions.Add(rowDef); + Cards.RowDefinitions.Add(rowDef); } } private void Redraw() { - GameScreen.Children.Clear(); - for (int i = 0; i < game.Cards.Count; i++) + Cards.Children.Clear(); + Score.Content = $"Score: {game!.Scoring.Points}"; + for (int i = 0; i < game!.Cards.Count; i++) { Card card = game.Cards[i]; if (!card.Completed) @@ -81,7 +115,7 @@ namespace Memory.Gui FinishGame(); } }; - GameScreen.Children.Add(button); + Cards.Children.Add(button); } } } diff --git a/Memory.Logic/Game.cs b/Memory.Logic/Game.cs index 4d116cd..6e767b1 100644 --- a/Memory.Logic/Game.cs +++ b/Memory.Logic/Game.cs @@ -1,11 +1,18 @@ namespace Memory.Logic { - public class Game(IScoreHandler scoreHandler) + public class Game(IScoreHandler scoreHandler, string player) { public const int DECKSIZE = 10; public const int GRIDSIZE = 5; + public const int MAXPOINTS = 100; + public const int MINPOINTS = 10; + public const int MAXTIME = 10000; + public const int STARTSCORE = 100; + public List Cards { get; } = CreateDeck(DECKSIZE); public IScoreHandler ScoreHandler { get; } = scoreHandler; + public Score Scoring { get; set; } = new(player, STARTSCORE); + public long LastGuess { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds(); private static List CreateDeck(int pairs) { @@ -19,6 +26,15 @@ return [..cards.OrderBy(card => random.Next())]; } + public void IncreaseScore() + { + long curTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + double percentage = 1.0 - (double)(curTime - LastGuess) / MAXTIME; + int points = (int)(percentage * MAXPOINTS); + Scoring.Points += Math.Min(Math.Max(points, MINPOINTS), MAXPOINTS); + LastGuess = curTime; + } + public Card? GetChoice1() { return Cards.FirstOrDefault(card => card.IsChoice1); @@ -59,7 +75,15 @@ { choice1.Completed = true; card.Completed = true; - // handle score etc + IncreaseScore(); + if (IsFinished()) + { + ScoreHandler.WriteScore(Scoring); + } + } + else + { + Scoring.Points -= 10; } } } diff --git a/Memory.Logic/IScoreHandler.cs b/Memory.Logic/IScoreHandler.cs index ca7eb53..5ddbec5 100644 --- a/Memory.Logic/IScoreHandler.cs +++ b/Memory.Logic/IScoreHandler.cs @@ -2,5 +2,7 @@ { public interface IScoreHandler { + public void WriteScore(Score score); + public List GetTopScores(); } } diff --git a/Memory.Logic/Score.cs b/Memory.Logic/Score.cs new file mode 100644 index 0000000..8c99b15 --- /dev/null +++ b/Memory.Logic/Score.cs @@ -0,0 +1,8 @@ +namespace Memory.Logic +{ + public class Score(string name, int points) + { + public string Name { get; } = name; + public int Points { get; set; } = points; + } +} diff --git a/Memory.Test/GameTest.cs b/Memory.Test/GameTest.cs new file mode 100644 index 0000000..d08942e --- /dev/null +++ b/Memory.Test/GameTest.cs @@ -0,0 +1,36 @@ +using Memory.Logic; +using Memory.Data; + +namespace Memory.Test +{ + [TestClass] + public class GameTest + { + [TestMethod] + public void IncreaseScore_QuarterTime_ShouldEqual75PercentOfPoints() + { + int score = (int)(Game.MAXPOINTS * 0.75); + Game game = new(new ScoreHandler()); + Thread.Sleep(Game.MAXTIME / 4); + game.IncreaseScore(); + Assert.IsTrue(score - 1 <= game.Score && score + 1 >= game.Score); + } + + [TestMethod] + public void IncreaseScore_TooLong_ShouldEqualLowerBoundOfPoints() + { + Game game = new(new ScoreHandler()); + Thread.Sleep(Game.MAXTIME); + game.IncreaseScore(); + Assert.AreEqual(Game.MINPOINTS, game.Score); + } + + [TestMethod] + public void IncreaseScore_Instant_ShouldEqualUpperBoundOfPoints() + { + Game game = new(new ScoreHandler()); + game.IncreaseScore(); + Assert.AreEqual(Game.MAXPOINTS, game.Score); + } + } +} \ No newline at end of file diff --git a/Memory.Test/Memory.Test.csproj b/Memory.Test/Memory.Test.csproj new file mode 100644 index 0000000..d53106c --- /dev/null +++ b/Memory.Test/Memory.Test.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/Memory.sln b/Memory.sln index a278b4b..abd8de2 100644 --- a/Memory.sln +++ b/Memory.sln @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memory.Gui", "Memory.Gui\Me EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memory.Cmd", "Memory.Cmd\Memory.Cmd.csproj", "{36DBAAC0-3FEC-4C5C-8330-C1BD2D08BD05}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memory.Data", "Memory.Data\Memory.Data.csproj", "{9ED8FC5D-4B8F-4FAA-AF87-087347548513}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memory.Data", "Memory.Data\Memory.Data.csproj", "{9ED8FC5D-4B8F-4FAA-AF87-087347548513}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memory.Test", "Memory.Test\Memory.Test.csproj", "{57EE3199-A942-4250-AD1E-B3EFBAEC89FF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +35,10 @@ Global {9ED8FC5D-4B8F-4FAA-AF87-087347548513}.Debug|Any CPU.Build.0 = Debug|Any CPU {9ED8FC5D-4B8F-4FAA-AF87-087347548513}.Release|Any CPU.ActiveCfg = Release|Any CPU {9ED8FC5D-4B8F-4FAA-AF87-087347548513}.Release|Any CPU.Build.0 = Release|Any CPU + {57EE3199-A942-4250-AD1E-B3EFBAEC89FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57EE3199-A942-4250-AD1E-B3EFBAEC89FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57EE3199-A942-4250-AD1E-B3EFBAEC89FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57EE3199-A942-4250-AD1E-B3EFBAEC89FF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE