Following on from my last post about creating a command line game in PHP we now have a mechanism to listen to keypresses. The next step from here is to create a simple game. After thinking about a game that would fit into the command line I decided that something simple like tic tac toe (also called noughts and crosses) would be a good starting point. The game board is small and the conditions for winning are pretty simple to understand.
Although tic tac toe is a two player game I won't be adding any AI controlled players, each player will just take turns at placing their piece. I won't be doing anything fancy here so there will be no objects or other best practice techniques. This is more of a proof of concept to try and get a game working and so there will be a few key functions to abstract repeated concepts with some global variables being passed to each of these functions.
Let's start with the game grid itself. Tic tac toe is played on a 3x3 grid, so to represent that in code we just need a 2-dimensional array of three arrays, each with three items in the array. This format allows us to store the state of each cell using a string. So, when a player takes their turn one of the items in the array will be changed to an 'X' or a 'O'.
$state = [
['', '', ''],
['', '', ''],
['', '', ''],
];
There are two other variables we need to define in order to get this working. The current player and what the active cell is. The active cell is how we will navigate around the game board in order to select the cell in which we want to place our token. Without this we would need some difficult to use grid reference interface as we can't use the mouse within the command line. As the active cell is a reference to an item in a 2-dimensional array we need to have one part for the x-coordinate and one part for the y-coordinate.
$player = 'X';
$activeCell = [0 => 0, 1 => 0];
With these variables defined we can start drawing out the game board. All we need to do is loop through the multi-dimensional array and convert it into a 3x3 grid represented as a string. The active cell and player variables also need to be taken into account to change the output of the board. Whilst the player variable just prints out the current player, the active cell is used to highlight one of the cells in the grid.
The following function will take in the $stage, $activeCell and $player variables and return a string containing the current state of play. This is probably more verbose than it needs to be, but shows clearly what is going on here.
function renderGame($state, $activeCell, $player) {
$output = '';
$output .= 'Player:' . $player . PHP_EOL;
foreach ($state as $x => $line) {
$output .= '|';
foreach ($line as $y => $item) {
// Select the current content of the cell.
switch ($item) {
case '';
$cell = ' ';
break;
case 'X';
$cell = 'X';
break;
case 'O';
$cell = 'O';
break;
}
if ($activeCell[0] == $x && $activeCell[1] == $y) {
// Highlight the active cell.
$cell = '-'. $cell . '-';
}
else {
$cell = ' ' . $cell . ' ';
}
$output .= $cell . '|';
}
$output .= PHP_EOL;
}
return $output;
}
To run this we just pass in the variables we defined earlier and print the output.
echo renderGame($state, $activeCell, $player);
This produces the following.
Player:X
|- -| | |
| | | |
| | | |
On its own this doesn't do very much. In fact the PHP script will start, print the game grid and finish, which doesn't make for a very exciting game. We therefore need a way of repeatedly printing out the game state into the command line in the same space. As it turns out, I have previously looked into doing this when running Conways' game of life in PHP on the command line. All we need is an infinite loop and a way of resetting the command line every time the game output is created. This is achieved using a while loop and a call to the system command 'clear'. With this in place, every time the game output is rendered it will be at the top of the screen, which gives the illusion of animating the game output.
The following will print the same game grid, but it will always be at the top of the command line output.
while (1) {
system('clear');
echo renderGame($state, $activeCell, $player);
}
Now we need some way of allowing players to move around the board and select their move. This is done by listening to the keys being inputted and acting to that input accordingly. There are a couple of simple rules we need to follow here.
- If the user enters an arrow key then update the active cell accordingly. Don't allow the active cell to be beyond the confines of the game grid.
- If the user presses enter and the current active cell is blank then fill the cell with the token for the current player. If this happens then swap the active player.
With these rules in place we can use the translateKeypress() function from my previous post to read and translate the keypresses into nicely readable strings. Using this we the keypresses to update the variables in different ways. Note that the user actions can change the state, active cell any player variables, so we must pass the variables by reference to the function in order to change them outside of the scope of this function.
function move($stdin, &$state, &$activeCell, &$player) {
$key = fgets($stdin);
if ($key) {
$key = translateKeypress($key);
switch ($key) {
case "UP":
if ($activeCell[0] >= 1) {
$activeCell[0]--;
}
break;
case "DOWN":
if ($activeCell[0] < 2) {
$activeCell[0]++;
}
break;
case "RIGHT":
if ($activeCell[1] < 2) {
$activeCell[1]++;
}
break;
case "LEFT":
if ($activeCell[1] >= 1) {
$activeCell[1]--;
}
break;
case "ENTER":
case "SPACE":
if ($state[$activeCell[0]][$activeCell[1]] == '') {
$state[$activeCell[0]][$activeCell[1]] = $player;
if ($player == 'X') {
$player = 'O';
} else {
$player = 'X';
}
}
break;
}
}
}
To integrate this into our loop we call the move() function before the renderGame() function so that the game board will update with any change made by the user. The players are now able to move around the game board and place tokens.
$stdin = fopen('php://stdin', 'r');
stream_set_blocking($stdin, 0);
system('stty cbreak -echo');
while (1) {
system('clear');
move($stdin, $state, $activeCell, $player);
echo renderGame($state, $activeCell, $player);
}
There is one problem left to solve here, and that is who actually wins the game. Thankfully, calculating the win state for a game of tic tac toe is relatively straight forward. We just need to check for the presence of a line of three tokens in a row on the horizontal, vertical and diagonal positions. If nothing is found after checking those states then we might be looking at a draw condition, so we also need to check for that state as well.
If a line is found then we just exit the program and print the result of the win state. Again, this is perhaps more verbose than it needs to be as are don't necessarily need to loop through the state arrays more than once, but shows clearly what steps are involved.
function isWinState($state) {
foreach (['X', 'O'] as $player) {
foreach ($state as $x => $line) {
if ($state[$x][0] == $player && $state[$x][1] == $player && $state[$x][2] == $player) {
// Horizontal row found.
die($player . ' wins');
}
foreach ($line as $y => $item) {
if ($state[0][$y] == $player && $state[1][$y] == $player && $state[2][$y] == $player) {
// Vertical row found.
die($player . ' wins');
}
}
}
if ($state[0][0] == $player && $state[1][1] == $player && $state[2][2] == $player) {
// Diagonal line top left to bottom right found.
die($player . ' wins');
}
if ($state[2][0] == $player && $state[1][1] == $player && $state[0][2] == $player) {
// Diagonal line bottom left to top right found.
die($player . ' wins');
}
}
// Game might be a draw.
$blankQuares = 0;
foreach ($state as $x => $line) {
foreach ($line as $y => $item) {
if ($state[$x][$y] == '') {
$blankQuares++;
}
}
}
if ($blankQuares == 0) {
// If there are no blank squares left and nothing else has been found then this is a draw.
die('DRAW!');
}
}
We look at the win state after going through the user moves (with the move() function) and printing out the game board (using the renderGame() function). This way, when the program exits, the game board is preserved and the final result is printed underneath. Here is what our loop looks like now.
while (1) {
system('clear');
move($stdin, $state, $activeCell, $player);
echo renderGame($state, $activeCell, $player);
isWinState($state);
}
After playing a game to its conclusion we see the following result.
Player:O
|-X-| X | O |
| O | X | O |
| X | O | X |
X wins%
That is tic tac toe, running entirely in PHP on the command line and whilst it looks a little crude it does function just like a game of tic tac toe. One slight niggle here is that although it says "X wins" the current player is "O". This is because when we run the move() function we swap the current player so by the time we reach the win state we have rendered the new game grid out with the next player as the current player. It still correctly identifies the winning player though.
The infinite loop method used here is actually widely used in most computer programs, especially games where a constant time signature is required to allow everything to work together. This concept is vitally important to point out here, especially if you are used to PHP being used as a web development language. Most of the time, web developers are very careful not to allow infinite loops to happen as this tends to cause websites to crash. Game developers, on the other hand, spend most of their time within the infinite loop and so are quite familiar with this concept.
If you want to see this code in full then I have created a GitHub gist that you can download and run.
In my next post I will look at something slightly more ambitious, including real time gameplay and graphics. Well, within the limitations of a command line interface...
Comments
It's not working, the variable "stdin" is undefined.
Submitted by Einstein on Mon, 05/23/2022 - 13:32
PermalinkThe github gitst in the article has the full working code, and that works fine.
This article follows on from the last in the series, which is where the $stdin variable is created.
Specifically, you need this block of code to define the $stdin variable.
I've updated the article to include that.
Submitted by giHlZp8M8D on Mon, 05/23/2022 - 14:36
PermalinkAdd new comment