732 lines
22 KiB
C#
732 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
public class HangmanController : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// The scriptable with all the themes, will be used to select a random word for hangman.
|
|
/// The spellingthemeList will be used for the words.
|
|
/// </summary>
|
|
public ThemeList themelist;
|
|
|
|
/// <summary>
|
|
/// The word that is currently being spelled
|
|
/// </summary>
|
|
private string currentWord;
|
|
|
|
/// <summary>
|
|
/// All of the words that can be used in this session
|
|
/// </summary>
|
|
private string[] words;
|
|
|
|
/// <summary>
|
|
/// This integer holds the total amount of wrong guesses the player has made
|
|
/// </summary>
|
|
private int wrongs;
|
|
|
|
/// <summary>
|
|
/// This integer holds the total amount of times a user can be wrong to lose.
|
|
/// </summary>
|
|
private int maxWrong = 9;
|
|
|
|
/// <summary>
|
|
/// This integer holds the amount of correct letters of the word that the user has guessed
|
|
/// </summary>
|
|
private int corrects;
|
|
|
|
/// <summary>
|
|
/// This integer holds the length of the word that needs to be guessed, game ends in victory if corrects == word_length
|
|
/// </summary>
|
|
private int wordLength;
|
|
|
|
/// <summary>
|
|
/// Indicates if the game is still going
|
|
/// </summary>
|
|
private bool gameEnded;
|
|
|
|
/// <summary>
|
|
/// Letter prefab
|
|
/// </summary>
|
|
public GameObject letterPrefab;
|
|
|
|
/// <summary>
|
|
/// Reference to letter prefab
|
|
/// </summary>
|
|
public Transform letterContainer;
|
|
|
|
/// <summary>
|
|
/// The Image component for displaying the appropriate sprite
|
|
/// </summary>
|
|
public Image hangmanImage;
|
|
|
|
/// <summary>
|
|
/// The GameObjects representing the letters
|
|
/// </summary>
|
|
private List<GameObject> letters = new List<GameObject>();
|
|
|
|
/// <summary>
|
|
/// This scriptable holds all the images for the different stages of hangman
|
|
/// </summary>
|
|
public HangmanImages images;
|
|
|
|
/// <summary>
|
|
/// This initially empty list holds all the previous guesses that the user has made, as to not allow repeated msitakes
|
|
/// </summary>
|
|
private List<char> guesses;
|
|
|
|
/// <summary>
|
|
/// This lists holds all valid guesses that the user can make
|
|
/// </summary>
|
|
private List<char> guessables = new List<char> { '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
|
|
/// <summary>
|
|
/// The panel holding the actual game
|
|
/// </summary>
|
|
public GameObject gamePanel;
|
|
|
|
/// <summary>
|
|
/// The panel holding the player-selection screen
|
|
/// </summary>
|
|
public GameObject playerPanel;
|
|
|
|
/// <summary>
|
|
/// The panel holding the screen where player 1 can input their word
|
|
/// </summary>
|
|
public GameObject inputPanel;
|
|
|
|
/// <summary>
|
|
/// This textfield holds the word that player 1 is typing
|
|
/// </summary>
|
|
public TMP_Text inputTextField;
|
|
|
|
/// <summary>
|
|
/// This int shows what mode we are in, used in update: 0 is playerselect, 1 is wordInput, 2 is the actual game
|
|
/// </summary>
|
|
private int mode;
|
|
|
|
// The following attributes are necessary to finish and gameover
|
|
|
|
/// <summary>
|
|
/// "Game over" or "You win!"
|
|
/// </summary>
|
|
public TMP_Text endText;
|
|
|
|
/// <summary>
|
|
/// The game over panel
|
|
/// </summary>
|
|
public GameObject gameEndedPanel;
|
|
|
|
/// <summary>
|
|
/// Button for restarting the game
|
|
/// </summary>
|
|
public Button replayButton;
|
|
|
|
/// <summary>
|
|
/// Letters
|
|
/// </summary>
|
|
public TMP_Text lettersText;
|
|
|
|
/// <summary>
|
|
/// Letters ( right | wrong )
|
|
/// </summary>
|
|
public TMP_Text lettersRightText;
|
|
public TMP_Text lettersWrongText;
|
|
|
|
/// <summary>
|
|
/// Accuracy
|
|
/// </summary>
|
|
public TMP_Text accuracyText;
|
|
|
|
/// <summary>
|
|
/// Words
|
|
/// </summary>
|
|
public TMP_Text wordsText;
|
|
|
|
/// <summary>
|
|
/// Score
|
|
/// </summary>
|
|
public TMP_Text scoreText;
|
|
|
|
// The following attributes are necessary for user interaction
|
|
|
|
/// <summary>
|
|
/// Reference to the minigame ScriptableObject
|
|
/// </summary>
|
|
public Minigame minigame;
|
|
|
|
/// <summary>
|
|
/// We keep the minigamelist as well so that the minigame-index doesn't get reset
|
|
/// DO NOT REMOVE
|
|
/// </summary>
|
|
public MinigameList minigamelist;
|
|
|
|
/// <summary>
|
|
/// Reference to the user list to access the current user
|
|
/// </summary>
|
|
public UserList userList;
|
|
|
|
/// <summary>
|
|
/// Reference to the current user
|
|
/// </summary>
|
|
private User user;
|
|
|
|
/// <summary>
|
|
/// Reference to the minigame progress of the current user
|
|
/// </summary>
|
|
private Progress progress = null;
|
|
|
|
/// <summary>
|
|
/// Reference to the scoreboard
|
|
/// </summary>
|
|
public Transform Scoreboard;
|
|
|
|
/// <summary>
|
|
/// Reference to the entries grid
|
|
/// </summary>
|
|
public Transform EntriesGrid;
|
|
|
|
/// <summary>
|
|
/// The GameObjects representing the letters
|
|
/// </summary>
|
|
private List<GameObject> entries = new List<GameObject>();
|
|
|
|
/// <summary>
|
|
/// Reference to the ScoreboardEntry prefab
|
|
/// </summary>
|
|
public GameObject scoreboardEntry;
|
|
|
|
/// <summary>
|
|
/// Accuracy feeback object
|
|
/// </summary>
|
|
public Feedback feedback;
|
|
|
|
/// <summary>
|
|
/// The button to go into the game
|
|
/// </summary>
|
|
public GameObject gottogamebutton;
|
|
|
|
/// <summary>
|
|
/// Current sign out of the predictor
|
|
/// </summary>
|
|
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
|
|
userList.Load();
|
|
user = userList.GetCurrentUser();
|
|
Progress progress = user.GetMinigameProgress(minigame.index);
|
|
if (progress == null)
|
|
{
|
|
progress = new Progress();
|
|
progress.AddOrUpdate<MinigameIndex>("minigameIndex", minigame.index);
|
|
progress.AddOrUpdate<List<Score>>("highestScores", new List<Score>());
|
|
progress.AddOrUpdate<List<Score>>("latestScores", new List<Score>());
|
|
user.minigames.Add(progress);
|
|
}
|
|
userList.Save();
|
|
|
|
// Set calllbacks
|
|
feedback.getSignCallback = () =>
|
|
{
|
|
return "A";
|
|
};
|
|
feedback.predictSignCallback = (sign) =>
|
|
{
|
|
currentsign = sign;
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hangman starts by asking the amount of players, so this function holds all the info needed to start the actual game
|
|
/// </summary>
|
|
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<char>();
|
|
// 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function is called when the "two player"-button is clicked, it goed to the input-screen
|
|
/// </summary>
|
|
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);
|
|
}
|
|
/// <summary>
|
|
/// This function is called if singleplayer is selected, we generate a random word for the player and start the game.
|
|
/// </summary>
|
|
public void SinglePlayer()
|
|
{
|
|
// This word is used for testing before dynamic word-fetching is added
|
|
PickRandomWord();
|
|
|
|
StartGame();
|
|
}
|
|
/// <summary>
|
|
/// Randomly select a word from a randomly selected theme, use this word for the hangman game for singleplayer.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function starts the game after player 1 has entered their word, but only if its length >= 2.
|
|
/// </summary>
|
|
public void TwoPlayer()
|
|
{
|
|
if (currentWord.Length >= 2)
|
|
{
|
|
StartGame();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update is called once per frame
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Change the image that is being displayed
|
|
/// </summary>
|
|
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;
|
|
}
|
|
/// <summary>
|
|
/// In this function, the letters of the word selected in DisplayWord are updated after a correct guess.
|
|
/// </summary>
|
|
/// <param name="c">The letter that needs to be updated</param>
|
|
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<Image>();
|
|
background.color = Color.green;
|
|
TMP_Text txt = letters[i].GetComponentInChildren<TMP_Text>();
|
|
txt.text = Char.ToString(c);
|
|
|
|
// You correctly guessed a letter
|
|
corrects++;
|
|
}
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// This function returns the score that the user currently has
|
|
/// </summary>
|
|
/// <returns>The current score of the user</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set score metrics
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Delete all letter objects
|
|
/// </summary>
|
|
private void DeleteWord()
|
|
{
|
|
for (int i = 0; i < letters.Count; i++)
|
|
{
|
|
Destroy(letters[i]);
|
|
}
|
|
letters.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays the word that needs to be spelled
|
|
/// </summary>
|
|
/// <param name="word">The word to display</param>
|
|
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<Image>();
|
|
background.color = Color.red;
|
|
TMP_Text txt = instance.GetComponentInChildren<TMP_Text>();
|
|
txt.text = Char.ToString(' ');
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomly shuffle the list of words
|
|
/// </summary>
|
|
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]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update and save the scores
|
|
/// </summary>
|
|
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<Score> latestScores = progress.Get<List<Score>>("latestScores");
|
|
List<Score> highestScores = progress.Get<List<Score>>("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<List<Score>>("latestScores", latestScores.Take(10).ToList());
|
|
progress.AddOrUpdate<List<Score>>("highestScores", highestScores.Take(10).ToList());
|
|
|
|
userList.Save();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display win screen
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays the game over panel and score values
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the scoreboard
|
|
/// </summary>
|
|
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<Tuple<string, Score>> allScores = new List<Tuple<string, Score>>();
|
|
foreach (User user in userList.GetUsers())
|
|
{
|
|
// Get user's progress for this minigame
|
|
progress = user.minigames.Find((p) => p != null && p.Get<MinigameIndex>("minigameIndex") == minigame.index);
|
|
if (progress != null)
|
|
{
|
|
// Add scores to dictionary
|
|
List<Score> scores = progress.Get<List<Score>>("highestScores");
|
|
foreach (Score score in scores)
|
|
{
|
|
allScores.Add(new Tuple<string, Score>(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<string, Score> 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<Image>().sprite = userList.GetUserByUsername(username).avatar;
|
|
|
|
// Set the player name
|
|
entry.transform.Find("PlayerName").GetComponent<TMP_Text>().text = username;
|
|
|
|
// Set the score
|
|
entry.transform.Find("Score").GetComponent<TMP_Text>().text = score.scoreValue.ToString();
|
|
|
|
// Set the rank
|
|
entry.transform.Find("Rank").GetComponent<TMP_Text>().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<TMP_Text>().text = formatted;
|
|
|
|
|
|
// Alternating colors looks nice
|
|
if (rank % 2 == 0)
|
|
{
|
|
Image image = entry.transform.GetComponent<Image>();
|
|
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>();
|
|
image.color = new Color(0, 229, 255, 233);
|
|
}
|
|
|
|
rank++;
|
|
}
|
|
}
|
|
}
|