← blog

Terminal Tic-Tac-Toe

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 screen grab

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