4
\$\begingroup\$

I'm a beginner at C#, and this is a console application that I wrote as I'm attempting to recreate simple games such as Hangman, TicTacToe, etc. I'm wondering if there's any logic in my code that's inefficient. Also, there's probably way too much nested statements around the part where the program checks the validity of the input, so is there any way to rearrange that so it's easier to read?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;

namespace HangmanGame
{
    internal class Program
    {
        static void Main(string[] args)
        {
            bool isPlaying = true;
            bool needSetup = false;

            Console.Title = "Hangman Game";
            Console.WriteLine("Welcome to Hangman!");

            HangmanGame hangmanGame = new HangmanGame();

            do
            {
                if (!needSetup)
                {
                    needSetup = true;
                    hangmanGame.GameSetup();
                    hangmanGame.Play();
                }

                Console.Write("Do you want to continue playing Hangman (Y/N)? ");
                switch (Console.ReadLine()?.ToUpper())
                {
                    case "Y":
                        isPlaying = true;
                        needSetup = false;
                        Console.Clear();
                        Console.WriteLine("Welcome to Hangman!");
                        break;
                    case "N":
                        isPlaying = false;
                        break;
                    default:
                        Console.WriteLine("Please enter an acceptable answer.");
                        break;
                }
            } while (isPlaying);
        }
    }

    class HangmanGame
    {
        Random random = new Random();
        List<string> wordList = new List<string>()
        {
            "scythe", "apple", "osmosis", "laughing", "extend", "brown", "expert", "tired", "humidity", "backpack", "wares", "soup",
            "mount", "crust", "dent", "market", "knock", "smite", "windy", "coin", "throw", "silence", "bluff", "downfall", "mass",
            "game", "stocking", "folly", "action",
        };
        char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();

        string[] hangmanStages =
        {
    // final state: head, torso, both arms, and both legs
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |     / \
        -",
    // head, torso, both arms, and one leg
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |     / 
        -",
    // head, torso, and both arms
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |      
        -",
    // head, torso, and one arm
    @"
        --------
        |      |
        |      O
        |     \|
        |      |
        |     
        -",
    // head and torso
    @"
        --------
        |      |
        |      O
        |      |
        |      |
        |     
        -",
    // head
    @"
        --------
        |      |
        |      O
        |    
        |      
        |     
        -",
    // initial empty state
    @"
        --------
        |      |
        |      
        |    
        |      
        |     
        -",
        };

        private List<char> correctGuesses = new List<char>();
        private List<char> incorrectGuesses = new List<char>();
        private List<char> availableGuesses = new List<char>();
        private string? correctWord;
        private char[]? currentGuess;
        private string? input;
        private char inputChar;
        private const int MAXIMUM_LIVES = 6;
        private int numOfLives;
        
        public void GameSetup()
        {
            correctGuesses = new List<char>();
            incorrectGuesses = new List<char>();
            availableGuesses = alphabet.ToList();
            correctWord = wordList[random.Next(0, wordList.Count)].ToUpper();
            currentGuess = new char[correctWord.Length];
            wordList.Remove(correctWord);
            numOfLives = MAXIMUM_LIVES;

            for (int i = 0; i < correctWord.Length; i++)
            {
                if (correctWord[i] == ' ')
                {
                    currentGuess[i] = ' ';
                }
            }
        }

        public void Play()
        {
            DrawDisplay();

            // Checks to end game if the current guess is correct or if all lives are lost
            while (correctWord != (new string(currentGuess)).ToUpper() && numOfLives > 0)
            {
                Console.Write("Please enter your guess: ");
                input = Console.ReadLine()?.ToUpper();

                // Allows the player to type the entire word, and if correct ends the game
                if (String.Equals(input?.ToUpper(), correctWord))
                {
                    break;
                }

                // If the player types nothing or guess more than one letter, replaces the guess with an incorrect character
                if (input?.Length != 1)
                {
                    input = "$";
                }

                inputChar = Convert.ToChar(input);

                /*
                 * Checks to see if the input is in the array of available guesses
                 * Matches against correct word if it does
                 * If the guess is correct, replace letters in current guess with the input
                 * If the guess is incorrect, deduct lives
                 */
                if (availableGuesses.Contains(inputChar))
                {
                    if (correctWord.Contains(inputChar))
                    {
                        for (int i = 0; i < correctWord.Length; i++)
                        {
                            if (correctWord[i] == inputChar)
                            {
                                currentGuess[i] = inputChar;
                                availableGuesses.Remove(inputChar);
                                correctGuesses.Add(inputChar);
                                Console.WriteLine("Added Char: " + inputChar);
                            }
                            else if (correctWord[i] == ' ')
                            {
                                currentGuess[i] = ' ';
                            }
                            else if (!(correctGuesses.Contains(correctWord[i])))
                            {
                                currentGuess[i] = '\0';
                            }
                        }
                    }
                    else
                    {
                        availableGuesses.Remove(inputChar);
                        incorrectGuesses.Add(inputChar);
                        numOfLives--;
                    }

                    DrawDisplay(true);
                }
                else if (correctGuesses.Contains(inputChar) || incorrectGuesses.Contains(inputChar))
                {
                    Console.WriteLine("You already entered that!");
                }
                else
                {
                    Console.WriteLine("Please enter an acceptable input.");
                }
            }

            Console.Clear();
            if (numOfLives > 0)
                Console.WriteLine($"Congratulations! You won with {numOfLives} {(numOfLives == 1 ? "life" : "lives")} left! "
                    + "The correct word was "
                    + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(correctWord.ToLower()));
            else
                Console.WriteLine("Sorry! You lost! The correct word was " +
                    CultureInfo.CurrentCulture.TextInfo.ToTitleCase(correctWord.ToLower()));
            currentGuess = correctWord.ToCharArray();
            DrawDisplay();
        }

        private void DisplayCharArray(char[] chars)
        {

            for (int i = (int) (8 - Math.Round((decimal) (chars.Length / 2), MidpointRounding.AwayFromZero)); i >= 0; i--)
            {
                Console.Write(" ");
            }

            // Converts a char array into hangman form with underscores or the guessed letters
            foreach (char c in chars)
            {
                if (c == '\0')
                {
                    Console.Write("_ ");
                }
                else
                {
                    Console.Write(c + " ");
                }
            }

            Console.WriteLine("\n");
        }

        private void DrawDisplay(bool isGuessing = false)
        {
            // Only writes when a guess has already occurred, so on the first guess it doesn't write it
            if (isGuessing)
            {
                Console.Clear();
                if (correctWord.Contains(inputChar))
                    Console.WriteLine("That guess was correct!");
                else
                    Console.WriteLine("That guess was incorrect!");

                Console.Write("Incorrect Guesses: ");
                for (int i = 0; i < incorrectGuesses.Count; i++)
                {
                    Console.Write(incorrectGuesses[i] + " ");
                }
                Console.WriteLine($"\nYou have {numOfLives} {(numOfLives == 1 ? "life" : "lives")} left!");
            }

            Console.WriteLine(hangmanStages[numOfLives]);
            DisplayCharArray(currentGuess);
        }
    }

}
\$\endgroup\$
1
  • \$\begingroup\$ Welcome to Code Review! This was fun to look through and get back into the swing of reviewing code here. I look forward to seeing more of your code here! \$\endgroup\$
    – Malachi
    Commented Jul 13 at 22:35

1 Answer 1

5
\$\begingroup\$

To Be honest with you, I think that you are doing too much inside of the Main method that should be done inside the HangmanGame class. The idea is that you should be able to call any game inside the Main method after creating the different game objects and methods, that is a little more advanced though and will include things like Interfaces and object inheritance and some other things.

Let's start cleaning up Main.

1 thing to note in your current Main method is something I noticed in this piece of code while I was trying to remove it from the DoWhile loop:

if (!needSetup)
{
    needSetup = true;
    hangmanGame.GameSetup();
    hangmanGame.Play();
}

If you needSetup this Boolean should be True, not False

I am going to try and remove this entire If statement and the DoWhile loop inside the Main method, but we will get to that in a minute.

There is a bug in your code when I ran it, I failed to guess the word and then tried to play again, but the game wasn't reset so the failure was still placed on the screen and the code asked me if I wanted to play again, repeatedly.

I just added

hangmanGame.GameSetup();

to the Y Case of the Switch statement, I haven't run into any issues thus far in my Code Reivew/Testing.


The name HangmanGame, tells me this is the game object, and this should have a method for setup and a method for playing and a method for resetting the game (You should be able to reuse the GameSetup method for this).

To start with, use your constructor to setup the game:

public HangmanGame()
{
    GameSetup();
}

A Constructor is a method that is called when the object is created from the class, in your case it's a HangmanGame object, I always make the constructor method the first method in the class after the class variables. Anything that you want done when the object is created can go here, then you don't have to put it inside the Main method.

What this does is automatically set up the object when you create the game object with this command in your Main method:

HangmanGame hangmanGame = new HangmanGame();

Basically, when you create an hangmanGame object, it automatically sets the game and because the wordList is created as part of the object, you have continued scope so you can keep removing words from it. I would suggest that you do something in the case that someone reaches the end of the wordList though, maybe something simple, like:

if (wordList.Count == 0 )
{
    Console.WriteLine("Thank you for playing, you have used all the words in the current word list.");
}

I added this to the GameSetup() method, so that when you reset it can trigger this.


You should also get into the habit of checking your code for things that you are repeating and turn them into methods or functions or even objects of their own.

You almost actually did this inside of the Play method with a variable.

input = Console.ReadLine()?.ToUpper();

I would make this it's own method though, so you can use it everywhere inside of the HangmanGame class.

I would just put this method in there:

public string? GetInput ()
{
    return Console.ReadLine()?.ToUpper();
}

and then use it wherever you used:

Console.ReadLine()?.ToUpper();

I also moved:

Console.WriteLine("Welcome to Hangman!");

inside the the DoWhile loop above the HangmanGame.Play(); command, this removes some duplicity in your code and makes it more DRY


I also removed some Using statements because they weren't being used, be careful, depending on what you add to this Main you may need to add them back in.


I was able to remove a few things from your code as a result of the changes I made, I was hoping to move the DoWhile Loop from Main into the HangmanGame class possibly inside of the Play method, but I don't have a good idea to do so at the moment, if you were picking between simple games in the Main Method, I would suggest something different for how to replay Hangman without leaving the object itself.


Here is what I have currently on my machine:

using System.Globalization;

namespace HangmanGame
{
    internal class Program
    {
        static void Main(string[] args)
        {
            bool isPlaying = true;
            Console.Title = "Hangman Game";
            HangmanGame hangmanGame = new HangmanGame();

            do
            {
                Console.WriteLine("Welcome to Hangman!");
                hangmanGame.Play();

                Console.Write("Do you want to continue playing Hangman (Y/N)? ");
                switch (Console.ReadLine()?.ToUpper())
                {
                    case "Y":
                        isPlaying = true;
                        Console.Clear();                        
                        hangmanGame.GameSetup();
                        break;
                    case "N":
                        isPlaying = false;
                        break;
                    default:
                        Console.WriteLine("Please enter an acceptable answer.");
                        break;
                }
            
            } while (isPlaying);
        }
    }

    class HangmanGame
    {
        Random random = new Random();
        List<string> wordList = new List<string>()
        {
            "scythe", "apple", "osmosis", "laughing", "extend", "brown", "expert", "tired", "humidity", "backpack", "wares", "soup",
            "mount", "crust", "dent", "market", "knock", "smite", "windy", "coin", "throw", "silence", "bluff", "downfall", "mass",
            "game", "stocking", "folly", "action",
        };
        char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();

        string[] hangmanStages =
        {
    // final state: head, torso, both arms, and both legs
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |     / \
        -",
    // head, torso, both arms, and one leg
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |     / 
        -",
    // head, torso, and both arms
    @"
        --------
        |      |
        |      O
        |     \|/
        |      |
        |      
        -",
    // head, torso, and one arm
    @"
        --------
        |      |
        |      O
        |     \|
        |      |
        |     
        -",
    // head and torso
    @"
        --------
        |      |
        |      O
        |      |
        |      |
        |     
        -",
    // head
    @"
        --------
        |      |
        |      O
        |    
        |      
        |     
        -",
    // initial empty state
    @"
        --------
        |      |
        |      
        |    
        |      
        |     
        -",
        };

        private List<char> correctGuesses = new List<char>();
        private List<char> incorrectGuesses = new List<char>();
        private List<char> availableGuesses = new List<char>();
        private string? correctWord;
        private char[]? currentGuess;
        private string? input;
        private char inputChar;
        private const int MAXIMUM_LIVES = 6;
        private int numOfLives;

        public HangmanGame()
        {
            GameSetup();
        }

        public void GameSetup()
        {
            correctGuesses = new List<char>();
            incorrectGuesses = new List<char>();
            availableGuesses = alphabet.ToList();
            correctWord = wordList[random.Next(0, wordList.Count)].ToUpper();
            currentGuess = new char[correctWord.Length];
            wordList.Remove(correctWord);
            numOfLives = MAXIMUM_LIVES;

            if (wordList.Count == 0 )
            {
                Console.WriteLine("Thank you for playing, you have used all the words in the current word list.");
            }

            for (int i = 0; i < correctWord.Length; i++)
            {
                if (correctWord[i] == ' ')
                {
                    currentGuess[i] = ' ';
                }
            }
        }

        public string? GetInput ()
        {
            return Console.ReadLine()?.ToUpper();
        }

        public void Play()
        {
            DrawDisplay();

            // Checks to end game if the current guess is correct or if all lives are lost
            while (correctWord != (new string(currentGuess)).ToUpper() && numOfLives > 0)
            {
                Console.Write("Please enter your guess: ");
                input = GetInput();

                // Allows the player to type the entire word, and if correct ends the game
                if (String.Equals(input?.ToUpper(), correctWord))
                {
                    break;
                }

                // If the player types nothing or guess more than one letter, replaces the guess with an incorrect character
                
                if (input?.Length != 1)
                {
                    input = "$";
                }

                inputChar = Convert.ToChar(input);

                /*
                 * Checks to see if the input is in the array of available guesses
                 * Matches against correct word if it does
                 * If the guess is correct, replace letters in current guess with the input
                 * If the guess is incorrect, deduct lives
                 */
                if (availableGuesses.Contains(inputChar))
                {
                    if (correctWord.Contains(inputChar))
                    {
                        for (int i = 0; i < correctWord.Length; i++)
                        {
                            if (correctWord[i] == inputChar)
                            {
                                currentGuess[i] = inputChar;
                                availableGuesses.Remove(inputChar);
                                correctGuesses.Add(inputChar);
                                Console.WriteLine("Added Char: " + inputChar);
                            }
                            else if (correctWord[i] == ' ')
                            {
                                currentGuess[i] = ' ';
                            }
                            else if (!(correctGuesses.Contains(correctWord[i])))
                            {
                                currentGuess[i] = '\0';
                            }
                        }
                    }
                    else
                    {
                        availableGuesses.Remove(inputChar);
                        incorrectGuesses.Add(inputChar);
                        numOfLives--;
                    }

                    DrawDisplay(true);
                }
                else if (correctGuesses.Contains(inputChar) || incorrectGuesses.Contains(inputChar))
                {
                    Console.WriteLine("You already entered that!");
                }
                else
                {
                    Console.WriteLine("Please enter an acceptable input.");
                }
            }

            Console.Clear();
            if (numOfLives > 0)
                Console.WriteLine($"Congratulations! You won with {numOfLives} {(numOfLives == 1 ? "life" : "lives")} left! "
                    + "The correct word was "
                    + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(correctWord.ToLower()));
            else
                Console.WriteLine("Sorry! You lost! The correct word was " +
                    CultureInfo.CurrentCulture.TextInfo.ToTitleCase(correctWord.ToLower()));
            currentGuess = correctWord.ToCharArray();
            DrawDisplay();
        }

        private void DisplayCharArray(char[] chars)
        {

            for (int i = (int)(8 - Math.Round((decimal)(chars.Length / 2), MidpointRounding.AwayFromZero)); i >= 0; i--)
            {
                Console.Write(" ");
            }

            // Converts a char array into hangman form with underscores or the guessed letters
            foreach (char c in chars)
            {
                if (c == '\0')
                {
                    Console.Write("_ ");
                }
                else
                {
                    Console.Write($"{c} ");
                }
            }

            Console.WriteLine("\n");
        }

        private void DrawDisplay(bool isGuessing = false)
        {
            // Only writes when a guess has already occurred, so on the first guess it doesn't write it
            if (isGuessing)
            {
                Console.Clear();
                if (correctWord.Contains(inputChar))
                    Console.WriteLine("That guess was correct!");
                else
                    Console.WriteLine("That guess was incorrect!");

                Console.Write("Incorrect Guesses: ");
                for (int i = 0; i < incorrectGuesses.Count; i++)
                {
                    Console.Write(incorrectGuesses[i] + " ");
                }
                Console.WriteLine($"\nYou have {numOfLives} {(numOfLives == 1 ? "life" : "lives")} left!");
            }

            Console.WriteLine(hangmanStages[numOfLives]);
            DisplayCharArray(currentGuess);
        }
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.