using DigitalRuby.Tween; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; public class HangmanController : AbstractFeedback { /// /// 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; /// /// 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; /// /// We keep the minigamelist as well so that the minigame-index doesn't get reset /// DO NOT REMOVE /// public MinigameList minigamelist; /// /// Reference to the current user /// private User user; /// /// Reference to the minigame progress of the current user /// private PersistentDataController.SavedMinigameProgress 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; /// /// The button to go into the game /// public GameObject gottogamebutton; /// /// Current sign out of the predictor /// private String currentsign = ""; /// /// Reference to the feedback field /// public TMP_Text feedbackText; /// /// Reference to the progress bar /// public Slider feedbackProgress; /// /// Reference to the progress bar image, so we can add fancy colors /// public Image feedbackProgressImage; /// /// Timer to keep track of how long a incorrect sign is performed /// protected DateTime timer; /// /// Current predicted sign /// protected string predictedSign = null; /// /// Previous incorrect sign, so we can keep track whether the user is wrong or the user is still changing signs /// protected string previousIncorrectSign = null; /// /// Start is called before the first frame update /// void Start() { StartController(); signPredictor.ChangeModel(ModelIndex.FINGERSPELLING); AddSelfAsListener(); } /// /// Called at the start of the scene AND when the scene is replayed /// public void StartController() { // 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 = user.GetMinigameProgress(minigame.index); if (progress == null) { progress = new PersistentDataController.SavedMinigameProgress(); progress.minigameIndex = minigame.index; user.AddMinigameProgress(progress); } UserList.Save(); } /// /// 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(StartController); // 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 != null && 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 != null && 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(' '); } } /// /// 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 progress = user.GetMinigameProgress(minigame.index); // Get the current list of scores List latestScores = progress.latestScores; List highestScores = progress.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.latestScores = latestScores.Take(10).ToList(); progress.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.GetMinigameProgress(minigame.index); if (progress != null) { // Add scores to dictionary List scores = progress.highestScores; foreach (Score score in scores) { allScores.Add(new Tuple(user.GetUsername(), 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).GetAvatar(); // 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++; } } /// /// The updateFunction that is called when new probabilities become available /// /// protected override IEnumerator UpdateFeedback() { // Get current sign string currentSign = "A"; // Get the predicted sign if (signPredictor != null && signPredictor.learnableProbabilities != null && currentSign != null && signPredictor.learnableProbabilities.ContainsKey(currentSign)) { float accuracy = signPredictor.learnableProbabilities[currentSign]; if (feedbackText != null && feedbackProgressImage != null) { if (accuracy > 0.90) { feedbackText.text = "Goed"; feedbackText.color = Color.green; feedbackProgressImage.color = Color.green; } else if (accuracy > 0.80) { feedbackText.text = "Bijna..."; Color col = new Color(0xff / 255.0f, 0x66 / 255.0f, 0x00 / 255.0f); feedbackText.color = col; feedbackProgressImage.color = col; } else { feedbackText.text = "Detecteren..."; feedbackText.color = Color.red; feedbackProgressImage.color = Color.red; } float oldValue = feedbackProgress.value; // use an exponential scale float newValue = Mathf.Exp(4 * (accuracy - 1.0f)); feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) => { if (feedbackProgress != null) { feedbackProgress.value = t.CurrentValue; } }); } // Check whether (in)correct sign has high accuracy foreach (var kv in signPredictor.learnableProbabilities) { if (kv.Value > 0.90) { predictedSign = kv.Key; // Correct sign if (predictedSign == currentSign) { yield return new WaitForSeconds(1.0f); CheckEquality(predictedSign); timer = DateTime.Now; predictedSign = null; previousIncorrectSign = null; } // Incorrect sign else { if (previousIncorrectSign != predictedSign) { timer = DateTime.Now; previousIncorrectSign = predictedSign; } else if (DateTime.Now - timer > TimeSpan.FromSeconds(2.0f)) { CheckEquality(predictedSign); timer = DateTime.Now; predictedSign = null; previousIncorrectSign = null; } } break; } } } else if (feedbackProgress != null) { feedbackProgress.value = 0.0f; } } private void CheckEquality(string sign) { currentsign = sign; } }