Archive note: This is an old devlog for Sudokudo, an experimental multiplayer Sudoku project. The implementation described here is no longer active, but the process, and the questionable first algorithm, was worth preserving.
Sudokudon’t
A new project?
Yes. This is the first devlog for a simple (not so simple) project. Let’s start with the basics.
Sudoku
Everybody knows Sudoku: a big 9×9 grid where rows, columns, and internal 3×3 squares don’t contain repeated numbers. When using a Sudoku app, there is so much we take for granted, from generating the Sudoku to displaying an incomplete version for us to fill.
Generating a Sudoku grid is not trivial. Fortunately, there are many posts and tutorials explaining how to do it.
But I’ll ignore them all.
For now, I’ll use a generic algorithm that creates a 9×9 grid from a seed. It does not follow Sudoku’s rules yet; I’ll take care of generating a valid grid later.
The main focus of this phase is allowing users to create rooms. Each new room receives a completed Sudoku, and the players receive an incomplete version to solve.
1 function generateSudoku(): Sudoku {
2 const codePoints = this.seed
3 .split('')
4 .map((char) => char.charCodeAt(0))
5
6 const grid = Array.from(
7 { length: 9 },
8 () => new Array<number>(9).fill(0),
9 )
10
11 let index = 0
12 for (let row = 0; row < 9; row++) {
13 for (let column = 0; column < 9; column++) {
14 grid[row][column] = (codePoints[index] % 9) + 1
15 index = (index + 1) % codePoints.length
16 }
17 }
18
19 this.grid = grid
20 return this
21 }
The generator parameter controls whether the incomplete grid is truly random or predictable. I’m using seedrandom, which provides exactly that predictable randomness.
Server
I’m using NestJS, of course. I already have experience with the framework, and I don’t want to spend time relearning the basics of another backend framework. Sadly, this means I’m creating a game server with Node.js. Performance lovers are allowed to scream now.
Authentication
To speed up development, I reused an authentication implementation from a previous Firebase project. I already regret that decision: it feels like overkill when all I need is a username/password flow with JWT tokens.
Room management
At first, I stored every room in memory. Constantly losing those rooms eventually forced me to persist them with Firebase.
Rooms are updated frequently, which will quickly become a problem because of Firebase’s quota limits and, especially, latency. I’ll probably switch to a non-relational database later, but that is not necessary during early development.
Socket.IO
Actions such as joining, leaving, starting or ending a game, and making a move are executed through WebSockets.
Fortunately, Socket.IO has built-in rooms, which means I can emit events to a specific room. Joining is managed by the server, so it should be straightforward to control who can and cannot make a move during a game.
@UsePipes(new ValidationPipe())
@UseGuards(WsAuthGuard)
@SubscribeMessage('joinRoom')
async function joinRoom(
@MessageBody() joinRoomDto: JoinRoomDto,
@GetSession() session,
@ConnectedSocket() client: Socket,
) {
const room = await this.roomsService.joinRoom(session, joinRoomDto)
client.join(`room:${room.code}`)
this.server.to(`room:${room.code}`).emit('userJoined', room)
return room
}
Next steps
There is still a lot to do, but for now I’ll focus on:
- Creating a proof of concept for web, Android, and iOS with Tamagui.
- Implementing a valid Sudoku-generation algorithm and a strategy for digging holes in the grid displayed to the client.
- Implementing matchmaking. Rooms are fun, but I’d also like players to tap one button and be matched with someone at a similar experience level.
