Tic Development (en)
Tic Development (en)
07 de julho de 2026 — Tuesday
My friend Renato and I got together and ended up falling into a conversation we frequently fell into: minimally cool software ideas for us to develop together.
Probably during yet another exchange of ideas about game development, the discussion turned toward WebSockets. Then came the idea, simple and straightforward: making a POC of a game using WebSockets. It was ideal: it did not seem that difficult, but neither of us had ever done anything like it.
The language we ended up choosing was Go, because of the language's popularity for handling network connections very well and, mainly, because we like Go.
Renato then did a quick search to find out how WebSockets are usually handled in Go and found the Gorilla WebSocket package. It seemed great and very widely used. For the frontend, our initial idea was for Renato to make a TUI, so we could run everything directly in the terminal.
Perfect. With the technologies defined, all that was left was to decide the concept of the game. And what would be the simplest possible game and idiot-proof? A game that had already been made so many times that there would be no way to mess it up?
The answer was obvious and came from Renato:
Well, we will make a tic-tac-toe game!
And so the research into how to make a tic-tac-toe game began. Step 1 was simple: what algorithm is normally used to determine the winner?
After searching for a while, we found an answer to a LeetCode problem on Stack Overflow explaining the algorithm. Unfortunately, I was not competent enough to find the original post.
And then we were ready.
What we needed now was a little project planning: a way to keep track of the tasks and a diagram explaining the system at a high level. The idea was to understand what made the thing tick: what routes would exist, what the information flow would be, etc.
So we discussed it, and Renato came up with this beautiful drawing:
Perfect. We had the diagram, the algorithm, and the technologies.
We now needed a way to keep track of the tasks. The point was that, being highly and dangerously rebellious, we did not want to use a tool actually meant for that (long live counterculture).
So we decided to use Dontpad, a website where you create notes and can share them using a generated unique link. Anyone who, accidentally or deliberately, gets the link can change everything that was written before.
Amazing.
Perfect. The tasks had been defined and we made an agreement: I would work 80% on the backend and 20% on the frontend, and Renato the other way around.
It was an ugly fight.
My first task was simple: create the repository, start a Go project, and add Renato to the project. As the hardworking man that I am, I did this hard work that very same night.
08 de julho de 2026 — Wednesday
On the second day of work, I was looking forward to the next task: actually applying the game's base algorithm to check the winner.
Initially, I thought all I needed was the model of a game and a Move method, since the algorithm covered the test to check whether that last move was the winning move or resulted in a draw.
And that is what I committed:
Commit — initial algorithm implementation
Having done that, I moved on to the next task: setting up the basics so we could run the project with Docker:
Commit — initial Docker configuration
That was all for that day. Applying the algorithm to the code took a considerable amount of work, and I had to sleep so I could work the next day.
10 de julho de 2026 — Friday
Renato finished setting up Docker and started Docker Compose so we could run MongoDB in another container.
That day I also remembered that we needed an algorithm to check whether a move was valid before registering it.
Yeah, I had not thought of that. Tough week.
So I made a simple algorithm. Even after doing that, honestly, something did not feel right. A slice of queued ints did not make much sense for communication with the frontend. We needed some kind of mapper or something like that.
This was my initial discomfort, which eventually evolved until I realized that a pretty important piece of the puzzle was missing. But, for that day, those were the commits.
I would worry about that later.
15 de julho de 2026 — Wednesday
Five days without touching the project.
Five days with Renato fervently pestering me to develop the unit tests.
So I decided to work on that that day.
The idea was to have tests to automatically check whether the moves worked, whether the illegal-move prevention system worked, and so on.
That was when the initial discomfort reached its maximum maturity.
The board mapping we had for the winner algorithm did not make sense in the tests, because we did not actually have the position of each move. We only had a slice of nine zeros over which we ran an algorithm to check who won.
That was when I realized:
The algorithm for determining the winner was just that: the algorithm.
It made no sense to treat it as our actual Board, because information was missing.
Thanks to Renato's insistence (congratulations, Renato) that we do the tests, I could finally conclude that my discomfort made sense: we needed an actual Board to handle communication between the backend and the frontend.
And that was one of my commits that day.
16 de julho de 2026 — Thursday
It was a short day.
I configured our MongoDB and got it running in Docker.
Very cool.
22 e 23 de julho de 2026 — Quarta e quinta-feira
Here the classic backend part began: developing the POST and GET, GET by ID, and GET all routes for games and users. Very simple and straightforward stuff, but extremely pleasant to do with Go.
I absolutely love developing in this language.
I do not remember whether I worked on the project on the 22nd and then on the 23rd or stayed up all night working on it, because I was very excited to make these parts. Even though they were basic, they were very satisfying because of the syntax and the way testing works in Go.
However, the simple days were about to end: I would have to start studying WebSockets in Go.
Dun dun duuuun.
13 de agosto de 2026 — Thursday
Yeah. A considerable break from the project ended up happening. Life stuff, the rush of work, distractions because I had just discovered the game Zomboid (simply incredible). Even so, I was ready to study WebSockets in Go and apply them to the project. On the days I was away from the project, it was all I could think about doing.
And so it started with the basics: reading the Gorilla WebSocket docs.
I had already worked with WebSockets before, in Python, the language I have worked with the most. The first difference between the two was quite interesting to me.
In Python I would simply create a WebSocket route and listen for messages. Something roughly like this:
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
message = await websocket.receive_text()
await websocket.send_text(message)
But, reading the Gorilla WebSocket docs, I understood that the basic flow actually consisted of starting with a normal HTTP request and then formally doing an "upgrade" to a WebSocket:
var upgrader = websocket.Upgrader{}
func websocketHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
_, message, err := conn.ReadMessage()
if err != nil {
return
}
conn.WriteMessage(websocket.TextMessage, message)
}
}
This upgrade performs a handshake that, if everything goes well, returns our *websocket.Conn, representing the connection with the client. Only then, instead of request/response, both sides send messages through the same connection.
Perfect.
I do not want to summarize the docs here. If you are interested, read them! They are very good:
https://pkg.go.dev/github.com/gorilla/websocket#section-readme
The important thing is that I now understood how it worked and already had example code. Besides that, as I always like to do when studying a new technology, I watched a YouTube video performing the basic WebSocket operations, which helped me a lot:
https://www.youtube.com/watch?v=qW_hGMUVVmg
Excellent. Now it was time to apply it to the game.
It was not that difficult: a messaging system and WebSocket connection for one player to send the board update to the other after every move, with a check to see whether they had won or whether the move was correct.
Commits pushed.
"Pretty much done" was an exaggeration, I admit.
18 de agosto de 2026 — Tuesday
This was when we decided how it would actually work to play with someone.
We thought about making a player pool, but decided that would be too complex for a simple POC, so we made an invite system.
You create a game, get a link to send to someone, and when that person clicks the link, they enter their little name and start playing.
Simple and straightforward.
"Invite working" was also an exaggeration.
19 de agosto de 2026 — Wednesday
Yeah, this one was a day of defeat.
Neither Renato nor I are big fans of AI in general, especially for projects that are supposed to be for fun, but sometimes...
Sometimes it is complicated.
Renato saw the backend and got very excited for us to make a frontend right away. However, being lazy, he did not want to do his part of running this in a terminal (he still promises me every day that he will do it).
So we changed plans and decided we would create a simple web frontend, 100% made with AI (because neither of us likes dealing with frontend).
What was the worst that could happen?
At that point, Renato and I were here at home, prompting and coding, coding and prompting, and some problems came up.
First, I had not made the logging very complete (to be fair, there was no task mapped for that, it is Renato's fault), and we also started having some little problems with our friend CORS when trying to run it on the frontend.
It was a stressful night, but eventually the problems were solved, resulting in two extremely depressing commits:
Hey, at least I made the redirect problems and draw result!
But anyway, with the project running with its basic functions, for a POC, everything was practically fine.
I put my best man in charge of deploying this bomb to AWS: Renato.
And, after very few problems (given his high expertise), jogodavelha.site came to life for the first time!
But we still had some things to solve as we started sending the game to friends and family to test.
01/02 de setembro de 2026 — Tuesday
(This one was in the middle of the night, I remember.)
Well, it took a little while longer before we worked on the project again. Some things related to Tic had happened in the meantime, but that is a story for later.
The point was: people played a game, it ended with a win or a draw, and that was it.
We still did not have a feature for rematches!
That was my next mission.
It ended up being a little difficult. I discussed with Renato what the best way to do it would be and it ended up being:
When a game ends, the player can request a rematch. The request goes to MongoDB and is sent through WebSocket messages to the players. The other player can accept and, from there, the game is created in a way linked to the previous one, swapping X and O (whoever was X becomes O and vice versa).
Besides that, I also had to handle concurrency to prevent multiple game creations and acceptances and also had to use the WebSocket Hub to centralize updates.
Uff, quite a lot, but I do not want to complicate the technical part too much.
The commit is there!
Oh, I also added a .env.example before Renato even asked me 45x to do it.
03 de setembro de 2026 — Thursday
This day...
There are no letters, symbols, words, or organizations of all that, the so-called languages, to describe what happened.
However, there are commits.
There is the story of a brave warrior, courageous, on his difficult journey.
There are the blood messages left on the wall.
The remnants of the soul of a tormented being.
04 de setembro de 2026 — Friday
Yes, friends, even after so many and so many commits, fighting with the infrastructure part (my best infrastructure guy, by the way), the problem still had not been solved.
However, fear not.
The true hero of this story would leave the final commit in the repository, the ultimate salvation of the arduous and harsh story of the warrior Renato.
An epic ending to an epic story.
Thank you for reading.
Renato's explanation (written by himself)
The tic-tac-toe game project was not an easy project, despite every deployment being the same thing: download and run the program, every system comes with its own difficulties.
In this case the difficulties were:
- WebSocket works differently regarding CORS and HTTPS, this was a pain in the ass.
- Financial issues, because Murilo has been a little frugal lately.
But I do not blame him for that, any little virtual machine on AWS costs at least 50 reais, and it is completely unpredictable when it comes to additional charges. I wonder how this can be. The internet has existed for more than 20 years and is still not accessible.
Anyway, after about two sessions suffering through the AWS deployment, and many valuable lessons (which I no longer remember), we put a first version online. After a few games and the rest of a night's sleep, we woke up to discover that the app had crashed. According to AWS it was due to lack of memory. I confess that initially I questioned my entire belief in languages with manual memory management and their promises of speed and optimization. But we soon discovered a very important first lesson: Mongo DB will consume all the memory it is given, like a black hole of free space.
Configuring dockercompose to limit the amount of memory the Mongo container could take solved the problem.
Until a 70-real bill arrived on Murilo's solid wood table decorated with a rustic lamp and he called me the same day. So I started looking for alternatives that were cheap but still allowed complete internal configuration.
We ended up going with fly.io, which I still do not understand how it charges, but it is working and no bill has arrived yet (yet). Initially there was a little configuration-hell because fly.io's system works through a configuration file, and there is not much documentation on how to release the enemy of every beginner developer (who continues to haunt us to this day): CORS. Besides that, for some reason, we had to configure it to allow WebSocket connections.
Why the hell does this not simply come enabled by default? What is the motivation for making the default as restrictive as possible instead of simply making it as generic as possible.
As for the front-end, which used to be a separate container, it has now become just another lost page in Vercel's system. With the number of new programmers entering the market and the death of Heroku's free plan, ~ soon Vercel will host 90% of the internet ~.
Anyway, the interesting thing about these challenges is that they are completely real, totally applicable to the real world of software. If you are going to deploy a product today, it is important that you know how to limit MongoDB's memory. After deploying it, it is important to try to reduce infrastructure costs, every company will look for that. And once again the importance of personal projects is proven: they are just as real as projects that are on the market. Maybe they do not have the scale that a project like this has, but everything else is the same.
Finalization (for real this time)
Well, those were our adventures developing an (extremely simple) tic-tac-toe game. Oh, and in case you want to take a look and play at most
two games before getting bored and never thinking about the game again, here is the link to the infamous
Tic - The tic-tac-toe game born from misadventures