using DigitalRuby.Tween; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; 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; /// /// reference to the fingerspelling-theme to reach the letter-thresholds /// public Theme fingerSpelling; /// /// 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 amount of correct letters of the word that the user has guessed /// private int corrects; /// /// 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 List images = new List(); /// /// This initially empty list holds all the previous guesses that the user has made, as to not allow repeated msitakes /// private List guesses = new List(); /// /// Holds a string of all the used letters, used to display them to the player /// public TMP_Text usedLettersText; /// /// 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; /// /// Reference to display the score /// public TMP_Text scoreDisplay; /// /// Reference to display the points lost/won /// public TMP_Text scoreBonus; ///// ///// This panel holds the panels for input and playing the game, sharing webcam and feedback ///// //public GameObject inputGamePanel; /// /// This int shows what mode we are in, used in update:

/// 0 : single or multiplayer?

/// 1 : multiplayer word input

/// 2 : signing letter

/// 3 : confirming choice gameplay /// 4 : confirming choice multiplayer input ///
private int mode; /// /// The game over panel /// public GameObject gameEndedPanel; /// /// 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; /// /// The button to go into the game /// public GameObject gotoGameButton; /// /// This textfield holds the word that player 1 is typing /// public TMP_Text inputTextField; /// /// 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; /// /// reference to the webcam background /// public RawImage webcamScreen; /// /// Previous incorrect sign, so we can keep track whether the user is wrong or the user is still changing signs /// protected string previousSign = null; /// /// variable to remember whether or not the timer needs to be updates. /// Only update the timer if the model is confident for a certain letter /// private bool runTime = false; /// /// Holds the time which the user needs to hold the sign for, for it to be accepted. /// private float maxTime = 0.3f; /// /// Holds the current amount of time the user has held their sign for /// private float currentTime = 0f; /// /// Holds a reference to the TimerCircle to update its fill /// public Image timerCircle; /// /// Hold a reference to the confirmPanel to toggle its activity /// public GameObject confirmPanel; /// /// Hold a reference to the confirmPanel to toggle its activity /// public TMP_Text confirmText; ///// ///// Temporary reference to timer to turn it off ///// //public GameObject timer; /// /// Maximum length of the words /// public const int MIN_INC_WORD_LENGHT = 3; /// /// Maximum length of the words /// public const int MAX_EXC_WORD_LENGHT = 17; /// /// Number of fails before the game ends /// public const int NUMBER_OF_FAILS_BEFORE_GAMEOVER = 7; /// /// Score obtained when guessing a correct letter /// private int correctLetterScore = 10; /// /// Score obtained when guessing an incorrect letter /// private int incorrectLetterScore = -5; /// /// Score obtained when guessing the entire word /// private int winScore = 25; /// /// Start is called before the first frame update /// void Start() { signPredictor.SwapScreen(webcamScreen); signPredictor.SetModel(ModelIndex.FINGERSPELLING); AddSelfAsListener(); StartController(); } /// /// 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); // Make sure that unneeded panels are inactive gameEndedPanel.SetActive(false); confirmPanel.SetActive(false); // Create entry in current user for keeping track of progress user = UserList.GetCurrentUser(); var progress = user.GetMinigameProgress(minigame.index); if (progress == null) { progress = new PersistentDataController.SavedMinigameProgress(); progress.minigameIndex = minigame.index; user.AddMinigameProgress(progress); } UserList.Save(); // Guesses needs to be created instantly because it is used in the FeedbackLoop //guesses = new List(); } /// /// 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); //inputGamePanel.SetActive(true); inputPanel.SetActive(false); playerPanel.SetActive(false); PanelHangmanGame script = gamePanel.GetComponent(); inputTextField = script.guessesTextField; feedbackText = script.feedbackText; feedbackProgress = script.feedbackProgressBar; feedbackProgressImage = script.feedbackProgressImage; webcamScreen = script.webcamScreen; timerCircle = script.timerCircle; confirmPanel = script.confirmPanel; confirmText = script.confirmText; confirmPanel.SetActive(false); signPredictor.SwapScreen(webcamScreen); // Reset values of parameters, so that the replaybutton works properly corrects = 0; wrongs = 0; guesses.Clear(); usedLettersText.text = ""; // Delete first, to make sure that the letters are empty DeleteWord(); DisplayWord(currentWord); ChangeSprite(); scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = ""; // Temporary //timer.SetActive(true); } /// /// 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.ToUpper(); // Activate the right panel gamePanel.SetActive(false); //inputGamePanel.SetActive(true); inputPanel.SetActive(true); playerPanel.SetActive(false); PanelMultiplayerInput script = inputPanel.GetComponent(); gotoGameButton = script.gotoGameButton; inputTextField = script.inputTextField; feedbackText = script.feedbackText; feedbackProgress = script.feedbackProgressBar; feedbackProgressImage = script.feedbackProgressImage; webcamScreen = script.webcamScreen; timerCircle = script.timerCircle; confirmPanel = script.confirmPanel; confirmText = script.confirmText; confirmPanel.SetActive(false); signPredictor.SwapScreen(script.webcamScreen); //temporarily turn off timer in input-mode //timer.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() { // vvv DEMO DAY STUFF vvv currentWord = "DEMO DAY"; return; // ^^^ DEMO DAY STUFF ^^^ // 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; int themeIndex = Random.Range(0, amountThemes); // Check how many words are in this theme int amountWords = themeList.themes[themeIndex].learnables.Count; int wordIndex = Random.Range(0, amountWords); // Take the word, but lowercase it. currentWord = themeList.themes[themeIndex].learnables[wordIndex].name.ToUpper(); } /// /// This function starts the game after player 1 has entered their word, but only if its length >= 2. /// public void TwoPlayer() { if (MIN_INC_WORD_LENGHT <= currentWord.Length) { // Reset the model-parameters previousSign = null; currentTime = 0; // Start the game StartGame(); } } /// /// Update is called once per frame /// public void Update() { if (mode == 1) { if (Input.GetKey(KeyCode.Backspace)) { // Remove the last letter from the currentword if (0 < currentWord.Length) { currentWord = currentWord[0..^1]; inputTextField.text = currentWord; } Input.ResetInputAxes(); } gotoGameButton.SetActive(MIN_INC_WORD_LENGHT <= currentWord.Length); } // The following logic is used to fill the timer if ((mode == 1 || mode == 2) && runTime) { currentTime += Time.deltaTime; // subtract the time since last frame if (currentTime > maxTime) { currentTime = maxTime; } float oldValue = timerCircle.fillAmount; float newValue = currentTime / maxTime; timerCircle.gameObject.Tween("TimerUpdate", oldValue, newValue, 1f / 60f, TweenScaleFunctions.CubicEaseInOut, (t) => { if (timerCircle != null) { timerCircle.fillAmount = t.CurrentValue; } }); } } /// /// Handles sign logic, so that it does not have to run every frame /// This function is called when the UpdateFeedback has accepted a letter /// public void UpdateSign() { switch (mode) { case 0: // Singleplayer or multiplayer? break; case 1: // Multiplayer word { if (currentSign != null && currentSign != "" && currentWord.Length < MAX_EXC_WORD_LENGHT) { confirmPanel.SetActive(true); confirmText.text = $"Letter '{currentSign.ToUpper()}' ?"; mode = 4; } } break; case 2: // Sign your letter if (!guesses.Contains(currentSign)) { mode = 3; ConfirmAccept(); } break; case 3: // Confirm signed letter break; } } public void ConfirmAccept() { string letter = currentSign; currentSign = ""; confirmPanel.SetActive(false); if (mode == 3) { if (currentWord.Contains(letter)) { // The guess was correct, we can display all the letters that correspond to the guess UpdateWord(letter); } else { // The guess was wrong, the wrongs integer needs to be incremented wrongs++; // Afterwards, the next stage needs to be displayed ChangeSprite(); } guesses.Add(letter); if (usedLettersText.text != "") usedLettersText.text += ", "; usedLettersText.text += letter.ToString().ToUpper(); // The current sign was accepted, return to the game mode = 2; if (corrects == currentWord.Replace(" ", "").Length) { // Victory, deactivate the model and show the scoreboard ActivateWin(); } else if (NUMBER_OF_FAILS_BEFORE_GAMEOVER < wrongs) { // You lost, deactivate the model and show the scoreboard ActivateGameOver(); } } else if (mode == 4) { currentWord += letter; inputTextField.text = currentWord.ToUpper(); mode = 1; } } public void ConfirmDeny() { confirmPanel.SetActive(false); // The current sign was rejected, return to the game-mode if (mode == 3) mode = 2; else if (mode == 4) mode = 1; } /// /// Change the image that is being displayed /// private void ChangeSprite() { // Load the new sprite from the HangmanImages scriptable Sprite sprite = images[wrongs]; // Set the new sprite as the Image component's source image hangmanImage.sprite = sprite; scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = $"{incorrectLetterScore}"; scoreBonus.color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f); } /// /// 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(string c) { int hits = 0; for (int i = 0; i < currentWord.Length; i++) { if (currentWord[i] == c[0]) { // Display the letter and change its background to green Image background = letters[i].GetComponent(); background.color = new Color(139f / 255f, 212f / 255f, 94f / 255f); TMP_Text txt = letters[i].GetComponentInChildren(); txt.text = c; // You correctly guessed a letter corrects++; hits++; } } scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = $"+{hits * correctLetterScore}"; scoreBonus.color = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } /// /// This function returns the score that the user currently has /// /// The current score of the user private int CalculateScore() { int won = corrects == currentWord.Length ? 1 : 0; return corrects * correctLetterScore + wrongs * incorrectLetterScore + winScore * won; } // 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) { foreach (Char c in word) { // Create instance of prefab GameObject instance = GameObject.Instantiate(letterPrefab, letterContainer); letters.Add(instance); // Dynamically load appearance Image background = instance.GetComponent(); background.color = Color.clear; TMP_Text txt = instance.GetComponentInChildren(); txt.text = c == ' ' ? "" : Char.ToString('_'); } } /// /// Update and save the scores /// private void SaveScores() { // Calculate new score int newScore = CalculateScore(); // 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 var 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() { // Deactivate the model mode = 0; // Save the scores and show the scoreboard SaveScores(); gameEndedPanel.GetComponent().GenerateContent( guessWord: currentWord.ToLower(), correctLetters: corrects, incorrectLetters: wrongs, sprite: hangmanImage.sprite, result: "GEWONNEN", score: CalculateScore() ); gameEndedPanel.SetActive(true); // @lukas stuff DeleteWord(); } /// /// Displays the game over panel and score values /// private void ActivateGameOver() { // Deactivate the model mode = 0; // Save the scores and show the scoreboard SaveScores(); gameEndedPanel.GetComponent().GenerateContent( guessWord: currentWord.ToLower(), correctLetters: corrects, incorrectLetters: wrongs, sprite: hangmanImage.sprite, result: "VERLOREN", score: CalculateScore() ); gameEndedPanel.SetActive(true); DeleteWord(); } /// /// The updateFunction that is called when new probabilities become available /// /// protected override IEnumerator UpdateFeedback() { // Get the sign with the highest prediction if ((mode == 1 || mode == 2) && signPredictor != null && signPredictor.learnableProbabilities != null) { KeyValuePair highestPrediction = signPredictor.learnableProbabilities.Aggregate((x, y) => x.Value > y.Value ? x : y); float accuracy = highestPrediction.Value; string predictedSign = highestPrediction.Key; // vvv TEMPORARY STUFF vvv if (predictedSign == "J" && accuracy <= 0.965f) { highestPrediction = signPredictor.learnableProbabilities.Aggregate((x, y) => x.Value > y.Value && x.Key != "J" ? x : y); } accuracy = highestPrediction.Value; predictedSign = highestPrediction.Key; // ^^^ TEMPORARY STUFF ^^^ // Grab the threshold for the most probable letter Learnable letter = fingerSpelling.learnables.Find((l) => l.name == predictedSign); float threshold = letter.thresholdPercentage; float oldValue = feedbackProgress.value; // use an exponential scale float newValue = Mathf.Exp(4 * (Mathf.Clamp(accuracy / threshold, 0.0f, 1.0f) - 1.0f)); feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) => { if (feedbackProgress != null) { feedbackProgress.value = t.CurrentValue; } }); if (accuracy > threshold) { feedbackText.text = $"Herkent '{predictedSign}'"; Color green = new Color(139.0f / 255.0f, 212.0f / 255.0f, 94.0f / 255.0f); feedbackText.color = green; feedbackProgressImage.color = green; } else if (accuracy > threshold * 0.9) { feedbackText.text = $"Lijkt op '{predictedSign}'"; Color orange = new Color(242.0f / 255.0f, 127.0f / 255.0f, 12.0f / 255.0f); feedbackText.color = orange; feedbackProgressImage.color = orange; } else { feedbackText.text = "Detecteren..."; Color red = new Color(245.0f / 255.0f, 73.0f / 255.0f, 61.0f / 255.0f); feedbackText.color = red; feedbackProgressImage.color = red; } if (accuracy > threshold) { if (previousSign != predictedSign) { // Reset the timer previousSign = predictedSign; currentTime = 0; if ((mode == 1) || (mode == 2 && !guesses.Contains(previousSign.ToUpper()))) { runTime = true; } timerCircle.fillAmount = currentTime; } else if (currentTime == maxTime) { // Set the predictedSign as your guess and update the Hangman currentSign = predictedSign; UpdateSign(); // reset the timer and look for a new prediction previousSign = null; currentTime = 0; runTime = false; timerCircle.fillAmount = currentTime; } } else { // The sign was dropped, reset the timer previousSign = null; currentTime = 0; runTime = false; timerCircle.fillAmount = currentTime; } } else if (feedbackProgress != null) { feedbackProgress.value = 0.0f; } yield return null; } }