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 , .
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!
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.
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.
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.
The actual game logic is nothing more than a simple state machine contained inside
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
GameController converts player inputs to indices which are meaningful for
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
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.
_join_lobby() with manual testing. I have plans to cover it with unit tests, but hey, contributions are welcome!
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…