Project: Telnet Tic-Tac-Toe Game

Telnet Tic Tac Toe
Telnet Tic Tac Toe

In a time of social distancing, working from home, and many local businesses being closed, getting together for an online game session is a great way to stay in contact with people and have fun playing a game.

I thought it would be fun to develop a simple text-based game playable by a client connecting via Telnet. The basic ideas in use here could be extended to create more full-featured text-based games.

For our basic client-server networked text-based game, we will use Ruby and implement a 2-player tic-tac-toe game. We’ll just need one script to listen for client connections and run the game logic. Clients can connect and play the game using the standard Telnet application. This project is being done in a “quick and dirty” style to show off the basics but not necessarily to show off recommended programming style. 🙂

We’ll start out our code by requiring the socket module to do TCP communication and declaring some variables we’ll use in the game server.

require "socket"

@server = TCPServer.new(5678)
@players = [@server.accept, @server.accept]
@labels = %w[X O]
@board = [[nil] * 3, [nil] * 3, [nil] * 3]
@turn = 0
@winner = nil

The @players array will hold the client socket handles. For this simple implementation, whichever player connects first is “X” and goes first. We initialize our @board to a 3×3 grid of nil values. @turn will hold whose turn it is and @winner will hold the winning player, if any.

Next we’ll define some helper methods. Here’s one to send a message to the player whose turn it is:

def write(msg)
  @players[@turn].write(msg)
end

Here’s one to print the current board along with a label for each cell:

def print_board
  position = 0
  3.times do |y|
    write("+---+---+---+\r\n")
    3.times do |row|
      3.times do |x|
        last = x == 2 ? "|\r\n" : ""
        case row
        when 0
          position = y * 3 + x + 1
          write("|#{position}  #{last}")
        when 1
          entry = @board[y][x] || " "
          write("| #{entry} #{last}")
        when 2
          write("|   #{last}")
        end
      end
    end
  end
  write("+---+---+---+\r\n")
end

We’ll want a method to ask a player to take their turn and enter the number of the cell they want to play in. We do a basic check that they entered a valid position and loop until they do:

def get_selection
  loop do
    write("Select a position:\r\n")
    input = @players[@turn].gets
    position = input.to_i
    if position >= 1 && position <= 9
      y = (position - 1) / 3
      x = (position - 1) % 3
      return x, y if @board[y][x].nil?
    end
  end
end

Ok, now we can start on some actual game logic. Every turn, we want to show the active player the board, ask them for their move, record their move, and then show the updated board:

def take_turn
  print_board
  x, y = get_selection
  @board[y][x] = @labels[@turn]
  print_board
end

To see if any player has won yet, we can add some methods to check for a win condition:

def check_win_row(y)
  if @board[y][0] && @board[y][0] == @board[y][1] && @board[y][0] == @board[y][2]
    return @board[y][0]
  end
end

def check_win_col(x)
  if @board[0][x] && @board[0][x] == @board[1][x] && @board[0][x] == @board[2][x]
    return @board[0][x]
  end
end

def check_end
  3.times do |i|
    if @winner = check_win_row(i)
      return
    elsif @winner = check_win_col(i)
      return
    end
  end
  if @board[0][0] && @board[0][0] == @board[1][1] && @board[0][0] == @board[2][2]
    return @winner = @board[0][0]
  end
  if @board[0][2] && @board[0][2] == @board[1][1] && @board[0][2] == @board[2][0]
    return @winner = @board[0][2]
  end
end

One last helper method will write an end-game message to each player:

def print_end
  if @winner
    win_msg = "#{@winner} wins!"
  else
    win_msg = "Tie game!"
  end
  @players.each do |client|
    client.write("Game over. #{win_msg}\r\n")
  end
end

Ok, we are ready to write the core game logic. With the methods defined above, the main game loop is actually quite simple:

9.times do
  take_turn
  @turn ^= 1
  check_end
  break if @winner
end

print_board
print_end

Here we have up to 9 turns, always alternating which player’s turn it is. After a player takes a turn, we check for a win condition. If a winner is found, @winner will be set, so we exit the game loop if that happens. Finally, we print the board for the player who didn’t take the last turn, so that they can see the final state of the board, and then we print the end of game message.

Full Source

Download the full source file here: https://www.tangibleabstraction.com/pub/tttt.rb.

Running

The game can be run with the command ruby tttt.rb. Clients can connect to the server with telnet name_or_ip 5678. If you are connecting through a router/firewall you would need to open or forward the port to be able to connect.

Future Improvements

This project showed off the extreme basics of a text-based telnet game. It gathered input a whole line at a time and did not do much error checking. There are several improvements that could be made to create a more robust game server:

  • Add a lobby, perhaps allowing the player to enter a name for themselves and join or create a new game. Loop to allow multiple games rather than exiting after a single game.
  • Use Telnet control sequences to switch the input mode from linewise to raw characters. Then the client could use a single keystroke to select an option without needing to press enter afterward.
  • Randomize the starting player.

Let me know if you create any interesting text-based Telnet games!

Comments are closed.