Closed evandroabukamel closed 3 years ago
Do you really need simultaneous move on framework level in a card game? May be you can check implementation in this repo, though it is too simple to cope with your issue. https://github.com/wsun/multibuzzer They changed the framework to allow simultaneous moves.
I am still wondering why a predetermined move order cannot handle your game. Why not specify turn order in rules?
Nice, I will give a look into that project. The reason I need this is because game rule: all players can send their cards at the same time, there is no order, and one player should not wait for the others because it would take too long.
That's a simple implementation, but I liked this library and I would like to help to improve it and follow the updates.
This out-of-sync issues are a can of worms, might sound simple to accept moves simultaneously, but to make it "feel" right and meet users expectations for real-time collaboration is really hard.
I would avoid going to this route and just re-issuing the new state and asking users to try again. Yes, it is annoying, but it less annoying than corrupted states and bad stuff that can happen of a bad real-time implementation.
It is feasible to explore some sort of phase option where out of order moves are acceptable (basically have the framework stop incrementing stateID
during that phase and ensure that clients are at the very least inside that phase in order to make a move).
There is the other question of serializing these moves at the infrastructure level so that there is no race condition on the server (like having the second move undo the state of the first). Some sort of per-match queuing mechanism like @delucis suggests will be helpful.
This is a low priority item, but we will be happy to look at a PR if someone is keen to work on this.
My personal leaning would be something like the following:
Add an option to the long-form move interface rather than a phase option. _stateID
is also used to process and accept updates on the client, so not updating it is likely to have side effects that might be hard to handle.
That said, I’m not quite sure what this option might be called, which could be a sign this is a conceptually muddy approach (especially because these checks are generally invisible to developers, they “just work” so it’s strange to surface them in this one obscure way). processIfStale
(if what is stale?)… acceptStaleStateID
(the move accepts it?)… allowOutOfOrder
(out of turn order?)… Names are hard…
moves: {
simultaneousMove: {
move: (G, ctx) => {},
processIfStale: true,
},
}
In the master, if the move has this new option enabled it gets processed even if the _stateID
checks fail. (Developers could still include more specific validity checks in the move function and return INVALID_MOVE
if necessary.)
This approach keeps the current behaviour and only allows out-of-sync processing when specifically enabled. I agree with @flamecoals that this can actually cause all kinds of UX issues — for example, a player makes a move, but the first update they see is someone else’s move, then their own move arrives, potentially in quick succession, potentially cancelling or overwriting transitions or UI updates in confusing ways. There are definitely moves that can work fine like this though, like a discard stage where order is irrelevant.
@evandroabukamel points out that the flip-side of this problem is that there’s no way to handle client errors (see #723), which could be the alternative fix for this. Instead of implementing something server-side like the above, if the client received errors, developers could decide to handle that error in an appropriate way there. For example, if a move is rejected because it lost a race condition, they could display a message to the player to try again, or automatically dispatch a retry for cases where they know that’s acceptable. (Not that #723 is any simpler to implement 😅, but it might address a range of more generic scenarios rather than just this specific one.)
Good point, @delucis . I was thinking about a queue implementation, where moves are enqueued and at some point they are processed and, only after that, the stateID is incremented. But I'm not sure what would trigger the queue processing, queue length, time or some event.
I'll give a look at #723 . That could be some way to solve the problem.
Well, #723 is a lot of work. I'm doing @delucis suggestion, it seems more simple to implement and the developers should be aware to check their game state to use the option to ignore stateID checking. What about "ignoreStateState", @delucis ?
@evandroabukamel Yes, #723 would be quite a bit of work! How about ignoreStaleStateID
?
@delucis , I'm testing it right now.
It seemed to work pretty well for me. I'll make more tests on other environments to be sure.
Is there a way to set this ignoreStaleStateID
at the Game level instead of each Move?
Also, if I am using Phases, how do I use this flag?
Is there a way to set this
ignoreStaleStateID
at the Game level instead of each Move?Also, if I am using Phases, how do I use this flag?
How do I do this?
Well, I participated in a conversation about this in Gitter and just read an old one. The thing is:
Two or more players make a move "in the same time" but just one is executed, the others throws an "Invalid stateID" error on the server.
Now that my card game, "cards against humanity"-ish, is in beta test we saw this problem occurring a lot of times and it's hard to handle it on the client since the server does not return the error. I think one way to handle this is to return the error, there is an issue open about that. Although, I believe that is not the best way to solve that as the clients will receive errors, that may cause a bad experience, so I believe making the server handle moves with the same stateID is a good approach, of course checking if the move is still allowed. Maybe we could make this optional.
What do you think? I will look the the code and test some implementations.
Follows the talks from Gitter that I found about this issue:
Another talk: