Welcome!
Welcome to Part 5 of our series on the NATS JetStream KV Store! This time, we're going to build a little something - nothing too big just yet, but we're making progress. Stay tuned, because we'll get there!
Where did we leave off?
We did a bunch of CLI commands but let's try to put them to some actual use.
We're going to make Tic Tac Toe in the bash shell!
Terminal Tic Tac Toe
Say that five times fast… actually, it's not that hard. I just did it - did you? You did. Admit it, you totally did.
Alright, enough fun - let's move on! Time to get our setup ready.
First things first let's start fresh again.
# Start NATS Jetstream if you haven't already.
$ ./nats-server -js
# Delete all your buckets!
$ nats kv del <bucket>
# Create the bucket named tictactoe
$ nats kv add tictactoe
It's time to design the main game loop for our Tic-Tac-Toe shell game. While you can simply copy the code, I'll also walk you through how it works. We'll implement five key functions:
Main
The main game loop.
Initialize Board
Resets the game board to its initial state.
Display Board
Displays the current state of the board.
Make Move
Handles moves for players X and O.
Check Winner
Determines if there's a winner. (Pretty self-explanatory!)
Let's get started!
Main Loop
Let's go over the main game loop.
# Main game loop
main() {
# Displays the main menu and handles user input.
echo "Welcome to Tic-Tac-Toe with NATS JetStream KV!"
echo "1. Initialize/Reset Board"
echo "2. Play Game"
echo "3. Exit"
echo "-------------------"
while :; do
# Infinite loop to keep showing the menu until the user exits.
echo -n "Choose an option: "
read choice
# Reads the user's menu choice.
case $choice in
1)
initialize_board
;;
2)
while :; do
echo "Player $current_player's turn."
echo "Enter your move (e.g., A1) or press 3 to display the board."
echo -n "Your input: "
read cell
# Handle special commands
if [[ $cell == "3" ]]; then
# If the user enters "3", display the board.
display_board
continue
fi
# Make the move if input is not a special command
if make_move $cell; then
if check_winner; then
# Check if there is a winner and respond if there is.
current_player="X" # Reset the current player
break # Exit to the main menu
fi
fi
done
;;
3)
echo "Goodbye!"
exit 0
;;
*)
echo "Invalid option. Try again."
;;
esac
done
}
This process is fairly straightforward. Here's how it works:
We start with an outer loop that continues until the user makes a choice between options 1–3:
- Clear the board for a new game.
- Start the game.
- Quit the program.
Once the game starts, we enter an inner loop, which serves as the main game loop. Here's what happens:
The loop pauses at the read command, waiting for the user to provide input.
If the input is the special character 3, it displays the board.
Otherwise, the input is passed to make_move
. If the input is valid, it checks for a winner.
Depending on the input and the result the loop either restarts to process more moves, or it exits if a winner is detected.
Initialize the Board
Now we get to use some good ol' fashioned NATS Jetstream KV Store!
# KV bucket name
BUCKET="tictactoe"
# Resets the Tic-Tac-Toe board by initializing all cells in the bucket.
initialize_board() {
echo "Initializing board..."
for cell in A1 A2 A3 B1 B2 B3 C1 C2 C3; do
# Sets each cell in the KV store to an empty space (" ").
nats kv put $BUCKET $cell " " >/dev/null 2>&1
done
echo "Board initialized."
# Displays the newly initialized board.
display_board
}
Alright, buckle up - this is where the magic happens, and it's simpler than you think!
We're going to loop through every cell of our Tic-Tac-Toe board and use the nats kv put command to stash an empty space for each one. Think of it like setting up a fresh board where every cell is just waiting for someone to make a move.
Each cell gets its own key-value pair in the bucket, like key=A1, Value=" "
. Boom, instant empty board!
-
>/dev/null
: This sneaky bit shushes the success messages from the nats kv put command. No bragging allowed here! -
2>&1
: And this? It's like saying, "Oh, you messed up? Cool, but we're not telling anyone." Any errors get tossed into the void too.
Once the board is all set up, we'll proudly display it like a freshly baked cake.
Display Board
Oh, this one's a piece of cake! 🍰
Here's the deal: we've got a super simple function called display_board()
that does exactly what it says-it shows off our Tic-Tac-Toe board in all its glory.
What's happening under the hood? We're just grabbing the values from JetStream using the nats kv get command for each cell (A1, A2, A3, and so on). Then, we're stitching them together in a way that looks like a proper Tic-Tac-Toe board.
Recognize the pattern? Yep, it's just echoing the values back with some fancy lines (--+---+--) to make it look legit. We use the raw flag to retrieve only the value of the Key from the KV Store - since NATS will give you more data (such as the revision number or time of the event etc) because it's awesome like that.
We don't need the extra data however; just the value.
# Function to display the board
display_board() {
echo "Current Board:"
echo "$(nats kv get $BUCKET A1 --raw 2>/dev/null) | $(nats kv get $BUCKET A2 --raw 2>/dev/null) | $(nats kv get $BUCKET A3 --raw 2>/dev/null)"
echo "--+---+--"
echo "$(nats kv get $BUCKET B1 --raw 2>/dev/null) | $(nats kv get $BUCKET B2 --raw 2>/dev/null) | $(nats kv get $BUCKET B3 --raw 2>/dev/null)"
echo "--+---+--"
echo "$(nats kv get $BUCKET C1 --raw 2>/dev/null) | $(nats kv get $BUCKET C2 --raw 2>/dev/null) | $(nats kv get $BUCKET C3 --raw 2>/dev/null)"
}
Easy enough. What's next?
Make Move
Ok now we're getting to the game logic.
current_player="X"
# Function to make a move
make_move() {
local cell=$1
# The cell the player wants to make a move on that was entered.
# Validate the cell
if ! [[ $cell =~ ^[ABC][123]$ ]]; then
# Checks if the input matches a valid cell format (e.g., A1, B2, C3).
echo "Invalid cell! Please enter a valid cell (e.g., A1, B2, C3)."
return 1
fi
# Check if the cell is already occupied
local value=$(nats kv get $BUCKET $cell --raw 2>/dev/null)
# Retrieves the current value of the cell.
if [[ $value == "X" || $value == "O" ]]; then
# If the cell is already occupied, show an error.
echo "Cell $cell is already occupied. Choose another cell."
return 1
fi
# Make the move
nats kv put $BUCKET $cell $current_player
# Updates the cell in the KV store with the current player's symbol.
echo "Player $current_player moved to $cell."
# Switch to the other player
if [[ $current_player == "X" ]]; then
# If the current player is X, switch to O.
current_player="O"
else
# Otherwise, switch to X.
current_player="X"
fi
display_board
# Show the updated board after the move.
}
Alright, this isn't too complex - it's mostly just JetStream functionality. Here's what happens step by step in make_move:
- We take the cell argument passed to the function.
- We validate it using a regex to ensure it's a valid board cell (A1 to C3).
- We check if the cell is already occupied (i.e., it contains X or O).
- If valid and unoccupied, we update the cell's key in the bucket with the current player's symbol (X or O).
- Finally, we switch to the next player and display the updated board.
Check Winner
The only semi-complicated function…. but not really.
# Function to check for a winner
check_winner() {
local board=()
# Creates an array to store the current state of the board.
for cell in A1 A2 A3 B1 B2 B3 C1 C2 C3; do
board+=("$(nats kv get $BUCKET $cell --raw 2>/dev/null)")
# Populates the board array with the values of each cell.
# board=("X" "O" " " " " "X" "O" "O" " " "X") etc
done
# Winning combinations
local win_patterns=(
"0 1 2" "3 4 5" "6 7 8" # Rows
"0 3 6" "1 4 7" "2 5 8" # Columns
"0 4 8" "2 4 6" # Diagonals
)
# We constructed the board AND it will look like this:
# board=("X" "O" " " " " "X" "O" "O" " " "X")
for pattern in "${win_patterns[@]}"; do
# Iterates through each winning pattern.
local i1=$(echo $pattern | cut -d' ' -f1)
local i2=$(echo $pattern | cut -d' ' -f2)
local i3=$(echo $pattern | cut -d' ' -f3)
# local i1=$(echo $pattern | cut -d' ' -f1)
# Just uses a delimeter of " " and grabs from a string.
# In our case it's a pattern like "0 1 2"
# local i1=$(echo $pattern | cut -d' ' -f1) echoes the pattern
# and pipes it to cut which cuts using the delimeter ' '
# so we get 0, 1, 2 and then -f1 grabs the first field. So 0.
# Then we put it in local variable i1.
# Check if the three cells match and are not empty (not " ")
if [[ ${board[$i1]} == "${board[$i2]}" && ${board[$i2]} == "${board[$i3]}" && ${board[$i1]} != " " ]]; then
# For "0 1 2" iimagine. Or "0 4 8
# If all three cells match and are not empty, a player wins.
echo "🎉 Player ${board[$i1]} wins! 🎉"
initialize_board
# Resets the board for the next game.
echo "Returning to the main menu..."
return 0
fi
done
# Check for a tie (no cells contain " ")
if [[ ! " ${board[*]} " =~ " " ]]; then
# If no cells are empty, the game is a tie.
display_board
echo "It's a tie! 🤝"
initialize_board
echo "Returning to the main menu..."
return 0
fi
return 1
# If no winner or tie is found, the game continues.
}
First we reconstruct the board locally - we iterate over the cell list and get the values from the NATS KV Store accordingly.
Then we set up the win conditions. We iterate over those win conditions and then check them against our local board. If there's a winner we show a celebration message and reset the board!
We then check for a tie and then reset the board if there is one.
If none of the above occurs the game continues.
The Full Game
Go ahead and just copy this.
#!/bin/bash
# Indicates that this script should be run using the Bash shell interpreter.
# KV bucket name
BUCKET="tictactoe"
# Defines the name of the NATS Key-Value bucket used for storing the game state.
current_player="X"
# Tracks the current player, starting with "X".
# Resets the Tic-Tac-Toe board by initializing all cells in the bucket.
initialize_board() {
echo "Initializing board..."
for cell in A1 A2 A3 B1 B2 B3 C1 C2 C3; do
nats kv put $BUCKET $cell " " >/dev/null 2>&1
# Sets each cell in the KV store to an empty space (" ").
# 1. >/dev/null: Suppresses the normal success message that
# the nats kv put command might generate.
# 2. 2>&1: Ensures that any errors produced (e.g., if the nats command fails)
# are also discarded.
done
echo "Board initialized."
display_board
# Displays the newly initialized board.
}
# Function to display the board
display_board() {
echo "Current Board:"
echo "$(nats kv get $BUCKET A1 --raw 2>/dev/null) | $(nats kv get $BUCKET A2 --raw 2>/dev/null) | $(nats kv get $BUCKET A3 --raw 2>/dev/null)"
echo "--+---+--"
echo "$(nats kv get $BUCKET B1 --raw 2>/dev/null) | $(nats kv get $BUCKET B2 --raw 2>/dev/null) | $(nats kv get $BUCKET B3 --raw 2>/dev/null)"
echo "--+---+--"
echo "$(nats kv get $BUCKET C1 --raw 2>/dev/null) | $(nats kv get $BUCKET C2 --raw 2>/dev/null) | $(nats kv get $BUCKET C3 --raw 2>/dev/null)"
}
# Function to make a move
make_move() {
local cell=$1
# The cell the player wants to make a move on that was entered.
# Validate the cell
if ! [[ $cell =~ ^[ABC][123]$ ]]; then
# Checks if the input matches a valid cell format (e.g., A1, B2, C3).
echo "Invalid cell! Please enter a valid cell (e.g., A1, B2, C3)."
return 1
fi
# Check if the cell is already occupied
local value=$(nats kv get $BUCKET $cell --raw 2>/dev/null)
# Retrieves the current value of the cell.
if [[ $value == "X" || $value == "O" ]]; then
# If the cell is already occupied, show an error.
echo "Cell $cell is already occupied. Choose another cell."
return 1
fi
# Make the move
nats kv put $BUCKET $cell $current_player
# Updates the cell in the KV store with the current player's symbol.
echo "Player $current_player moved to $cell."
# Switch to the other player
if [[ $current_player == "X" ]]; then
# If the current player is X, switch to O.
current_player="O"
else
# Otherwise, switch to X.
current_player="X"
fi
display_board
# Show the updated board after the move.
}
# Function to check for a winner
check_winner() {
local board=()
# Creates an array to store the current state of the board.
for cell in A1 A2 A3 B1 B2 B3 C1 C2 C3; do
board+=("$(nats kv get $BUCKET $cell --raw 2>/dev/null)")
# Populates the board array with the values of each cell.
# board=("X" "O" " " " " "X" "O" "O" " " "X") etc
done
# Winning combinations
local win_patterns=(
"0 1 2" "3 4 5" "6 7 8" # Rows
"0 3 6" "1 4 7" "2 5 8" # Columns
"0 4 8" "2 4 6" # Diagonals
)
# We constructed the board AND it will look like this:
# board=("X" "O" " " " " "X" "O" "O" " " "X")
for pattern in "${win_patterns[@]}"; do
# Iterates through each winning pattern.
local i1=$(echo $pattern | cut -d' ' -f1)
local i2=$(echo $pattern | cut -d' ' -f2)
local i3=$(echo $pattern | cut -d' ' -f3)
# local i1=$(echo $pattern | cut -d' ' -f1)
# Just uses a delimeter of " " and grabs from a string.
# In our case it's a pattern like "0 1 2"
# local i1=$(echo $pattern | cut -d' ' -f1) echoes the pattern
# and pipes it to cut which cuts using the delimeter ' '
# so we get 0, 1, 2 and then -f1 grabs the first field. So 0.
# Then we put it in local variable i1.
# Check if the three cells match and are not empty (not " ")
if [[ ${board[$i1]} == "${board[$i2]}" && ${board[$i2]} == "${board[$i3]}" && ${board[$i1]} != " " ]]; then
# For "0 1 2" iimagine. Or "0 4 8
# If all three cells match and are not empty, a player wins.
echo "🎉 Player ${board[$i1]} wins! 🎉"
initialize_board
# Resets the board for the next game.
echo "Returning to the main menu..."
return 0
fi
done
# Check for a tie (no cells contain " ")
if [[ ! " ${board[*]} " =~ " " ]]; then
# If no cells are empty, the game is a tie.
display_board
echo "It's a tie! 🤝"
initialize_board
echo "Returning to the main menu..."
return 0
fi
return 1
# If no winner or tie is found, the game continues.
}
# Main game loop
main() {
# Displays the main menu and handles user input.
echo "Welcome to Tic-Tac-Toe with NATS JetStream KV!"
echo "1. Initialize/Reset Board"
echo "2. Play Game"
echo "3. Exit"
echo "-------------------"
while :; do
# Infinite loop to keep showing the menu until the user exits.
echo -n "Choose an option: "
read choice
# Reads the user's menu choice.
case $choice in
1)
initialize_board
;;
2)
while :; do
echo "Player $current_player's turn."
echo "Enter your move (e.g., A1) or press 3 to display the board."
echo -n "Your input: "
read cell
# Handle special commands
if [[ $cell == "3" ]]; then
# If the user enters "3", display the board.
display_board
continue
fi
# Make the move if input is not a special command
if make_move $cell; then
if check_winner; then
# Check if there is a winner and respond if there is.
current_player="X" # Reset the current player
break # Exit to the main menu
fi
fi
done
;;
3)
echo "Goodbye!"
exit 0
;;
*)
echo "Invalid option. Try again."
;;
esac
done
}
# Run the main function
main
# Starts the Tic-Tac-Toe game.
Create a file and call it whatever you want - I used tictactoe.sh
.
Then chmod +x tictactoe.sh
to make it executable.
To Play
Before we finally jump into the game, let me show you one more JetStream command I intentionally held back earlier. After all, this is about JetStream, right?
If you haven't started the NATS Server, do it.
./nats-server -js
Open two terminal windows. In the first one, run:
$ nats kv watch tictactoe
Then, in the second window, start the game by running:
./tictactoe.sh
Now take a look - you're actively watching the stream for the tictactoe Bucket! Pretty awesome, right? You can see all the updates happening in real-time!
./tictactoe.sh
Welcome to Tic-Tac-Toe with NATS JetStream KV!
1. Initialize/Reset Board
2. Play Game
3. Exit
-------------------
Choose an option: 1
Initializing board...
Board initialized.
Current Board:
| |
--+---+--
| |
--+---+--
| |
Choose an option: 2
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: A1
X
Player X moved to A1.
Current Board:
X | |
--+---+--
| |
--+---+--
| |
Player O's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: B2
O
Player O moved to B2.
Current Board:
X | |
--+---+--
| O |
--+---+--
| |
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: C1
X
Player X moved to C1.
Current Board:
X | |
--+---+--
| O |
--+---+--
X | |
Player O's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: F3
Invalid cell! Please enter a valid cell (e.g., A1, B2, C3).
Player O's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: A2
O
Player O moved to A2.
Current Board:
X | O |
--+---+--
| O |
--+---+--
X | |
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: B1
X
Player X moved to B1.
Current Board:
X | O |
--+---+--
X | O |
--+---+--
X | |
🎉 Player X wins! 🎉
Initializing board...
Board initialized.
Current Board:
| |
--+---+--
| |
--+---+--
| |
Returning to the main menu...
Choose an option:
This is what you will see in the terminal watching the KV Store.
nats kv watch tictactoe
[2025-01-20 01:23:51] PUT tictactoe > A1:
[2025-01-20 01:23:51] PUT tictactoe > A2:
[2025-01-20 01:23:51] PUT tictactoe > A3:
[2025-01-20 01:23:51] PUT tictactoe > B1:
[2025-01-20 01:23:51] PUT tictactoe > B2:
[2025-01-20 01:23:51] PUT tictactoe > B3:
[2025-01-20 01:23:51] PUT tictactoe > C1:
[2025-01-20 01:23:51] PUT tictactoe > C2:
[2025-01-20 01:23:51] PUT tictactoe > C3:
[2025-01-20 01:24:12] PUT tictactoe > A1: X
[2025-01-20 01:24:14] PUT tictactoe > B2: O
[2025-01-20 01:24:19] PUT tictactoe > C1: X
[2025-01-20 01:24:29] PUT tictactoe > A2: O
[2025-01-20 01:24:34] PUT tictactoe > B1: X
[2025-01-20 01:24:34] PUT tictactoe > A1:
[2025-01-20 01:24:34] PUT tictactoe > A2:
[2025-01-20 01:24:34] PUT tictactoe > A3:
[2025-01-20 01:24:34] PUT tictactoe > B1:
[2025-01-20 01:24:34] PUT tictactoe > B2:
[2025-01-20 01:24:34] PUT tictactoe > B3:
[2025-01-20 01:24:34] PUT tictactoe > C1:
[2025-01-20 01:24:34] PUT tictactoe > C2:
[2025-01-20 01:24:34] PUT tictactoe > C3:
Another thing you can do is make a few moves and then close the terminals. If you restart the game you will see it will have maintained the state from where you left off.
$ ./tictactoe.sh
Welcome to Tic-Tac-Toe with NATS JetStream KV!
1. Initialize/Reset Board
2. Play Game
3. Exit
-------------------
Choose an option: 1
Initializing board...
Board initialized.
Current Board:
| |
--+---+--
| |
--+---+--
| |
Choose an option: 2
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: A1
X
Player X moved to A1.
Current Board:
X | |
--+---+--
| |
--+---+--
| |
Player O's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: B3
O
Player O moved to B3.
Current Board:
X | |
--+---+--
| | O
--+---+--
| |
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: C2
X
Player X moved to C2.
Current Board:
X | |
--+---+--
| | O
--+---+--
| X |
Player O's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: ^C
We quit the game close Jetstream and closed the terminals.
Let's restart it all and start the game.
$ ./tictactoe.sh
Welcome to Tic-Tac-Toe with NATS JetStream KV!
1. Initialize/Reset Board
2. Play Game
3. Exit
-------------------
Choose an option: 2
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input: 3
Current Board:
X | |
--+---+--
| | O
--+---+--
| X |
Player X's turn.
Enter your move (e.g., A1) or press 3 to display the board.
Your input:
See? State is preserved!
So yeah all of that is pretty cool. I want to apologize for not showing you the watch command earlier because it's rad. Don't worry though you'll be using it again differently soon. We're going to do some actual development now in Part 5!
See ya there!
Top comments (0)