using System.Collections; using System.Collections.Generic; using System; using System.Linq; using UnityEngine; using UnityEngine.UI; using TMPro; public class HangmanGameController : MonoBehaviour { /// /// The scriptable with all the themes, will be used to select a random word for hangman. /// The spellingthemeList will be used for the words. /// public ThemeList themelist; /// /// The word that is currently being spelled /// private string currentWord; /// /// All of the words that can be used in this session /// private string[] words; /// /// This integer holds the total amount of wrong guesses the player has made /// private int wrongs; /// /// This integer holds the total amount of times a user can be wrong to lose. /// private int maxWrong = 9; /// /// This integer holds the amount of correct letters of the word that the user has guessed /// private int corrects; /// /// This integer holds the length of the word that needs to be guessed, game ends in victory if corrects == word_length /// private int wordLength; /// /// Indicates if the game is still going /// private bool gameEnded; /// /// Letter prefab /// public GameObject letterPrefab; /// /// Reference to letter prefab /// public Transform letterContainer; /// /// The Image component for displaying the appropriate sprite /// public Image hangmanImage; /// /// The GameObjects representing the letters /// private List letters = new List(); /// /// This scriptable holds all the images for the different stages of hangman /// public HangmanImages images; /// /// This initially empty list holds all the previous guesses that the user has made, as to not allow repeated msitakes /// private List guesses; /// /// This lists holds all valid guesses that the user can make /// private List guessables = new List { 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'w', 'x', 'c', 'v', 'b', 'n' }; // The following attributes are necessary for multiplayer interactions /// /// The panel holding the actual game /// public GameObject gamePanel; /// /// The panel holding the player-selection screen /// public GameObject playerPanel; /// /// The panel holding the screen where player 1 can input their word /// public GameObject inputPanel; /// /// This textfield holds the word that player 1 is typing /// public TMP_Text inputTextField; /// /// This int shows what mode we are in, used in update: 0 is playerselect, 1 is wordInput, 2 is the actual game /// private int mode; // The following attributes are necessary to finish and gameover /// /// "Game over" or "You win!" /// public TMP_Text endText; /// /// The game over panel /// public GameObject gameEndedPanel; /// /// Button for restarting the game /// public Button replayButton; /// /// Letters /// public TMP_Text lettersText; /// /// Letters ( right | wrong ) /// public TMP_Text lettersRightText; public TMP_Text lettersWrongText; /// /// Accuracy /// public TMP_Text accuracyText; /// /// Words /// public TMP_Text wordsText; /// /// Score /// public TMP_Text scoreText; // The following attributes are necessary for user interaction /// /// Reference to the minigame ScriptableObject /// public Minigame minigame; /// /// Reference to the user list to access the current user /// public UserList userList; /// /// Reference to the current user /// private User user; /// /// Reference to the minigame progress of the current user /// private Progress progress = null; /// /// Reference to the scoreboard /// public Transform Scoreboard; /// /// Reference to the entries grid /// public Transform EntriesGrid; /// /// The GameObjects representing the letters /// private List entries = new List(); /// /// Reference to the ScoreboardEntry prefab /// public GameObject scoreboardEntry; /// /// Accuracy feeback object /// public Feedback feedback; /// /// The button to go into the game /// public GameObject gottogamebutton; /// /// Current sign out of the predictor /// private String currentsign = ""; // Start is called before the first frame update void Start() { // Make sure the mode starts at zero mode = 0; // Make sure that only the player-selection panel is the one shown gamePanel.SetActive(false); inputPanel.SetActive(false); playerPanel.SetActive(true); // Create entry in current user for keeping track of progress user = userList.GetCurrentUser(); Progress progress = user.GetMinigameProgress(minigame.index); if (progress == null) { progress = new Progress(); progress.AddOrUpdate("minigameIndex", minigame.index); progress.AddOrUpdate>("highestScores", new List()); progress.AddOrUpdate>("latestScores", new List()); user.minigames.Add(progress); } userList.Save(); // Set calllbacks feedback.getSignCallback = () => { return "A"; }; feedback.predictSignCallback = (sign) => { currentsign = sign; }; } /// /// Hangman starts by asking the amount of players, so this function holds all the info needed to start the actual game /// public void StartGame() { // Change the mode mode = 2; // Activate the right panel gamePanel.SetActive(true); inputPanel.SetActive(false); playerPanel.SetActive(false); // Reset values of parameters, so that the replaybutton works properly corrects = 0; wrongs = 0; guesses = new List(); // Display a test-word to test the functionality wordLength = currentWord.Length; gameEnded = false; gameEndedPanel.SetActive(false); // Delete first, to make sure that the letters are empty DeleteWord(); DisplayWord(currentWord); replayButton.onClick.AddListener(Start); // Call to display the first image, corresponding to a clean image. ChangeSprite(); } /// /// This function is called when the "two player"-button is clicked, it goed to the input-screen /// public void GoToInput() { // Change the mode mode = 1; // Initialise the word to an empty String currentWord = ""; inputTextField.text = currentWord.ToString(); // Activate the right panel gamePanel.SetActive(false); inputPanel.SetActive(true); playerPanel.SetActive(false); } /// /// This function is called if singleplayer is selected, we generate a random word for the player and start the game. /// public void SinglePlayer() { // This word is used for testing before dynamic word-fetching is added PickRandomWord(); StartGame(); } /// /// Randomly select a word from a randomly selected theme, use this word for the hangman game for singleplayer. /// private void PickRandomWord() { // Get a random index for the themes // Then get a random index for a word to pull // First get random index for the themes int amountThemes = themelist.themes.Count; System.Random r = new System.Random(); int themeIndex = r.Next(amountThemes); // Check how many words are in this theme int amountWords = themelist.themes[themeIndex].learnables.Count; int wordIndex = r.Next(amountWords); // Take the word, but lowercase it. currentWord = themelist.themes[themeIndex].learnables[wordIndex].name.ToLower(); } /// /// This function starts the game after player 1 has entered their word, but only if its length >= 2. /// public void TwoPlayer() { if (currentWord.Length >= 2) { StartGame(); } } /// /// Update is called once per frame /// public void Update() { if(mode == 1) { if (currentsign != "") { char letter = currentsign.ToLower()[0]; currentsign = ""; currentWord = currentWord + letter.ToString(); inputTextField.text = currentWord.ToString(); } // We are in the word-input mode // We want to show the user what they are typing // Check to make sure the inputfield is not empty if (Input.inputString != "") { char firstLetter = Input.inputString.ToLower()[0]; if (Input.GetKey(KeyCode.Backspace)) { // Remove the last letter from the currentword if(currentWord.Length > 0) { currentWord = currentWord.Substring(0, currentWord.Length - 1); inputTextField.text = currentWord.ToString(); } } else if (guessables.Contains(firstLetter)) { // Append the letter to the currentWord and display it to the user currentWord = currentWord + firstLetter.ToString(); inputTextField.text = currentWord.ToString(); } } gottogamebutton.SetActive(currentWord.Length >2); } if (mode == 2) { // We are in the actual game if (!gameEnded) { // Get keyboard input // For the first input char given by the user, check if the letter is in the word that needs to be spelled. // Check to make sure the inputfield is not empty if (currentsign != "") { char firstLetter = currentsign.ToLower()[0]; currentsign = ""; // Check if the letter is a valid guess and that it has not been guessed before if (!guesses.Contains(firstLetter) && guessables.Contains(firstLetter)) { if (currentWord.Contains(firstLetter)) { // The guess was correct, we can display all the letters that correspond to the guess UpdateWord(firstLetter); } else { // The guess was wrong, the wrongs integer needs to be incremented wrongs++; // For now, we will loop back to stage zero after we reach the losing stage 10 wrongs = wrongs % 11; // Afterwards, the next stage needs to be displayed ChangeSprite(); } guesses.Add(firstLetter); } } if (corrects == wordLength) { // Victory ActivateWin(); } else if (wrongs == maxWrong) { // You lost ActivateGameOver(); } } } } /// /// Change the image that is being displayed /// private void ChangeSprite() { // Load the new sprite from the HangmanImages scriptable Sprite sprite = images.hangmanStages[wrongs]; // Set the new sprite as the Image component's source image hangmanImage.sprite = sprite; } /// /// In this function, the letters of the word selected in DisplayWord are updated after a correct guess. /// /// The letter that needs to be updated private void UpdateWord(char c) { for (int i = 0; i < currentWord.Length; i++) { if (currentWord[i] == c) { // Display the letter and change its background to green Image background = letters[i].GetComponent(); background.color = Color.green; TMP_Text txt = letters[i].GetComponentInChildren(); txt.text = Char.ToString(c); // You correctly guessed a letter corrects++; } } } /// /// This function returns the score that the user currently has /// /// The current score of the user private int getScore() { // Scoring works as follows: // You get 3 points for each letter in the word that is correctly guessed (corrects * 3) // You get 9 points for each part of the hangman figure which is NOT displayed ((10 - wrongs) * 9) return 3 * corrects + (10 - wrongs) * 9; } /// /// Set score metrics /// private void SetScoreMetrics() { // Letters ( right | wrong ) total lettersRightText.text = corrects.ToString(); lettersWrongText.text = wrongs.ToString(); lettersText.text = (corrects + wrongs).ToString(); // Accuracy if (corrects + wrongs > 0) { accuracyText.text = ((corrects) * 100f / (corrects + wrongs)).ToString("#.##") + "%"; } else { accuracyText.text = "-"; } // The word that needed to be guessed wordsText.text = currentWord.ToString(); // Score scoreText.text = "Score: " + getScore().ToString(); } // The following functions originate from Spellingbee /// /// Delete all letter objects /// private void DeleteWord() { for (int i = 0; i < letters.Count; i++) { Destroy(letters[i]); } letters.Clear(); } /// /// Displays the word that needs to be spelled /// /// The word to display private void DisplayWord(string word) { for (int i = 0; i < word.Length; i++) { // Create instance of prefab GameObject instance = GameObject.Instantiate(letterPrefab, letterContainer); letters.Add(instance); // Dynamically load appearance Image background = instance.GetComponent(); background.color = Color.red; TMP_Text txt = instance.GetComponentInChildren(); txt.text = Char.ToString(' '); } } /// /// Randomly shuffle the list of words /// private void ShuffleWords() { for (int i = words.Length - 1; i > 0; i--) { // Generate a random index between 0 and i (inclusive) int j = UnityEngine.Random.Range(0, i + 1); // Swap the values at indices i and j (words[j], words[i]) = (words[i], words[j]); } } /// /// Update and save the scores /// private void SaveScores() { // Calculate new score int newScore = getScore(); // Save the score as a tuple: < int score, string time ago> Score score = new Score(); score.scoreValue = newScore; score.time = DateTime.Now.ToString(); // Save the new score user = userList.GetCurrentUser(); Progress progress = user.GetMinigameProgress(minigame.index); // Get the current list of scores List latestScores = progress.Get>("latestScores"); List highestScores = progress.Get>("highestScores"); // Add the new score latestScores.Add(score); highestScores.Add(score); // Sort the scores highestScores.Sort((a, b) => b.scoreValue.CompareTo(a.scoreValue)); // Only save the top 10 scores, so this list doesn't keep growing endlessly progress.AddOrUpdate>("latestScores", latestScores.Take(10).ToList()); progress.AddOrUpdate>("highestScores", highestScores.Take(10).ToList()); userList.Save(); } /// /// Display win screen /// private void ActivateWin() { // @lukas stuff DeleteWord(); endText.text = "YOU WIN!"; SetScoreMetrics(); gameEndedPanel.SetActive(true); gameEndedPanel.transform.SetAsLastSibling(); gameEnded = true; //Save the scores and show the scoreboard SaveScores(); SetScoreBoard(); } /// /// Displays the game over panel and score values /// private void ActivateGameOver() { DeleteWord(); endText.text = "GAME OVER"; SetScoreMetrics(); gameEndedPanel.SetActive(true); gameEndedPanel.transform.SetAsLastSibling(); gameEnded = true; // Save the scores and show the scoreboard SaveScores(); SetScoreBoard(); } /// /// Sets the scoreboard /// private void SetScoreBoard() { // Clean the previous scoreboard entries for (int i = 0; i < entries.Count; i++) { Destroy(entries[i]); } entries.Clear(); // Instantiate new entries // Get all scores from all users List> allScores = new List>(); foreach (User user in userList.GetUsers()) { // Get user's progress for this minigame progress = user.minigames.Find((p) => p != null && p.Get("minigameIndex") == minigame.index); if (progress != null) { // Add scores to dictionary List scores = progress.Get>("highestScores"); foreach (Score score in scores) { allScores.Add(new Tuple(user.username, score)); } } } // Sort allScores based on Score.scoreValue allScores.Sort((a, b) => b.Item2.scoreValue.CompareTo(a.Item2.scoreValue)); // Instantiate scoreboard entries int rank = 1; foreach (Tuple tup in allScores.Take(10)) { string username = tup.Item1; Score score = tup.Item2; GameObject entry = Instantiate(scoreboardEntry, EntriesGrid); entries.Add(entry); // Set the player icon entry.transform.Find("Image").GetComponent().sprite = userList.GetUserByUsername(username).avatar; // Set the player name entry.transform.Find("PlayerName").GetComponent().text = username; // Set the score entry.transform.Find("Score").GetComponent().text = score.scoreValue.ToString(); // Set the rank entry.transform.Find("Rank").GetComponent().text = rank.ToString(); // Set the ago // Convert the score.time to Datetime DateTime time = DateTime.Parse(score.time); DateTime currentTime = DateTime.Now; TimeSpan diff = currentTime.Subtract(time); string formatted; if (diff.Days > 0) { formatted = $"{diff.Days}d "; } else if (diff.Hours > 0) { formatted = $"{diff.Hours}h "; } else if (diff.Minutes > 0) { formatted = $"{diff.Minutes}m "; } else { formatted = "now"; } entry.transform.Find("Ago").GetComponent().text = formatted; // Alternating colors looks nice if (rank % 2 == 0) { Image image = entry.transform.GetComponent(); image.color = new Color(image.color.r, image.color.g, image.color.b, 0f); } // Make new score stand out if (diff.TotalSeconds < 1) { Image image = entry.transform.GetComponent(); image.color = new Color(0, 229, 255, 233); } rank++; } } }