» Home

  » Developer Guide


Brass SSE Developer Guide - Tutorials

 

Tutorial 6 - Adding Game Logic and using Functions

Grid? Check. Images? Check. Grid state stored? Check. Display the symbols? Check. We're nearly there, all we need now is to let the player click on the grid to make their move.

Download the source for this tutorial

 

Back to Input Handling

By now you could probably write this code yourself without help, so let's write the input handling code fairly quickly so we can get onto more interesting things. The human player will play as X and the plugin-computer-player will play as O.

There are 2 conditions we need to handle: first, the user clicking in the grid and second, the user clicking outside the grid. That's our "top level" condition.

If the user clicks in the grid we need to figure out what square they clicked on and check its status. If it's empty (FREESQUARE) then we need to set the grid array element for that square to be XSQUARE - the user's symbol. If it's not empty we just ignore the click until the user visits ye olde clue shoppe :)

Once the user has made their move the plugin needs to make a move too. You could make this code as smart as you like with proper logic to figure out the best move, but for now we're just going to get our code to make a move in the first free square it finds in the array.

Enough theory, let's get coding.

 

The OnMouseLButtonUp Handler

Let's go back to our old OnMouseLButtonUp handler. At the moment we've still got some code from way back in tutorial 3 in there. Let's get rid of that because we don't need it any more.

Remove the PosX = iX; and PosY = iY; lines of code from the handler

Delete the global PosX and PosY declarations at the top of your source

 

Code time. Task 1 - check the click was inside the grid. To do that we need to have access to the grid position, width and height in the OnMouseLButtonUp handler.

Add the following global declarations to the top of your code:

int global XSQUARE = 1;
int global OSQUARE = 2;

// The grid size
declare global iGridTopX int;
declare global iGridTopY int;
declare global iGridWidth int;
declare global iGridHeight int;

 

Change the start of the Render handler code to remove the variable declarations:

function Render()
{
    int iWidth = getpanelwidth();
    int iHeight = getpanelheight();

    // Setup the grid position and size
    iGridTopX = 10;
    iGridWidth = iWidth - iGridTopX - 10;

    iGridTopY = iHeight / 4;
    iGridHeight = iHeight / 2;

 

The grid size and position is now global, so we can test for a click inside it in the OnMouseLButtonUp handler.

Add the following code to the OnMouseLButtonUp handler:

function OnMouseLButtonUp(int iX, int iY)
{
    if(iX > iGridTopX and iY > iGridTopY and
       iX < iGridTopX + iGridWidth and
       iY < iGridTopY + iGridHeight)
    {
        // Store the size of grid 1/3rd's
        int iGridThirdW = iGridWidth / 3;
        int iGridThirdH = iGridHeight / 3;

        // Set the click to zero based index
        iX = iX - iGridTopX;
        iY = iY - iGridTopY;

        // Calculate the element clicked on
        int iElementClicked = iX / iGridThirdW;
        iElementClicked = iElementClicked + ((iY / iGridThirdH) * 3);

        // Click was on an empty square?
        if(GridStatus[iElementClicked] == FREESQUARE)
        {
            // Yes!
            GridStatus[iElementClicked] = XSQUARE;

            // Do the computer move
            int iGridElement = 0;
            bool bFoundFreeSquare = False;

            // Loop while the conditions are true

            while(iGridElement < 9 and bFoundFreeSquare == False)
            {
                // Is this a free square?
                if(GridStatus[iGridElement] == FREESQUARE)
                {
                     GridStatus[iGridElement] = OSQUARE;

                     // Flag we want to exit the loop
                     bFoundFreeSquare = True;
                }

                iGridElement++;
             }
         }
    }

    redraw();
}

 

The code in the sample download for this tutorial commented in a lot of detail so it's a good idea if you download and read through it. This code is straightforward, there's just a little bit more of it needed to calculate where the click was, and to make a computer-move.

That's all the logic we need for the game to work, time to test it out!

Click the "Compile" icon ( )on the toolbar

Click the "Execute" icon ( ) on the toolbar

 

Reusable Code in Functions

If you've played the game you just coded you'll notice a pretty serious limitation. Once you've filled up the grid squares and it's game over, it really is game over. Unless you unload and reload the plugin you can't play another game. What we need to do is to reset the grid once it's been filled.

We already have the code to reset the grid status array in our Init handler, it's this code:

for(int iGridElement = 0; iGridElement < 9; GridElement = iGridElement + 1)
{
    // Set each element to a free square
    GridStatus[iGridElement] = FREESQUARE;
}

 

The problem is, we need this code in the mouse handler because that's where we know when the last move has been made. We could just copy and paste this code into the mouse handler but that's very messy. Duplicating code isn't a good idea because if you need to make a change in future (maybe to use to a 6*6 grid instead) you have to remember all the places you pasted the array reset code.

You might be wondering why we don't just add the reset code to the Render handler. This would actually work perfectly, but it's not very efficient. We're adding more code to the Render handler that has to be executed every time the plugin redraws its display. If we add the reset code to the mouse handler we can make sure we only reset the array when the last mouse click (the one that completes the grid) is received.

It's much better to put the code in a function and call it whenever we need it.

C coders! Unlike C, Shiny allows use of functions without declaring/prototyping them first. You can quite happily call a function on line 1 of your code and not implement it until line 100. Smarter than a C compiler, that's us ;)

 

In Shiny, functions look exactly like handlers. Creating them is very easy - just pick a name for your function and declare it like this:

function ResetGridStatus()
{
    // Function code goes in here
}

 

To use the function (execute the code inside it), simply call it like this:

// Now reset the grid status
ResetGridStatus();

 

Simple! Let's move the array reset code into a function first.

Create a new function declaration at the bottom of your source code, like this:

function ResetGridStatus()
{
}

 

Move the array reset code from the Init handler to the new function:

function ResetGridStatus()
{
    for(int iGridElement = 0; iGridElement < 9;
        iGridElement = iGridElement + 1)
    {
        // Set each element to a free square
        GridStatus[iGridElement] = FREESQUARE;
    }
}

 

In the Init handler add a function call where the array reset code was:

createimage(SymbolX, "SSE\\TicTacImages\\tictac-x.gif")
createimage(SymbolO, "SSE\\TicTacImages\\tictac-o.gif");

ResetGridStatus();

 

Although we moved some code around we didn't change the logic of the game. Build and test the plugin just to confirm everything still works.

Click the "Compile" icon ( ), then the "Execute" icon ( ) on the toolbar

 

Handling Game Over

Now that we can call our array reset function at any time, we can add some code to the OnMouseLButtonUp handler to call it when the grid is full. So how do we know when the grid is full?

Here's a segment of the code we added to the OnMousLButtonUp handler earlier in the tutorial.

bool bFoundFreeSquare = False;

// Loop while the conditions are true

while(iGridElement < 9 and bFoundFreeSquare == False)
{
    // Is this a free square?
    if(GridStatus[iGridElement] == FREESQUARE)
    {
        GridStatus[iGridElement] = OSQUARE;

        // Flag we want to exit the loop
        bFoundFreeSquare = True;
    }

    iGridElement++;
}

(it starts on line 149 of the sample code download for this tutorial).

If you look closely at this loop you can see that the bFoundFreeSquare variable is used to track whether (unsurprisingly) a free square was found in the grid. If one wasn't found it remains set to False.

So to check if there were any free squares left in the grid all we need to do is to check the bFoundFreeSquare variable after the while loop has finished. If it equals False, we call our grid reset function to start the game again.

Add the following code after the end of the while loop:

    iGridElement++;
}

// No free squares? Reset the grid for another game
if(bFoundFreeSquare == False)
    ResetGridStatus();

 

If you test the code now, when the last click is made in the last free grid square the game is reset and we can play again.

Click the "Compile" icon ( ), then the "Execute" icon ( ) on the toolbar

 

Okay, so it's not 100% user friendly because the grid is wiped immediately. If we were going to develop a full blown game then we'd add a variable that tracks if the game is over and probably waits for another click to signify the user wants to restart the game. We could even add an image of a button saying "Reset Game" that resets the grid no matter when the user clicks on it. As a learning exercise you should add some features to the game to make sure you understand all the principles we've covered so far.

 

A Little More on Functions

Although this isn't related to the game it's a good time to discuss how to use the full range of function features. So far we've seen how to call a function that performs all its actions internally, but what about a function that acts on some values?

Imagine that adding 2 to a number was a very long and complex operation, and we needed to do it many times in our code. The obvious solution is a function that reuses the same addition code, but how do we get it to add 2 to a number?

The answer is function parameters. Parameters are values you pass to a function so that it can use them in whatever code it needs to. It means we don't need loads of global variables just to get data moving between parts of our code. Here's how to define a function that accepts a parameter of one integer:

function AddTwo(int Victim)
{
    Victim = Victim + 2;
}

 

To call this function simply add the value into the function call brackets:

// Add 2 to 1
AddTwo(1);

// Add 2 to the value of MyVal
AddTwo(MyVal);

 

To accept more than one parameter in a function simply add another declaration separated by a comma:

function AddTwo(int Victim, string Message)
{
    Victim = Victim + 2;
    drawtext(Message, 0, 0);
}

 

And to call it just add another value or variable to the call:

// Add 2 to 1
AddTwo(1, "Now adding 2 to 1");

// Add 2 to the value of MyVal
AddTwo(MyVal, "Now adding 2 to MyVal");

 

All this code has one small problem though. When a function is doing something "internally" (eg: our ResetGridStatus function did all the work inside itself and didn't need to accept parameters or provide any new values) then we're ok. But what if we need our function to give us something back? Everything in the function is local scope (scope was covered in tutorial 3) so unless we make a global variable we can't get any data out of it. And we just said that we didn't want to use lots of global variables.

Wouldn't our AddTwo function be far more useful if it returned the result of adding 2? Sure it would, and it's very easy to do. We simply declare the type of variable our function returns before the function declaration, like this:

int function AddTwo(int Victim)
{
}

 

Then to return the result of the operation we add a return statement to the end of the function body:

int function AddTwo(int Victim)
{
    Victim = Victim + 2;

    return Victim;
}

 

The return statement says, "make this value available to the statement that called this function". Call a function that returns a value like this:

int iMyValPlus2 = AddTwo(MyVal);

 

It is perfectly fine to call a function that returns a value without doing anything to it. For example, this is legal:

AddTwo(MyVal);

 

Even though AddTwo() returns an integer and we're not assigning it to anything, this is perfectly legal. You don't always have to store or act on a return value. This is useful if you sometimes want to ignore an error check:

function Init()
{
    SetupStuff();
}

bool function SetupStuff()
{
    if(Today = "Monday")
    {
        return True;
    
}
    else
    {
        return False;
    }
}

Here we're calling the SetupStuff function and ignoring the return value.

Although you can ignore returned values when you call a function, the function itself must always obey its return type. For example, if you declare a function like this:

string function SetupStuff()
{  
}

 

You must include a return statement that returns a string, like this:

string function SetupStuff()
{
    return "Any old string";
}


If you forget to include a return statement the compiler will generate an error:

(Line 39) Function requires a return statement

 

If you return an incompatible type, or forget to return a type at all like this:

string function SetupStuff()
{
    return;
}

 

The compiler will generate an error like this:

(Line 39) Function contains incompatible return type statements

 

Something a little curious

All this talk of returning variable types brings up another important part of Shiny. Add the following function to your code (it doesn't matter where, this is just to demonstrate something - you can delete it right after):

string function ThatsOdd()
{
    return 99;
}

 

Compile the code (don't bother executing it). You're going to get a compiler error, right? We declared the function as one that returns a string, and we tried to return a number. Just a few lines above we learned about incompatible return types, so there's no way this is going to work...

0 error(s) were found

Er, what? We're returning an integer in a function that has a return type of string. How can that possibly work?

Good question, and it's all part of the Shiny language. You'll find out the answer in the next tutorial!

 

 

A Quick Recap

This tutorial has seen us complete the fundamentals of the tictactoe game, so from now on everything is just adding features and frills. You should try to make your own additions too.

In this tutorial you saw:

  • How to properly handle user input
  • How to create and call functions

In the next tutorial we're going to take a short diversion away from our game and learn about the internals of the Shiny language. You'll see how the variable typing works, how to review the Shiny compiler output to check your code and most importantly, how to access the Brass Virtual Machine debug interface.