If you come from central Europe you have probably already heard of the game nine mens morris (commonly referred to as the respective translation of mill in most central European languages). Especially in Austria, where i come from, almost everyone i know has played this simple board game at least a couple of times in their childhood.
But even though many games such as chess have found their way into the online space, nothing of the sorts seems to have happened for nine mens morris. So when me and my friend Leopold Kainz we're looking for school project ideas last fall we came up with an online "mill" playing site(the name was chosen as the english translation because few people from central Europe would actually recognize the game under the name "nine mens morris"). Half a year later we finished a website originally inspired by the likes of chess.com now almost matching the full functionality.
As previously mentioned the functionality is very much inspired by chess.com and similar sites. If you wanted to be completely honest you could go as far as calling the site a chess.com clone for another game.
Going with this theme it was clear to us from the get to that the site should be able to:
Actually implementing these features was a whole different story and took a lot of learning from me and my accomplice. In the end we settled on a tech stack of:
Of course this is an extremely simplified representation of the technologies and techniques we used to build the game. If you are just interested in the code you can go take a look at it on Github. Otherwise i will use this article to give some further depth to those interested.
The first thing we had to figure out was how to connect two players over the internet. This was a problem we had never faced before and we had no idea how to solve it. After some research we decided to use socket.io to connect the players. This was a good choice as it allowed us to easily send messages between the players and the server. The only problem was that we had to figure out how to connect the players in the first place. We solved this by having the server create a room for each game and then sending the room id to the players. The players could then use this id to join the room and start playing.
The game logic was the next big problem we had to solve. We had to figure out how to represent the game state and how to check if a move was valid. We decided to represent the game state as an entry in a separate Postgres database, which deleted the entries after a game was finished. This table housed information such as the time left for each player, the actions awaited by the players and the board state. The board state is represented as a json object of all the different spaces on the board and the pieces in each players hand. Each of these slots could be x
for empty, w
for white and b
for black.
{
a1: 'x',
a4: 'x',
a7: 'x',
d7: 'x',
g7: 'x',
g4: 'x',
g1: 'x',
d1: 'x',
d2: 'x',
b2: 'x',
b4: 'x',
b6: 'x',
d6: 'x',
f6: 'x',
f4: 'x',
f2: 'x',
d3: 'x',
c3: 'x',
c4: 'x',
c5: 'x',
d5: 'x',
e5: 'x',
e4: 'x',
e3: 'x',
wside: ['w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w'],
bside: ['b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b'],
}
This decision was mostly made with the visual representation of the game in mind. Of course it would have been possible to represent the pieces in a players hand as an integer. But doing it this way would have made it very hard to visualize pieces that a player has collected from his opponent. The rest of the game logic was implemented on a separate node.js server that was connected to the temporary Postgres database. This server was responsible for checking if a move was valid and updating the game state accordingly. After the game was finished it handed the result and the move history over to the main server and deleted the game state from the database.
The AI was the last big problem we had to solve. We decided to use the minimax algorithm to implement the AI. This algorithm is used in many games and is very well documented. The only problem was that we had to figure out how to represent the game state in a way that the algorithm could understand. As we had already decided to represent the game state as a json object we had to sanitize the data before we could use it. We decided that the AI should take the game state as an input and predict the next best move. The AI was implemented using TensorFlow and deployed as a rest api using TensorFlow. This allowed us to offload as many tasks as possible to the client and only use the server for the most basic tasks.
Finally we had to figure out how to deploy the ensemble. We decided to use docker-compose to deploy the whole thing. This allowed us to easily deploy the whole thing on a single server. We used Nginx as a reverse proxy to route the requests to the correct docker container. This allowed us to easily scale the whole thing if we ever needed to. The docker compose file looked something like this:
version: '3'
services:
db:
build:
context: ./db/
network: host
restart: always
env_file:
- ./.env
environment:
POSTGRES_USER: milldaemon
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
PGDATA: /var/lib/postgresql/data/db-files/
web:
build:
context: ./web/
network: host
restart: always
depends_on:
- db
ports:
- '8082:8080'
db2:
build:
context: ./db2/
network: host
restart: always
env_file:
- ./.env
volumes:
- pgdata:/var/lib/postgresql/data
web2:
build:
context: ./web2/
network: host
restart: always
ports:
- '8080:8080'
depends_on:
- db2
- web
next:
build:
context: ./next/
network: host
restart: always
depends_on:
- web2
ports:
- '3100:3000'
volumes:
db-config:
pgdata:
driver: local
If you've read this far i hope you enjoyed this little writeup. If you want to play the game you can do so here. If you want to take a look at the code you can do so here. If you want to contact me you can do so here.