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;
}
}