using DigitalRuby.Tween;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
public class HangmanController : AbstractMinigameController
{
[Header("ConcreteVariables")]
///
/// 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 and to pass to the signPredictor
///
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
///
protected 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 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 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 image, so we can add fancy colors
///
public Image feedbackProgressImage;
///
/// 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
///
public const int CORRECT_LETTER_SCORE = 10;
///
/// Score obtained when guessing an incorrect letter
///
public const int INCORRECT_LETTER_SCORE = -5;
///
/// Score obtained when guessing the entire word
///
public const int WIN_SCORE = 25;
///
/// Set the AbstractMinigameController variable to inform it of the theme for the signPredictor
///
protected override Theme signPredictorTheme
{
get { return fingerSpelling; }
}
///
/// 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
SwitchMode(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
SwitchMode(1);
// Activate the right panel
gamePanel.SetActive(false);
//inputGamePanel.SetActive(true);
inputPanel.SetActive(true);
playerPanel.SetActive(false);
// Initialise the word to an empty String
currentWord = "";
PanelMultiplayerInput script = inputPanel.GetComponent();
script.inputTextField.text = "";
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()
{
PickRandomWord();
StartGame();
}
///
/// Randomly select a word from a randomly selected theme, use this word for the hangman game for singleplayer.
///
public 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;
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))
{
BackSpacePressed();
}
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;
}
});
}
}
///
/// Functionality to be called when the backspace-key is pressed during input-mode
///
public void BackSpacePressed()
{
// Remove the last letter from the currentword
if (0 < currentWord.Length)
{
currentWord = currentWord[0..^1];
inputTextField.text = currentWord;
}
Input.ResetInputAxes();
}
///
/// 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()}' ?";
SwitchMode(4);
}
}
break;
case 2: // Sign your letter
if (!guesses.Contains(currentSign))
{
SwitchMode(3);
ConfirmAccept();
}
break;
case 3: // Confirm signed letter
break;
}
}
///
/// Takes the currentSign and tries to enter it into the word if playing
/// When in input-mode it will just add the letter to the currentWord
///
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
SwitchMode(2);
if (corrects == currentWord.Length)
{
// Victory, deactivate the model and show the scoreboard
ActivateEnd(true);
}
else if (NUMBER_OF_FAILS_BEFORE_GAMEOVER < wrongs)
{
// You lost, deactivate the model and show the scoreboard
ActivateEnd(false);
}
}
else if (mode == 4)
{
currentWord += letter;
inputTextField.text = currentWord.ToUpper();
SwitchMode(1);
}
}
///
/// The letter got rejected, start the letter-fetching process again
///
public void ConfirmDeny()
{
confirmPanel.SetActive(false);
// The current sign was rejected, return to the game-mode
if (mode == 3)
SwitchMode(2);
else if (mode == 4)
SwitchMode(1);
}
///
/// Outside function to switch the modes this allows the gameIsactive-logic to be properly attached to the modes
///
///
public void SwitchMode(int mode)
{
this.mode = mode;
// In mode 1 and 2, the signPredictor needs to run, otherwise it does not
if (mode == 1 || mode == 2)
{
gameIsActive = true;
}
else
{
gameIsActive = false;
}
}
///
/// 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 = $"{INCORRECT_LETTER_SCORE}";
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 * CORRECT_LETTER_SCORE}";
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
public override int CalculateScore()
{
int won = corrects == currentWord.Length ? 1 : 0;
return corrects * CORRECT_LETTER_SCORE + wrongs * INCORRECT_LETTER_SCORE + WIN_SCORE * won;
}
///
/// 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('_');
}
}
///
/// The logic to process the signs sent by the signPredictor
///
/// The accuracy of the passed sign
/// The name of the passed sign
public override void ProcessMostProbableSign(float distance, string predictedSign)
{
// Grab the threshold for the most probable letter
Learnable letter = fingerSpelling.learnables.Find((l) => l.name == predictedSign);
float threshold = letter.thresholdDistance;
// If there is a feedback-object, we wil change its appearance
if (feedbackText != null && feedbackProgressImage != null)
{
float oldValue = feedbackProgress.value;
// use an exponential scale
float newValue = 1 - Mathf.Clamp(distance - threshold, 0.0f, 3.0f) / 3;
feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) =>
{
if (feedbackProgress != null)
{
feedbackProgress.value = t.CurrentValue;
}
});
if (distance < 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 (distance < threshold * 1.5)
{
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;
}
}
// The logic for the internal workings of the game
if (distance < threshold)
{
// A different sign was predicted compared to the last call of this function
if (previousSign != predictedSign)
{
// Reset the timer
previousSign = predictedSign;
currentTime = 0;
// If you are entering a word the timer needs to work
// If you are playing the game and haven't guessed the letter yet, then the timer needs to work
if ((mode == 1) ||
(mode == 2 && !guesses.Contains(previousSign.ToUpper())))
{
runTime = true;
}
timerCircle.fillAmount = currentTime;
}
// The same sign was predicted as last time and said sign has been held for a sufficiently long time
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;
}
}
///
/// The logic to set the scoreboard of hangman
///
/// SHows whether or not the player won
protected override void SetScoreBoard(bool victory)
{
string resultTxt;
if (victory)
{
resultTxt = "GEWONNEN";
}
else
{
resultTxt = "VERLOREN";
}
gameEndedPanel.GetComponent().GenerateContent(
guessWord: currentWord.ToLower(),
correctLetters: corrects,
incorrectLetters: wrongs,
sprite: hangmanImage.sprite,
result: resultTxt,
score: CalculateScore()
);
}
///
/// The hangman-specific logic that needs to be called at the start of the game
///
protected override void StartGameLogic()
{
// Make sure the mode starts at zero
SwitchMode(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);
}
///
/// The Hangman-specific logic that needs to be called at the end of a game
///
///
protected override void EndGameLogic(bool victory)
{
// Deactivate the model
SwitchMode(0);
DeleteWord();
}
// The following functions are only used for testing
public string getCurrentWord()
{
return currentWord;
}
public int getCurrentMode()
{
return mode;
}
public int getCorrects()
{
return corrects;
}
public int getWrongs()
{
return wrongs;
}
public string getUsedLetters()
{
return usedLettersText.text;
}
public float getCurrentTime()
{
return currentTime;
}
}