Writing a Mini Game - Tic Tac Toe - Part One

This is the first version of vscript to let you play Tic-Tac-Toe in game. It's a bit verbose, but good for learning purposes.

*Note: this video is old, it will now mop the floor with you every time, too good





Later, I'll expand on it by getting a zombie to play with us, making the AI seem more "human" and fallible (it's TOO good), and also how to make our life easier by spawning everything from templates and cleaning up when we're down, but one step at a time ;)

Source - Map VMF

tictactoe1.vmf

Source - Script

tictactoe01.nut

Instructions: Compile the map in hammer, copy the tictactoe01.nut script into your scripts/vscripts folder

/*
example_tictactoe01.nut
author: Lee Pumphret
https://www.leeland.info
This software is free for use and comes with NO WARRANTY what so ever. Use at your own risk :)
------------------------------------------------------------------------------
Example .NUT script for Left 4 Dead 2. This implements a game of Tic Tac Toe
(the first version in a series I'm going to build on)

Variables are passed to logic_script entities through the EntityGroup array
(which correspond to the labels in Hammer).

For this example, I'm using entries 1-9 for the corrosponding board tiles, they should be
func_button entities that call RunScriptCode on damage with a PlayerMove(Num) where num is the selected tile
 -----------
| 1 | 2 | 3 |
 -----------
| 4 | 5 | 6 |
 -----------
| 7 | 8 | 9 |
 -----------

Entities 11 anad 12 should be ambient_generics to play on win and lose respectively.


For the sake of simplicity and not muddying up the waters just yet, in this first
version, we're going to always assume that the player goes first, and is always X
(will fix that in a later version)

A note on variables in vscript. You're script is parsed and run when called with begin script,
and whatever variables you define, will exist until you call EndScript or the map is unloaded.
(or the script is killed)

Basically everything not inside a function will be executed on script load.
!Note: I really should add error checking, but again, to keep this example simple...
*/

// This is a comment, Squirrel ignores everthing following the double slashes to the end of the line

/*
This is also a comment, but can be span multiple
lines. You can't "nest" these, one inside another will cause a syntax error.
*/



// Variables and constants --------------------------------------------
ClearColor <- "255 255 255"; // Normal "empty" board rendercolor

const Xplayer = "X";  // const just means "constant", you cannot change the value assigned once it's created.
const Oplayer = "O";  // it's a good idea for vals that don't change so you can't mix up (a==b) with (a=b)

XColor <- "255 0 0";  // The rendercolor for a tile selected X
OColor <- "0 255 0";  // The rendercolor for a tile selected O


IsInGame <- 0;						// If this variable is non-zero (ie "true") we are in the middle of a game
GameBoard <- [];					// Array to hold our game state, will have TEN entries 0-9 (zero is unused)
GameMoves <- 0;					// Variable to track how many turns, so we can detect a draw.
PlayersTurn <- 1;					// "true" if player's turn
PlayersLastMoveTime <- 0;		// Time of last move by player in milliseconds
ProcessingPlayerInput <- 0;	// True if we are processing a move by player, so they can't hit more than one tile.


// The following array contains a list of all the winning positions in the game.
WinningMoves <- [
				[1,2,3], //across
				[4,5,6],
				[7,8,9],
				[1,4,7], //down
				[2,5,8],
				[3,6,9],
				[1,5,9], //diag
				[3,5,7],
];

PreferredMoves <- [5, 1, 3, 7, 9]; // Preferred moves to take when there is no winner
OtherMoves <- [2,4,6,8];  // If preferred are available, try these, shuffling these would make it a bit more random.



/*
L4D2 let's you specify a function to be called every 100 milliseconds (ten times a second) for
the duration of your script. You tell it which function to call by adding a thinkfunction key value
in your logic_script, setting the value to the name of the function. I used "Think" because it's
what they use in the Dark Carnival maps, but you can whatever you like.

You don't want to do much in this function. If it takes more than a 100 milliseconds , it may cause issues.
*/

/* function declarations ------------------------*/
function Think()
{
	if (IsInGame){
		if (Time() - PlayersLastMoveTime > 60){  // it's been sixty seconds. Time() function returns fractional seconds instead of milliseconds
			// we could tell em to hurry up here, or add logic to default game
			printl("Player has been thinking for over a minute");
		}
	}

}

/*-----------------------------------------------*/
function StartGame(){
	// starts the game
	printl("Starting Tic_Tac_Toe"); // printl is just a print function that adds a linebreak automatically
	InitBoard(); // Set up the board
}
/*-----------------------------------------------*/
function EndGame(){
	// starts the game
	printl("Game Over Man!, Game Over!");
	IsInGame = 0;
}
/*-----------------------------------------------*/
function InitBoard(){
	printl("Initializing board for play");
	// Initialize board start, called every new game to reset things.
	GameBoard = array(10,null);	// Clear the board by setting all positions in the board to null (which is "false")
	GameMoves = 0;						// Reset the GameMoves
	PlayersTurn = 1; 				   // Make it the player's turn
	PlayersLastMoveTime = Time();
	ProcessingPlayerInput = 0;		// Unblock player input.
	// Loop through our tiles and resset the render color
	for (local i = 1; i <= 9; i++){
		EntFire(EntityGroup[i].GetName(), "addoutput", "rendercolor "+ClearColor);
	}
	IsInGame = 1;
}
/*-----------------------------------------------*/
function CheckMove(tile){
	/*
		Can't pick the same tile if it's already been selected, return true if ok, false if not.
		Again, this is verbose, but it's an example for people who don't program...
	*/
	if (!GameBoard[tile]){  // if it's empty (ie "false"), we're good.
		return 1;
	}
	// If you return nothing from a function, that equates to false ;)
}
/*-----------------------------------------------*/
function MarkMove(tile, val){
	printl("Marking move "+val+" in position "+tile);
	local color = val == Xplayer ? XColor : OColor;  // Set the color according to current player
	// Fire the event to color the tile
	EntFire(EntityGroup[tile].GetName(), "addoutput", "rendercolor "+color);
	GameBoard[tile] = val;  // Save the move in the board.
	CheckForWin();				// um, exactly what it says ;)
	GameMoves++;				// Keep track of how many turns
	if (GameMoves >=9 && IsInGame){
		GameIsDraw()
	}

}
/*-----------------------------------------------*/
function GameIsDraw(){
	printl("Game is draw!");
	IsInGame = 0;	// Reset so they can play again
	EntFire(EntityGroup[11].GetName(),"PlaySound"); // losing sound
}
/*-----------------------------------------------*/
function CheckForWin(){
	printl("Checking for Winner");
	// Scan table for win.
	foreach (c in WinningMoves){
		/*
		Check that 0 and 1 are the same and 1 and 2, you can't say  if(a == b == c) in Squirrel,
		so have to make it two tests.
		*/
		if (GameBoard[c[0]] == GameBoard[c[1]] && GameBoard[c[1]] == GameBoard[c[2]]){
			/*
				For a win, all three values need to be the same, but also need to check
			   they are not all null (otherwise it will think there's a win first move,
			   because they are all the same, null
			*/
			if (GameBoard[c[0]]){  // anything not null or zero is "true"
				printl(GameBoard[c[0]]+" won the game!");
				if ( GameBoard[c[0]] == "X"){
					// player won, so play won sound
					EntFire(EntityGroup[10].GetName(),"PlaySound");
				}else {
					// AI won, play the lose sound
					EntFire(EntityGroup[11].GetName(),"PlaySound");
				}
				EndGame(); // Game over!
				break;     // Break out of loop, as we found what we wanted
			}

		}
	}

}
/*-----------------------------------------------*/
function PlayerMove(tile){
	/* Again, should add error checking to make sure it's between 1-9 */
	if (!IsInGame){
		/*
			I'm triggering it off of shooting any of the tile entities, but you could start the
		   game with a RunScriptCode output from a button or trigger with a parameter of StartGame(),
		   this code let's me do it either way
		*/
		StartGame();
	}

	PlayersLastMoveTime = Time();  // Record move time, valid or not or regardless of whether it's players turn.
	if (PlayersTurn){
		if (ProcessingPlayerInput == 0){
			printl("Player Move");
			ProcessingPlayerInput = 1; /* Want to make sure they only get one turn, but is vscript threadsafe???*/
			if (CheckMove(tile)){  		// If it returns any true value, player selected a valid move so....
				MarkMove(tile,Xplayer); // Mark it
				PlayersTurn = 0;  // AI's turn
				OurMove();
			}else{
				printl("Invalid move, tile is occupied");

			}
			ProcessingPlayerInput = 0; //"MARK IT ZERO SMOKEY!",  unblock player so they can try again

		}else {
			printl("Ignoring input as we already got one this turn");
			// We could skip this "else", but it's for illustration, we could also do something here, like so "hold on!"
		}
	} else {
		printl("Not you're turn!");
	}

}
/*-----------------------------------------------*/
function LookForWinningMove(player){
	// This scans the board for a potentially winning move, might lose some of you here.
	local Move = 0;		//  zero is not a valid move in our board, so it's the default
	foreach (c in WinningMoves){
		local pcount = 0;		// pcount is just how many of "player" there are in a row (x or o).
		foreach(i in c){
			if (GameBoard[i] == player){
				pcount++; // mark it by adding one to the count
			}
		}
		if (pcount == 2){ // This is a winning strategy, we have 2 out of the 3, find out which tile we're missing
			foreach (win in c){
				if (!GameBoard[win]){
					// we found it, select it and break out of loop
					printl("We found a winning move!:"+win);
					Move = win;
					break;
				}
			}
		}
	}
	return Move;

}
/*-----------------------------------------------*/
function OurMove(){
	printl("Script's Move");
	local AIMove = 0;
	local tmp = 0;
	// look for winning move for AI
	tmp = LookForWinningMove(Oplayer);
	if (tmp){
		// we found a winner
		AIMove = tmp;
	}

	if (!AIMove){
		// Didn't find one, look for a move that would let player win and select that to block them
		tmp = LookForWinningMove(Xplayer);
		if (tmp){ // we found a win to block
			AIMove = tmp;
		}
	}

	if (!AIMove){
		// haven't found a move yet, let's try these
		foreach(tile in PreferredMoves){
			if (CheckMove(tile)){
				printl("AI Choosing preferred move:"+tile);
				AIMove = tile;
				break;
			}
		}
		if (!AIMove){  /* no move? keep looking */
			foreach(tile in OtherMoves){
				if (CheckMove(tile)){
					printl("AI Choosing move:"+tile);
					AIMove = tile;
					break;
				}
			}
		}
	}

	// The following might seem like an error, but it's not. If we have no move, the game is over.
	if (AIMove){
		MarkMove(AIMove,Oplayer);
	}
	PlayersTurn = 1;

}
Scroll to top