Megatech


Standard C++ Tic-Tac-Toe

Introduction

Early in 2016, I really started to pick up C++. I was taking an introductory level city college course that was a little too easy for me because I had taken a similar course several years prior. It was pretty standard, in my opinion, for a basic computer programming course. Most of the assignments involved writing toy applications, which is something I’ve always hated doing. On the other hand, I was also assigned a project for the duration of the course, which I ended up greatly enjoying. Essentially, the project was to be a demonstration of the course’s material. The project would be graded twice, once at the half-way mark of the course alongside the midterm exam and once again at the end of the course. More importantly, the project needed to be creative and impressive. It wasn’t a requirement, but the professor’s expectation was that each student would write a game in more or less standard C++ (e.g., C++11 or C++14 at the time).

Technically the professor allocated the entire duration of the course to the project. However, the reality was that I had closer to two weeks to develop each version. Two weeks were available to develop the first version in time for the midterm exam. Two more weeks were available to improve it in time for the final. Despite being pulled in many directions at the time, I found the experience of writing a whole game that quickly enjoyable. The result is a small hangman game with some RPG styling on top. I think I was being a bit conservative about what I could and couldn’t do in the available time, so I wasn’t especially satisfied with the result.

Luckily, I ended up taking another C++ course with the same professor that fall. Once again, I got to write a game in about four weeks total. For the second game, I ended up writing a simplistic roguelike. I called it Overflow and, somewhat embarrassingly, I’m still fairly proud of it. After completing it though, I never really revisited the idea.

In December of last year, I started to think about these little game projects again. Unlike the games I wrote in the past though, I wanted to really consider what a game written in only standard C++ (i.e., C++20) would look like. To keep things simple, I decided to implement Tic-Tac-Toe. Besides being simple to implement, my hope is that anyone else who is interested in writing these kinds of games can use the source code as an example.

The Problem

When I sat down to actually implement this game the first thing I considered was what I absolutely needed. The common types of computer games (i.e., games written for hardware 3D APIs and graphical user interface systems) have a whole variety of tools at their disposal that aren’t available in any version of the C++ standard. The most problematic deficiencies are the lack of any interactive input and a lack of any inter-process communication.

In general, computer games are basically looping programs that read input from one set of devices and write output to another. Once per loop iteration, the program reads its input, integrates the input into its state, and writes output. These games are interactive because the devices they read input from and write output to are handled separately from the game loop itself. That is to say, the game can begin its next loop before the user has actually seen the output, and it can continue even when there is no input at all.

Unfortunately, C++20 lacks any concept of this type of interactivity. The closest that you can get, as far as I know, is to rely upon a hosted environment connecting standard input to an interactive terminal. If it does, then you can rely on reads blocking until a user writes input into the terminal, and you can rely on never reaching an end-of-file state. This is basically what Overflow and other student applications do. It’s doubly unfortunate then that this can’t be relied upon either. If standard input is not an interactive terminal then there’s no reliable way to receive input interactively. As I understand it, there’s no standard way to detect this behavior either.

To make things more difficult, there’s no standard way to send messages to other processes or receive responses. C++20 has plenty of ways to do this within a single process (e.g., std::raise or std::binary_semaphore) but no way to communicate outside without extensions to the standard library. This effectively rules out most naive strategies for interactivity, but it also causes another problem.

Since there’s no standard way to communicate between processes its impossible for a standard C++ program to synchronize access to resources that it might require. For example, if a user spawned two or more instances of a game process, they might end up corrupting any files that the processes write to. In a non-standard setting multiple instances of a single program could communicate and decide how to handle this issue. A standard program needs to find its own solution though.

The Solution

For my implementation of Tic-Tac-Toe, I ended up solving both problems by leveraging the standard I/O library and the standard file system library. Instead of relying on a single looping process, my implementation is actually four separate applications. Each application reads, potentially modifies, and writes a shared data file. Of course, issues would still arise if multiple applications could access the data file at the same time. To synchronize access to the shared data file, my implementation provides a lockfile type using only standard components.

Although it requires some finesse, controlling file access is possible using C++20. The way it works is that an application passes each lockfile object a std::filesystem::path that addresses the file to lock. During the locking procedure, the lockfile attempts to create another file by prepending ".~lock" to the file name. To do this, the lockfile object calls std::fopen with the special "wx" mode. This mode ensures that std::fopen only creates a new file if a file with the same name doesn’t exist. Unfortunately, the equivalent behavior for std::fstream isn’t available until C++23. The lockfile then stores the resulting pointer. This procedure fails if the lockfile already owns a pointer or if the call to std::fopen fails. As long as the applications used to implement the game obey these locks then they can effectively synchronize file access.

To make the game interactive, each application accesses a single binary file. This file, which is described in detail in the README, is 17 bytes long and contains the entire state of a Tic-Tac-Toe game. Actually, the game state only requires 22 bits. The rest of the file is metadata. The applications are completely serial programs that implement one major game function each. To play interactively then, a player executes a sequence of programs. First, he or she creates a game by executing ttt-new-game. Next, he or she executes ttt-take-turn to take a turn. In the single player mode, ttt-take-turn will also handle the computer player’s turn. In the multiplayer mode, the second player needs to invoke ttt-take-turn with his or her selection. Finally, the player can delete the game data by executing ttt-delete-game. The fourth application, ttt-display-game, will output the current game state on command without modifying it.

Of the four applications, only two applications expect any user input. The applications handle user input through program arguments passed to main() (i.e., values stored in argv). ttt-new-game accepts a game mode which is completely optional. On the other hand, ttt-take-turn requires the user to provide the column and row index of a location on the game board that he or she wants to mark.

Thoughts

I’m more or less content with the resulting approach to standard C++ games. Obviously, Tic-Tac-Toe is the least exciting game imaginable, but I think the approach could be extended to much more complex games. Eventually, I’d like to try another roguelike or some other kind of role-playing game written this way. My solution, in particular, suffers from the lack of any advanced argument parsing functionality. That’s a conscious choice on my part. If I had a module similar to the GNU getopt_long available then I would have wrapped the entire game in a single application. The implementation of getopt_long doesn’t require any nonstandard functionality, but I felt it was beyond the scope of a Tic-Tac-Toe game. For a more complex game, I think argument parsing is an absolute requirement.

Another deficiency is the lockfile mechanism. Unfortunately, I don’t think there’s a fix for this that only requires standard C++ code. The lockfile implementation is sufficient if every process on a system obeys it, but that’s not much of a guarantee. It also exhibits all the fun problems that the POSIX openat() function resolves. Actually, being able to use the POSIX interface makes implementing text-mode games much simpler.

Right now, I think the next step for me will be something more complex. This project was a fun exercise, but I want to keep busy for a longer period of time. It’s tempting to jump back to doing something with OpenGL or Vulkan. On the other hand, a game with a better text-mode interface feels like a more logical next step. In any case, whatever I do next will be more interesting to me than Tic-Tac-Toe.

By the way, there are a couple of secrets included in this game. For a while now, I’ve been lingering on what William Gibson says about cyberspace in Neuromancer. Which is to say that cyberspace is a consensual hallucination. Lately, I feel too many games try to be wholly realistic. I think the best games are a bit dream-like. They should have a quality, separate from any design, that is just slightly unreal. Clocks should have five hands. People should speak in a way that sounds like a language but isn’t one. The same road should sometimes lead to different locations. In a dream, things are just coherent enough to tell the story and no more coherent than that. Anyway, this is all to say that I think it’s important for games to have their own curious little behaviors. A good game always needs a secret or two.