Simple LightsOut game in Pharo Smalltalk (following tutorial)
Notes are based on Pharo by Example, a free book suggested by a Mooc called Live Object Programming in Pharo
At the moment I'm working through book examples to get familiar with the developer environment and interacting with the language itself. This is one of the exercises.
The lightsOut game is a board of 100 'tiles' that are all one color. When you click on one of the tiles, the four surrounding cells change to a different color (lights have been turned on). When you click on the same cell again, the lights toggle off and on. The aim of the game is to turn as many lights on as possible.
According to the chapter, this game contains two objects, the game board itself, and the individual cells. So we will be writing two classes, one for each of these.
Creating a new package
The first step was to open the 'System Browser' from the 'World Menu'. To create a new package, we right click on any of the existing packages in the package panel and select "Add package...". Then we type in the package name and click anter. I gave my package the same name as the example in the book did: "PBE-LightsOut".
Defining the cell class
To create a new class, click on the package we want to create a class for. The main editing pane will display a template for creating a new class easier. This is the code for the class we are defining for our LightsOut Cells.
In this example, we are sending a message to the class SimpleSwitchMorph, asking it to create a subclass called LOCell, the message is in the form of a symbol (#LOCell).
Then we defined an instance variable called 'mouseAction', which will be used to defined the behaviour that happened if the cell is clicked on with a mouse.
We have not set any class variable names right now, the tutorial doesn't mention why it has been included in the code at all. A quick google shows that a class variable when added is applied to all instances of the class. I'm assuming we are going to use it to set the default color of all the cells but don't know that for sure yet. Whilst the instanceVariables can be set for each individual object that is created from that class (each instance).
The last line of the class definition above specifies which package the object belongs to, in this case it was the package we created for our game (PBE-LightsOut).
To actually send our subclass message, we save our code with Command-S, which results in our new class being compiled. The name of our compiled class then shows up in the class pane of the System Browser, which is right next to the Package pane.
At this point you will also get a feedback from the Quality Assistent at the bottom of the screen. In this case, there were four messages:
- Class not referenced
- No class comment
- [mouseAction] Instance variable not read or not written
I like to really try and understand the meaning behind error messages, so usually google them one by one to find out what they mean. In this case they are self explanatory except for the 'No class comment' message, but it's always a good idea to look them up even if you think you know what they mean.
We can find out the rationale behind the error messages by clicking on the question mark next to the error message (which I have just found out is actually called 'critic text'.
- Class not referenced: Check if a class is referenced either directly or indirectly by a symbol. If a class is not referenced, it can be removed.
- No class comment: Classes should have comments to explain their purpose, collaborations with other classes, and optionally provide examples of use.
- [mouseAction] Instance variable not read or not written: This smell (Reference to Code Smells) arises when an instance variable is not both read and written. If an instance variable is only read, the reads can be replaced by nil, since it could not have been assigned a value. If the variable is only written, then it does not need to store the result since it is never used. This check does not work for the data modal classes since they use the #instVarAt:put: messages to set instance variables.
Another thing I was interested is what the little icons represent next to the critic text. I looked up Pharo Quality Assistant and found a repo which explains what they represent. The '(i)' icons represent information, '/!\' icons represent warnings and '(-)', '(!)' icons represent errors. There are a few more icons listed but those are the common icons.
Comments are important
According to the authors, good quality comments are a priority in Pharo as well as high code readability. This is to encourage people to be intentional about everything that they write.
Method comments shoud explain what the method is doing, its context or the reasons behind it's implementation.
A strong class comment is based on CRC cards (I wrote about these here). CRC cards stand for Class, Responsibility and Collaborators. Pharo has a build in class comment template that has the basic structure of CRC cards. However, Kent Beck and Ward Cunningham (the creators of the CRC method) advise you NOT to use digital versions of these, but to write them down on paper such as index cards so that you can get a personal connection to your object. Out of sight is out of mind.
Though it could be a good idea to have both. To open the class comment, you can click on the '? Comment' menu option which was just above my main editor containing the class code. It was a little bunched up and hard to see.
When you write your class comment using the CRC format, you are basically stating the responsibility of your class briefly, and how it collaborates with other classes to achieve the responsibilities. It can also be a good idea to include an API (messages that the object understands - a lightbulb moment for me), give an example, and some details about the internal representation or implementation rational.
Adding methods to our class
To add a new message to our class, we start by selecting a protocol from the protocol pane. In this case, the example told us to select the 'no messages' protocol. A quick google says that the 'no messages' protocol makes all of your subsequently added methods 'as yet unclassified', which removes any quality assistant messages like 'no protocols' without having to include protocols. Answer found here
However, after right-clicking the protocals pane, selecting 'new protocol' and searching for 'no messages' with and without spaces, and with a capital NO like some of the other protocol names listed, I couldn't find the 'no messages' protocol. I also didn't see the no protocol message in my quality assistant panel so am assuming it was okay. Instead, I clicked on the '--all--' option in the protocol pane and replaced the method template that showed up in the editor with the following:
I'm assuming the self label is assigning an empty label to the cell. I'm not sure why it's there if it's empty. My assumption is that it will be used to store a value like 'on' and 'off' for toggling purposes.
The border width could be like setting the border to 2 pixels wide in CSS. The bounds is the most confusing expression to me. The whole expression is drawing the cell as 16 pixels by 16 pixels (I think). The offColor sets the cell to yellow when the cell is clicked 'off', whilst the onColor sets the cell color to blue when the cell is clicked 'on'. The cell is instructed to use square corners (in HTML corners are square by default, you set the border radius in CSS to set the roundness of the corners). I assume the turnOff expression will be used to toggle the cell 'off', though if that's the case, where is the equivalent 'turnOn' option?
Okay, now I'll read and take note of what the example explains these to mean.
- I was right about what initialize and superinitialize do. Some extra info to keep note of. You call initialize super whenever you inherit from a class. The initialize method on the super class may do things like set up some needed variables or behaviour etc. If you don't initialize it, you'll likely get unexpected behaviour from not having a 'clean' state.
- The expression 0@0 represents a Point object with both x and y coordinates set to 0. We are actually sending '@' as a message to the object 0 with an argument of 0 here. Then we are sending the corner: 16@16 message to the point object. This creates a rectangle with corners 0@0 and 16@16. I still find this a little confusing, though the message explanation is still a lightbulb moment.
- The book says that the rest of the messages are self explanatory, which I can agree with. The authors go on to say that it's a good idea to write your code so that it reads like English as much as possible. They say it helps to imagine the Object talking to itself so that it says things like "Self, use square corners!". I like this idea.
- The 'initialize' method we just created is listed in the methods pane. There is a little green arrow next to it which meanst that the method exists in thte superclass, and is overridden in your class.
Inspect your newly created object in the playground
You can inspect your object by opening the playground and typing the
LOCell new and running the
This opens an inspector that shows you the variables contained in your object as well as the values contained within those variables. Clicking on one of the variables will split the screen into two panels, the right panel showing the details of the instance variables you clicked on.
In the bottom pane of the inspector, you can run expressions like you would
in the playground. Typing in
openInWorld and running it with the Do It command (Command-D) will add
your cell object to the top left of the World window. When I clicked on the cell
it changed from yellow to blue, and when I clicked on it again it changed back
to yellow. I was surprised by this because I didn't expect it to actually work
based on the code that we had written.
On the World window, you can meta click on the cell (Control-shift-click) to bring up the Morphic halo, a bunch of handles that let you move the cell around and resize it etc. The bounds in the inspector will change to match the new positioning. You can also delete it with the 'x' handle.
Defining the board class
Add a new class called LOGame and write a class comment for it. Then initialize the game with the following code:
When you save this code, you will get an error message popup box that says "Unknown variable: cells please correct, or cancel:" with a list of options for correcting the problem. We chose the 'Declare new instance variable' option because we want our cells to be an instance variable.
After selecting this, the following Critic texts were provided by the Quality Assistant:
- [borderWidth] Super and Self Messages sent but not implemented: Checks if messages sent to self or super exist in the hierachy, since these can be statically typed. Reported methods will certainly cause a doesNotUnderstand: message when they are executed.
- The same message above was repeated for [bounds:], [cellsPerSide] and [newCellAt:at;] x 2.
The next step is to open the playground and type in
and run it with the Do It (Command-D) command. A debugger method will pop up
complaining that it doesn't know what some of the terms are. Instead of closing
the debugger, click on the Create button and select "LOGame" which will contain
the method and click "ok". When prompted for a method protocol, select
"accessing". This will cause the debugger to create the method on the fly and
invoke it for you immediately. The resulting method will look like the
Replace the code stub with the following method
All this code does is specify how many cells go along the side of the game board.
Okay, so after trying to run this code, there was a bug. Turns out I made a mistake earlier in the process. So I went back and rewrote all of the code. I forgot to capture the steps again because I was so focused on getting it right. This is the finished result: