| Fourplay: A multiplayer word game
Fourplay is a word game I built years ago using Rust and Svelte in order to learn more about Svelte 3.0 and the actor model via Actix.
Nov

Introduction

When Svelte 3.0 was released in 2019 I came up with Fourplay in order to give it a try. At the same time I wanted to try out Actix, and the actor model. Four years later, with a mind mostly wiped of the experience, I figure it’s time to write about it.

In looking over the code, I find myself both surprised and ashamed. Surprised by the ridiculous backend I built, and ashamed by the mess of what I called a frontend. To be fair, the frontend is not that bad, but having switched to Typescript and SvelteKit makes it all seem that much more messy.

The game

In creating this game I want to make something which is collaborative, in that you and three others will compete against other four teams to create words from 3 to 8 characters in length. Each day a new 8x8 puzzle is given, and players work within their own 4x4 block of space in order to create words that will work the other three players.

Board
The board with the four player sections (section active for the player is darker in color).

To make words you swap adjacent tiles, either horizontally or vertically within your 4x4 space. A three-letter word is 1 point, a two-letter word is 2 points, and so on doubling up to 32 points for an 8-letter word. Only by collaborating can players make anything past four letters.

Board
Example of a finished game.

You’ll notice that there are a lot of valid three words you’ve likely never heard of. A dictionary is used to check for valid words, though I’m sure that it could be improved. I chose the largest one I could find so as not to exclude any particular word.

The backend

Looking at this you might think to yourself, I could build the backend for this with node and socket.io in a day, and you’d be right. But why waste a day getting something to work when you can waste weeks building something with a model you don’t quite understand in a language you’ve only just started learning?

As mentioned, this uses the actor model. If you’re not sure what the actor model is, it’s basically a message passing system that allows for designing concurrent systems and is completely unnecessary for something like this. If you’re familiar with workers in Node, then you can think of them sort of like that, except that they exist at higher level of abstraction. You could implement an actor type system in Node.

In our case we four types of actors, though at any given time there could be multiple instances of any given one. We have the websocket, server, game and database. The server and database are single instances, while the the websocket has an instance per connected player and game has as many as the number websockets divided by four, since there are four players per game.

Each actor has an inbox and outbox, found in the server/src/handlers/ folder of the project and the messages are passed uni or bidirectionally as per the diagram below.

Message passing
Basic flow of information (The orange line indicates that from database->server is only on the initial setup to ensure the database is connected).

In principle, with actors, you want each actor to control its own state, with changes to the state being mediated with the messages passed back and forth. With Fourplay the only thing I shared between actors is the link to other actors. This can be seen in the server.rs file when creating the server actor in that each actor has either an Addr or Recipient that is used to send messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/// In the server
pub struct Server {
    pub database: Arc<Addr<Database>>,
    ...
}

pub struct User {
    pub addr: Arc<Recipient<WebSocketMessage>>,
    ...
}

pub struct GameData {
    pub addr: Arc<Addr<Game>>,
    ...
}

If you look at the other actors you’ll see similar fields in the corresponding structs. Getting these to communicate was somewhat difficult. In comparison to the frontend, which has one singular state in a store, which can be read from and saved to from anywhere in the application, the backend had no such luxury.

To give an example of some of the difficulty, I had to think about to store the user state once connected. When a user first connects via the /connect route a websocker actor is created.

1
2
3
4
5
let web_socket = WebSocket::new(
    params.user.to_owned(),
    server.get_ref().clone(),
    req.connection_info().remote_addr().unwrap().to_string(),
);

This socket gets a reference (Addr) to the server actor so it can send it messages, but it also retains some information about the user that connected, such as an uuid, name, and ip. It then connects to the server, waiting for a reply back to ensure it has connected:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
self.server
    .send(ServerConnect {
        id: self.id,
        username: self.username.clone(),
        ip: self.ip.clone(),
        addr: addr.recipient(),
    })
    .into_actor(self)
    .then(|res, _, ctx| {
        match res {
            Ok(_res) => (),
            _ => ctx.stop(),
        }
        fut::ready(())
    })
    .wait(ctx);

At this point the server also needs to track some information, which as you can see is sent along. In this case we’ve duplicated state, which is generally not a good thing, but let’s pretend we don’t see it.

Alright, so now we’re connected to the server, but we want to join a game. The game ALSO needs information about the server, but it can’t share what the server or websocket actor, we instead we send along just the name and id associated with that user.

There are some ways we could fix this duplicated of data, such as creating a central User actor, which I didn’t try at one point, but found that I was sending too many messages. Duplicated a few strings seemed easier in the end.

By the end, I rewrote the server three times I think, with the first two iterations getting to maybe 30% and 50% complete, respectively. The final version, in looking at it now, is could be significantly improved upon.

The frontend

This was built in my pre-SvelteKit with vanilla Javascript, and looking at it now it pains me, but it is what it is. There’s nothing particularly interesting here, aside from the work I ended up putting in to get these animations to work correctly using CSS.

Swapping
The swapping animations

The difficult part was not getting the swaps to happen horizontally or vertically as much as it was getting both at the same time, and determining which to do.

Now I can use an LLM to create this with little difficulty, but I distinctly remember spending significant time getting that to work exactly the way I wanted. You can view the final CSS here.

Wrap up

This was a fun project, and I’m currently using some of what I learned to work on Nimp, though less from the technical side and more from the gameplay perspective.