Advertisement

Build an Intelligent Tic-Tac-Toe Game with AS3

by

"Tic-Tac-Toe, that's boring!" you might think. In this tutorial I will show you that building a Tic-Tac-Toe game is anything but boring. It's the ideal game to create when you would like to learn ActionScript 3.0. While building it you'll learn a great deal about functions and event listeners, you will see how easy it is to customize the graphics of a game and you will even learn how to program Artificial Intelligence (AI)!

Final Result Preview

Let's take a look at the final result we'll be working towards:


Step 1: Preparations

We'll begin by creating a new Flash file (File > New > Flash File (ActionScript 3.0)). Set the stage's dimensions to 300x300 and use a black background. In the properties (Window > Properties), fill in "TicTacToe" as the class definition. Now save the file in your project folder (e.g. Desktop\MyFirstGame\) as "TicTacToe.fla".

Next, we'll set up the AS3 files which we'll be using. We begin with three .as files, which we also save in our project folder. Write the following code and save the files under the same name as the class name. So the TicTacToe class you save as "TicTacToe.as", etc.

package
{
	import flash.display.MovieClip;
	
	public class TicTacToe extends MovieClip
	{
		public function TicTacToe():void
		{
			trace("TicTacToe");
		}
	}
}
package
{
	import flash.display.MovieClip;

	public class Game extends MovieClip
	{
		public function Game(num:uint):void
		{
			trace("Game");
		}
	}
}
package
{
	import flash.display.MovieClip;
	
	public class Tile extends MovieClip
	{
		public function Tile():void
		{
			trace("Tile");
		}
	}
}

The first class, TicTacToe.as, will be our document class. Not sure what this means? Read this quick introduction to document classes.


Step 2: A Bit More Background

We now have four files in our project folder: one .fla file and three .as files. When you test your Flash movie (Control > Test Movie), you will see a stage with a black background and "TicTacToe" in the Output screen (Window > Output). Great, but what will we do with it?

The TicTacToe.as file will handle our menu functionality, which we'll write at the end of this tutorial (it's more fun to get a working game as fast as possible, right?). The menu will have only two buttons, one player or two players.

The Game.as file will handle our game. This is also why we ask for an argument "num"; this will define the number of human players. we'll start off with 2, but later on we also want to write functionality for when there is only one player. In this case, the computer must play as player two.

The last file, Tile.as, we'll be using to create a (you guessed it), tile. By tile I mean one of the nine places where you can put an O or X (or an image of a flower, your pet, a logo, you name it).


Step 3: Graphics Grid Lines

We'll begin by creating the lines. You can be as creative as you want, but make sure the lines do divide the stage into 9 equal blocks. Here's what I did: I made a new MovieClip (Insert > New Symbol) and drew 2 horizontal lines and 2 vertical lines of length 300px. The horizontal lines I set at x = 0 and ( y = 100 || y = 200 ). The vertical lines I set at ( x = 100 || x = 200 ) and y = 0.

In your library (Window > Library), right-click your new MovieClip, go to Properties and check the box that says "Export for ActionScript". Fill in "Grid" as the class definition.


Step 4: Graphics Tiles

Next we'll make the O and X tiles. You can use any image you'd like, although you should keep one thing in mind: our grid divides our stage in 9 square tiles of 100x100px, so each tile should be no bigger than 100x100. Here's what I did: I created a new MovieClip and made a shape with the Rectangle Tool of 100x100. This shape I aligned so the left top is the registration point of the MovieClip. The fill color can be anything, but set it to alpha 0%. This way your tile MovieClip is always 100x100 and you can draw whatever image you'd like over the 100x100 shape.

You can delete the 100x100 background after you draw the X and O shape, but it shouldn't really matter. It's a tiny game so there's no need to delete the shapes with performance in mind. I find it easy to leave them be so I can change the graphics later on without losing the orientation of where I can place everything. You just have to select both graphics (the 100x100 shape and your own graphic) and align them to center (make sure Align To Stage is off), and you're done.

These are the two MovieClips I made. The two small eggs on the right are the final results. I will not go into detail of how to create cool egg figures, this tutorial is about creating the game. Experiment with your own graphics, and try out new things like using the timeline of your MovieClip.

Export both MovieClips for ActionScript the same way you did with the Grid. Make sure you supply the class definition names TileO and TileX. We now have three items in our library: Grid, TileO and TileX. We only need two more before we can do some scripting!


Step 5: Graphics Highlighter

Like I said, we are almost set to do some scripting, but we need two more graphics. When someone wins the game, we do not want to start a new game right away but rather see the winning line first. For this we need a shape to position on top of the tile when the tile is part of the winning line. I simply created a white 100x100 shape with alpha set to 50%, but I know you can do better. You can create a nice green V sign, a happy smile, you name it. Export the MovieClip for ActionScript with the class definition TileE (E for "END"; I have a thing about short names, and in a simple game such as this you'll remember what it stands for).

The last graphic you need to create is the "button: tile, to which we apply all event listeners in a tile. Again, create a 100x100 shape with alpha set to 0%. Make sure, like all tile graphics we created, that the registration point is set to the top left corner. Export the MovieClip for ActionScript with the class definition TileB (B for "BUTTON").

We now have five MovieClips in our library which we'll be using to build the Tic-Tac-Toe game.


Step 6: New Game

We'll start slowly. Go to your document class (TicTacToe.as) and apply the following changes:

package
{
	import flash.display.MovieClip;
	
	public class TicTacToe extends MovieClip
	{
		public function TicTacToe():void
		{
			var game:Game = new Game(2); // Add a new game
			addChild(game); // Add the game to the display list
		}
	}
}

That's all. When we now test the Flash Movie, the Output window will show Game. We supply the 2 as an argument for the Game() constructor because we want to begin with building the game for two players. We also add the game to the display list, but won't see anything because the game class only contains a trace statement at the moment.


Step 7: Tiles!

Would be fun if this was step 9, wouldn't it?

...

Anyway! We're going to add the tiles to the game. I'll add a lot of code at once, and the result will be that we won't see anything except 9 traces with the word "Tile", but it's worth it. I will explain all code, but try to read and understand it yourself first.

package
{
	import flash.display.MovieClip;
	import flash.events.Event;

	public class Game extends MovieClip
	{
		public var numPlayers:uint;
		public var turn:uint;
		private var _grid:Grid = new Grid();
		private var _tiles:Array = new Array();
		
		public function Game(num:uint):void
		{
			numPlayers = num;
			turn = 0;
			addVisuals();
			addTileListeners();
			nextTurn();
		}
		
		private function addVisuals():void
		{
			var tile1:Tile = new Tile();
			var tile2:Tile = new Tile();
			var tile3:Tile = new Tile();
			var tile4:Tile = new Tile();
			var tile5:Tile = new Tile();
			var tile6:Tile = new Tile();
			var tile7:Tile = new Tile();
			var tile8:Tile = new Tile();
			var tile9:Tile = new Tile();
			tile1.x = tile4.x = tile7.x = tile1.y = tile2.y = tile3.y = 0;
			tile2.x = tile5.x = tile8.x = tile4.y = tile5.y = tile6.y = 100;
			tile3.x = tile6.x = tile9.x = tile7.y = tile8.y = tile9.y = 200;
			addChild(tile1);
			addChild(tile2);
			addChild(tile3);
			addChild(tile4);
			addChild(tile5);
			addChild(tile6);
			addChild(tile7);
			addChild(tile8);
			addChild(tile9);
			addChild(_grid);
			_tiles.push(tile1, tile2, tile3, tile4, tile5, tile6, tile7, tile8, tile9);
		}
		
		private function addTileListeners():void
		{
			for (var i:int = 0; i < _tiles.length; i++) {
				_tiles[i].addEventListener("TILE_ACTIVE", nextTurn);
			}
		}
		
		public function nextTurn(e:Event = null):void
		{
			trace("Next turn!");
			turn += 1;
		}
	}
}

As you can see, first I create some variables. The numPlayers variable keeps track of how many players there are playing. We need this later on when we add the AI. Same goes for the turn variable, which keeps track of whose turn it is. We also create a _grid, an instance of the Grid symbol, which we want to display. Finally, the _tiles array will keep track of all tiles.

Functions are your friend, so I wrote a new function to add the tiles, addVisuals() (you can write all code in the constructor, but this is tidier). In this function, we first create nine new tiles. The x and y positions of the tiles are set in such a way that the tile will be positioned as a mobile phone keypad:

            x = 0    x = 100   x = 200

y = 0       tile1     tile2     tile3
y = 100     tile4     tile5     tile6
y = 200     tile7     tile8     tile9

We add each tile to the display list (which we won't see, because the tiles do not have visuals yet), and also push it to the _tiles list (in the correct order, this is very important for the AI/win check later on). The final addChild() call is for the grid, which we want to be positioned on top of all tiles. Last but not least, we add event listeners to all tiles with the function addTileListeners(). The event we listen to does not exist yet, but we'll dispatch it from the tile class later on. When the event is triggered, it fires the function nextTurn which will handle the turns and check for winning lines. Of course we call the nextTurn() function in the constructor so we can begin the game.


Step 8: AI The Beginnings

It's possible to create a very complex AI system, but since Tic-Tac-Toe is a rather small game with limited options, it's better to take the easy way out. We added all tiles to the _tiles Array. The first tile (tile1) is added first, so it has index = 0. The second tile has index = 1, etc. If we visualize this on screen, it would look like this:

0 1 2
3 4 5
6 7 8

Here is how we'll check whether someone has three in a row: we create an array of all possible combinations with which you can win. For example, 0 1 2 is a winning combination, and so is 1 4 7 and 2 4 6 (diagonal). Add the following variable to your Game.as script:

private var _combos:Array = new Array(new Array(0, 1, 2), new Array(3, 4, 5), new Array(6, 7, 8),
                                      new Array(0, 3, 6), new Array(1, 4, 7), new Array(2, 5, 8),
                                      new Array(0, 4, 8), new Array(2, 4, 6));

Later on, we'll check the index of all O and X tiles in the _tiles Array. When the indices of, for example, the four X tiles are 0 4 5 and 8, we can check if a winning combination matches these indices. For example, there is a 0, but no 1 and 2, so the X tiles didn't match the 0 1 2 winning combination. We'll do this for every combination, and eventually see that the combination 0 4 8 has been made. The script for this will be provided a few steps later.


Step 9: .. Tiles on Screen!

Got to make the joke after all. I might spook you with the following code for Tile.as, but I'll ask you once more: try to figure it out yourself and after read my comments on the code. It might seem a lot, but if you go through it and read the comments you'll understand how this works.

package
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;
	
	public class Tile extends MovieClip
	{
		public var isSet:String; // Variable to hold which player clicked this tile
		
		private var tileB:TileB = new TileB();
		private var tileE:TileE = new TileE();
		private var tileO:TileO = new TileO();
		private var tileX:TileX = new TileX();
		
		public function Tile():void
		{
			isSet = "not set"; // The tile is still free for players to use
			tileO.visible = false; // Set the tile visibility to false
			tileX.visible = false;
			tileE.visible = false;
			addChild(tileO); // Add the graphics in this order!
			addChild(tileX);
			addChild(tileE);
			addChild(tileB); // Button needs to be on top
			addButtonListeners(); // Function to add the listeners
		}
		
		public function addButtonListeners():void // Public, as we want to use it from the game class later on
		{
			this.buttonMode = true;
			tileB.addEventListener(MouseEvent.MOUSE_OVER, showGraphic);
			tileB.addEventListener(MouseEvent.MOUSE_OUT, hideGraphic);
			tileB.addEventListener(MouseEvent.CLICK, chooseGraphic);
		}
		
		public function removeButtonListeners():void // Public, as we want to use it from the game class later on
		{
			this.buttonMode = false;
			tileB.removeEventListener(MouseEvent.MOUSE_OVER, showGraphic);
			tileB.removeEventListener(MouseEvent.MOUSE_OUT, hideGraphic);
			tileB.removeEventListener(MouseEvent.CLICK, chooseGraphic);
		}
		
		private function showGraphic(e:MouseEvent):void
		{
			if (Game(this.parent).turn % 2 == 0) { // turn % 2 is always either 0 or 1: 0%2 = 0, 1%2 = 1, 2%2 = 0, 3%2 = 1, etc.
				tileO.visible = true; // It's O's turn, show the O graphic
			} else {
				tileX.visible = true; // It's X's turn, show the X graphic
			}
		}
		
		private function hideGraphic(e:MouseEvent):void
		{
			tileO.visible = false; // Hide the graphics
			tileX.visible = false; // For both O and X
		}
		
		private function chooseGraphic(e:MouseEvent):void
		{
			removeButtonListeners(); // Remove the listeners (this tile is now disabled)
			isSet = (Game(this.parent).turn % 2 == 0) ? "O" : "X"; // Set the isSet variable to O or X
			trace(isSet);
			dispatchEvent(new Event("TILE_ACTIVE")); // Dispatch the event
		}
	}
}

Did you already test it? Because if all went well, you'll see a working game now! Let's discuss the script for a bit.

We start with creating all the graphics. Each one of the nine tiles has the TileB, TileE, TileX and TileO graphic. In the constructor (public function Tile()) we add these graphics to the display list and set the visibility to false. Only the TileB graphic stays on visible = true, because we want to add our event listeners to this graphic. No problem; that's why we set the alpha to 0%.

Both the addButtonListeners() and removeButtonListeners() functions speak for themselves. When we position the mouse over a tile, we want the correct graphic to display. When we move the mouse out of a tile, we want to hide the graphic. When we click, we want it to show the graphic and keep it visible so the next player can play. We do this with the modulo operator.

In computing, the modulo operation finds the remainder of division of one number by another - Wikipedia.

When we click the tile, the chooseGraphic(e:MouseEvent) function triggers. In this function we remove all listeners, set the isSet variable (trace it) and fire the TILE_ACTIVE event. Next step: check who won!


Step 10: Good Game!

Let's begin easy: add the following script to the Tile.as file to handle a winning situation:

public function showEnd():void { // Added to show the tileE
		tileE.visible = true;
}

Easy right? It's a public function so we can call it from the Game class, and all it does is set the visibility of tileE to true. Now open up Game.as again and get ready for some serious scripting. We'll add one variable, edit the nextTurn() function and add two new functions:

package
{
	import flash.display.MovieClip;
	import flash.events.Event;

	public class Game extends MovieClip
	{
		public var numPlayers:uint;
		public var turn:uint;
		public var left:uint = 10; // Added to keep track of free tiles; 10 because we do -1 once without marking a tile
		private var _grid:Grid = new Grid();
		private var _tiles:Array = new Array();
		private var _combos:Array = new Array(new Array(0, 1, 2), new Array(3, 4, 5), new Array(6, 7, 8),
											  new Array(0, 3, 6), new Array(1, 4, 7), new Array(2, 5, 8),
											  new Array(0, 4, 8), new Array(2, 4, 6));
		
		public function Game(num:uint):void
		{
			numPlayers = num;
			turn = 0;
			addVisuals();
			addTileListeners();
			nextTurn();
		}
		
		private function addVisuals():void
		{
			var tile1:Tile = new Tile();
			var tile2:Tile = new Tile();
			var tile3:Tile = new Tile();
			var tile4:Tile = new Tile();
			var tile5:Tile = new Tile();
			var tile6:Tile = new Tile();
			var tile7:Tile = new Tile();
			var tile8:Tile = new Tile();
			var tile9:Tile = new Tile();
			tile1.x = tile4.x = tile7.x = tile1.y = tile2.y = tile3.y = 0;
			tile2.x = tile5.x = tile8.x = tile4.y = tile5.y = tile6.y = 100;
			tile3.x = tile6.x = tile9.x = tile7.y = tile8.y = tile9.y = 200;
			addChild(tile1);
			addChild(tile2);
			addChild(tile3);
			addChild(tile4);
			addChild(tile5);
			addChild(tile6);
			addChild(tile7);
			addChild(tile8);
			addChild(tile9);
			addChild(_grid);
			_tiles.push(tile1, tile2, tile3, tile4, tile5, tile6, tile7, tile8, tile9);
		}
		
		private function addTileListeners():void
		{
			for (var i:int = 0; i < _tiles.length; i++) {
				_tiles[i].addEventListener("TILE_ACTIVE", nextTurn);
			}
		}
		
		public function nextTurn(e:Event = null):void
		{
			if (!checkWin()) {      // Only if the checkWin function returns false, continue
				left -= 1;          // Someone chose a tile so there's one less left to choose from
				if (left > 0) {     // While there are tiles left...
					turn += 1;      // Add one to the turn (next player is up)
				} else {
					endGame();      // Else end the game (it's a draw)
				}
			}
		}
		
		private function checkWin():Boolean
		{
			var won:Boolean = false;                            // First set a boolean to false
			for each (var combi:Array in _combos) {             // For each combi (array) in our _combos array
				var xwin = true;                                // Assume x won
				var owin = true;                                // Assume o won
				for each (var tile:int in combi) {              // For each tile index (number) in our combi array
					xwin = xwin && _tiles[tile].isSet == "X";   // Set xwin and owin to true, but only when there is an X or O
					owin = owin && _tiles[tile].isSet == "O";   // with the same index as the number in the combi array
				}
				if (xwin || owin) {                             // If either xwin or owin is true
					endGame(combi);                             // End the game (supply the combi)
					won = true;                                 // Set won to true
					break;                                      // Break out of the loop (important! there is a winning combination so no more checking needed)
				} else {
					won = false;                                // Else set won to false (no combination yet)
				}
			}
			return won;                                         // Return won
		}
		
		private function endGame(combi:Array = null):void
		{
			for (var i:int = 0; i < _tiles.length; i++) {       // For each tile in the tiles Array
				_tiles[i].removeButtonListeners();              // Remove the listeners
				if (combi) {                                    // If there is a combi supplied (someone won)
					for each (var j:int in combi) {             // Check each number in that combi...
						if (i == j) _tiles[i].showEnd();        // And highlight the corresponding tile with showEnd()
					}
				}
			}
		}
	}
}

We begin with the left variable. When this hits zero, it means there are no more tiles left and the game is over. Usually this means a draw.

Next up is the nextTurn(e:Event = null) function, which first only had turn += 1 in it. As you can see, it now has more conditions. Each time the function is called, we check if someone won. If not, we know there is one tile less to choose from, so we do left -= 1. As long as there are tiles left, we can let the other player make his/her move. No more tiles left means the end of the game (a draw, since we only reach this part of the script if there wasn't a winning situation yet).

The checkWin() function is best explained with an example. Let's take the following situation:

tile1 = X     tile 4 = O
tile2 = X     tile 5 = O
tile3 = X

Tile1 has index 0 in the _tiles Array. The first for each statement checks all the Arrays nested inside the _combos Array. The first one contains "0 1 2". The second for each statement handles these numbers (which represent the indices of the tiles). This is what happens in the second for each statement:

first combo  = 0 1 2

- FIRST PASS                   _tiles[0].isSet = X
var tile     = 0
xwin (true)  = xwin (true)  && _tiles[0].isSet == X (true)
owin (flase) = owin (true)  && _tiles[0].isSet == O (false)

- SECOND PASS                  _tiles[1].isSet = X
var tile     = 1
xwin (true)  = xwin (true)  && _tiles[1].isSet == X (true)
owin (flase) = owin (false) && _tiles[1].isSet == O (false)

- THIRD PASS                   _tiles[2].isSet = X
var tile     = 2
xwin (true)  = xwin (true)  && _tiles[2].isSet == X (true)
owin (flase) = owin (false) && _tiles[2].isSet == O (false)

As you can see, when the second for each statement is complete, xwin will be true and owin will be false. This triggers the if statement and calls for the endGame(combi:Array = null) function. With it, it supplies the winning combination which we'll use in the function. The checkWin() function returns a true or false, which I explained before.

The endGame() function is easy to understand. First, for every tile in the _tiles Array, we disable the event listeners. We also check if there is a combination supplied (meaning someone won instead of a draw). If this is the case, we check which tiles were part of the combination and call the showEnd() function on those tiles. This will show the 'TileE' graphic.


Milestone 1

We now have a working game. For the source of all we did so far, check _milestone1.zip. Results!


Step 11: AI Functions

I hope you're not confused by all the scripting we did so far. we'll go on with the AI part of the game, which requires some functions. First, let's make some changes and add the function to add the AI. In TicTacToe.as, change the following line:

var game:Game = new Game(1); // Changed number of human players to 1

Next, in Game.as, add the following variables:

private var _xlist:Array = new Array(); // keep track of all x tiles
private var _olist:Array = new Array(); // keep track of all o tiles
private var _flist:Array = new Array(); // keep track of all free tiles

In the addVisuals() function, add the following line (after you push the tiles to the _tiles Array):

_flist.push(tile1, tile2, tile3, tile4, tile5, tile6, tile7, tile8, tile9); // added all tiles to the free list

Then change the nextTurn(e:Event = null) function into the following:

public function nextTurn(e:Event = null):void
{
    if (!checkWin()) {
        if (e) { // added check for an event (we call the function once without marking a tile)
            if (e.currentTarget.isSet == "X") { // if the tile is set to X
                _xlist.push(e.currentTarget); // add it to the X list
            }
            if (e.currentTarget.isSet == "O") { // if the tile is set to O
                _olist.push(e.currentTarget); // add it to the O list
            }
            _flist.splice(_flist.indexOf(e.currentTarget), 1); // remove the tile from the free list
        }
        left -= 1;
        if (left > 0) {
            turn += 1;
            if (turn % 2 == 0 && numPlayers == 1) handleAI(); // check if the PC should choose a tile
        } else {
            endGame();
        }
    }
}

Of course, also add the handleAI() function to the Game.as class:

private function handleAI():void
{
    trace(_xlist);
    trace(_olist);
    trace(_flist);
}

When we now test the Flash movie, after our first turn we see the three lists in our Output Window. The _xlist will be one element long (you picked a tile to mark). The _olist is empty (player two didn't pick a tile yet) and the _flist is eight elements long (there are eight tiles left to choose from). We'll use these lists in the next step to let the AI choose a tile.


Step 12: AI How Smart?

There are many ways to script the behavior of the computer. As you probably know, if both players pay attention and know the rules, a game of Tic-Tac-Toe always ends in a draw. We can simply script all situations so that the outcome is always a draw or a win for the computer. But what's the fun in that? I choose to create an AI which isn't smart, but isn't stupid either. These are the rules we'll be writing:

  • for each free tile, choose it if this tile plus all already chosen O-tiles wins the game
  • for each free tile, choose it if this tile plus all already chosen X-tiles wins the game
  • otherwise, pick a random free tile

So the computer first should check if it can win the game. If the computer can't win with this free tile, it should check if the human player can win with this tile. When neither the computer nor the human player can win, it should pick a random tile. We can make it think ahead, and instead of picking a random tile let it choose from the tiles with which it makes it possible to win next round. But again, we won't do that because it would make the game pretty boring.


Step 13: AI Enabling Tiles

It doesn't get any more easy than this step. We need to enable the tiles so the computer can mark them. Normally, when we play with two players, the mouse is used to trigger the swap in graphics and dispatch the event that sets the turn to plus one. Because the computer doesn't use MouseEvents, we need to change two functions in Tile.as so they can be used from the Game.as file. These are those functions:

public function showGraphic(e:MouseEvent = null):void // Changed to public and set MouseEvent to null
public function chooseGraphic(e:MouseEvent = null):void // Changed to public and set MouseEvent to null

This way, we can call the functions showGraphic() and chooseGraphic() (because they are public) without the supply of a MouseEvent (because if it's not supplied, we supply null).


Step 14: AI Script

Let's write the script for the AI now. This is what your Game.as file should look like. I've written comments beside every line I changed.

package
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.TimerEvent; // We now use a timer event
	import flash.utils.Timer; // And also a timer

	public class Game extends MovieClip
	{
		public var numPlayers:uint;
		public var turn:uint;
		public var left:uint = 10;
		private var _grid:Grid = new Grid();
		private var _tiles:Array = new Array();
		private var _combos:Array = new Array(new Array(0, 1, 2), new Array(3, 4, 5), new Array(6, 7, 8),
											  new Array(0, 3, 6), new Array(1, 4, 7), new Array(2, 5, 8),
											  new Array(0, 4, 8), new Array(2, 4, 6));
		private var _xlist:Array = new Array();
		private var _olist:Array = new Array();
		private var _flist:Array = new Array();
		private var _choose:Boolean = false; // Added variable to check if the computer made a choice
		
		public function Game(num:uint):void
		{
			numPlayers = num;
			turn = 0;
			addVisuals();
			addTileListeners();
			nextTurn();
		}
		
		private function addVisuals():void
		{
			var tile1:Tile = new Tile();
			var tile2:Tile = new Tile();
			var tile3:Tile = new Tile();
			var tile4:Tile = new Tile();
			var tile5:Tile = new Tile();
			var tile6:Tile = new Tile();
			var tile7:Tile = new Tile();
			var tile8:Tile = new Tile();
			var tile9:Tile = new Tile();
			tile1.x = tile4.x = tile7.x = tile1.y = tile2.y = tile3.y = 0;
			tile2.x = tile5.x = tile8.x = tile4.y = tile5.y = tile6.y = 100;
			tile3.x = tile6.x = tile9.x = tile7.y = tile8.y = tile9.y = 200;
			addChild(tile1);
			addChild(tile2);
			addChild(tile3);
			addChild(tile4);
			addChild(tile5);
			addChild(tile6);
			addChild(tile7);
			addChild(tile8);
			addChild(tile9);
			addChild(_grid);
			_tiles.push(tile1, tile2, tile3, tile4, tile5, tile6, tile7, tile8, tile9);
			_flist.push(tile1, tile2, tile3, tile4, tile5, tile6, tile7, tile8, tile9);
		}
		
		private function addTileListeners():void
		{
			for (var i:int = 0; i < _tiles.length; i++) {
				_tiles[i].addEventListener("TILE_ACTIVE", nextTurn);
			}
		}
		
		public function nextTurn(e:Event = null):void
		{
			if (!checkWin()) {
				if (e) {
					if (e.currentTarget.isSet == "X") {
						_xlist.push(e.currentTarget);
					}
					if (e.currentTarget.isSet == "O") {
						_olist.push(e.currentTarget);
					}
					_flist.splice(_flist.indexOf(e.currentTarget), 1);
				}
				left -= 1;
				if (left > 0) {
					turn += 1;
					if (turn % 2 == 0 && numPlayers == 1) handleAI();
				} else {
					endGame();
				}
			}
		}
		
		private function handleAI():void
		{
			_choose = false; // The computer didn't choose a tile yet
			for (var i:int = 0; i < _flist.length; i++) {
				_flist[i].removeButtonListeners(); // Remove all listeners on the free tiles so the human can't play
			}
			var timer:Timer = new Timer(500, 1); // Set a timer (make it look like the computer 'thinks' for half a second)
			timer.addEventListener(TimerEvent.TIMER_COMPLETE, handleAIChoice); // Add a listener to the timer
			timer.start(); // Start the timer
		}
		
		private function handleAIChoice(e:TimerEvent):void
		{
			var i:int;
			for (i = 0; i < _flist.length; i++) {
				if (!_choose) { // If the computer didn't make a choice yet
					var ocheck:Array = _olist.concat(); // Make a copy of the _olist
					var xcheck:Array = _xlist.concat(); // Make a copy of the _xlist
					ocheck.push(_flist[i]); // Add a free tile to the ocheck array
					xcheck.push(_flist[i]); // Add a free tile to the xcheck array
					if (checkAIWin(ocheck)) break; // Check if with the free tile, o wins (this function also places the tile, if so)
					if (checkAIWin(xcheck)) break; // Check if with the free tile, x wins
				}
			}
			if (!_choose) {
				// If the computer didn't pick a tile yet (no win possible for x or o)
				trace("Pick random spot");
				var tile:int = Math.floor(Math.random() * _flist.length); // Pick a random index
				_flist[tile].showGraphic(); // Set the tile with the random index to show the graphic
				_flist[tile].chooseGraphic(); // Set the tile with the random index to choose the graphic
			}
			for (i = 0; i < _flist.length; i++) {
				if (!checkWin()) _flist[i].addButtonListeners(); // Add all listeners on the free tiles again so the human can play
			}
		}
		
		private function checkAIWin(check:Array):Boolean
		{
			for each (var combi in _combos) {
				var win = true;
				for each (var tile in combi) {
					if (check.indexOf(_tiles[tile]) == -1) { // Same function, but now we supply a check array
						win = false // If the check array doesnt include the tile of the combination, it's not a winning situation
					}
				}
				if (win) {
					// If there is a winning situation, choose the tile
					trace("Block or check winning tile");
					check[check.length - 1].showGraphic(); // We added the free tile at the end of the check array
					check[check.length - 1].chooseGraphic(); // So set the tile with index check.length - 1 to show/choose
					_choose = true; // Make sure we set _choose to true (all other checks in handleAIChoice will be skipped)
					break;
				}
			}
			return _choose;
		}
		
		private function checkWin():Boolean
		{
			var won:Boolean = false;
			for each (var combi in _combos) {
				var xwin = true;
				var owin = true;
				for each (var tile in combi) {
					xwin = xwin && _tiles[tile].isSet == "X";
					owin = owin && _tiles[tile].isSet == "O";
				}
				if (xwin || owin) {
					endGame(combi);
					won = true;
					break;
				} else {
					won = false;
				}
			}
			return won;
		}
		
		private function endGame(combi:Array = null):void
		{
			for (var i:int = 0; i < _tiles.length; i++) {
				_tiles[i].removeButtonListeners();
				if (combi) {
					for each (var j in combi) {
						if (i == j) _tiles[i].showEnd();
					}
				}
			}
		}
	}
}

Take a deep breath. The first changes I should explain are in the handleAI function, which triggers when the computer should choose a tile. We use the _choose variable here and set it to false (the computer didn't yet choose a tile). We then remove all listeners from the free tiles. This way, the human player can't click at any time to make a move for the computer. Last but not least, we set a timer. I did this because it looks a bit more cool if the computer actually "thinks" about his move. The script for making its choice will be run in a few miliseconds, but this way you can make it look like it really needs something to think about.

The second big change is the handleAIChoice function. I added comments which should explain everything here. We use the concat() trick to make a copy of the _olist and _xlist (var copy = original does not copy the original array! If you modify copy you will also be modifying original). The most important part is the call to the function checkAIWin(), to which we supply all the already chosen X or O tiles with a free tile (we do this for each free tile).

The checkAIWin() looks like the checkWin() function we created earlier, but this time we supply an array. We'll not check for all the X or O tiles, but for the array of tiles we supplied (which means: all the X tiles plus one free tile, or all the O tiles plus one free tile). If the supplied array contains all the tiles needed for a winning combination, the variable win will stay true. If not, it will be set to false.

In the case win == true, this means that the free tile will create a winning situation. The computer always wants to choose this tile, because if it makes the computer win, thats good. And if the free tile lets the human player win, it should block the free tile so the human player can't win.


Milestone 2

We now have a working game with AI. For all we did so far, check _milestone2.zip. Results!


Step 15: Graphics Menu

Create two Buttons for the "One Player" button and the "Two Players" button. Export them for ActionScript with class names B1 and B2 (B for "Button"). These are mine:


Step 16: Adding menu functionality

Of course, your menu has to be added to the stage and work. This is quite easy (yes, you can relax, the hard part is over). Look at your TicTacToe.as file and write the following code:

package
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.MouseEvent;
	
	public class TicTacToe extends MovieClip
	{
		private var b1:B1 = new B1();
		private var b2:B2 = new B2();
		private var game:Game;
		
		public function TicTacToe():void
		{
			b1.x = 120;
			b1.y = 60;
			b2.x = 180;
			b2.y = 240;
			addChild(b1);
			addChild(b2);
			b1.addEventListener(MouseEvent.CLICK, startGame);
			b2.addEventListener(MouseEvent.CLICK, startGame);
			addMenu();
		}
		
		private function startGame(e:MouseEvent):void
		{
			b1.visible = false;
			b2.visible = false;
			game = (e.currentTarget == b1) ? new Game(1) : new Game(2);
			game.addEventListener("restart", addMenu);
			addChild(game);
		}
		
		private function addMenu(e:Event = null):void
		{
			if (game != null) {
				removeChild(game);
				game = null;
			}
			b1.visible = true;
			b2.visible = true;
		}
	}
}

Because all of the above script is new, I didn't write any comment lines. First, we make three variables. We create the buttons and a game variable. In the constructor (TicTacToe()) we set the x- and y-positions of our buttons. Make sure you position your own buttons at the correct coordinates. Then we add the buttons to the display list and add the MouseEvent listeners. Last but not least, we call the addMenu() function.

In startGame(e:MouseEvent) we set both buttons to visible = false. This way, we won't see them anymore. If you would like to skip this part, you should make sure the Grid MovieClip has a solid background. You won't be able to see the buttons then, because you place the game over it. I prefer to be sure (you might want to position your game elsewhere) and set the visibility to false. We then check which button has been pressed. If it's b1, we want call for new Game(1) (1 player, vs computer). If it's not, we call for new Game(2) (2 human players). Then we add the RESTART event listener to the game, so we can check when the game is done. Last but not least: add the game to the display list so we can actually play.

The addMenu(e:Event = null) function doesn't do much. It's called the first time we open our Flash Movie, and every time a game ends. It first checks whether there is a game, and if so it removes it from the display list and sets the game to null. Next it sets the visibility of the buttons to true so we can click on them again.


Step 17: It Doesn't Work!

When you now test the game, it works fine. But when you finish your first game, it doesn't show the menu. This is because we didn't add the RESTART event dispatcher to the Game.as file yet. Let's do that. In the endGame(combi:Array = null) function, add the following lines:

var timer:Timer = new Timer(1500, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, restart);
timer.start();

This starts a timer of 1.5 seconds (so you'll have some time to see who actually won). Next, create the function which is called by the timer:

private function restart(e:TimerEvent):void
{
    dispatchEvent(new Event("restart"));
}

All this function does is dispatch the RESTART event, for which we are waiting in the TicTacToe class. Game complete!


Milestone 3

We now have a working game with AI and a menu. For all we did so far, check _milestone3.zip. Results!


Step 18: Can You Spot the Bug?

I hope you are still with us. We do have a working game now, but if you tested the game excessively you might have noticed a bug. The computer AI checks for each free tile if it can win with that tile. If not, it checks if it can lose by that tile. So think about it: the computer may choose a tile, and can win with tile 4. It can also lose with tile 2. So when it checks tile 2, it will choose that tile because it will lose next round if it doesn't, never to reach the check on tile 4 with which it wins this round. Solution: first check all free tiles for a winning situation. Then check for a losing situation. This will be your new handleAIChoice(e:TimerEvent) function (split the first for statement into two):

private function handleAIChoice(e:TimerEvent):void
{
    var i:int;
    for (i = 0; i < _flist.length; i++) {
        if (!_choose) {
            var ocheck:Array = _olist.concat();
            ocheck.push(_flist[i]);
            if (checkAIWin(ocheck)) break;
        }
    }
    for (i = 0; i < _flist.length; i++) {
        if (!_choose) {
            var xcheck:Array = _xlist.concat();
            xcheck.push(_flist[i]);
            if (checkAIWin(xcheck)) break;
        }
    }
    if (!_choose) {
        var tile:int = Math.floor(Math.random() * _flist.length);
        _flist[tile].showGraphic();
        _flist[tile].chooseGraphic();
    }
    for (i = 0; i < _flist.length; i++) {
        if (!checkWin()) _flist[i].addButtonListeners();
    }
}

Step 19: Suggestions Graphics

Although this is a tutorial, I like to see people learn something and build on that. To help you on your way, I'll write some suggestions on things you can alter and/or add.

Visually, you have very few limitations. Note that you can edit the MovieClips of the Grid, Tiles and Buttons. Don't limit yourself to one frame: these assets are MovieClips for a reason. For instance, you can create an animation of a flying bird and use it as a Tile. You can make the color of your Grid change, let your Buttons bounce, you name it. You can create a nice Loading MovieClip and add this to the game. Set its visibility to false, and to true while the computer is "thinking". Instead of ending the game with a pause, show an image of who won with fireworks and people cheering. Be creative.


Step 20: Suggestions Script

Although you already have a working Tic-Tac-Toe game, you can always add more functionality and change the functionality it already has. For instance, instead of the rules I gave you, you can build new rules for the AI. Always pick a corner first. Start with the center tile if it's available. Or let the computer think one step ahead - you're half way there!

An other suggestion I can make, is randomly decide who may begin when you play versus the computer. This might look pretty easy, but make sure you think it through. It begins by changing the following line in Game.as:


if (turn % 2 == 0 && numPlayers == 1) handleAI(); // Change turn % 2 == 0 to turn % 2 == randomBegin

In the Game(num:uint) constructor, set randomBegin to 0 or 1. This is all you need to do, but take a look at our handleAIChoice function. In this function we first check for the winning situations for O, then for X. This is correct when the computer plays with the O tiles, but when you make this part random... So, maybe change the two for statements into functions and call them in the correct order, depending on randomBegin.


Conclusion

I really hope you liked this tutorial. It was my first, I tried to keep it simple but fun and I hope I succeeded. If you have questions about this tutorial, please leave comments here. I would also love to see what you create with the help of this tutorial, so don't hesitate to send URLs to your projects.

Thanks for reading and enjoy Flash and ActionScript!