/* Ingus Zemturis 110 grupa Match Tiles projekts ir atmiņas spēle, kurā jums ir jāsaskaņo 2 simboli, veicot pēc iespējas mazāk darbību, lai to vieglāk izdarītu jums ir dotas 2 spējas, neatgriezeniski atklāt 1 simbolu, vai atklāt 9 simbolus ap kursoru uz 2.5 sekundēm (spēju skaits atkarīgs no spēļu laukuma un dublikātu simbolu daudzuma). A- kustība pa kreisi, D- kustība pa labi, W- kustība uz augšu, S- kustība uz leju, Spacebar- izvēlēties simbolu, R- atklāt 9 simbolus ap sevi, F- neatgriezeniski atklāt 1 simbolu 29.05.2026 (start) 31.05.2026(end) Visual Studio Community 2022 */ using System; namespace PLAYGROUND; class Program { public static bool ProgramRunning = true; // dictionary is to allow me to use math.random, as enums dont allow values to start with a non letter. public static readonly Dictionary Symbols = new() { {1, 'a' }, {2, 'b' }, {3, 'c' }, {4, 'd' }, {5, 'e' }, {6, 'f' }, {7, 'g' }, {8, 'h' }, {9, 'i' }, {10, 'j' }, {11, 'k' }, {12, 'l' }, {13, 'm' }, {14, 'n' }, {15, 'o' }, {16, 'p' }, {17, 'q' }, {18, 'r' }, {19, 's' }, {20, 't' }, {21, 'u' }, {22, 'v' }, {23, 'w' }, {24, 'x' }, {25, 'y' }, {26, 'z' }, {27, '<' }, {28, '>' }, {29, '@' }, {30, '$' }, {31, '+' }, {32, '&' } }; struct Tile // storing these things with lists and arrays caused a lot of problems that this struct solved { public char Symbol; public bool selected; public bool Revealed; public bool Matched; public long Timer; // in milliseconds public long PermanentTimer; // to allow the functionality of the permanent reveal ability } public class GameMaster // all things logic? the game board. { //=================================== //Variables //=================================== private List AbilityList = new (); public int BoardSize { get; private set; } public double DuplicatePercentage { get; private set; } Tile[,] Board; public char[,] LastDrawn { get; private set; } public int SelectionCursorX { get; private set; } public int SelectionCursorY { get; private set; } public int LastSelectionCursorX { get; private set; } public int LastSelectionCursorY { get; private set; } private readonly int DisplayCursorX = 4; private readonly int DisplayCursorY = 2; public bool CanSelect { get; private set; } = true; private long SelectionTimeout = 0; public bool GameOngoing { get; private set; } = true; public int ActionCount { get; private set; } public double Difficulty { get; private set; } private Random RNG = new (); private ConsoleKey SinglePlayerInput; //=================================== //Methods (this part alone is like 50-70% of the code) //=================================== private void DifficultyValueCalculator() // wanted to use it for a score system, currently used to alter ability usage amounts { Difficulty = (Math.Pow(BoardSize, 2)/10) * (1.1-DuplicatePercentage); } private void BoardSymbolGenerator() { double UniqueSymbols = Math.Floor(((BoardSize * BoardSize) / 2) * (1-DuplicatePercentage)); double DuplicateSymbols = Math.Ceiling((BoardSize * BoardSize) / 2 - UniqueSymbols); int StartingSeed = RNG.Next(1, Convert.ToInt16(UniqueSymbols+1)); int StepCount; //how many times the next symbol ID will be skipped when generating duplicates, 1 is not a skip List BoardCharacters = new(); Board = new Tile[BoardSize,BoardSize]; // filling up the list with symbols for (int i = 1; i <= UniqueSymbols; i++) { BoardCharacters.Add(Symbols[i]); BoardCharacters.Add(Symbols[i]); } for (int d = 1; d <= DuplicateSymbols; d++) { StepCount = RNG.Next(1, 6); BoardCharacters.Add(Symbols[StartingSeed]); BoardCharacters.Add(Symbols[StartingSeed]); StartingSeed += StepCount; if (StartingSeed > UniqueSymbols) { StartingSeed -= Convert.ToInt16(UniqueSymbols); } } // shuffling the symbols for (int Shuffle = BoardCharacters.Count - 1; Shuffle >= 0; Shuffle--) { int YateShuffle = RNG.Next(Shuffle + 1); (BoardCharacters[Shuffle], BoardCharacters[YateShuffle]) = (BoardCharacters[YateShuffle], BoardCharacters[Shuffle]); } // double shuffling the symbols for (int Shuffle = BoardCharacters.Count - 1; Shuffle >= 0; Shuffle--) { int YateShuffle = RNG.Next(Shuffle + 1); (BoardCharacters[Shuffle], BoardCharacters[YateShuffle]) = (BoardCharacters[YateShuffle], BoardCharacters[Shuffle]); } int index = 0; for (int h = 0; h < BoardSize; h++) // setting up the Board { for (int w = 0; w < BoardSize; w++) { Board[h, w].Symbol = BoardCharacters[index]; Board[h, w].selected = false; Board[h, w].Revealed = false; Board[h, w].Matched = false; index++; } } // probably not the most fitting place, but this is where any 2d arrays used within GM would be set up LastDrawn = new char[BoardSize,BoardSize]; } public void BoardCreationOptions() { bool ValidSelection = false; bool SelectingSizeLoop = true; bool SelectingDuplicatesLoop = true; bool Selection = true; while (Selection && ProgramRunning) { while (SelectingSizeLoop && ProgramRunning) // used to allow the player to redo choices { Console.Clear(); Console.WriteLine("do you want a 4x4, 6x6 or 8x8 board?\n(input the number corresponding to the board, for example if you want 4x4 type 4 in the console)"); SinglePlayerInput = Console.ReadKey(true).Key; switch (SinglePlayerInput) // confirming valid player input before moving on { case ConsoleKey.D4: BoardSize = 4; ValidSelection = true; break; case ConsoleKey.D6: BoardSize = 6; ValidSelection = true; break; case ConsoleKey.D8: BoardSize = 8; ValidSelection = true; break; case ConsoleKey.Escape: ProgramRunning = false; break; default: Console.Write("that is not a valid input"); Thread.Sleep(1000); Console.Clear(); ValidSelection = false; break; } if (ValidSelection && ProgramRunning) { Console.Write($"you have chosen a {BoardSize}x{BoardSize} board, press Y or Enter to continue, press any other key to redo selection."); SinglePlayerInput = Console.ReadKey(true).Key; if (SinglePlayerInput == ConsoleKey.Y || SinglePlayerInput == ConsoleKey.Enter) { SelectingSizeLoop = false; } else if (SinglePlayerInput == ConsoleKey.Escape) { ProgramRunning = false; } } } while (SelectingDuplicatesLoop && ProgramRunning) // Confirming desired duplicate amount before moving on { Console.Clear(); Console.WriteLine("how many duplicates do you want?\n1. 0% 2. 10% 3. 20% 4. 30% 5. 40% 6. 50%\n(type the number to the left of the percentage you want)"); SinglePlayerInput = Console.ReadKey(true).Key; switch (SinglePlayerInput) // confirming valid player input { case ConsoleKey.D1: DuplicatePercentage = 0; ValidSelection = true; break; case ConsoleKey.D2: DuplicatePercentage = 0.1; ValidSelection = true; break; case ConsoleKey.D3: DuplicatePercentage = 0.2; ValidSelection = true; break; case ConsoleKey.D4: DuplicatePercentage = 0.3; ValidSelection = true; break; case ConsoleKey.D5: DuplicatePercentage = 0.4; ValidSelection = true; break; case ConsoleKey.D6: DuplicatePercentage = 0.5; ValidSelection = true; break; case ConsoleKey.Escape: ProgramRunning = false; break; default: Console.Write("that is not a valid input"); Thread.Sleep(1000); Console.Clear(); ValidSelection = false; break; } if (ValidSelection && ProgramRunning) { Console.Write($"you have chosen {DuplicatePercentage*100}% duplicates, press Y or Enter to continue, press any other key to redo selection."); SinglePlayerInput = Console.ReadKey(true).Key; if (SinglePlayerInput == ConsoleKey.Y || SinglePlayerInput == ConsoleKey.Enter) { SelectingDuplicatesLoop = false; } else if (SinglePlayerInput == ConsoleKey.Escape) { ProgramRunning = false; } } } // allowing the player to change their choice of board size or duplicate quantity before the game starts if (ProgramRunning) { Console.Clear(); Console.Write($"board size: {BoardSize}x{BoardSize} duplicate symbols: {DuplicatePercentage * 100}%\nif you want a different board size, type 1\nif you want a different duplicate percentage, type 2\nif you want to start the game, press any other key"); SinglePlayerInput = Console.ReadKey(true).Key; } if (SinglePlayerInput == ConsoleKey.D1 && ProgramRunning) { SelectingSizeLoop = true; } else if (SinglePlayerInput == ConsoleKey.D2) { SelectingDuplicatesLoop = true; } else if (SinglePlayerInput == ConsoleKey.Escape) { ProgramRunning = false; } else { Selection = false; } } BoardSymbolGenerator(); DifficultyValueCalculator(); } private char GetDisplayCharacter(int x, int y) // to still allow the use of LastDrawn, otherwise who knows how annoyingly complex Display() would get { if (Board[y, x].Matched || Board[y, x].selected || Board[y, x].Revealed) { return Board[y, x].Symbol; } else { return '.'; } } public void Display() { for (int height = 0; height < BoardSize; height++) { for (int width = 0; width < BoardSize; width++) { char CurrentCharacter = GetDisplayCharacter(width, height); if (CurrentCharacter != LastDrawn[height, width]) { Console.SetCursorPosition(DisplayCursorX + width * DisplayCursorX, DisplayCursorY + height * DisplayCursorY); Console.Write(CurrentCharacter); LastDrawn[height, width] = CurrentCharacter; } } } Console.SetCursorPosition(DisplayCursorX * BoardSize + 6, DisplayCursorY); Console.Write($"Total Pairs Selected: {ActionCount}"); Console.SetCursorPosition(DisplayCursorX * BoardSize + 6, DisplayCursorY * 2); Console.Write($"you have {AbilityList[0].RemainingUses} uses of Reveal 3x3 left (R to activate)"); Console.SetCursorPosition(DisplayCursorX * BoardSize + 6, DisplayCursorY * 3); Console.Write($"you have {AbilityList[1].RemainingUses} uses of Permanent Reveal left (F to activate)"); } private void MatchingLogic() // the part that allows matching to occur { List<(int x, int y)> SelectedTiles = new(); for (int y = 0; y < BoardSize; y++) // saving coordinates of any selected tiles { for (int x = 0; x < BoardSize; x++) { if (Board[y, x].selected) { SelectedTiles.Add((x, y)); } } } if (SelectedTiles.Count >= 2 && CanSelect) // the actual part responsible for confirming matches, or resetting selection of tiles if it wasn't a match { ActionCount++; (int X_one, int Y_one) = SelectedTiles[0]; (int X_two, int Y_two) = SelectedTiles[1]; if (Board[Y_one, X_one].Symbol == Board[Y_two, X_two].Symbol) { Board[Y_one, X_one].Matched = true; Board[Y_two, X_two].Matched = true; Board[Y_one, X_one].selected = false; Board[Y_two, X_two].selected = false; EndingCheck(); } else { if (Board[Y_one, X_one].Timer < Environment.TickCount + 1100) Board[Y_one, X_one].Timer = Environment.TickCount + 1100; if (Board[Y_two, X_two].Timer < Environment.TickCount + 1100) Board[Y_two, X_two].Timer = Environment.TickCount + 1100; CanSelect = false; SelectionTimeout = Environment.TickCount + 1100; // added this timer so that i could prevent player selections from causing tiles revealed by the 3x3 reveal to become hidden early } } } private void EndingCheck() { int CurrentMatches = 0; int MaximumMatches = BoardSize * BoardSize; foreach (Tile tile in Board) { if (tile.Matched) { CurrentMatches++; } } if (CurrentMatches == MaximumMatches) { GameOngoing = false; } } private void CursorDisplay() // the reason you can reasonably know what is being selected { Console.SetCursorPosition(DisplayCursorX - 1 + LastSelectionCursorX * DisplayCursorX, DisplayCursorY + LastSelectionCursorY * DisplayCursorY); Console.Write(' '); Console.SetCursorPosition(DisplayCursorX + 1 + LastSelectionCursorX * DisplayCursorX, DisplayCursorY + LastSelectionCursorY * DisplayCursorY); Console.Write(' '); Console.SetCursorPosition(DisplayCursorX - 1 + SelectionCursorX * DisplayCursorX, DisplayCursorY + SelectionCursorY * DisplayCursorY); Console.Write('>'); Console.SetCursorPosition(DisplayCursorX + 1 + SelectionCursorX * DisplayCursorX, DisplayCursorY + SelectionCursorY * DisplayCursorY); Console.Write('<'); } private void SaveLastPosition() // used to allow proper removal of the display arrows so you actually know where you are { LastSelectionCursorX = SelectionCursorX; LastSelectionCursorY = SelectionCursorY; } private bool ValidCoordinate(int x, int y) // no out of bounds here! { if (x < 0 || x >= BoardSize) { return false; } else if (y < 0 || y >= BoardSize) { return false; } return true; } public void MoveCursor(int DirX, int DirY) { if (DirX > 1) { DirX = 1; } if (DirX < -1) { DirX = -1; } if (DirY > 1) { DirY = 1; } if (DirY < -1) { DirY = -1; } int NewPosX = SelectionCursorX + DirX; int NewPosY = SelectionCursorY + DirY; if (ValidCoordinate(NewPosX, NewPosY)) { SaveLastPosition(); SelectionCursorX = NewPosX; SelectionCursorY = NewPosY; CursorDisplay(); } } public void SelectTile() // used to be in the player's input handler, now GM has taken over this role (along with every other board altering role) { if (Board[SelectionCursorY, SelectionCursorX].Matched || Board[SelectionCursorY, SelectionCursorX].selected) { return; } else { Board[SelectionCursorY, SelectionCursorX].selected = true; MatchingLogic(); } } public void TemporaryRevealSetup(int x, int y, long duration) // maybe the name is a bit confusing, this allows abilities to create either temporary or permanent revealed tiles (perm reveal is the next method) { if (ValidCoordinate(x, y)) { Board[y, x].Revealed = true; Board[y, x].Timer = Environment.TickCount + duration; } } public void PermanentRevealSetup(int x, int y, long blinkingSpeed) { if (ValidCoordinate(x, y)) { Board[y, x].Revealed = true; Board[y, x].PermanentTimer = blinkingSpeed; } } public void TileRevealUpdating() // all the logic and timer checking needed to allow Revealing and unRevealing of tiles { // Selection Visibility Handling int CurrentSelectionCount = 0; foreach (Tile tile in Board) { if (tile.selected) { CurrentSelectionCount++; } } if (CurrentSelectionCount >= 2) { for (int y = 0; y < BoardSize; y++) { for (int x = 0; x < BoardSize; x++) { if (Board[y, x].selected) { Board[y, x].selected = false; Board[y, x].Revealed = true; } } } } // Temporary Reveal Handling for (int height = 0; height < BoardSize; height++) { for (int width = 0; width < BoardSize; width++) { if (Board[height, width].Revealed) { if (Board[height, width].Timer <= Environment.TickCount) { Board[height, width].Revealed = false; } } } } // Permanent Reveal Handling for (int height = 0; height < BoardSize; height++) { for (int width = 0; width < BoardSize; width++) { if (Board[height, width].PermanentTimer > 0) { if (Board[height, width].Timer <= Environment.TickCount - Board[height, width].PermanentTimer / 4 && !Board[height, width].Matched) { Board[height, width].Revealed = true; Board[height, width].Timer = Environment.TickCount + Board[height, width].PermanentTimer; } } } } } public void UseAbility(int index, int x, int y) { if (index < 0 || index >= AbilityList.Count) return; if (index == 1 && (Board[y, x].PermanentTimer > 0 || (Board[y, x].Matched))) return; if (index == 0 && Board[y, x].Timer > Environment.TickCount + 750) return; AbilityList[index].TryUse(this, x, y); } public void InitializeAbilities() { AbilityList.Clear(); AbilityList.Add(new Reveal3x3(Difficulty)); AbilityList.Add(new RevealPermanent(Difficulty)); } public void LockoutHandler() // i thought i might put a timer on using 3x3 reveal, but i didn't, so i ended up adding some of the program exit functionality here { if (Environment.TickCount >= SelectionTimeout) { CanSelect = true; } if (!ProgramRunning) { GameOngoing = false; } } } abstract class Abilities { protected int Uses; public int RemainingUses => Uses; public bool CanUse => Uses > 0; public void TryUse(GameMaster gm, int x, int y) // the solution to having the ability use amount never decrease { if (!CanUse) return; Uses--; AbilityFunction(gm, x, y); } public abstract void AbilityFunction(GameMaster GM, int x, int y); } class Reveal3x3 : Abilities { public Reveal3x3(double difficulty) { Uses = Convert.ToInt16(Math.Ceiling(difficulty)); } public override void AbilityFunction(GameMaster GM, int x, int y) { for (int horizontal = x - 1; horizontal <= x + 1; horizontal++) { for (int vertical = y - 1; vertical <= y + 1; vertical++) { GM.TemporaryRevealSetup(horizontal, vertical, 2500); } } } } class RevealPermanent : Abilities { public RevealPermanent(double difficulty) { Uses = Convert.ToInt16(Math.Ceiling(2 * difficulty)); } public override void AbilityFunction(GameMaster GM, int x, int y) { GM.PermanentRevealSetup(x, y, 800); } } class Player (GameMaster GM) { public void InputHandler(ConsoleKey key) { switch (key) { case ConsoleKey.A: GM.MoveCursor(-1, 0); break; case ConsoleKey.D: GM.MoveCursor(1, 0); break; case ConsoleKey.W: GM.MoveCursor(0, -1); break; case ConsoleKey.S: GM.MoveCursor(0, 1); break; case ConsoleKey.Spacebar: if (GM.CanSelect) { GM.SelectTile(); } break; case ConsoleKey.R: GM.UseAbility(0, GM.SelectionCursorX, GM.SelectionCursorY); break; case ConsoleKey.F: GM.UseAbility(1, GM.SelectionCursorX, GM.SelectionCursorY); break; case ConsoleKey.Escape: ProgramRunning = false; break; } } } static void Main() { while (ProgramRunning) { GameMaster GM = new(); GM.BoardCreationOptions(); GM.InitializeAbilities(); Player player = new(GM); // the reason GM is passed into the player is so that i can access the variables within GM (though im sure that also allows significant reduction in parameter amount for the methods within GM) Console.Clear(); Console.CursorVisible = false; GM.MoveCursor(0, 0); while (GM.GameOngoing) { if (Console.KeyAvailable) { ConsoleKeyInfo Kinfo = Console.ReadKey(true); player.InputHandler(Kinfo.Key); } Thread.Sleep(5); GM.Display(); GM.TileRevealUpdating(); GM.LockoutHandler(); } Console.Clear(); if (ProgramRunning) { Console.Write($"You beat the {GM.BoardSize}x{GM.BoardSize} board with {GM.DuplicatePercentage * 100}% duplicates in {GM.ActionCount} Turns!\nPress any key to restart, press ESC to exit the app (you could have used Esc to exit this app at any point during its runtime, maybe you already did before seeing this message)"); bool EndScreen = true; while (EndScreen) { ConsoleKey key = Console.ReadKey(true).Key; if (key == ConsoleKey.Enter) { EndScreen = false; } else if (key == ConsoleKey.Escape) { EndScreen = false; ProgramRunning = false; } } Console.Clear(); } } } }