Exploring the use of a CRDT for "game state"

Hello all!

I have been interested in the prospect of moving some of our application’s shared “game state” into a CRDT but am struggling to wrap my head around how to handle resolution when changes conflict with our game logic.

For example, one piece of our game state is the concept of a table with 8 available seats that users can occupy and move about freely (as long as the seat they are moving to is available). Currently we represent this using an in-memory JS object on the server (loosely you could imagine something like: {'seatOne': userOne, 'seatTwo': userTwo} etc) where updates to the object get emitted to all listeners. So, when a client requests a new seat, the server is the arbiter of the resolution of that request and emits out the updated room state to all listeners once the new state is resolved.

For our MVP, this solution worked well enough as long as everyone’s connections were stable – even though you had to wait for a server round trip for your change to occur locally, it felt ‘ok’. However, as we look to polish the user experience and introduce optimistic updates on clients, you can imagine scenarios with our current implementation where many users are moving around simultaneously and you will end up visually bouncing back and forth between what to your client is a prior state as other user’s requests resolve after you’ve made an optimistic change locally but before your request has resolved.

As far as I can tell, introducing this game state to a shared Y.doc would perfectly handle the idea of clients making local edits that eventually sync with each other; however, it would introduce other issues as I am unsure how to handle when changes are logically inconsistent with our game logic. For instance, if two clients move to the same open seat, when those two actions merge, the end results should be that one user ends up in the seat and the other ends up staying where they were; however, it seems without any outside intervention, the end result would be that one user would end up in the seat and the other would be missing as they were overwritten. I believe that we could have an arbiter (probably the server which is acting as the sync provider) listening to changes to the doc and making modifications when these inconsistencies happen, but I want to make sure I am not reaching for the wrong tool (CRDTs / a Y.map) as it feels like having a central authority somewhat defeats the purpose of a CRDT.

With all that said, I guess my question is: is a CRDT suited to representing this kind of ‘game state’ or am I completely lost in the sauce? I feel like I am too close to the problem, anchoring on our current implementation, and not thinking clearly.

YJS seems really well-suited for your use case. You can use a shared map to store the user on the seats.

The fact that YJS have websocket as well as webRTC providers will really help you to develop the game. You can even not have a server at all and only use peer-to-peer connection.