Dana Vrajitoru
I254/C297 2D Games Programming

I254/C297 Lab 4 / Homework 4

Date: Wednesday, January 31, 2024.
Due date: Wednesday, February 6, 2024.

In this lab and homework, we will create a card game similar to TriPeaks Solitaire (Microsoft Solitaire Collection). We will also learn about using fonts and saving a subset of nodes as a scene so that we can easily create multiple copies of it. Here is an example of the scene:

Lab Part

Ex. 1. In this lab we'll create a project using images and fonts to create the nodes in the scene.

Create a project called solitaire and a scene called main inside. Create a Node2D object called Root in the scene. Switch the scene editor to 2D.

a. Card Scene

Choose one of the following images and download it, then drag it to the folder of the project. You can also find these images in Canvas - Files - Week 4.

You should see the file in the File Browser in the Godot project.

With the root node selected, drag the selected card to the scene to create a Sprite2D object. Edit the name to be just Card. Scale it down so that its height covers about a quarter of the game window. Move it close to the top of the window in the center.

With the card selected, add a node as its child of type RichTextLabel. Give it the name number and write the number 12 in its text.

From the following site: https://www.1001fonts.com/, choose a font that you like and download it. In the Downloads folder, find a zip file with the font name and open it. Drag either the .ttf or the .otf file from this zip file into your project folder in Godot.

With the object Number selected, under Control click on Theme to add a new theme, then click on the Theme word on the right. This will open a panel to edit the theme. The default font in the theme should say <empty>. Drag and drop the font file over this box.

Choose a pretty good size for the Default Font Size such as 40. Then in the toolbox above the window, click on the Scale button and scale the label so that the text is large enough on the card. Center it as well as you can on the card.

Let's add a script to the card. In this script, at the top declare a class variable num initialized as 0 and a variable num_ref initialized as null. In the function _ready initialize the num_ref as

num_ref = find_child("Number")

Add the following function in this class:

func set_number(n:int):
    self.num = n
    num_ref.text = str(n)

Now we can save the card together with the label as a scene so we can create multiple copies more easily. In the hierarchy, right-click on the card and do Save Branch as Scene. The default name of card.tscn is fine. Rename the existing card as Card0.

Drag the file card.tscn to the scene to create 9 more cards and arrange them in the pattern seen in the figure above. Notice that the first card you added this way was called Card and the others, Card2, Card3, and so on. Rename the one called Card as Card1. Finally, add one more card in the top left corner and call it Discard.

b. Randomizing the Cards

We want to assign a random number to each of the cards at the beginning. Since we have multiple instances of cards, we don't want each of them to run its own random number generator. So, at the top of the class, add the following declaration:

static var rand = RandomNumberGenerator.new()

This way, the variable rand has a single instance for the whole class, while num and num_ref will be separate entities for each card.

In the script, in the function _ready, call the function set_number with a parameter that is a random number between 1 and 13.

Play the scene to see if it looks ok. You will need to confirm this scene as the main one. All the cards should now show a random number.

c. Click Detection

We would like to detect when we click on a particular card to be able to play it. For this, we need to add an object of type Area2D to the cards, and also a CollisionShape2D object to define its shape. Fortunately, we can edit an existing scene and add more elements to it.

In the FileSystem area, right-click on card.tscn and choose Open Scene. This will open the scene containing a single card in another tab.

With the card selected, add a new node of type Area2D as its child. Then with this new node selected, add a child to it of type CollisionShape2D. Then with this node selected, in the Inspector, click on the null next to Shape and select a New RectangleShape2D. Resize the shape to cover the card well enough on all sides. Save the scene and go back the main one.

You should notice that the collider box has been added to all the cards in the scene.

Going back to the card scene, we need to connect a signal from the Area2D object to a function in the script. Click on this object, then on the Node tab next to the Inspector, and then double-click on the signal input_event(.... Select the script on the card and add the function that is being proposed.

Going to the script, add the following code to the function:

if event is InputEventMouseButton \
and event.button_index == MOUSE_BUTTON_LEFT \
and event.is_pressed():
    print("Clicked on ", num)

Run the main scene to test this. When you click on a card, its number should be printed out.

One thing you may notice is that the card does not react when you click on the number displayed on it. This is because the default mouse behavior for a label is set to Stop, which prevents the clicks from being detected by other objects, even if they are above them. To fix this, click on the Number object and expand Mouse under Control. Then change the Mouse Filter property from Stop to Pass. Now things should work fine.

d. Node Id and Order

Add a non-static class variable to the script called id initialized as 0. Then in the function _ready, set its value the following way:

var name = get_name().replace("Card", "")
if name == "Discard":
    id = 10
else:
    id = int(name)
print(id)

This will let us know which card has been clicked specifically.

We also need to make sure that the cards are shown in the right order. Set the z index of the top card to 0, of the next row to 1, the next one to 2, and the bottom one to 3.

e. Root Script

The functionality of this game will not be created only in the card script. We need to add another script that can handle all the cards.

Add a script to the root node. In this script, start by declaring one reference to the discard card called discard_ref and declare a variable cards that will hold an array of references to the cards in play, initialized as an empty array. Then in the function _ready, initialize the array like this:

for i in 10:
    var ref = find_child("Card" + str(i))
    cards.append(ref)

Connect discard_ref to its card the same way.

Let's make the cards disappear when we click on them, while assigning their number to the Discard. For that, add the following function to the main script:

func play(id, num):
    if id < 0 or id > 9 or cards[id] == null:
       return
    discard_ref.set_number(num)
    cards[id].set_visible(false)
    cards[id] = null

Going back to the card script, in the function _on_area_2d_input_event, instead of printing the id, call the function

owner.play(id, num)

Try this new functionality. Now clicking on cards should make them disappear.

Homework Part

Ex. 2 a. New Game

Add a button to the scene with the caption New Game where you call a function from the main (root) script doing

get_tree().reload_current_scene()

b. Card Number

To implement the game properly, cards should only be made to disappear if their number is either one less or one more than the one on the Discard. To implement this, modify the function play in main.gd so that before you make the card disappear, you check this condition. You can access the num attribute of the discard with discard_ref.num.

c. Check Playable

We are only supposed to be able to play a card when the cards above it have been played. If you placed the card in ascending order of their names and from left to right, then the id of the two cards on top of each card can be computed like this:

Let row be equal to int(sqrt(1.75*id)). Then the two cards on top of the card have ids equal to

top1 = (row + 1)*(row + 2)/2 + id - row*(row + 1)/2 and
top2 = top1 + 1.

These formulas should work fine for up to 6 rows of cards.

In the function play, check if either of the elements in the cards array at indexes top1 or top2 is not null, and if they are, return without doing anything else.

But also, if the card id is larger than 5, there is no card on top of it, so it can be played without condition. So add a condition at the beginning of the test that the id is less than or equal to 5 and the other two conditions are true. Since the previous conditions should be combined by an or, you'll need to place that part in parentheses. Like, 3 and (1 or 2), where 1, 2, and 3 are the 3 conditions I described. This is because the and operator has a higher priority than the or.

d. Randomizing the Discard

The game as it is can probably not be won very often. To make it more winnable, add a button below the discard that says Deal. This button should assign to the discard a new random number.

To make the game more challenging, we will only allow for a given number of deals, let's say 10. You can calibrate this number and choose a better value if you want. Declare a class variable in the main script at the top for the number of deals left and initialize it as 5 in the function _ready. Then when Deal is clicked to randomize the discard, decrease the number of deals left. When it reaches 0, do not randomize the discard anymore.

Add a label that shows the number of deals left, so that the player knows what's happening.

e. Winning the Game

Add a function that checks if the game was won. The easier way is to have a variable in the class main that keeps track of the number of cards still in the game. Initialize it as 10 in the function _ready. Then in the function play, every time a card is made to disappear, decrease this variable. The game is won when this variable has become 0. Use the label you created to display a winning message when this happens. The function checking for the game being won should be called from the function play.

Homework Submission

Take a screen shot of your running program, showing the content of the screen after a couple of attacks, and save it as png or jpg. Submit it along with the scene files main.tscn and card.tscn, and the script files Card.gd and main.gd to Canvas, Assignments - Homework 4.