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!