There is something about shell scripting that brings me a lot of joy, particularly with Bash. I can come up with a lot of reasons like its universal, but truth be told its not the most efficient programming language. It is useful for tying together command line programs but it has a lot of quirks in its syntax that makes it fun to work with. As I'm always trying to up my shell scripting chops, I decided to give writing a game for the terminal using Bash.
Tic Tac Toe is a common game for beginner coders to make for the shell. It requires some understanding of programming, particularly conditionals, data structures, and user input. Although there are many languages that are more natural than Bash, Bash has all of the aforementioned features.
The version of Tic Tac Toe I will make has two players. Each turn a player alternates picking a square, and the winner occurs when a player has three squares chosen in a row, either horizontally, diagonally, or diagonally. If there are no consecutive squares than there is a draw.
To start a create a file called tic-tac-toe.sh
with #!/bin/bash
as the first line.
I set up symbols for the two players, I initiate a variable that keeps track of the turns, and I create a variable that keeps track of the state of the game. Finally, I know that there are 9 potential moves, and I keep track of them by creating an array from 1-9.
player_1="X"
player_2="O"
turn=1
game_on=true
moves=( 1 2 3 4 5 6 7 8 9 )
Next I write a function that prints a welcome message that starts the game.
welcome_message() {
echo "========================"
echo "=== LETS PLAY A GAME ==="
echo "========================"
}
I know that I will need to repeated draw the board, so I create a function that does this. I reference the moves
array and print the array value in place. Its helpful that I list the numbers of the square, as I will use them later for selecting them. Also, one feature I like about bash is that it is simple to interpolate values. Also, I clear the shell before printing the game board to keep it clean.
print_board () {
clear
echo " ${moves[0]} | ${moves[1]} | ${moves[2]} "
echo "-----------"
echo " ${moves[3]} | ${moves[4]} | ${moves[5]} "
echo "-----------"
echo " ${moves[6]} | ${moves[7]} | ${moves[8]} "
echo "============="
}
An important part of Tic tac toe, is to keep track of whose turn it is. I use the turn variable to determine whose turn it is, by using a modulus operator. Odd value turns are player one, and even values are player two.
Then I request a move from the player, and I check the value of the selection that they pick. I use regex to determine if their selection is out of bounds of 1-9, and also whether the square has been chosen. If the move is invalid then it prompts the player to pick another one, and if it is valid the player's symbol takes the place of the square's number.
player_pick(){
if [[ $(($turn % 2)) == 0 ]]
then
play=$player_2
echo -n "PLAYER 2 PICK A SQUARE: "
else
echo -n "PLAYER 1 PICK A SQUARE: "
play=$player_1
fi
read square
space=${moves[($square -1)]}
if [[ ! $square =~ ^-?[0-9]+$ ]] || [[ ! $space =~ ^[0-9]+$ ]]
then
echo "Not a valid square."
player_pick
else
moves[($square -1)]=$play
((turn=turn+1))
fi
space=${moves[($square-1)]}
}
I then create a function that tests win conditions. Because I have to check many different conditions, I find it useful to create a helper function called check_match
that checks for matching symbols. I want to reiterate that a match occurs when there three consecutive symbols of the same value. I start with a helper function that checks if three arguments are the same. If they are the same, then the game state changes to false and I print the winner.
Next I use the helper function in a check_winner()
function that supplies the winning patterns to check against. After every check match call, I want to check for the game state. If the game is over I want to break out of the function.
check_match() {
if [[ ${moves[$1]} == ${moves[$2]} ]]&& \
[[ ${moves[$2]} == ${moves[$3]} ]]; then
game_on=false
fi
if [ $game_on == false ]; then
if [ ${moves[$1]} == 'x' ];then
echo "Player one wins!"
return
else
echo "player two wins!"
return
fi
fi
}
check_winner(){
if [ $game_on == false ]; then return; fi
check_match 0 1 2
if [ $game_on == false ]; then return; fi
check_match 3 4 5
if [ $game_on == false ]; then return; fi
check_match 6 7 8
if [ $game_on == false ]; then return; fi
check_match 0 4 8
if [ $game_on == false ]; then return; fi
check_match 2 4 6
if [ $game_on == false ]; then return; fi
check_match 0 3 6
if [ $game_on == false ]; then return; fi
check_match 1 4 7
if [ $game_on == false ]; then return; fi
check_match 2 5 8
if [ $game_on == false ]; then return; fi
if [ $turn -gt 9 ]; then
$game_on=false
echo "Its a draw!"
fi
}
Finally, after having all the pieces declared and instantiated, I use a simple while loop that checks the state of the game, and loops through player_pick
, print_board
, and check_winner
.
welcome_message
print_board
while $game_on
do
player_pick
print_board
check_winner
done
There you have it. The main order of the program takes place in only a few lines. I think it makes it a lot easier to break apart a program into manageable pieces. This makes it quite readable in the end. Bash is a pretty fun language to play with, and quite powerful, but the syntax is quite different and there is a lot to learn about what each symbol does.
One of the most egregious examples is in the player_pick
function, where I check the value of the space using: space=${moves[($square -1)]}
To break this one line down, I declare the variable space, where variables are declared with =
without any spaces. I use ${}
to check the moves array. Within the []
where I give an array index I have to use ()
to do arithmetic as I'm subtracting one to the value of $square
. Ooof thats a lot of steps to perform a relatively simple action. But thats what makes it fun.
Below is the complete program:
# tic-tac-toe.sh
#!/usr/bin/bash
player_1="X"
player_2="O"
turn=1
game_on=true
moves=( 1 2 3 4 5 6 7 8 9 )
welcome_message() {
clear
echo "========================"
echo "=== LETS PLAY A GAME ==="
echo "========================"
sleep 3
}
print_board () {
clear
echo " ${moves[0]} | ${moves[1]} | ${moves[2]} "
echo "-----------"
echo " ${moves[3]} | ${moves[4]} | ${moves[5]} "
echo "-----------"
echo " ${moves[6]} | ${moves[7]} | ${moves[8]} "
echo "============="
}
player_pick(){
if [[ $(($turn % 2)) == 0 ]]
then
play=$player_2
echo -n "PLAYER 2 PICK A SQUARE: "
else
echo -n "PLAYER 1 PICK A SQUARE: "
play=$player_1
fi
read square
space=${moves[($square -1)]}
if [[ ! $square =~ ^-?[0-9]+$ ]] || [[ ! $space =~ ^[0-9]+$ ]]
then
echo "Not a valid square."
player_pick
else
moves[($square -1)]=$play
((turn=turn+1))
fi
space=${moves[($square-1)]}
}
check_match() {
if [[ ${moves[$1]} == ${moves[$2]} ]]&& \
[[ ${moves[$2]} == ${moves[$3]} ]]; then
game_on=false
fi
if [ $game_on == false ]; then
if [ ${moves[$1]} == 'x' ];then
echo "Player one wins!"
return
else
echo "player two wins!"
return
fi
fi
}
check_winner(){
if [ $game_on == false ]; then return; fi
check_match 0 1 2
if [ $game_on == false ]; then return; fi
check_match 3 4 5
if [ $game_on == false ]; then return; fi
check_match 6 7 8
if [ $game_on == false ]; then return; fi
check_match 0 4 8
if [ $game_on == false ]; then return; fi
check_match 2 4 6
if [ $game_on == false ]; then return; fi
check_match 0 3 6
if [ $game_on == false ]; then return; fi
check_match 1 4 7
if [ $game_on == false ]; then return; fi
check_match 2 5 8
if [ $game_on == false ]; then return; fi
if [ $turn -gt 9 ]; then
$game_on=false
echo "Its a draw!"
fi
}
welcome_message
print_board
while $game_on
do
player_pick
print_board
check_winner
done