Dana Vrajitoru
I254/C297 2D Games Programming

I254/C297 Lab 2 / Homework 2

Date: Wednesday, January 17, 2024.
Due date: Wednesday, January 24, 2024.

In this lab, we will continue the project started in Lab 1 implementing a word anagram game.

Premise

After completing Lab 1 and Homework 1, your application window should look something like this:

The root node of the scene should be called Root2D. You should have one label node called ShowLetters with a script attached, one line input node called UserEntry, and 3 more label nodes. The one at the bottom (or wherever you placed it) with the text "Accepted words" should have the name Accepted.

Ex. 1. Lab Part

In this lab, we'll mostly be implementing the game using code.

a. Shuffle

Let's shuffle the letters of the word. Here is a pseudo code of the algorithm we want to use, where a is an array:

for i from 0 to size(a)
    j = random_range(i, size(a)-1)
    swap(a[i], a[j])

We would like this functionality to be attached to clicking on the label showing the letters in the scene. For this, click on the node ShowLetters in the hierarchy, then in the inspector, expand Mouse in the Control area and then switch the Default Cursor from Arrow to Pointing Hand. That way, the user will be able to tell that something is happening on this label. Save the scene and try the functionality.

Still in the inspector, change the Filter under Mouse in Control to Stop. This will cause the node to receive information about the mouse being over it and about a mouse click happening on it. It will also prevent (stop) any other node in the scene from receiving the same mouse events and possibly processing them more than once.

Go to the script attached to ShowLetters and delete the code added last week in the function _ready. Replace the code with the empty instruction pass.

Then add the following function to the script:

func _input(event):
    if event is InputEventMouseButton and event.pressed:
        print("Mouse clicked")

After copying this code into the script, you will probably have to delete the spaces at the beginning of the lined and replace them with tabs.

Save the script and check that this is working. This will capture all mouse clicks. Maybe we want to restrict it to right-clicks so that it happens more intentionally. In the function _input, add the following to the condition at the end:

and event.button_index == 2

The button_index property of mouse events tells us which mouse button was clicked: 1 for the left, 2 for the right, 3 for the middle (scroll). Now the message should be printed only when we click the right button.

b. Accessing the Text

Now we need to access and change the content of the label in the code. This is fairly easy, as the Label class has an attribute called text that gives us access to the displayed text.

Let's write a function shuffle that shuffles this text. Copy the following function into the script below the exiting ones, and replace each 4 spaces with a tab:

func shuffle():
    var j = 0 
    var ch1 = ""
    var ch2 = ""
    var rand = RandomNumberGenerator.new()
    for i in range(len(text)):
        j = rand.randi_range(i, len(text)-1)
        if i != j:
            ch1 = text[i]
            ch2 = text[j]
            text = text.erase(j, 1)
            text = text.erase(i, 1)
            text = text.insert(i, ch2)
            text = text.insert(j, ch1)

This works, but the spaces don't appear neatly after each character anymore. To fix this, first we need to remove the spaces from the text before shuffling it. For that, add the following line before the for loop:

text = text.replace(" ", "")

Try the effect of this line on the text. The spaces should have disappeared. To add them back, we'll write a separate function because this might be useful again later. Add the following function below the previous one:

func add_spaces():
    for i in range(len(text)):
        text = text.insert(2*i+1, " ")

Then add a call to this function add_spaces() in the previous function at the end, indented by a single tab to make it happen after the for loop ends. Now the shuffle should be working as intended.

c. Getting User Input

Let's switch focus to the node UserEntry. This node will not need a script. When the user edits the text and then hits Enter to submit it, this node will emit a signal called text_submitted(new_text:String), taking one parameter of type string that will contain the string submitted by the user. We need to designate a node as the destination for this signal. This will be the node being notified when the signal is emitted and given a chance to react to it.

Since we already have a script attached to the node ShowLetters, we can delegate this node as the recipient of the signal. For this, with the node UserEntry selected in the hierarchy, click on the Node tab next to the Inspector. This shows a list of signals available to connect for this node. Under LineEdit at the top, double-click on the signal text_submitted. A dialog will open letting you connect the signal to a method. Choose the node ShowLetters and check that the Receiver Method at the bottom shows _on_user_entry_text_submitted. Then click Connect. The method should be added to the script. Open it if it's not already open and check that the function is there at the bottom.

Replace the instruction pass in this function with printing the parameter so that we can test that the connection works. It should be working, but the text does not get deleted from the entry box when it is submitted. Since that's the usual behavior, let's make the entered text disappear.

For this, we need to have a reference to this node from the script. Let's declare a class variable (attribute) for it at the top of the script, just below extends Label:

var entry_node = null

This will hold a reference to that node. However, we cannot assign it that reference yet, because the node may not have been created at this point. We need to assign it something in the function _ready. Replace the instruction pass in this function with the following:

entry_node = owner.get_node("UserEntry")

This should work as intended.

Now let's go back to the function _on_user_entry_text_submitted. Then after printing the text, add the following line with the same indentation:

entry_node.text = ""

This replaces the text shown by the box with an empty string.

If we're not sure in which order the nodes in the scene are created, we can check that the variable entry_node is not empty before using it. Add the following test at the top of the function recently created:

if entry_node == null:

Then copy the instruction assigning the node reference to the variable entry_node from the function _ready into this test, indented by an additional tab (two tabs total).

d. Checking the Entry

We need to check that the user has entered a word that is correct. For this, we need to add a bit more information to the class. Scroll to the top of the script, just below the declaration of the variable entry_node, add the following line:

var words = ["bag", "dab", "cab"]

Then add any other word you can think of that is an anagram of the shown letters. We will use this class variable (attribute) to check if the words entered by the user are correct.

Then in the function at the end of the script, add a test for new_text being in words, and if true, print that it is accepted.

Furthermore, before we check this, we need to remove all the spaces from what the user has submitted and to convert it to all lowercase. After that, we'll check that it only uses the shown letters. For that, replace the test we just added with the following lines of code:

new_text = new_text.to_lower().replace(" ", "")
var word = text.to_lower().replace(" ", "")
var correct = true
for ch in new_text:
    var where = word.find(ch)
    if where >= 0:
        word = word.erase(where, 1)
    else:
        correct = false
if correct and new_text in words:
   print("accepted: ", new_text)
Ex. 2. Homework Part

a. More Words

Complete the array words with a list of words to use and words that can be made of the same letters. I suggest going to this site:

thewordfinder.com/anagram-solver/

and enter some words of 6 letters or more, look for their anagrams, and copy all those words to the array.

b. Accepted Words Display

Declare another variable in the class called accepted_node and initialize it the same way the entry_node is initialized, but with the name of the node Accepted.

Then in the function _on_user_entry..., if the submitted text is correct and can be found in the array words, instead of printed that the word is accepted, add it to the text of the Accepted label like:

accepted_node.text = accepted_node.text + "\n" + new_text

If the entry is not accepted, print a message saying so.

c. Random Word

In the function shuffle, make the variable rand a class variable by moving it to the top of the class below the other variables already declared, and initialize it as null. Then assign to it a new random number generator in the function _ready.

In the function _ready, get a random element from the array words and store it in a variable, change it to all uppercase using the function to_upper in a similar way to how we used to_lower, and assign it to the attribute text. Then call the function shuffle(). This should start every new game with a new word to guess.

d. New Game

Add a button to the application with the text "New Game". Connect the signal pressed of this button with the object ShowLetters. This should create a new function called _on_button_pressed in this script. Copy the code from the function _ready that initialized the text randomly and shuffles it into this new function. Then assign to the text of accepted_node the value "Accepted words:".

This should complete the program and the game should now be playable.

Homework Submission

Take a screen shot of your running program, showing the content of the screen after a couple of accepted words, and save it as png or jpg. Submit it along with the scene file root_2d.tscn and the script file ShowLetters.gd to Canvas, Assignments - Homework 2.