Under the hood: Pet the Cat

In my quest to better understand Decker and how some things work, I made a simple game called Pet the Cat.

It’s not overly complex in any way, shape, or form, but it’s something I did to test some systems that might be useful in the future. Before I get to that, though, I want to explain how the game works.

The idea is simple: there’s a cat and the player needs to pet him.

To pet, the player just clicks anywhere on the cat. The mouse cursor turns into a small hand wherever that’s possible. There’s a trick, though: the game has a hidden score that goes up or down, at different values, depending on where it’s clicked. The starting screen (above) only adds points, but it already has two different clickable areas.

Every following screen is like this, at varying degrees, with some giving negative points — which means the score is reduced — depending on where the cat is petted.

Every clickable square is, actually, a button widget with the “invisible” property turned on, so the picture of the cat in the background is visible.

Now, herein lies the first problem: each button needs to have some code to do something, and that’s a lot of buttons. If any piece of the code was wrong, I would have to change, manually, every script in every button. That’s a big nope for many reasons, most important of all being that it would just be a big pain to deal with.

So, what’s the solution?

Well, it’s actually in examples that ship with Decker: they have actual code in field widgets, and then buttons call up that code with the eval[] function. And now comes the first “trick”: there’s a “secret screen” that the player doesn’t see, and this screen holds a few widgets that are used throughout the game: the “Control” card.

Since this project is simple, there isn’t much here, but I’ll explain what everything does later. For now, the most important is the field with the code: this is what each button calls when it’s clicked. The code is a conditional that evaluates how many cards were seen, then if it’s below a certain count, chooses another one randomly; if the limit set was reached, it counts how many points were made and, if they are below a certain threshold, the player gets one ending; if it’s above, they get another ending.

Here’s a more detailed breakdown:

if control.widgets.counter.value < 5

There’s a “Counter” widget in this card that counts how many cards the player has seen. If it’s less than 5, then it executes the following code:

control.widgets.counter.value:control.widgets.counter.value+1

This adds +1 to the “Counter”.

c:extract value where value..widgets.visited.value=1 from deck.cards

This one is kind of complex: it checks the value of another widget that each card has and, if the value is 0, that card is ignored, so the next line

go[random[c]]

never chooses the same card twice, never returns to the starter card, and never goes to the “Control” card by mistake. I’ll explain later in detail how it works.

So, if the conditional is true — “Counter” is lower than 5 — it will repeat this code until 4 cards have been seen. When the 5th card enters the scene, the conditional is false, since now the “Counter” is higher than 5 (not included), so it jumps to the next one:

elseif control.widgets.points.value < 13

This conditional will check the number stored in the widget “Points”. If the value is lower than 13, then the player

go[end1]

and gets the “bad ending”, which isn’t bad, it’s just the cat being a spoiled little brat.

However, if the value is higher than 13 (not included), it will run

else
 go[end2]

leading to the “good ending”, with the cat just being spoiled and not acting like a little brat.

The widgets “Count” and “Points” don’t have any code, but the button “Reset visited” does:

on click do
 deck.cards..widgets.visited.value:1
 points.value:0
 counter.value:0
end 

This code resets the invisible widget that marks which card has been seen and resets the “Counter” and “Score” to 0. This is done to help with the testing, otherwise I would have to reset every value by hand, or by running Lil commands on the Listener. This code is also reused in the “Try again” button that both endings have, with one addition:

go[home]

This extra line takes the player back to the home card, the first one, to start playing again.

Right, so, how to make each card be chosen randomly, but at the same time avoiding cards the player shouldn’t see? The answer is with one more button tucked away in each card the player will see. The game has 14 photos, of which 5 will be randomly chosen by the code above at every game restart. This button will also make sure we don’t see the same photo twice in one playthrough. To do all of this, it should be turned into a checkbox.

Ok, so, it’s a checkbox, which means “invisible” can’t be selected. How come it’s not showing up? Because it’s set as “Show None” on the Widgets menu.

“Invisible” means the button is invisible, but interactable, like the ones over the cat.

“Show None” means the button not only is invisible, but it’s also non-interactable, making sure the player doesn’t accidentally click on it by mistake.

Now, here’s the part that’s a bit tricky to understand: a “checkbox” has two values, 0 and 1. It’s 0 when unchecked, and it’s 1 when checked. Let’s check that code again:

c:extract value where value..widgets.visited.value=1 from deck.cards

To be honest, I’m not sure how this works in detail because… well, I’m not a coder, I just “stole” it from the page “Learn Lil in 10 Minutes“. I just know that it searches for the value of every widget called “visited” in the deck, makes some sort of list of the cards with them, and then it’s possible to use whatever command one wants on that list, like a go[random[list]], for example. In this case, the list is called c.

The code didn’t work right out of the box, it had some strange behavior, so after some investigation, I confirmed the answer in the Decker community on itch.io, and here’s the part that might cause some confusion: every card, even the ones that don’t have a “visited” widget, has a visited.value=0. I won’t pretend to understand how this works or why, but that’s how it is.

So, what does this mean in practice? It means that we can’t search for visited.value=0, or it will return every card in the deck and might take the player to unintended ones. We need to search for visited.value=1, meaning that the game starts with every invisible checkbox already checked, and then it unchecks them after the card has been seen. We do the unchecking by going into Card -> Script… and inserting this line of code on each card that has the widget “visited”:

on view do
 visited.value:0
end

Ok, so, finally, the last piece of the puzzle: how are the points calculated? The code shown didn’t interact with the “Points” widget in the “Control” card. The answer is… each invisible button has a line of code manipulating the value of that widget. If we open the script of one of the buttons, we are met with this:

The line

control.widgets.points.value:control.widgets.points.value-1

manipulates that widget. In this case, it’s decreasing the value by 1 (meaning the player petted the cat somewhere he doesn’t like!). Every button has this same code, with the value at the end going from -1 to +3. It’s a line trivial enough to just copy and paste and then change the value.

The second line

eval[control.widgets.buttoncode.text () 1]

just executes the code in the field shown before, in the “Control” card. Again, I’m not sure how it works, I just know it does, but I think the () means the eval[] can access variables in the code, and the 1 allows it to change their values. The first part, control.widgets.buttoncode.text just points to the field that holds the code.

Right, but why make it read from a field in another card? Well, look at how many interactable buttons this game has. All executing the same code. If I wanted to make any change, even a small one, I would have to edit each one by hand. It would be a miserable experience. Why go through that when I can have an easily accessible and editable field just outside of view?

In the end, that’s why I made this little experiment: to learn how to make a widget read code from a field in another card, and how to select random cards based on conditionals. Both things might seem trivial, but I think they may come in handy for the next project I’m starting.

Tip: Players can natively move around the cards with arrow keys, which could break the flow of any game. To avoid that, go to File -> Properties, click on Script and put this code there:

on navigate do

end

This will make sure the arrow keys don’t work when Decker is running in “Interact mode”.

One final note: it’s noticeable that I didn’t use the typical <widget>.<function> anywhere (I think), opting for a longer syntax. This is actually required when interacting with widgets in another card, and the syntax go

<cardName>.widgets.<widgetName>.<function>

So, to control the value on the widget “Points” in the card “Control”, I write

control.widgets.points.value

Since Decker doesn’t hold values in variables, they need a place to live, and so it happens that widgets in a card the player will never see are the ideal place for them.

Anyway, I hope this explanation wasn’t too difficult to follow. The game itself is very simple, and the logic behind it too, but some complex code might scare newcomers. My best advice is just to… mess around with stuff to see how they work and how they break.

Here’s a non-crunched-down picture of the cat for your enjoyment.

2 comments Under the hood: Pet the Cat

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.