Hacker News new | past | comments | ask | show | jobs | submit login
A multiplayer board game in Rust and WebAssembly (mattkeeter.com)
188 points by lukastyrychtr 10 months ago | hide | past | favorite | 54 comments



The best part of having the same language on both the server and the client is that you can have the same code running client side validations and do the same move on the server to avoid cheating. If validations are only done on the client, they will just send a winning board state.


There's zero need to have any validations[0] on the client in a turn-based online multiplayer game. If your state and logic aren't pretty much entirely on the server for that, you're doing it wrong.

[0] Of course, for smoothness/convenience one could do some basic checks on the client.


For client side prediction, probably, yes. But having a single library that could tell you "given game state X, which plays are legal" would be nice for rendering the UI without needing to ask the server for every last detail.


For many if not most turn-based (board) games (like Ludo, which I'm actually currently implementing for myself/my friends, or Reversi) the set of legal plays is small enough that the server can simply send them to the player along with the information about the last turn taken. Even chess can be implemented in such a way if you get a little creative with the move data format. I prefer to build board games this way because (ironically enough) it really decouples the client/rendering and the server/game logic, greatly simplifying building additional or even alternative clients.


I run my logic on the client and the server, and I'm not doing it wrong.

Your techniques may work for you but don't make the mistake of thinking they're universal.


I didn't say they were universal. I do stand by what I actually said: for this category of games, running logic on the "client" isn't the best of architectures.


I run it on both. If you need to provide some UI context, being able to reach in to the state and query it is a life saver.

Imagine Advance Wars. Their unit movement is very subtle.

You can choose the path the units travel, which can turn in to a forced action if you stumble onto an ambush unit. When you reach the end it tells you the outcome of your battle before you confirm you want to engage in it.

Doing logic only on the server and sending down a list of possible moves is going to get pretty heavy. Querying for each square of movement is going to be a latency nightmare.

Edit: actually, I think the movement ambush is from a clone. Although the arrow movement from the original is still complicated. That doesn't weaken the argument: imagine getting half way in to dev and realizing you can't add that feature with your architecture.


I haven't played Advance Wars, so that definitely affects my answer, but by looking at it and reading a couple of reviews it seems rather like chess (and was directly compared to it) - and chess is something I've implemented like I describe with no problem. It's surprising how far you can get by putting some thought into the data interchange format between your client and server.


Chess with potentially thousands of moves per unit.


On any given turn there really isn't that many possible moves. https://chess.stackexchange.com/questions/4490/maximum-possi...


They are describing Advance Wars, not chess.


Why? For latency reasons?


There is value in making the game progress client-side without waiting for a response from the server. Having the game logic run on both sides gives you the best of both worlds: 1. One authoritative version of the game state on the server. 2. Lag-free experience on the client.


You can make the game progress without running validations or duplicating game logic on the client.

For example, in the online multiplayer version of Ludo I'm currently building, I trigger the die roll animation on the client but the actual dice are rolled on the server, which then calculates what moves are possible based on the game state and sends that + the value of the roll to the client. The client doesn't know anything about the rules of Ludo, it simply knows how to parse and render the move format as well as send the ID of the picked move to its source of truth (in this case, the server).

https://en.wikipedia.org/wiki/Ludo_(board_game)


I spent the last two years coding a reimplementation of A Game of Thrones: The Board Game[1], and I would use this paradigm if I had to recode it. Sharing the code between the client and the server brings a lot to the table.

Having to wait for the server for the set of legal moves feels like a hack. It's way simpler to have the client have its own copy of the game state, and allow "query" methods (methods that fetches data about the game state but don't modify it). This allows you to make _powerful_ UI, which can show any kind of information the user needs about the game like predictions about what would happen if a move is done, computing things so the user doesn't need to.

If I had to redo it, I would use a paradigm similar to how boardgame.io[2] works, which is similar to how fixed lockstep works. The server and the clients all keep a full state of the game and when a player makes an action, it is transmitted to all the actors, who apply this action to their own state of the game. Since the game rules are deterministic, the final state of the game will be the same for each actor. The only 2 complex to handle are randomness and secrets (the former can actually be solved by the later), but overally, the complexity is managed elegantly and you can separate networking code from gameplay code easily.

Actually, one of my future possible project would be to make a library similar to boardgame.io, but less opiniated, and in the future, offer a platform to easily host and launch a game coded with it.

[1] https://boardgame.io/

[2] https://swordsandravens.net/


> Having to wait for the server for the set of legal moves feels like a hack

But you are waiting for the server anyway - that's the "turn" in "turn-based". The way I build, the notification about your turn simply comes with the moves you can make as well - and the moves you can make don't change, because _it's your turn_, so there's no limit to how powerful a UI you can make with it.

Take chess for example (it was while implementing chess that I worked out this paradigm, actually) - a move from the server (sent in a packed binary format) was a mapping of start and end positions and a metadata byte that linked it to a kill, a promotion, or castling. and it was rare indeed that a complete payload (with a full set of currently legal moves + effects) would be up to two kilobytes. The client would deserialise that and of course could show you possible moves when you selected a piece and so on.

> The server and the clients all keep a full state of the game and when a player makes an action, it is transmitted to all the actors, who apply this action to their own state of the game.

This is actually what my current approach was born out of! But then I wanted to build a second client app, and I felt it was a pain to rewrite the game logic in another language - one solution to that is to have an isomorphic application, but in my case I decided to try out moving as much as possible to the server and just focusing on data parsing and rendering on the client and it worked out surprisingly well. It also let me do things like implement custom rulesets with much greater ease, because the rule logic exists solely on the server and you can deploy/modify that without requiring the clients to update.


Fair enough!

I can understand the reasoning if you need to code a client in a different language, then you would effectively need to code the whole game once again. I feel like it would quickly bore me to code the rendering of each set of moves for each possible action. I feel like it's easier to have access to the complete state of the game client-side when you need to code a UI. There might be a solution if the server broadcasts the diff of the game state to the clients after each action has been performed (instead of letting the clients re-apply the action on their game state), but I'm not sure it would be nough.

How do you handle state synchronization, though? When a player makes an action, how do you propagate the changes to the game state?


Ditto; the client side code needs no game logic, only aiding UX; dont let the user click outside a minesweeper board, but if it does, the server still validates.

I made a System inspired by boardgame.io (but it was too integrated with react for me), which means i can write the game on the server without writing any client code at all. the client gets a list of valid moves, and my debug view gets error messages and a bunch of buttons & drop downs for parameters.

https://github.com/NewChromantics/PopNotPoker/blob/master/Se... here's multiplayer minesweeper:) Http://not.poker

All the real work is in the lobby code anyway.


@soylentgraham: You should check out the recent releases of https://boardgame.io/. The tutorials are now rewritten with a focus on the plain JS client (no React).


oh cool, I gave up with boardgame.io in the end (I liked the API design though) because I spent 99% of my time trying to rip it out of react (both server and client) so I could use it in a non-web situation. (I have a game engine with high-level JS, and the game server/client is only one small part of it)

Maybe I can go back, although I'm writing game logic much faster now I'm in NIH territory, it would be nice to contribute to someone else's project


Damn, I missed this. I hated having to install create-react-app (with 100s of MB of dependencies)


If I understand your architecture correctly, your client has to wait for the network roundtrip before being able to see the result of the die roll, correct?


Yes. This is over websockets though, and in all of my testing/profiling (pretty shitty ISP in West Africa, game server is currently hosted in the US) I haven't had an exchange take longer than one second. Also the mandatory wait is just for the die - for picking a move, for instance, the client can go straight into rendering the selected move while sending it to the server.


Not sure if you challenged or agreed. You somehow did both?


Ah - sorry if I wasn't clear. My point is that if you're implementing a turn-based multiplayer game like this, your game logic preferably shouldn't be on the client at all. By "concessions" I mean you might have little things like starting the animation for a move immediately rather than waiting for the server to acknowledge receipt, but the ideal turn-based board game client is one that knows nothing about the rules of the game it's playing - just how to parse and render the moves and state it gets from the server.


No I would very much prefer client side validations instead of hogging the server every time client requires a feedback that concerns the rules.

For example: Playing backgammon, you choose a piece, you get highlights on where it can go. That requires the full working ruleset on the client side. There is no case to be made for requiring this feedback from the server no matter how confident you are that your server can handle it or how good client's internet connection is.


Multiplayer backgammon doesn't require a full working ruleset on the client side at all.

Like I mentioned, these are _turn-based games_. To an extent, you're thinking about them like they're real time, and that's where the problem is. With backgammon (as with Ludo, which I'm building at the moment, so I have direct experience with this) you _have_ to roll the dice on the server or else the enterprising player is just going to game them. And if you're returning the value of the rolled die from the server, you can simply return the set of legal moves as well.


You could return the set of moves, or you could -not- do that, and just run the state machine in the client.

That way, you don't have to complicate the server by making it calculate future moves and write yet another client which is purposefully less featured, but still intelligent enough to parse the returned moves and map them to whichever piece is selected and you have less code to maintain.

You can keep on gambiting around the problem with "concessions" which is easily solved by activaing code you've already written on the client side but that's no more than a mental exercise and has little to do with efficiency in problem solving.


It only requires the client have a list of currently legal moves, not the complete ruleset. Though I generally agree with you, it's convenient and not at all an antipattern to have some of the rules logic in the client - for example, you might have no other reason to generate an exhaustive list of legal moves server side.


There is not really any novelty in this kind of approach, I guess? Just that with .js it hasn't always been possible, but with other server-client games, it always was.


it has been possible with transpilers. Typescript works out of the box. Opal for ruby, Transcrypt for python, etc.


This is the same thing Javascript programmers used to say to justify node.


Original author here, happy to answer any questions.

I also appreciate the load testing! For the curious, we're at 30 open rooms, and I'm seeing:

- 0.0 server load

- 2.6 MB of RAM used by nginx + its worker process

- 4 MB used by pont-server


Will the rooms stick around if people leave the page in the middle of a game? My wife loves the game btw. I enjoy it as well but I appreciate the stack behind it more. Ty for your efforts.


I'm glad that your wife enjoys the game! At the end of the day, it's meant to be played and shared, rather than just existing as an interesting software artifact.

The rooms persist as long as there's at least one connected player in them. Exiting + rejoining a room under the same name will let you continue to accumulate points under that name.

(Rejoining isn't perfectly seamless: you won't necessarily be dealt the exact same hand, and if you exit + rejoin while it's your turn, your turn will be skipped)


Some persistence timeout even without any players would be nice.


> "Is there a Rust + wasm version of Svelte yet?"

Here is a Svelte starter template for Rust (webasm) + Svelte:

https://github.com/HugoDaniel/svelte-template


OP is asking for a Rust port of Svelte, rather than using Svelte itself and adding WASM code alongside.

I started such a thing a while back, but progressively decided the design wasn’t right for Rust, and so headed in a somewhat different direction, and then shelved the project after deciding to implement what I had had in mind differently.


how much better can it be than doing

`import module from './crate/Cargo.toml'`

from Svelte directly, and get Hot Module Reloading for free ?


Nice!

I'm using a very similar architecture for https://boardgamelab.app/.

The WebAssembly performance boost will make a difference once I've implemented MCTS bots for the platform.


Very nice. How do you auto generate the bots from the game rules? I'm assuming you just have them play the game a lot against each other and use genetic algorithms/reinforcement learning (you'd need some rules about scoring)?

Do you have the option to generate different bot difficulty levels?


I'm assuming that you're asking about Boardgame Lab (from the other comment).

The basic version just uses Monte Carlo Tree Search and the game's victory condition to play the game. This won't result in a very strong bot for most games, but is a reasonable opponent to play your game against (which is very useful when designing a game).

Options to make the bot stronger involve giving it hints and shorter term objectives.


> The game server using async Rust, which was... exciting

As someone interested in Rust but not currently using it, what is the state of async? An expansion on the bullet points following regarding runtime scope would be interest.


I’m using Tokio and Async_std (Actix Web, redis, reqwest ), everything’s great and feels pretty mature, except for the scary part of no warning when using sync code from it.


Cute game.

You can get into a state where there are no valid moves though: https://imgur.com/1WLufcx


It's true!

I considered adding some kind of fix for that, but a fresh game is a mere refresh away, and I liked the opinion on Board Game Geek [1]:

> It's probably also worth noting that if this happens then all players involved need to re-visit some ideas of basic strategy.

[1] https://boardgamegeek.com/thread/861192/if-game-stuck-perfec...


Very nice.. I am probably going to containerize this, would you be interested in a pull request?


Thanks for the offer!

I'd rather not have it in the main repo, since I won't be maintaining it. If you make your own pont-docker repo that contains the Dockerfile, I'd happily accept a PR that links to it in the README.


I'd be happy to do that.


Do you have the license or permission from the original creators to duplicate their IP?


At least in the U.S. game rules aren't covered by copyright even though the art and any expressions of the rules are.

https://www.copyright.gov/fls/fl108.pdf


This is great stuff. Thanks for sharing it, I'll be checking it out to learn more!


I’ve been thinking about writing a boardgame with websockets a lot. The technology is here, the current climate for online boardgame is here, but weirdly it’s not happening.


That's great. Really awesome. Thanks for sharing




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: