diff --git a/MathGame/MathGame.csproj b/MathGame/MathGame.csproj new file mode 100755 index 00000000..6af5a5d1 --- /dev/null +++ b/MathGame/MathGame.csproj @@ -0,0 +1,8 @@ + + + Exe + net10.0 + enable + enable + + diff --git a/MathGame/MathGame.slnx b/MathGame/MathGame.slnx new file mode 100755 index 00000000..b337a196 --- /dev/null +++ b/MathGame/MathGame.slnx @@ -0,0 +1,3 @@ + + + diff --git a/MathGame/README.md b/MathGame/README.md new file mode 100755 index 00000000..d0934133 --- /dev/null +++ b/MathGame/README.md @@ -0,0 +1,58 @@ +# Math + +this project was developed as part of csharp-academy challenge and doesn't represent a core or a useful business product however you can use it to play with your children and young siblings who happened to be a computer nerd but will not receive any further core update but pull requests are welcomed + +this project was a real testing playground to measure what really I can do with this language keep in mind that my level is beginner by the time of writing this repo with previous experience in python and basic understanding in c++ and java (don't ever try learning java 8) + +I didn't plan to make it full oop and I was gonna make a monolithic class with many many partial members but I discarded the idea and refactored everything into oop base + +and to be honest I didn't planned to make it fully function TUI but I guess I love over-engineering stuff always and over think it + +## to the csharp-academy mentors reviewing my project + +please guide me through how to plan your project structure and what type of problems generally in programming exercise but please I want guidance on how to think not like a coder like a system designer how to make multiple components communicate with each other and lastly I want an objective brutally honest review thing I can do better based on my current knowledge thing I shouldn't have done am not aiming for praises I want to get a jop in the .net marked and it to be a key for getting outside of my home country so I want to know what wrong and what right to enhance myself + +I truly appreciate your review in advance. + +## AI honesty + +about 60% of feature was designed and planned by me however due to the level of inexperience in debugging generally and c# specifically and to be honest I used the debugger sometimes to watch how my variables change to guess the issue and in other times I would simply trace it if it's simple and proceed in random changes till it got fix magically + +however due to inexperience in code deigning and how to connect my ideas some features like dictionary, using abstract class, using interfaces and workarounds to pass the threading locks such as readline or looping to get the timer, orchestrating multiple objects through interface, using Enums, searching a better way of generating random number and shuffling them, better code optimizations and lastly as I remember how to wire your ideas into robust execution + +however I'm not a big fan of copy pasting sense I learn by search , read and apply so I wrote almost all the code by hand but I used rider's built-in tools to help in code refactoring + +## commit philosophy + +I followed when committing Conventional Commits.Standard Commit Structure + +```text +[optional scope]: +``` + +- Common Commit Types + - feat: A new feature for the user. + - fix: A bug fix for the user. + - docs: Changes to the documentation. + - style: Formatting, missing semi-colons, etc. (no production code changes). + - refactor: Refactoring production code (neither fixing a bug nor adding a feature). + - perf: Code changes that improve performance. + - test: Adding missing tests or correcting existing tests. + - build: Changes that affect the build system or external dependencies. + - ci: Changes to CI configuration files and scripts. + - chore: Other changes that don't modify src or test files. + - revert: Reverts a previous commit. + +## What I discovered and learned on top of my current knowledge + +- `StringBuilder` and `ConsoleKeyInfo` input combination : at least now I know a new way of processing user input alongside with `Readline()` that would not pause the user execution but sure it super hard and easy to mess things up +- How to get the user's console size and utilize it for dictating some conditions like what I did +- How to set the default value of your Properties and I learned the hard way that properties cannot be passed by reference +- `"\a"` is the `Console.Beep()` cross-platform alt +- basic understanding of Enums +- basic understanding of struct and when to use them +- basic understanding of records and its types and when to use both +- how to work with abstraction +- how to work with interfaces and use them +- how to use Random.shared +- the basics of using collection and LINQ diff --git a/MathGame/docs/planning.md b/MathGame/docs/planning.md new file mode 100755 index 00000000..9b19cecf --- /dev/null +++ b/MathGame/docs/planning.md @@ -0,0 +1,111 @@ +# project + +## Requirements + +- You need to create a game that **consists** of asking the player what's the result of a math question (i.e. 9 x 9 = ?), collecting the input and adding a point in case of a correct answer. + + + +- A game needs to have at least 5 questions. +- The divisions should result on INTEGERS ONLY and dividends should go from 0 to 100. Example: Your app shouldn't present the division 7/2 to the user, since it doesn't result in an integer. +- Users should be presented with a menu to choose an operation +- You should record previous games in a List and there should be an option in the menu for the user to visualize a history of previous games. +- You don't need to record results on a database. Once the program is closed the results will be deleted. + +## challenges + +- Try to implement levels of difficulty. +- Add a timer to track how long the user takes to finish the game. +- Create a 'Random Game' option where the players will be presented with questions from random operations +- To follow the DRY Principle, try using just one method for all games. Additionally, double check your project and try to find opportunities to achieve the same functionality with less code, avoiding repetition when possible. + +## Main menu + +- the window must be constantly update to much the screen (18 X 80) + +```text +███╗ ███╗ █████╗ ████████╗██╗ ██╗ ██████╗ █████╗ ███╗ ███╗███████╗ +████╗ ████║██╔══██╗╚══██╔══╝██║ ██║ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝ +██╔████╔██║███████║ ██║ ███████║ ██║ ███╗███████║██╔████╔██║█████╗ +██║╚██╔╝██║██╔══██║ ██║ ██╔══██║ ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ +██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ +╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ [this will take about 6 X 76] + + + a. Start The Game + b. History Record + c. About The Game + d. Exit + + + +[this will be in the end of the screen] +[hint of how the user will choose] [this will take about 1 X 50] +Type your answer : [your choice] (MUST TRIM AND LOWER THE USER'INPUT) [this will take about 1 X 37] +``` + +### logic + +- the update loop must check if window width and hight are (18 X 80) if not it will pause the execution until the user follow the instructions +- there should be an option in the menu for the user to **visualize a history of previous games**. +- there should be an option for **starting the game which** which will give the following set of question to answer + - choose a difficulty (type the Menu Keys:[a,b,c,d]) + - a. easy (digits from 0 to 10) + - b. normal (digits from 0 to 100) + - c. hard (digits from 0 to 10 + TIMED 10s) + - d. insane (digits from 0 to 100 + TIMED 10s and 15s if random) + - e. impossible (digits from 0 to 100 + TIMED 5s and 10s if random) + > upon choosing the difficulty it will then show the type of operation + - choose operation + - a. addition (+) + - b. subtraction (-) + - c. multiplication (×) + - d. division (÷) + - e. random operation + > upon choosing the operation it will then choose the type of question + - choose your type of question + - a. MCQ + - b. true or false + - c. fill the gaps + - d. normal + - e. random + +## setup menu + +- the window of the start setup Which will define how the game will work + +```text +Choose From The Following Options + +choose a difficulty + + a. easy (digits from 0 to 10) 4 + b. normal (digits from 0 to 100) + c. hard (digits from 0 to 10 + TIMED 10s) + d. insane (digits from 0 to 100 + TIMED 10s and 15s if random operation) + e. impossible (digits from 0 to 100 + TIMED 5s and 10s if random operation) 8 + + +Difficulty : NONE Operation : NONE Question Type : NONE + + +██ ██ ▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄ ▄▄▄▄ +██████ ██ ███▄██ ██ ███▄▄ +██ ██ ██ ██ ▀██ ██ ▄▄██▀ + +- type a letter from [a, b, c, d, e] +- to go back to main menu [q] soft exit +- type [w] to wipe all selections +- press [ctrl+c] to hard exit + + +Type your answer : 23 +``` + +### logic and design + +- hight from 4 to 8 is the selection place and the maximum width is 76 the maximum that the window could reach is 23 rows and 76 columns +- add a method to wipe-out all the selection upon selection +- Add a method to print the current selection then print the user selection in a different location +- define How would you deal with q, w and general user input +- handle the input process correctly diff --git a/MathGame/src/AbstractBases/AnswerBase.cs b/MathGame/src/AbstractBases/AnswerBase.cs new file mode 100755 index 00000000..002cd9fe --- /dev/null +++ b/MathGame/src/AbstractBases/AnswerBase.cs @@ -0,0 +1,16 @@ +using MathGame.GameQuestion; + +namespace MathGame.AbstractBases; + +// a generic abstract class and each class name represent its purpose +public abstract class AnswerBase +{ + // Store the question Type Itself + public abstract GameQuestionType QuestionType {get;} + + + + // Validate the Answer either with a string or a char + public abstract bool ValidateAnswer(T userInput); + +} \ No newline at end of file diff --git a/MathGame/src/AbstractBases/IQuestion.cs b/MathGame/src/AbstractBases/IQuestion.cs new file mode 100644 index 00000000..61aa73f4 --- /dev/null +++ b/MathGame/src/AbstractBases/IQuestion.cs @@ -0,0 +1,11 @@ +using MathGame.GameQuestion; + +namespace MathGame.AbstractBases; + +// this is to orchestrate all the questions in the game window +public interface IQuestion +{ + GameQuestionType QuestionType { get; } + string QuestionPrompt { get; } + bool CheckAnswer(string userInput); +} \ No newline at end of file diff --git a/MathGame/src/AbstractBases/QuestionBase.cs b/MathGame/src/AbstractBases/QuestionBase.cs new file mode 100755 index 00000000..b95a6896 --- /dev/null +++ b/MathGame/src/AbstractBases/QuestionBase.cs @@ -0,0 +1,21 @@ +using MathGame.GameQuestion; + +namespace MathGame.AbstractBases; + +// a generic abstract class and each class name represent its purpose +public abstract class QuestionBase(Problem problem) : IQuestion +{ + // they will hold our problem itself + protected Problem Problem = problem; + // to define the type Effectively + public abstract GameQuestionType QuestionType{get;} + + + // question prompt + public abstract string QuestionPrompt { get; } + // Answer Holder + public abstract AnswerBase Answer{get;} // note the T must be of the same type + public abstract bool CheckAnswer(string answer); + + // this method will use the Problem Object in Order to prepare the question text +} diff --git a/MathGame/src/AbstractBases/WindowBase.cs b/MathGame/src/AbstractBases/WindowBase.cs new file mode 100755 index 00000000..d4b889e1 --- /dev/null +++ b/MathGame/src/AbstractBases/WindowBase.cs @@ -0,0 +1,14 @@ +using MathGame.GameWindows; + +namespace MathGame.AbstractBases; + +// A generic class to hold all the windows and unifying the handling for modularity and simplicity +public abstract class WindowBase +{ + + // the method to draw and construct the current window + public abstract void Render(); + + // method to process the user's option if they enters [a,b,c] based on the active valid options + public abstract WindowMap ProcessInput(string userInput); +} diff --git a/MathGame/src/CoreUtilities/GameEngine.cs b/MathGame/src/CoreUtilities/GameEngine.cs new file mode 100755 index 00000000..6961467f --- /dev/null +++ b/MathGame/src/CoreUtilities/GameEngine.cs @@ -0,0 +1,156 @@ +using MathGame.GameQuestion; +using MathGame.GameWindows; + +namespace MathGame.CoreUtilities; + +public static class GameEngine +// this class handle the window control as well as orchestration the gameplay +{ + + #region Fields + // our shared game option storage + // public static GameOptions SGameOptions = new(); + public static Options SGameOptionsRecord = new(); + // this will act as an id for our history handling + public static int STotalNumberOfQuestions; + + // to track weather a game finished or not yet so it can commit it safely + public static bool SFinishedGame; + + // our game history List or dynamic array + public static readonly List GameHistoryTable = new List(1000); + + // Global tracker for both the record id and detection of array current size + public static int TotalGamesPlayed = 0; + + #endregion + + #region Properties + // this is one-time use property to retrieve the game info from the window in which we play in + // to commit it to a record and store it in the array + public static DateTime StartTime{get;set;} // the date in which we start playing + public static int Score{get;set;} // the game score + #endregion + + #region Records + // this will be our record for storage + public record GameRecord(int Id,DateTime PlayedDate, int TotalScore, GameDifficulty Difficulty,GameOperation Operation,GameQuestionType QuestionType,int TotalNumberOfQuestions); + + #endregion + + #region Methods + public static void GameSetup() + // a setup method to set the environment before entering the loop + { + // Clear the Terminal + Console.Clear(); + + // preload the main window in advance + WindowManager.SwitchState(WindowMap.MainMenu); + WindowManager.ActiveWindow.Render(); + } + public static void Render() + // this will be our game loop + { + GameSetup(); + while (true) + { + WindowManager.UpdateWindow(); + + if (WindowManager.CurrentWindow == WindowMap.QuitBanner) + { + return; + } + + // to make sure that our next logic is executed correctly + // we need to execute the following if the screen is valid + if (WindowManager.CurrentWindow == WindowMap.GameWindow) + { + QuestionTimer.UpdateTimer(); + } + + // skip the iteration if the screen doesn't fulfill the minimal required screen size + if (!WindowManager.CheckValidScreen()) continue; + + + // true if a key press is available + if (Console.KeyAvailable) + { + /* Why Not ReadLine() + this was better than readline method since it stop the code execution and + for my code implementation it was unforgiving since it's single-threaded execution + and I didn't want to start learning about the multi-threading and processing at least + not yet and I felt it will be a shortcut for execution which for a beginner like + me nah it will be bad + */ + + // listen to user's keystroke + ConsoleKeyInfo keyInfo = Console.ReadKey(true); // true hide the automatic key echoing + Console.TreatControlCAsInput = false; // so to prevent accidental catch of the control + + // this return a string weather it's 1 char or a full string + string userInput = InputHandler.HandleUserInput(keyInfo); + + // of course, I handled nullability in the InputHandler, + // but it will result System.IndexOutOfRangeException once typing in the game + // if not handled + if (!string.IsNullOrEmpty(userInput)) + { + + // receive ActiveWindow's ProcessInput method output state + WindowMap nextState = WindowManager.ActiveWindow.ProcessInput(userInput : userInput); + + // if state has change pass the new state to the window manager to handle it + if (WindowManager.CurrentWindow != nextState) + { + WindowManager.SwitchState(nextState); + } + + // this to force update our window if there is an error next time + // user enters something why to clear that error as well internally + else if (!InputHandler.HasError) + { + Console.Clear(); + WindowManager.ActiveWindow.Render(); + } + } + + } + + + // Force penalty on timeout + else if (WindowManager.CurrentWindow == WindowMap.GameWindow && QuestionTimer.IsTimeFinished) + { + // pass a special flag to denote it + WindowManager.ActiveWindow.ProcessInput("TIMEOUT"); + + // reset the timer and console + QuestionTimer.IsTimeFinished = false; + Console.Clear(); + + // refresh the game window to move to the next question + WindowManager.ActiveWindow.Render(); + } + + // checks if a game is finished to save the result + if (SFinishedGame) + { + CommitHistory(); + SFinishedGame = false; + } + } + + } + + public static void CommitHistory() + // this method creates and push a GameRecord object to the history table + { + GameRecord record = new GameRecord(TotalGamesPlayed, StartTime, Score, SGameOptionsRecord.Difficulty, + SGameOptionsRecord.Operation, SGameOptionsRecord.QuestionType, STotalNumberOfQuestions); + GameHistoryTable.Add(record); + } + + + #endregion + +} \ No newline at end of file diff --git a/MathGame/src/CoreUtilities/InputHandler.cs b/MathGame/src/CoreUtilities/InputHandler.cs new file mode 100755 index 00000000..c4e1d758 --- /dev/null +++ b/MathGame/src/CoreUtilities/InputHandler.cs @@ -0,0 +1,223 @@ +using System.Text; + +namespace MathGame.CoreUtilities +{ + // A unified utility class for handling user input effectively + public static class InputHandler + { + #region fields + // this was preferred since I wanted to make most of my functions modular and window + // agnostic our buffer char array which will hold our current ongoing valid options + private static char[] _sActiveOptionsBuffer = ['a', 'b', 'c']; + #endregion + + #region Properties + + // our buffer holder + private static StringBuilder? InputBuffer { get; set; } = new(); + public static bool QuestionMode { get; set; } + + // saving the input location + private static int InputX{get; set;} + + private static int InputY{get; set;} + + // this is for controlling s_activeOptionBuffer it reset the current options into new ones + public static string? SetActiveOptions + { + set + { + // ReSharper disable once PropertyFieldKeywordIsNeverUsed + field = value; // Saves the string text into the hidden string aka field + + // I know this isn't a best practice + // but window updates happens one a time not in a constant loop + // so it's better to go with that option for easy modularity and extensibility + _sActiveOptionsBuffer = (value ?? "").ToCharArray(); + } + } = "abc"; + + + // indicators for wrong typing or invalid inputs + public static bool HasError { get; set; } + + // the place in which will display our errors in the current active window + public static int CurrentErrorLocation { get; set; } + + #endregion + + #region Methods + + + // this method handles the input buffer itself + public static string HandleUserInput(ConsoleKeyInfo key) + { + var userInput = ""; // this is the return variable + + // based on the given key it will handle these cases + switch (key.Key) + { + // to submit the userInput to process it in the windows and handle any possible + // error like empty inputs + case ConsoleKey.Enter: + // store this input into the string we are returning + userInput = InputBuffer?.ToString() ?? ""; + + // to check for empty input before submitting the string + if (string.IsNullOrEmpty(userInput)) + { + HasError = true; + ShowErrorMessage("You didn't type anything please enter something"); + } + // clear the submitted input for reusing + for (int i = 0; i < userInput.Length; i++) + { + Console.Write("\b \b"); + } + // clear the buffer to reuse it again + InputBuffer?.Clear(); + // check if there has been any errors flagged or no + if (HasError) + { + // if true fallback the default case + goto default; + } + break; + + // to add back spacing logic since we give up ReadLine + case ConsoleKey.Backspace: + + // checking the length of the input buffer to avoid accidental invalid indexing + if (InputBuffer?.Length > 0 ) + { + + // remove the element once from the buffer + InputBuffer.Remove(InputBuffer.Length - 1, 1); + Console.Write("\b \b"); // Erase character visually from console screen + + // Clean errors if spotted + if (HasError) + { + CleanErrors(); + } + } + break; + + // pass the key itself to be check in handle key method + default: + HandleInputKeys(key); + break; + } + return userInput; + } + + + // this method handles the user keystrokes and store it in the InputBuffer + private static void HandleInputKeys(ConsoleKeyInfo key) + { + // since the keys are capitalized I decided to lower them for easy processing + var loweredKey = char.ToLower(key.KeyChar); + // checking if our key is one of the valid options or a "q" and its length is 0 + bool isValidOptions = _sActiveOptionsBuffer.Contains(loweredKey) || loweredKey == 'q'; + + // check generally if there is a wrong input and return if so + if (!isValidOptions) + { + if (!HasError) + { + // print the error message for the user + ShowErrorMessage( + $"You type wrong option you can only use[{string.Join(", ", _sActiveOptionsBuffer)}, q]"); + // flip the error flag to true + HasError = true; + } + return; + } + + // to check if the length is greater than 1 and question mode is off and return if so + if (InputBuffer?.Length >= 1 && !QuestionMode) + { + if (!HasError) + { + // print the error message for the user + ShowErrorMessage($"You Cannot type more than 1 letter option"); + HasError = true; + } + return; + } + + // to clean errors and proceed if nothing wrong was done + if (HasError) + { + CleanErrors(); + } + + + // if everything is ok append this key to the buffer and visually prints it + InputBuffer?.Append(loweredKey); + Console.Write(loweredKey); + + } + + // to check and clear any error + private static void CleanErrors() + { + + // passing an empty string will clear the error as well + ShowErrorMessage(""); + + // reset all the error flags as well + HasError = false; + } + + + // just to show the user what is wrong + public static void ShowErrorMessage(string currentErrorType) + { + + + // setting the cursor into the specified location to display the message + Console.SetCursorPosition(0, CurrentErrorLocation); + // return a padded version to overWrite it + string paddedError = currentErrorType.PadRight(Console.WindowWidth - 1, ' '); + // print the message and over-write the rest of the line with blanks + Console.Write($"\e[31m{paddedError}\e[0m"); + // reset the cursor back to the old position + int returnX = InputX + (InputBuffer?.Length ?? 0); + Console.SetCursorPosition(returnX, InputY); + } + + + // Rather than ask for input in each time the user move to a new menu this method + // will handle this Idea as well as displaying tips to the user + public static void InputPrompt(string[]? inputTips = null) + { + // separation to avoid cluttering the input area with the menu of the current window + Console.Write("\r\n\r\n"); + + + // to address the null array I used null coalescing operator + // if it founds that the array is null assign it to an empty array + inputTips ??= []; + if (inputTips is not { Length: 0 }) + { + Console.Write($"NOTES:\r\n"); + Console.Write($"\t{string.Join("\r\n\t", inputTips)}\r\n"); + } + + + // this to hold the current active options + string inputHints = $"type a letter from [{string.Join(", ", _sActiveOptionsBuffer)}, q]\r\n"; + // a prompt to encourage the user to type here + string askForInput = "\r\nType your answer : \t"; + Console.Write(inputHints); + Console.Write(askForInput); + // the null check is for safety, but this will just show what the user has typed + InputX = Console.CursorLeft; + InputY = Console.CursorTop; + + Console.Write(InputBuffer?.ToString().ToLower()); + } + #endregion + } +} \ No newline at end of file diff --git a/MathGame/src/CoreUtilities/WindowManger.cs b/MathGame/src/CoreUtilities/WindowManger.cs new file mode 100755 index 00000000..d12e8a12 --- /dev/null +++ b/MathGame/src/CoreUtilities/WindowManger.cs @@ -0,0 +1,136 @@ +// using System.Collections; + +using MathGame.AbstractBases; +using MathGame.GameWindows; +using MathGame.GameWindows.SetupMenu; +using MathGame.src.GameWindows; + +namespace MathGame.CoreUtilities +{ + public static class WindowManager + // A static utility class solely for managing window size update and switching + { + #region Fields + // a constant array to define our minimum valid screen + static readonly byte[] MinWindowCoordinates = [25, 85]; // (height,width) (y,x) + + + // this field dictionary to be a general way to invoke and work with the windows rather than + // handling each window separately + private static readonly Dictionary SWindows = new() + { + { WindowMap.MainMenu, new MainMenu() }, + {WindowMap.QuitBanner, new QuitWindow()}, + {WindowMap.SetupMenu,new SetupWindow()}, + { WindowMap.GameWindow , new GameWindow()}, + { WindowMap.GameOverWindow , new GameOverWindow()}, + { WindowMap.HistoryWindow , new HistoryWindow()}, + {WindowMap.AboutWindow , new AboutWindow()} + }; + #endregion + + #region Properties + // Track the current window and change it on demand + public static WindowMap CurrentWindow { get; set; } = WindowMap.MainMenu; + + // this property get the active window based on the CurrentWindow Property + public static WindowBase ActiveWindow { get => SWindows[CurrentWindow]; } + + // Stores the current window height or what equivalent to the Y axis + private static int UserWindowHeight { get; set; } = Console.WindowHeight; + // Stores the current window width or what equivalent to the X axis + private static int UserWindowWidth { get; set; } = Console.WindowWidth; + + #endregion + + #region Methods + // it checks if the user window width[x] and height[y] exceeds the minium required Coordinates + public static bool CheckValidScreen() + { + // store the current user window size and did that to minimize the props invoke + // since they still technically methods + int y = Console.WindowHeight; + int x = Console.WindowWidth; + + + // stored the minium window size in arrays for simplicity of use as well + int minY = MinWindowCoordinates[0]; + int minX = MinWindowCoordinates[1]; + + // compares between the current size the minimum window size + if (y < minY || x < minX) + { + // clear the window to properly display the message + Console.Clear(); + + // a string variable to tell the user what coordinate needs adjusting + var whatToAdjust = (y < minY, x < minX) switch + // used tuple expression to simplify the logic and make it more readable + { + (true, true) => $"both of Windows'Height and Width so (y: {y}) be >= {minY} and (x: {x}) be >= {minX}", + (false, true) => $"the Window Width so (x: {x}) be >= {minX}", + (true, false) => $"the Window Hight so (y: {y}) be >= {minY}", + (false, false) => string.Empty + + }; + + // printing the message to the user + Console.WriteLine($"Window must be greater than or equal to ({minY} x {minX}) please adjust {whatToAdjust}"); + Thread.Sleep(100); // Prevents high CPU usage and 100 for me was the sweet spot + return false; // this method utilize the new c# 14's return guard for simplicity + } + return true; + } + + + // A method that update the window size on demand + public static void UpdateWindow() + { + // if there is a change on the width or height clear console and redraw it with + // the new width or height + if (Console.WindowWidth != UserWindowWidth || Console.WindowHeight != UserWindowHeight ) + { + Console.Clear(); + int newX = Console.WindowWidth; + int newY = Console.WindowHeight; + if (!CheckValidScreen()) + { + return; // Halt the execution flow until the user fixes the window size + } + + // update the stored width or height + UserWindowWidth = newX; + UserWindowHeight = newY; + + + Thread.Sleep(30); // Prevents high CPU usage + + // force a redraw for the user screen after resize + ActiveWindow.Render(); + } + + } + + + // this method handle the switching mechanism + public static void SwitchState(WindowMap newState) + { + + if(CurrentWindow == newState) + // check if the state have been change or still the same + { + // return if that is there is no change + return; + } + // assign the new state to the CurrentWindow property + CurrentWindow = newState; + // clear the console for the new window + Console.Clear(); + + // renders the new window + ActiveWindow.Render(); + + } + #endregion + } +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/Answers/AnswerFillGap.cs b/MathGame/src/GameQuestion/Answers/AnswerFillGap.cs new file mode 100755 index 00000000..f95b32e6 --- /dev/null +++ b/MathGame/src/GameQuestion/Answers/AnswerFillGap.cs @@ -0,0 +1,56 @@ + +using MathGame.AbstractBases; + +namespace MathGame.GameQuestion.Answers; + +public class AnswerFillGap : AnswerBase +{ + #region Fields + private int[] _operationList = new int[3]; + #endregion + + #region Properties + public int TargetGabAnswer{get;} // this will store the target gap's answer + public int TargetGabIndex{get;} // this will store the gap's index + public override GameQuestionType QuestionType => GameQuestionType.FillGaps; + #endregion + + #region Constructor + public AnswerFillGap(Problem problem) + { + _operationList = [problem.FirstNum,problem.Operation,problem.SecondNum]; + TargetGabAnswer = GetTheTargetGapValue(); + TargetGabIndex = GetTheTargetGapIndex(); + } + + #endregion + + #region Methods + private int GetTheTargetGapValue() + // get the target's Value + { + int gap = Random.Shared.GetItems(_operationList,1)[0]; + return gap; + } + private int GetTheTargetGapIndex() + { + int gapIndex = _operationList.IndexOf(TargetGabAnswer); + return gapIndex; + } + + public override bool ValidateAnswer(string userInput) + { + if (userInput is "+" or "-" or "*" or "/") + { + return userInput[0] == TargetGabAnswer; + } + if (int.TryParse(userInput, out int answer)) + { + return answer == TargetGabAnswer; + } + return false ; + } + + #endregion + +} diff --git a/MathGame/src/GameQuestion/Answers/AnswerMCQ.cs b/MathGame/src/GameQuestion/Answers/AnswerMCQ.cs new file mode 100755 index 00000000..baf52894 --- /dev/null +++ b/MathGame/src/GameQuestion/Answers/AnswerMCQ.cs @@ -0,0 +1,69 @@ +using System.Collections; +using System.Runtime.InteropServices; +using MathGame.AbstractBases; + +namespace MathGame.GameQuestion.Answers; + +public class AnswerMcq : AnswerBase, IEnumerable +{ + + #region Fields + // A list that contains our answers + private readonly List _answerList = new(4); + + // Important for using string.Join + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + #region Properties + public override GameQuestionType QuestionType => GameQuestionType.Mcq; + + // Important for using string.Join + public IEnumerator GetEnumerator() => _answerList.GetEnumerator(); + + #region Indexer + // indexer to iterate through the answers + public int this[int index] => _answerList[index]; + public int AnswerIndex {get;set;} + #endregion + #endregion + + #region Constructor + public AnswerMcq(Problem problem) + { + var answer = problem.Answer; + + // add the answers + _answerList.AddRange(GenerateAnswerCandidates(answer)); + + // shuffle the elements in place + Random.Shared.Shuffle(CollectionsMarshal.AsSpan(_answerList)); + AnswerIndex = _answerList.IndexOf(answer); + } + #endregion + + #region Methods + private List GenerateAnswerCandidates(int a) + // this method generate the keys and there values in the dictionary + { + int spreadBound = Math.Max(0, a - 10); + // basically using to + // generate 21 number starting from the spread bound + return + [ + .. Enumerable.Range(spreadBound, 21) + .Where(x => x != a) // exclude any real answer from the generation + .OrderBy(x => Random.Shared.Next()) // shuffle + .Take(3), // take 3 of that group + + a + ]; + + } + + + // check if the answer is correct without mapping overhead + public override bool ValidateAnswer(char userInput) => (char)(AnswerIndex + 97) == userInput; + #endregion + +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/Answers/AnswerNormal.cs b/MathGame/src/GameQuestion/Answers/AnswerNormal.cs new file mode 100755 index 00000000..f0b06b5c --- /dev/null +++ b/MathGame/src/GameQuestion/Answers/AnswerNormal.cs @@ -0,0 +1,15 @@ +using MathGame.AbstractBases; + +namespace MathGame.GameQuestion.Answers; + +public class AnswerNormal(Problem problem) : AnswerBase +{ + + public override GameQuestionType QuestionType => GameQuestionType.Normal; + public int CorrectAnswer { get; } = problem.Answer; + + public override bool ValidateAnswer(string userInput) + { + return int.Parse(userInput) == CorrectAnswer; + } +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/Answers/AnswerTF.cs b/MathGame/src/GameQuestion/Answers/AnswerTF.cs new file mode 100755 index 00000000..66e3f639 --- /dev/null +++ b/MathGame/src/GameQuestion/Answers/AnswerTF.cs @@ -0,0 +1,71 @@ +using MathGame.AbstractBases; + +namespace MathGame.GameQuestion.Answers; + +public class AnswerTf : AnswerBase +{ + #region Properties + // the type of question of which the answer hold + public override GameQuestionType QuestionType => GameQuestionType.TrueFalse; + + // the answer + public bool StateAnswer{get;} + + // the wrong or correct answer in which it be displayed + public int AnswerContainer{get;} + #endregion + + + #region Fields + private const int WrongAnswerChances = 70; + #endregion + #region Constructor + public AnswerTf(Problem problem) + { + // to make the hard chances more common had to come with this + // tbh I did use an LLM to construct this logic but almost 95% of code is written by me + StateAnswer =Random.Shared.Next(100) >= WrongAnswerChances; + int answer = problem.Answer; // get the correct answer + if (StateAnswer) + { + // if state is true + AnswerContainer = answer; + return; + } + // if not generate wrong answer + AnswerContainer = GenerateWrongAnswer(answer); + } + #endregion + + #region Methods + + private int GenerateWrongAnswer(int correct) + { + int wrongAnswer=0; + do + { + // get an offset near the correct answer by ranged 1 to 5 + int offset = Random.Shared.Next(1,6); + // Randomly add or subtract offset but never go below zero + wrongAnswer = + Random.Shared.Next(2) == 0 ? + correct + offset : + Random.Shared.Next(0,Math.Abs(correct-offset)); + } while (wrongAnswer == correct); // iterate till the wrong and correct answer are unequal + return wrongAnswer; + } + public override bool ValidateAnswer(char userInput) + { + // map userInput to boolean + bool checkCorrectInput = userInput switch + { + 't'=> true, + 'f'=> false, + _ => throw new InvalidOperationException("received unknown input") + }; + // validate + return StateAnswer == checkCorrectInput; + } + #endregion + +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/GameOptions.cs b/MathGame/src/GameQuestion/GameOptions.cs new file mode 100755 index 00000000..c0dc3279 --- /dev/null +++ b/MathGame/src/GameQuestion/GameOptions.cs @@ -0,0 +1,14 @@ +namespace MathGame.GameQuestion; + +// our game options +public enum GameDifficulty : byte {None,Easy,Normal,Hard,Insane,Impossible,} +public enum GameOperation : byte {None,Addition,Subtraction,Multiplication,Division,Random} +public enum GameQuestionType : byte {None,Mcq,TrueFalse,FillGaps,Normal,Random,} + + + + +// I deliberately record structs that as I used ENUMs [less than 16bytes] +// , embeddable , mutable , represent single type, and +// lastly I'm sure it won't be boxed +public record struct Options(GameDifficulty Difficulty, GameOperation Operation, GameQuestionType QuestionType); diff --git a/MathGame/src/GameQuestion/Problem.cs b/MathGame/src/GameQuestion/Problem.cs new file mode 100755 index 00000000..359d8121 --- /dev/null +++ b/MathGame/src/GameQuestion/Problem.cs @@ -0,0 +1,113 @@ +namespace MathGame.GameQuestion; + + +// this struct will be embedded to all questions and it size less than 16 byte +public readonly struct Problem +{ + + #region fields + + private readonly int _range ; // the maximum range of generations + private readonly char[] _operation = ['+', '-', '*', '/']; // operator list + + #endregion + + + #region Properties + // this represent a simple operation operands and the answer + public int FirstNum { get; } + public char Operation { get; } + public int SecondNum { get;} + public int Answer { get; } + #endregion + + public Problem(Options options) + { + // set the difficulty + var difficulty = options.Difficulty; + + // get the range + _range = difficulty switch + { + GameDifficulty.Easy => 11, + GameDifficulty.Normal => 101, + GameDifficulty.Hard => 501, + GameDifficulty.Insane => 1001, + GameDifficulty.Impossible => 2001, + _ => throw new IndexOutOfRangeException() + }; + + + Operation = GenerateOperation(options.Operation); + + // using tuples to assign multiple values at once [Didn't know that at first] + (FirstNum,SecondNum,Answer ) = Operation switch + { + '+'=>GenerateAdditionProblem(), + '-' => GenerateSubtractionProblem(), + '*'=> GenerateMultiplicationProblem(), + '/' => GenerateDivisionProblem(), + _ =>throw new InvalidOperationException("Unsupported operation") + }; + } + + // get current operation + private readonly char GenerateOperation(GameOperation operation) => operation switch + { + GameOperation.Addition => _operation[0], + GameOperation.Subtraction => _operation[1], + GameOperation.Multiplication => _operation[2], + GameOperation.Division => _operation[3], + // get a random index for the operation array ranged from 0 to Length + GameOperation.Random => _operation[Random.Shared.Next(_operation.Length)], + _ => throw new ArgumentOutOfRangeException($"unknown {nameof(operation)} : {operation} type") + }; + + #region Problem Operands Generators + private (int first,int second,int answer) GenerateAdditionProblem() + { + int first = Random.Shared.Next(0, _range); + int second = Random.Shared.Next(0, _range); + + // return a tuple containing all the problem parts + return (first,second,first+second); + } + private (int first,int second,int answer) GenerateSubtractionProblem() + { + int first = Random.Shared.Next(0, _range); + int second = Random.Shared.Next(0, first+1); // to make sure it's alway positive + + + // return a tuple containing all the problem parts + return (first,second,first-second); + } + private (int first,int second,int answer) GenerateMultiplicationProblem() + { + int first = Random.Shared.Next(0, _range); + int second = Random.Shared.Next(0, _range); + + + // return a tuple containing all the problem parts + return (first,second,first*second); + } + private (int first,int second,int answer) GenerateDivisionProblem() + { + // get the second operand + int second = Random.Shared.Next(1, 101); + + // it's the maximum possible number to be generated + int maxMultiplayer = (_range-1)/ second; + + // using the multiplayer to a valid possible random answer + int answer = Random.Shared.Next(0, maxMultiplayer + 1); + + // get the first operand from the second and the answer + int first = second * answer; + + // return a tuple containing all the problem parts + return (first,second,answer); + } + #endregion + + +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/QuestionTimer.cs b/MathGame/src/GameQuestion/QuestionTimer.cs new file mode 100644 index 00000000..8ba4613d --- /dev/null +++ b/MathGame/src/GameQuestion/QuestionTimer.cs @@ -0,0 +1,105 @@ +namespace MathGame.GameQuestion; + +/* this will be responsible for the following + 1. counting the specified timer per question + 2. printing the timer out to the user provide by a public static utility + 3. return a signal denoting that the timer is finished + 4. have a public prop to access that return the remaining time on demand +*/ +public static class QuestionTimer +{ + #region Fields + // we will use that to get the expected end time + private static DateTime _targetTime; + + // this will be the actual count down + private static int _lastPrintedTime; + #endregion + + + + #region Properties + + public static int CountDown => _lastPrintedTime; + private static int TimerLocationX { get; set; } // the exact print location + public static bool IsTimeFinished { get; set; } // to declare a timeout event + private static bool IsActive { get; set; } // internally to + + #endregion + + /////////////////// + + #region Methods + + // set up the timer countdown and initialize the timer + public static void StartTimer(int timerLocationX,int countdown) + { + TimerLocationX = timerLocationX; + // calculate the exact end time + _targetTime = DateTime.Now.AddSeconds(countdown); + _lastPrintedTime = countdown; + + IsTimeFinished = false; + IsActive = true; + + + PrintTimer(countdown); + + } + + // update the timer countdown till the end + public static void UpdateTimer() + { + // stop if this method isn't in use + if (!IsActive) + { + return; + } + + // get the time span between the expected time and the current date + TimeSpan remainingTime = _targetTime -DateTime.Now; + + // print the difference result according to the nearest value + // and If value is halfway between two whole numbers, + // the even number is returned; that is, 4.5 is converted to 4, and 5.5 is converted to 6 + int currentPrintedTime = Convert.ToInt32(remainingTime.TotalSeconds); + + + // check the timer reached zero + if (currentPrintedTime <= 0) + { + IsActive = false; + IsTimeFinished = true; + PrintTimer(currentPrintedTime); + return; + } + + + // update the timer + if (currentPrintedTime < _lastPrintedTime) + { + _lastPrintedTime = currentPrintedTime; + PrintTimer(currentPrintedTime); + } + + } + + + + // print the timer to the user + private static void PrintTimer(int countdown) + { + int tempX = Console.CursorLeft; + int tempY = Console.CursorTop; + + + // move to the timer's location + Console.SetCursorPosition(TimerLocationX,0); + + // update the countdown + Console.Write($"Remaining Time: {(countdown<10 ? $"\e[31m{countdown}\e[0m".PadRight(25, ' ') : countdown)}"); + Console.SetCursorPosition(tempX,tempY); + + } + #endregion +} \ No newline at end of file diff --git a/MathGame/src/GameQuestion/Questions/QuestionFillGaps.cs b/MathGame/src/GameQuestion/Questions/QuestionFillGaps.cs new file mode 100755 index 00000000..6c452918 --- /dev/null +++ b/MathGame/src/GameQuestion/Questions/QuestionFillGaps.cs @@ -0,0 +1,49 @@ + +using MathGame.AbstractBases; +using MathGame.GameQuestion.Answers; + +namespace MathGame.GameQuestion.Questions; + +public class QuestionFillGaps : QuestionBase +{ + public QuestionFillGaps(Problem problem) : base(problem) + { + _answer = new(problem); + _problemList = + [ + problem.FirstNum.ToString(), + problem.Operation.ToString(), + problem.SecondNum.ToString(), + ]; + var questionHeaderTypeHint = "Fill The Gaps"; + var questionCaption = $"\r\n{GetQuestionCaption()}= {Problem.Answer}"; + var questionAnswers = $"\r\nWhat must be written to satisfy the problem?"; + QuestionPrompt = questionHeaderTypeHint + questionCaption + questionAnswers; + } + + public override GameQuestionType QuestionType => GameQuestionType.FillGaps; + + public override string QuestionPrompt{get;} + + public override AnswerBase Answer => _answer; + + public override bool CheckAnswer(string answer) => _answer.ValidateAnswer(answer); + + + + private readonly AnswerFillGap _answer; + private readonly string[] _problemList; + + + private string GetQuestionCaption() + { + string temp = ""; + for (int i = 0; i < 3; i++) + { + temp += _answer.TargetGabIndex == i ? "?" : $"{_problemList[i]}"; + temp += " "; + } + + return temp; + } +} diff --git a/MathGame/src/GameQuestion/Questions/QuestionMCQ.cs b/MathGame/src/GameQuestion/Questions/QuestionMCQ.cs new file mode 100755 index 00000000..d32d9844 --- /dev/null +++ b/MathGame/src/GameQuestion/Questions/QuestionMCQ.cs @@ -0,0 +1,25 @@ +using MathGame.AbstractBases; +using MathGame.GameQuestion.Answers; + +namespace MathGame.GameQuestion.Questions; + +public class QuestionMcq : QuestionBase , IQuestion +{ + private readonly AnswerMcq _answer; + public override GameQuestionType QuestionType => GameQuestionType.Mcq; + + public QuestionMcq(Problem problem) : base(problem) + { + _answer = new(problem); + var questionHeaderTypeHint = "Choose the Correct Answer"; + var questionCaption = $"\r\n{Problem.FirstNum} {Problem.Operation} {Problem.SecondNum} = ?"; + var questionAnswers = $"\r\na) {_answer[0]}\r\nb) {_answer[1]}\r\nc) {_answer[2]}\r\nd) {_answer[3]}\r\n"; + QuestionPrompt = questionHeaderTypeHint + questionCaption + questionAnswers; + } + public override string QuestionPrompt { get; } + public override bool CheckAnswer(string answer) => _answer.ValidateAnswer(answer[0]); + + public override AnswerBase Answer => _answer; + + +} diff --git a/MathGame/src/GameQuestion/Questions/QuestionNormal.cs b/MathGame/src/GameQuestion/Questions/QuestionNormal.cs new file mode 100755 index 00000000..0eab5ecf --- /dev/null +++ b/MathGame/src/GameQuestion/Questions/QuestionNormal.cs @@ -0,0 +1,26 @@ +using MathGame.AbstractBases; +using MathGame.GameQuestion.Answers; + +namespace MathGame.GameQuestion.Questions; + +public class QuestionNormal : QuestionBase,IQuestion +{ + public QuestionNormal(Problem problem) : base(problem) + { + _answer =new(problem); + var questionHeaderTypeHint = "What Is The Output Of The Following Problem ?"; + var questionCaption = $"\r\n{problem.FirstNum} {problem.Operation} {problem.SecondNum} = ?"; + var questionAnswers = ""; + QuestionPrompt = questionHeaderTypeHint + questionCaption + questionAnswers; + } + + public override GameQuestionType QuestionType =>GameQuestionType.Normal; + + public override string QuestionPrompt {get;} + public override bool CheckAnswer(string answer) => _answer.ValidateAnswer(answer); + + public override AnswerBase Answer => _answer; + + private readonly AnswerNormal _answer; + +} diff --git a/MathGame/src/GameQuestion/Questions/QuestionTF.cs b/MathGame/src/GameQuestion/Questions/QuestionTF.cs new file mode 100755 index 00000000..2c042f1e --- /dev/null +++ b/MathGame/src/GameQuestion/Questions/QuestionTF.cs @@ -0,0 +1,27 @@ +using MathGame.AbstractBases; +using MathGame.GameQuestion.Answers; + +namespace MathGame.GameQuestion.Questions; + +public class QuestionTf : QuestionBase , IQuestion +{ + public QuestionTf(Problem problem) : base(problem) + { + _answer = new(problem); + var questionHeaderTypeHint = "Is This True or False ?"; + var questionCaption = $"\r\n{Problem.FirstNum} {Problem.Operation} {Problem.SecondNum} = {_answer.AnswerContainer}"; + var questionAnswers = $"\r\n- True[t]\r\n- False[f]\r\n"; + QuestionPrompt = questionHeaderTypeHint + questionCaption + questionAnswers; + } + + public override GameQuestionType QuestionType => GameQuestionType.TrueFalse; + + public override string QuestionPrompt {get;} + public override bool CheckAnswer(string answer) => _answer.ValidateAnswer(answer[0]); + + public override AnswerBase Answer => _answer; + #region fields + private readonly AnswerTf _answer; + #endregion + +} diff --git a/MathGame/src/GameWindows/AboutWindow.cs b/MathGame/src/GameWindows/AboutWindow.cs new file mode 100644 index 00000000..61fff2d2 --- /dev/null +++ b/MathGame/src/GameWindows/AboutWindow.cs @@ -0,0 +1,27 @@ +using System; +using MathGame.AbstractBases; +using MathGame.CoreUtilities; +using MathGame.GameWindows; + +namespace MathGame.src.GameWindows; + +public class AboutWindow : WindowBase +{ + public override WindowMap ProcessInput(string userInput) + { + return userInput switch + { + "q" => WindowMap.MainMenu, + _ => throw new ArgumentOutOfRangeException($"what is {userInput} ???"), + }; + } + + public override void Render() + { + Console.Write("this project was developed as part of csharp-academy challenge and doesn't represent a core or a useful business product however you can use it to play with your children and young siblings who happened to be a computer nerd but will not receive any further core update but pull requests are welcomed"); + InputHandler.SetActiveOptions = ""; + InputHandler.CurrentErrorLocation = 9; + InputHandler.InputPrompt(["press q to go back to the main menu","press ctrl+c to force stop the program"]); + + } +} diff --git a/MathGame/src/GameWindows/GameOverWindow.cs b/MathGame/src/GameWindows/GameOverWindow.cs new file mode 100644 index 00000000..ea2767ff --- /dev/null +++ b/MathGame/src/GameWindows/GameOverWindow.cs @@ -0,0 +1,51 @@ +using MathGame.AbstractBases; +using MathGame.CoreUtilities; + +namespace MathGame.GameWindows; + +public class GameOverWindow : WindowBase +{ + + public override void Render() + { + + // input handling configuration + InputHandler.CurrentErrorLocation = 14; + InputHandler.SetActiveOptions = "rc"; + + // menu construction + string label = + """ + ▄▄ ▗▄▖ + █▀▀▌ █▀█ + ▐▌ ▟██▖▐█▙█▖ ▟█▙ ▐▌ ▐▌▐▙ ▟▌ ▟█▙ █▟█▌ + ▐▌▗▄▖ ▘▄▟▌▐▌█▐▌▐▙▄▟▌ ▐▌ ▐▌ █ █ ▐▙▄▟▌ █▘ + ▐▌▝▜▌▗█▀▜▌▐▌█▐▌▐▛▀▀▘ ▐▌ ▐▌ ▜▄▛ ▐▛▀▀▘ █ + █▄▟▌▐▙▄█▌▐▌█▐▌▝█▄▄▌ █▄█ ▐█▌ ▝█▄▄▌ █ + ▀▀ ▀▀▝▘▝▘▀▝▘ ▝▀▀ ▝▀▘ ▀ ▝▀▀ ▀ + """; + Console.Write($"{label}\r\n"); + + // input tips + string[] inputTips = + [ + "[q] will wipe all the game data and exit", + "[r] will reset the game with the settings", + "[c] will go back to the main menu", + $"{(GameEngine.SFinishedGame ? "your data was save successfully\r\n\t\tcheck the game history to find out" : "Couldn't Save Your Last Game's Data\r\nplease play at least 5 games to save your game")}" + ]; + InputHandler.InputPrompt(inputTips); + } + + // process input + public override WindowMap ProcessInput(string userInput) + { + return userInput switch + { + "q"=>WindowMap.QuitBanner, + "r"=>WindowMap.GameWindow, + "c"=> WindowMap.MainMenu, + _ => throw new ArgumentOutOfRangeException(nameof(userInput), userInput, null) + }; + } +} \ No newline at end of file diff --git a/MathGame/src/GameWindows/GameWindow.cs b/MathGame/src/GameWindows/GameWindow.cs new file mode 100755 index 00000000..8be9e563 --- /dev/null +++ b/MathGame/src/GameWindows/GameWindow.cs @@ -0,0 +1,187 @@ +using MathGame.AbstractBases; +using MathGame.CoreUtilities; +using MathGame.GameQuestion; +using MathGame.GameQuestion.Questions; + +namespace MathGame.GameWindows; + +public class GameWindow : WindowBase +{ + + + private IQuestion? _question; // an interface for all the Questions + private Options _options; // this is need + private int _chances = 5; // let the user at least have 5 chances each game + private int _score; // user score + private int _totalQuestionNumbers=1; // this must be one + private int _totalQuestionNumbersSnapshot; // a snapshot of the total for comparison + + public override WindowMap ProcessInput(string userInput) + { + // check if the user didn't force quit or lost all the chances if so move to the GameOver + if (_chances != 0 && userInput[0] != 'q') + { + // check either the time out occurred or is the answer wrong + bool isAnswerWrong = userInput == "TIMEOUT" || !(_question != null && _question.CheckAnswer(userInput)); + + // if the user answered correctly + if (!isAnswerWrong) + { + // increment the total number + _totalQuestionNumbers++; + + // increment the score based on timing + _score += QuestionTimer.CountDown + 1; + } + // if the user answered wrongly + else + { + _totalQuestionNumbers++; + _chances--; + } + // if there is any chances left keep the window + if (_chances > 0) + { + // reload another question + return WindowMap.GameWindow; + } + + } + // commit if five or more games are played + if (_totalQuestionNumbers >= 5) + { + GameEngine.TotalGamesPlayed++; + GameEngine.Score = _score; + GameEngine.STotalNumberOfQuestions = _totalQuestionNumbers; + GameEngine.SFinishedGame = true; + + } + else + { + GameEngine.SFinishedGame = false; + } + + // reset the Game Window + _chances = 5; + _score = 0; + _totalQuestionNumbers = 1; + _totalQuestionNumbersSnapshot = 0; + return WindowMap.GameOverWindow; + } + + // copy the main option into a temp variable + + private void CheckTimer() + { + switch (_options.Difficulty) + { + case GameDifficulty.Easy: + QuestionTimer.StartTimer(60, _options.Operation== GameOperation.Random ? 15:10); + break; + case GameDifficulty.Normal: + QuestionTimer.StartTimer(60, _options.Operation== GameOperation.Random ? 15:20); + break; + case GameDifficulty.Hard: + QuestionTimer.StartTimer(60, _options.Operation== GameOperation.Random ? 45:30); + break; + case GameDifficulty.Insane: + QuestionTimer.StartTimer(60, _options.Operation== GameOperation.Random ? 60:45); + break; + case GameDifficulty.Impossible: + QuestionTimer.StartTimer(60, _options.Operation== GameOperation.Random ? 75:60); + break; + default: + throw new IndexOutOfRangeException($"add {_options.Difficulty} please"); + } + } + + // print out the menu + public override void Render() + { + // store the date for archiving + GameEngine.StartTime = DateTime.Now; + + // store the current global option into a temp one + _options = GameEngine.SGameOptionsRecord; + + // this check is crucial to prevent wiping and resetting the game window accidentally + // on refreshes + if (_totalQuestionNumbers - _totalQuestionNumbersSnapshot == 1) + { + // update the snapshot to the new number + _totalQuestionNumbersSnapshot = _totalQuestionNumbers; + + // get a new question + _question = GetQuestion(); + + // reset the timer + CheckTimer(); + + } + // prepare the input settings + ConfigureInputSettings(_question?.QuestionType); + + // print the screen + Console.Write($"Question numbers: {_totalQuestionNumbers}\r\n"); + Console.Write(_question?.QuestionPrompt); + Console.Write("\r\n"); + Console.Write($"\r\nChances Left: {_chances}"); + Console.Write($"\r\nCurrent Score: {_score}"); + InputHandler.InputPrompt(["type [q] to end the game"]); + } + + // return an interface of the question based on the game options + private IQuestion GetQuestion() + { + // using a ternary operator we check if the question type is random or no + // if it's select a random num from 1 to 5 then turn it into a question + // else set it the presented type + var type = _options.QuestionType == GameQuestionType.Random + ? (GameQuestionType)Random.Shared.Next(1, 5) + : _options.QuestionType; + + // generate a new problem struct + var problem = new Problem(_options); + // pass it to the returned object + switch (type) + { + case GameQuestionType.Mcq: + return new QuestionMcq(problem); + case GameQuestionType.Normal: + return new QuestionNormal(problem); + case GameQuestionType.FillGaps: + return new QuestionFillGaps(problem); + case GameQuestionType.TrueFalse: + return new QuestionTf(problem); + default: + throw new IndexOutOfRangeException($"the hell you mean {type}???"); + } + } + + + // set the input settings + private void ConfigureInputSettings(GameQuestionType? questionType) + { + InputHandler.CurrentErrorLocation = 17; + InputHandler.QuestionMode = false; + switch (questionType) + { + case GameQuestionType.Mcq: + InputHandler.SetActiveOptions = "abcd"; + break; + case GameQuestionType.TrueFalse: + InputHandler.SetActiveOptions = "tf"; + break; + case GameQuestionType.Normal: + InputHandler.QuestionMode = true; + InputHandler.SetActiveOptions = "0123456789"; + break; + case GameQuestionType.FillGaps: + InputHandler.QuestionMode = true; + InputHandler.SetActiveOptions = "0123456789+-*/"; + break; + default: + throw new Exception("unknown question type"); + } + } +} \ No newline at end of file diff --git a/MathGame/src/GameWindows/HistoryWindow.cs b/MathGame/src/GameWindows/HistoryWindow.cs new file mode 100644 index 00000000..5340fc42 --- /dev/null +++ b/MathGame/src/GameWindows/HistoryWindow.cs @@ -0,0 +1,133 @@ +using MathGame.AbstractBases; +using MathGame.CoreUtilities; +using MathGame.GameQuestion; + +namespace MathGame.GameWindows; + +public class HistoryWindow : WindowBase +{ + private int _currentPage = 1; + private readonly int _rowsPerPage = 15; + + // get the total page + private int TotalPages + { + get + { + int actualTotalRecords = GameEngine.GameHistoryTable.Count; + return Math.Max(1, (actualTotalRecords + _rowsPerPage - 1) / _rowsPerPage); + } + } + + + public override void Render() + { + // setting up the input handler + InputHandler.CurrentErrorLocation = 2; + InputHandler.SetActiveOptions = "np"; + + // calculating the total number of element to skip + int skipAmount = (_currentPage - 1) * _rowsPerPage; + + // getting via link the total pages to use + var rowsToDisplay = GameEngine.GameHistoryTable + .OrderByDescending(r => r.Id) // Get the latest games first + .Skip(skipAmount) + .Take(_rowsPerPage) + .ToList(); + // setting the space to offset before printing the table + + int offset = 11; + // this is generated by AI not me to save some time + string rowFormat = "| {0,-4} | {1,-10} | {2,-7} | {3,-10} | {4,-8} | {5,-10} | {6,-10} |"; + + // set the cursor to the offset to begin flushing the table date + Console.SetCursorPosition(0, offset); + Console.Write(string.Format( + rowFormat, "Id", "start Time", " Score", "Difficulty", "Op", "Q Type", + "Total Qs" + )); + Console.Write("\r\n"); + Console.Write("|------|------------|---------|------------|----------|------------|------------|"); + Console.Write("\r\n"); + offset++; + + // iterate through the rows to display to flush the data + foreach (var record in rowsToDisplay) + { + + offset++; + Console.SetCursorPosition(0, offset); + Console.Write(rowFormat, + record.Id, + record.PlayedDate.ToString("HH:mm:ss"), + record.TotalScore, + record.Difficulty, + record.Operation switch + { + GameOperation.Random => "Random", + GameOperation.Subtraction => "-", + GameOperation.Multiplication => "x", + GameOperation.Division => "÷", + GameOperation.Addition => "+", + _ => "" + }, + record.QuestionType, + record.TotalNumberOfQuestions); + Console.Write("\r\n"); + } + + // reset the cursor back + Console.SetCursorPosition(0, 0); + Console.WriteLine($"Page {_currentPage} of {TotalPages}"); + + // prepare the tips + string[] tip = + [ + "type [n] to move to the next page", + "type [p] to move back to the previous page", + "type [q] to return back to the main menu" + ]; + + // ask for inputs + InputHandler.InputPrompt(tip); + } + + // process inputs + public override WindowMap ProcessInput(string userInput) + { + switch (userInput) + { + // move next page + case "n": + // check the possibility of the transition + if (_currentPage < TotalPages) + { + _currentPage++; + } + + WindowManager.CurrentWindow = WindowMap.HistoryWindow; + break; + + // move back to the previous page + case "p": + // check the possibility of the transition + if (_currentPage > 1) + { + _currentPage--; + } + + WindowManager.CurrentWindow = WindowMap.HistoryWindow; + break; + + case "q": + // go back to main maun + WindowManager.CurrentWindow = WindowMap.MainMenu; + break; + default: + break; + } + + return WindowManager.CurrentWindow; + } +} \ No newline at end of file diff --git a/MathGame/src/GameWindows/MainMenu.cs b/MathGame/src/GameWindows/MainMenu.cs new file mode 100755 index 00000000..d40f8486 --- /dev/null +++ b/MathGame/src/GameWindows/MainMenu.cs @@ -0,0 +1,75 @@ +using MathGame.AbstractBases; +using MathGame.CoreUtilities; + +namespace MathGame.GameWindows; + +public class MainMenu : WindowBase +{ + + // window drawing + public override void Render() + { + InputHandler.SetActiveOptions = "abc"; + // set the input handler's valid options to + InputHandler.CurrentErrorLocation = 17; + // game components + string[] gameLabel = [ + @"███╗ ███╗ █████╗ ████████╗██╗ ██╗ ██████╗ █████╗ ███╗ ███╗███████╗", + @"████╗ ████║██╔══██╗╚══██╔══╝██║ ██║ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝", + @"██╔████╔██║███████║ ██║ ███████║ ██║ ███╗███████║██╔████╔██║█████╗" , + @"██║╚██╔╝██║██╔══██║ ██║ ██╔══██║ ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ", + @"██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗", + @"╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝" + ]; + + string[] menu = + [ + "\t\t\t A. Start the game", + "\t\t\t B. History records", + "\t\t\t C. About the game", + ]; + + // printing the game components + Console.Write(string.Join("\r\n", gameLabel)); + + // separation to avoid cluttering the window + Console.Write("\r\n\r\n"); + Console.Write(string.Join("\r\n", menu)); + + + // input tips + string[] notes = ["to end the program type [q] soft exit or [ctrl+c] hard exit"]; + // input prompt + InputHandler.InputPrompt(notes); + } + + // process the user input + public override WindowMap ProcessInput(string userInput) + { + switch (userInput) + { + case "a": + return WindowMap.SetupMenu; + case "b": + if (GameEngine.TotalGamesPlayed == 0) + { + InputHandler.HasError = true; + // Push a completely custom error to the handler on demand + InputHandler.ShowErrorMessage("No history found. You must play a game first."); + + // Return the same state so the window doesn't switch + return WindowMap.MainMenu; + } + return WindowMap.HistoryWindow; + case "c": + return WindowMap.AboutWindow; + case "q": + return WindowMap.QuitBanner; + default: + return WindowMap.MainMenu; + + } + + } +} + diff --git a/MathGame/src/GameWindows/QuitWindow.cs b/MathGame/src/GameWindows/QuitWindow.cs new file mode 100755 index 00000000..d4311cb2 --- /dev/null +++ b/MathGame/src/GameWindows/QuitWindow.cs @@ -0,0 +1,19 @@ +using MathGame.AbstractBases; + +namespace MathGame.GameWindows; + +// goofy simple goodbye display +public class QuitWindow : WindowBase +{ + + public override WindowMap ProcessInput(string userInput) + { + return WindowMap.None; + } + + public override void Render() + { + Console.WriteLine("Goodbye"); + Thread.Sleep(1000); + } +} diff --git a/MathGame/src/GameWindows/SetupMenu/SetupOptionHandler.cs b/MathGame/src/GameWindows/SetupMenu/SetupOptionHandler.cs new file mode 100755 index 00000000..d4879a4f --- /dev/null +++ b/MathGame/src/GameWindows/SetupMenu/SetupOptionHandler.cs @@ -0,0 +1,127 @@ +using MathGame.GameQuestion; + +namespace MathGame.GameWindows.SetupMenu +{ + internal static class SetupOptionHandler + { + // this method returns bool denoting that the user have entered all the options + // and it also modified the Setup window's stored option variable + public static bool SetQuestionOptions(string userInput,ref Options options) + { + if (options.Difficulty == GameDifficulty.None) + { + options.Difficulty = SetGameDifficulty(userInput); + } + else if (options.Operation == GameOperation.None) + { + options.Operation = SetGameOperation(userInput); + } + else if (options.QuestionType == GameQuestionType.None) + { + options.QuestionType = SetGameQuestionType(userInput); + // to indicate that the all options is set + if (options.QuestionType != GameQuestionType.None) + { + return true; + } + } + return false; + } + + // this method render the next option set and their options + public static void RenderQuestionsSetup(Options options) + { + string infoPanel = $"Difficulty : {options.Difficulty}\t\tOperation : {options.Operation}\t\tQuestion Type : {options.QuestionType}"; + if (options.Difficulty == GameDifficulty.None) + { + RenderDifficultyOptions(); + } + else if (options.Operation == GameOperation.None) + { + RenderOperationOptions(); + } + else if (options.QuestionType == GameQuestionType.None) + { + RenderQuestionTypeOptions(); + } + // an info panel to display the selection summary + Console.Write("\r\n"); + Console.WriteLine($"{infoPanel}\r\n"); + } + + // this region is responsible for printing out all options based on the current option type + #region Rendering Options + private static void RenderDifficultyOptions() + { + Console.Write("\tchoose a difficulty}\r\n"); + Console.Write("\t\ta. easy (digits from 0 to 10)\r\n"); + Console.Write("\t\tb. normal (digits from 0 to 100)\r\n"); + Console.Write("\t\tc. hard (digits from 0 to 500)\r\n"); + Console.Write("\t\td. insane (digits from 0 to 1000)\r\n"); + Console.Write("\t\te. impossible (digits from 0 to 2000)\r\n"); + } + private static void RenderOperationOptions() + { + Console.Write("\tchoose the operation}\r\n"); + Console.Write($"\t\ta. addition (+)\r\n"); + Console.Write($"\t\tb. subtraction (-)\r\n"); + Console.Write($"\t\tc. multiplication (x)\r\n"); + Console.Write($"\t\td. division (÷)\r\n"); + Console.Write($"\t\te. random operation\r\n"); + } + private static void RenderQuestionTypeOptions() + { + Console.Write("\tchoose the question type\r\n"); + Console.Write("\t\ta. MCQ\r\n"); + Console.Write("\t\tb. true or false\r\n"); + Console.Write("\t\tc. fill the gaps\r\n"); + Console.Write("\t\td. normal\r\n"); + Console.Write("\t\te. random\r\n"); + } + #endregion + + // this region is responsible for processing input based on the current option type + #region Process Input Options + + // these method return a difficulty based on the input + private static GameDifficulty SetGameDifficulty(string userInput) => userInput switch + { + "a" => GameDifficulty.Easy, + "b" => GameDifficulty.Normal, + "c" => GameDifficulty.Hard, + "d" => GameDifficulty.Insane, + "e" => GameDifficulty.Impossible, + _ => GameDifficulty.None + }; + // these method return a Operation based on the input + private static GameOperation SetGameOperation(string userInput) => userInput switch + { + "a" => GameOperation.Addition, + "b" => GameOperation.Subtraction, + "c" => GameOperation.Multiplication, + "d" => GameOperation.Division, + "e" => GameOperation.Random, + _ => GameOperation.None + }; + // these method return a QuestionType based on the input + private static GameQuestionType SetGameQuestionType(string userInput) => userInput switch + { + "a" => GameQuestionType.Mcq, + "b" => GameQuestionType.TrueFalse, + "c" => GameQuestionType.FillGaps, + "d" => GameQuestionType.Normal, + "e" => GameQuestionType.Random, + _ => GameQuestionType.None + }; + + #endregion + + // this method reset the options + public static void ResetOptions(ref Options options) + { + options.Difficulty = GameDifficulty.None; + options.Operation = GameOperation.None; + options.QuestionType = GameQuestionType.None; + } + } +} diff --git a/MathGame/src/GameWindows/SetupMenu/SetupWnidow.cs b/MathGame/src/GameWindows/SetupMenu/SetupWnidow.cs new file mode 100755 index 00000000..b274b0f2 --- /dev/null +++ b/MathGame/src/GameWindows/SetupMenu/SetupWnidow.cs @@ -0,0 +1,82 @@ +using MathGame.AbstractBases; +using MathGame.CoreUtilities; +using MathGame.GameQuestion; + +namespace MathGame.GameWindows.SetupMenu; + +public class SetupWindow : WindowBase +{ + private bool IsOptionsSet { get; set; } + // private GameOptions _gameOptions = GameEngine.SGameOptions; + private Options _gameOptions = GameEngine.SGameOptionsRecord; + // process inputs + public override WindowMap ProcessInput(string userInput) + { + // smart trick so instead of handling or the input I just checks the mean three keys [q,w,r] + // and based on the IsOptionsSet I move one to the next option + switch (userInput) + { + case "w" : + SetupOptionHandler.ResetOptions(ref _gameOptions); + IsOptionsSet = false; + return WindowMap.SetupMenu; + case "q": + return WindowMap.MainMenu; + case "y": + // saves all our selection back to the Game Engine to utilize it in the game mech + GameEngine.SGameOptionsRecord = _gameOptions; + return WindowMap.GameWindow; + default: + if (!IsOptionsSet) + { + IsOptionsSet = SetupOptionHandler.SetQuestionOptions(userInput, ref _gameOptions); + } + return WindowMap.SetupMenu; + } + } + + public override void Render() + { + // checks if IsOptionsSet is true to print out special format for it and return guard then + if (IsOptionsSet) + { + InputHandler.CurrentErrorLocation = 20; + // change the Active Options to + InputHandler.SetActiveOptions = "yw"; + // A final display of user input + Console.Write($"Difficulty : {_gameOptions.Difficulty}\t\tOperation : {_gameOptions.Operation}\t\tQuestion Type : {_gameOptions.QuestionType}\r\n"); + + // Confirming the input + Console.Write("Are you sure about your inputs"); + + + InputHandler.InputPrompt( + [ + "- type [q] to go back to main menu", + "- type [y] to Confirm", + "- type [w] to wipe all selections", + "- press [ctrl+c] to hard exit" + ] + ); + return; + } + + // configure the input handle's active inputs and error location + InputHandler.SetActiveOptions = "abcdew"; + InputHandler.CurrentErrorLocation = 20; + Console.Write($"Choose From The Following Options:\r\n"); + Console.Write("\r\n"); + // render the options only + SetupOptionHandler.RenderQuestionsSetup(_gameOptions); + // ask for user input + InputHandler.InputPrompt( + [ + "- type [q] to go back to main menu", + "- type [w] to wipe all selections", + "- press [ctrl+c] to hard exit" + ] + ); + + } + +} \ No newline at end of file diff --git a/MathGame/src/GameWindows/WindowMap.cs b/MathGame/src/GameWindows/WindowMap.cs new file mode 100755 index 00000000..1f81fed9 --- /dev/null +++ b/MathGame/src/GameWindows/WindowMap.cs @@ -0,0 +1,16 @@ +// a full map for our game +namespace MathGame.GameWindows; + +public enum WindowMap +// this is for switching between windows easily as well as defining our current window +{ + None, + MainMenu, + SetupMenu, + GameWindow, + GameOverWindow, + HistoryWindow, + AboutWindow, + QuitBanner +} + diff --git a/MathGame/src/Program.cs b/MathGame/src/Program.cs new file mode 100755 index 00000000..ec82f19f --- /dev/null +++ b/MathGame/src/Program.cs @@ -0,0 +1,3 @@ +using MathGame.CoreUtilities; + +GameEngine.Render(); \ No newline at end of file