TacTicToeTwo

This is a continuation from this post. I'll recall, from the that blog post for posterity:
  1. Player x selects a space on the grid to place their block.
  2. Game checks to make sure that space is empty.
  3. Game places block on that space, updates the grid in code.
  4. Game checks if a 3-in-a-row has occurred. If true, step 5, else step 6.
  5. Game stops, congratulates Player x on a Win.
  6. Game checks if all spaces are filled, in which case it is a Draw.
  7. Otherwise, Game swaps control to Player y, and moves back to step 1 with x and y reversed.
Time to actually get started on the OnGridSelected. Since I am already using GridBox to give the location clicked, I may as well use it for the actual in-game positions that I will be placing the blocks from, so I've added in an extra parameter asking for the Vector3 position of the GridBox.
NOTE: It seems to be some kind of universal good etiquette to refer to components of a GameObject by an instantiation rather than a direct reference.

if (grid[x, y] != 0)
{
    //not empty, so ignore it.
    Debug.Log("[" + x + "," + y + "]" + " is already set to " + grid[x, y] + "! Scram!!");
    return;
}

GameObject blockObj;

if (turn) //P1
{
    blockObj = Nought;
}
else //P2
{
    blockObj = Cross;
}

Vector3 blockPosition = new Vector3(
    gridPos.x,
    gridPos.y + 1,
    gridPos.z
    );

Instantiate(blockObj, blockPosition, new Quaternion());

This block of code when tested does indeed spawn a NOUGHT block (since turn doesn't currently ever change). The first part (if (grid[x, y] != 0)) handles step 2 of my planned code structure, and the code following fulfils the first part of step 3, seen below.



In fact it spawns many NOUGHT blocks without limit, so I also need add in the code to keep track of what space is filled (as I have disabled additional placement on occupied tiles from step 2). I could opt for checking if the actual 3D space is occupied, but that's going a bit overboar, after all - KISS. Since its a simple grid I'll stick to adding a number (either 1 or -1, I'll explain why shortly) to an array and check against that instead, as it's more binary/controlled than looking at if an object occupies a certain 3D space. 

int blockNum;
if (turn) //P1
{
    blockNum = 1;
}
else //P2
{
    blockNum = 2;
}
grid[x, y] = blockNum;

There is a bit of redundancy, so its better to pair off the if (turn) conditions here with the code above. Build and run and as you can see, the blocks are limited to one per space, with the console declaring further spaces invalid.



Now for actually checking. Since any three-in-a-row is a win, then I can approach the problem of finding out if a win has occurred using a method similar to magic squares. This is also the reason why I've assigned Os and Xs to 1 and -1. For each vertical, horizontal and diagonal line, a win event should only occur when the total adds up to 3 or -3. There are several ways to go about checking win conditions in this manner. In its raw form, I would be checking for [0,0]+[0,1]+[0,2], then [1,0]+[1,1]+[1,2] and so on. But I'm too lazy, and its rather time-consuming to type out all the conditions, so I'll instead opt to let the computer fill in the values for me using FOR loops in place of the x co-ordinate, making it more of [i,0]+[i,1]+[i,2] checking against all values of i (in this case 0 to 2). Additionally, in terms of removing redundancy, I tried to use:
if ((grid[0, i] + grid[1, i] + grid[2, i]) == 3)
if ((grid[0, i] + grid[1, i] + grid[2, i]) == -3)
To check if a nought of a cross win had been met, but instead I opted to go for adding the values together and storing it as a local integer "lineTotal" then checking against that.
NOTE 1: For those like myself who for a long time have never known what is or seen an enumerable, it's effectively like a multiple choice boolean, good for things like object states. Read more here.
NOTE 2: I'm only doing it for the sake of brevity; please try not to stuff all of a condition's code into one line. It's not very good etiquette. It's like the social equivalent of a junior delinquent.

int lineTotal = 0;

//horizontal
for (int i = 0; i < 3; i++)
{
    lineTotal = grid[0, i] + grid[1, i] + grid[2, i];
    //early break out of code since the game status has changed.
    if (lineTotal == 3) { gStatus = GameStatus.WinO; return; }
    if (lineTotal == -3) { gStatus = GameStatus.WinX; return; }
}
//vertical
for (int i = 0; i < 3; i++)
{
    lineTotal = grid[i, 0] + grid[i, 1] + grid[i, 2];
    if (lineTotal == 3) { gStatus = GameStatus.WinO; return; }
    if (lineTotal == -3) { gStatus = GameStatus.WinX; return; }
}
//diagonals
//top left to bottom right
lineTotal = grid[0, 0] + grid[1, 1] + grid[2, 2];
if (lineTotal == 3) { gStatus = GameStatus.WinO; return; }
if (lineTotal == -3) { gStatus = GameStatus.WinX; return; }
//top right to bottom left
lineTotal = grid[2, 0] + grid[1, 1] + grid[0, 2];
if (lineTotal == 3) { gStatus = GameStatus.WinO; return; }
if (lineTotal == -3) { gStatus = GameStatus.WinX; return; }

//draw condition
bool noSpaces = true;
for (int y = 0; y < 3; y++)
{
    for (int x = 0; x < 3; x++)
    {
        if (grid[x, y] == 0) noSpaces = false;
    }
}
if (noSpaces) gStatus = GameStatus.Draw;

Implementing the Xs is easily done by toggling the "turn" boolean between grid placing (turn = !turn;). A quick test and a look at the debug log shows the code running successfully.




That just about does it for gameplay. Next is sorting out the UI, so X wins or O wins or Draw when the event happens, and then a click to restart. 

No comments:

Post a Comment