Ensar Basri Kahveci

overly distributed

Writing a simple game with Python coroutines: Match symbols

Posted at — May 27, 2020

I have been writing Python code occasionally here and there, but never learnt Python properly. Thanks to the ongoing coronavirus crisis, we have been under lockdown in Ankara for more than 2 months and I have plenty of time to try new things. So I decided to build a small fun project with Python. It took me only a few days to grasp its fundamentals and start to understand how to get things done in Python. Actually, I have been learning C++ on my own for the last couple of months and when I switched to Python, it felt like I was in heaven :) It is no wonder that Python is one of the most popular programming languages [1], [2].

I developed this game in a weekend. Actually it is an old habit for me to build this game when I start to dig a new programming language. You can check out the game on Github. Its source code is available with the MIT license and contributions are welcome!

How to play

The game is played by 2 people in turns. There is a deck of symbols and initially all symbols are closed. Each player opens 2 cells on her turn. If both cells have the same symbol, the player’s score is incremented and the player opens 2 other cells. Otherwise, it becomes the other player’s turn. The game is completed when either one of the players manages to match more than half of the symbols, or both match the same number of symbols, which is a tie.

The game is played via a simple and text-based TCP server. The game server supports an arbitrary number of players. Since the game is for 2 players, the game server pairs connecting players in the FIFO order. When a player joins the server, she will wait in the lobby until another player chimes in. When a player disconnects during the game, the other player returns back to the lobby. Games and communication with connected clients are handled with coroutines.

Demo

You can see a quick demo below. In this demo, I start a game server with the default settings, then connect 2 TELNET clients. When the first client connects, it waits for another client to join. When the second client chimes in, the game starts. Each client opens 2 cells on its turn. After some time, one of the clients just disconnect and the other client goes back to the lobby.

Try it yourself

You can start the game server as below. It binds to localhost:10670 by default. Make sure you have Python 3.7+.

Once the server is up and running, you can connect to it with telnet localhost 10670. If you want to disconnect your TELNET client, press CTRL+], then type close. It is shown in the demo above.

How does it work?

The actual game logic is nothing more than a simple state machine contained inside src/game.py. Game class is initialized with a string of unique symbols and 2 players. It duplicates and shuffles the given symbols, and puts them on a list. So each symbol is present in 2 random indices in the list.

Game class knows nothing about handling of player inputs and visualization of the game deck. This part is realized by GameController, which is available in src/game_controller.py. This class represents the game deck as a 2-dimensional array. Rows are labelled from 1 to 9 and columns are labeled from A to Z. Player inputs are received as strings of length = 2 where the first character is the row label and the second character is the column label. For instance, in the demo above, the game is shown with a 4 x 6 matrix so valid inputs are between 1A and 4F. GameController converts player inputs to indices which are meaningful for Game.

src/game_server.py uses the curio library for running coroutines. The game server starts a coroutine for each connected client with the _client_handler() function. This function is quite simple. It first awaits on _get_player_name() until the user provides her name. Then, it creates a second coroutine via _join_lobby() and awaits on it. _join_lobby() returns only when the client is disconnected, so _client_handler() also returns afterwards.

_Player is a simple class for encapsulating a client’s stream and input/output queues. It contains a few async methods to read from and write to client streams. You can check src/game_server.py for its code.

_join_lobby() is the most complex function in the src/game_server.py source file. It uses _Randezvous objects to match connected players in FIFO order. curio.TaskGroup is used for grouping coroutines together and managing their execution. So _Randezvous uses curio.TaskGroup to run coroutines for the game logic and communication with players. _play_game() coroutine implements the game logic and the player I/O is handled via the coroutines spawned by _start_player_io(). These coroutines pass messages to each other via curio.Queue objects. When _join_lobby() runs for the first player, it starts the I/O coroutines for that player along with the _play_game() coroutine, then joins to the task group of the current _Randezvous object. In this point, it practically waits until another player joins to the server. _join_lobby() coroutine also runs for the second player and starts the game by notifying the _play_game() coroutine. The current _Randezvous becomes full after the second client connects, hence the second player initializes a new _Randezvous object for new clients which will connect to the server.

One of the most tricky parts of working with coroutines is graceful handling of termination paths. _Randezvous class creates curio.TaskGroup objects in a way that await calls inside a _join_lobby() coroutine return when any coroutine spawned by curio.TaskGroup returns, i.e., either the _play_game() coroutine returns on game over, or a _player_inbound() coroutine returns on player disconnection. When the execution comes back to the _join_lobby() coroutine, if both players are still active, it starts a new game. Otherwise, it joins the active player to the current randezvous state.

I developed _join_lobby() with manual testing. I have plans to cover it with unit tests, but hey, contributions are welcome!

What is next?

I am mostly done with the development of this game for now. I might do some minor updates to make it more Pythonic as I learn more about Python. I just published it on Github with the MIT license in case other Python developers may find it interesting or useful. So, have fun!

Till next time…

References

comments powered by Disqus