Wednesday, July 18, 2012

Let's Make Us A Game II-1: Basic Logic



Before I get into anything fancy with my Tic-Tac-Toe Bastard game, I'm going to sit down and work through some basic game logic. I want to code the foundations in pure Lua with text output to the console, to get a bare-bones version of the game working for faster and easier debugging.  We should be able to reuse a lot of this material as we layer graphics, sound and GUI elements into the build later on, but there's quite a bit of groundwork we can do first and it's good to keep things simple while we create the core.

We'll represent the tic-tac-toe grid using a simple 9-cell table structure.  While our visual representation is a 3 x 3 grid, we really don't need to make the table that complicated; we'll just lay out the 9 cells in a single row, numbered as follows, and display the grid to the user in the expected manner:

1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9

(Note that Lua, unlike many computer languages, natively starts numbering at 1 -- this makes intuitive sense, but it takes some getting used to for programmers used to counting from 0!)

Each of these cells will start in an empty state, and will be marked by either the player or the CPU as the game progresses.  For readability, we'll declare three useful constants at the top of the routine -- the numeric values really don't matter, as long as they are unique:

EMPTY_ID = 0;
MY_ID = 1;
CPU_ID = 2;


Now we'll declare and initialize the tic-tac-toe grid accordingly:


gGrid = { EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID };

With this structure and the constants we've set up, all we need to do to record a player's move is set an element of the gGrid array to either MY_ID or the CPU_ID.


One of the logic challenges we need to deal with is recognizing when somebody has won the game.  We could do this heuristically, searching through the grid for patterns indicating a win, but there are a finite number of combinations to consider.  Our code will run faster and more simply if we just define all of the winning combinations in a table of tables, like so:


POSSIBLE_WINS = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9},
                  {1, 4, 7}, {2, 5, 8}, {3, 6, 9},
                  {1, 5, 9}, {7, 5, 3} };


(We could also use this approach to impose rule variations on the player -- an "only verticals win!" round, for example, although that seems unlikely to be much fun in a game of Tic-Tac-Toe, so we probably won't do that.)

For reasons that may (or may not) become clear later, I'm going to treat the human player and the CPU as "players" by setting up another table:

   PLAYERS = { MY_ID, CPU_ID };


This approach makes the main game loop simpler -- we just need to loop through the players, giving each one a turn until somebody wins.  (This will also simplify our lives later on if we decide to add a player-vs.-player option, or a CPU-vs.-CPU exhibition mode, or even an online mode or a more-than-two-players mode.)


We will still need to distinguish between the human player and the CPU at another level -- when the main game loop looks for input from the current player, we'll want to solicit interactive input from a human player and call upon an algorithm for the CPU's move.  But the main game loop can just solicit a move from the current "player", however that is defined, check for victory, and end the game if that player has won.


My initial scripting goal for this installment is to establish the Tic-Tac-Toe grid, mark it up, and verify that we can detect a victory condition and identify the winner.  So I'm not going to implement any real user or CPU action -- I'll just let the code select a random number from 1 to 9.  In fact, I'm not even going to check for duplicates, so our players can just mark right over each other.  I just want to set things up so that the game can run until somebody wins a single match.

My main game loop logic needs some work at this stage -- it always forces the first player listed in the PLAYERS table to go first, and it's not even really using the PLAYERS table correctly; it only works because our indices {1, 2} happen to match the PLAYER_ID and CPU_ID values.  If we wanted to mix up the play order we would have to fix that, so that then we could just restructure the PLAYERS table to reorder the participants.  My code also breaks out of the for-player loop using a "break" command, which is less than elegant.  And there are some conditions it doesn't catch, like playing to a draw, which is entirely possible.  But we'll have time to refine this in a future installment.

The checkVictory function accepts a grid and a player ID, and uses the POSSIBLE_WINS table to see if that ID has occupied a winning set of squares on the grid.  This routine will probably not change much as we develop the game.  (Note that I could reference the actual game grid directly as a global variable, but passing it in as a table keeps the local code clean, and the ability to handle multiple grid instances might prove very useful for our AI implementation later on.)

For now, I've defined a getMove function that takes an ID from the main loop, and then decides how to obtain move input.  Right now, the supporting getPlayerInput and getCPUInput functions do exactly the same thing, picking a random number from 1 to 9, but we'll change that later.


I've also written a throwaway utility function -- doPrintGrid renders the game grid to the Lua console, in a readable text format.  This routine is handy for now but will probably be discarded later in the development process.


All we can really do with this initial version is run the code and see whether it stops when victory is randomly achieved -- which it does!  The code so far is below the fold.





-- set up constants

EMPTY_ID = 0;
MY_ID = 1;
CPU_ID = 2;

PLAYERS = { MY_ID, CPU_ID };

POSSIBLE_WINS = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9},
                  {1, 4, 7}, {2, 5, 8}, {3, 6, 9},
                  {1, 5, 9}, {7, 5, 3} };

-- set up structures

gGrid = { EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID, EMPTY_ID };

-- utility functions

function doPrintGrid(grid)

    print("GRID:");
    print(grid[1], "|", grid[2], "|", grid[3]);
    print("----------------------------------");
    print(grid[4], "|", grid[5], "|", grid[6]);
    print("----------------------------------");
    print(grid[7], "|", grid[8], "|", grid[9]);
    print " ";

end;

-- game logic functions

function checkVictory(grid, id)

   local won = false;

   for i in pairs(POSSIBLE_WINS) do

       if (grid[POSSIBLE_WINS[i][1]] == id and
           grid[POSSIBLE_WINS[i][2]] == id and
           grid[POSSIBLE_WINS[i][3]] == id) then

          won = true;
       
        end;

   end;

   return won;

end;

-- GUI functions

function getPlayerInput()

   square = math.random(1, 9);

   return square;

end;

function getCPUInput()

   square = math.random(1, 9);

   return square;

end;

function getMove(id)

   local square = nil;

   if id == MY_ID then
      square = getPlayerInput();
   else
      square = getCPUInput();
   end;

   return square;

end;

-- main code

local gameOver = false;
local winner = EMPTY_ID;

while not gameOver do

   for i in pairs(PLAYERS) do

      gGrid[getMove(i)] = i;

      if checkVictory(gGrid, i) then
         winner = i;
         gameOver = true;
       end;

       print("PLAYER ", i, " moved...");
       doPrintGrid(gGrid);

       if gameOver then break; end;

    end;
end;

print ("GAME OVER!", winner, "WINS!");

No comments:

Post a Comment