boardgameio / boardgame.io

State Management and Multiplayer Networking for Turn-Based Games
https://boardgame.io
MIT License
10.01k stars 706 forks source link

Timer #92

Open nicolodavis opened 6 years ago

nicolodavis commented 6 years ago

This has come up a few times, so I think we should prioritize this.

Suggestions:

akaguny commented 6 years ago

Suggestions:

akaguny commented 6 years ago

it's can be inplemented with simple timeout or react use specific as $timeout in angularjs?

nicolodavis commented 6 years ago

Yes, a simple timeout would also work (probably not necessary to record anything in ctx).

akaguny commented 6 years ago

@nicolodavis what api timer should take? when i create game i was need pause func(tur on/off activity of timer) and state variable, that take me: How much seconds left. But my time leav in React component and store state in this component https://github.com/akaguny/alias-boardgame/blob/master/src/App.js#L54

akaguny commented 6 years ago

I think we just need a simple setTimeout that is called at the beginning of the turn that ends the turn automatically when the callback is invoked. We can use a similar one for phases as well.

@nicolodavis i'm agree that we should make a simple one timer for first time. But when we configure it? Configure like maxMoves?

akaguny commented 6 years ago

i started work on this feature, please set label tacked or some familiar

nicolodavis commented 6 years ago

@akaguny Yes, let's start simple with something similar to movesPerTurn and expand it later. Will take a look at your PR. Thanks for working on this!

sedobrengocce commented 6 years ago

Greetings, someone is woriking on this issue? If not I can take it

nicolodavis commented 6 years ago

@akaguny is busy right now, so feel free to take it.

nicolodavis commented 6 years ago

My thoughts on how this should be implemented:

I think it makes sense for this to wait for #251 so that we can create some hooks to send synthetic events that appear to come from a client, but are actually initiated from the server itself.

lw1990 commented 6 years ago

The way heroiclabs.com handles this is by using a customizable 'tick rate', as explained: "Tick rate¶ The server will periodically call the match loop function even when there is no input waiting to be processed. The logic is able to advance the game state as needed. It can also validate incoming input and kick players who've been inactive for some time.

This periodic call is known as Tick Rate and represents a desired fixed frequency at which the match should update. Tick Rate is configurable and typical frequencies range from once per second for turn-based games to dozens of times per second for fast-paced gameplay.

All incoming client data messages are queued until each tick when they are handed off to the match loop to be processed. Tick Rate is expressed as a number representing desired executions per second. For example a rate of "10" represents 10 ticks to the match loop per second (on average once every 100ms)."

This allows them to implement fast paced 'realtime' turn based games, medium paced games (clash royale), and slow paced games (words with friends).

Why would this be better?

  1. Having a fixed secondsPerTurn or secondsPerPhase is very restrictive, imagine a blitz chess game implementation - turn times change sometimes depending on how much time you spent on your previous turns - maybe you played a bunch of sequential turns very quickly and should have more time per turn than your opponent. We would be forced to implement this on the client side (a cheating vector) if all we had on the server were fixed variables of secondsPerTurn or secondsPerPhase.

  2. Just because there is a high tick rate, if nothing changes, there's no need to send updates to the client for that tick.

  3. Being able to have the server run the game loop itself without input from the client is useful for detecting disconnected players, reconnected players, broadcasting newly joined players/spectators during someone's turn, and giving all players notifications of this, as well as the obvious case of turn timeouts. Imagine playing a game and getting notified that the opponent has disconnected - it doesn't make sense to sit there and wait for 2 hours for them to make a move. How would you know they disconnected if your 'setTimeout' for the turn is 2 weeks? Answer: by having the server notify you during the middle of your or their turn.

  4. What if you want to have a randomized event happen during player turns, such as unit drops? You could have unit drops for a strategy board game happen randomly every day, maybe 5 times per day, during all matches, no matter whose turn it is, all at the same time. Whoever wins the current match gets the unit drop (good way to encourage playing more). This is just one more use case that benefits from an active server loop rather than a passive one. (Unit drops do not have to affect the current game, but can affect the arsenal of units the player has for setting up the board for all future games).

nicolodavis commented 6 years ago

I'm open to having a:

flow: {
  tickDelay: 1,
  onTick(G, ctx) { ... }
}

that's called at a configured frequency. We can launch it as an experimental feature to support some of these use-cases. Note that the framework is not designed for realtime games, so bad things will happen performance-wise if the tick delay is too low.

I still think this is orthogonal to secondsPerTurn. That doesn't need the tick mechanism to be implemented, and also avoids having the developer do any calculations to figure out if the turn has ended.

So, my suggestion would be to wait for #251, and then we can implement these as two separate changes.

Note that [3] is already possible today (look at the examples for how to check connects / disconnects).

Stefan-Hanke commented 6 years ago
lw1990 commented 6 years ago

@nicolodavis my understanding is that isConnected only tells the player on their respective client if they are connected or disconnected from the server. While that's useful a good addition to this is knowing if your opponent(s) are connected/disconnected as well. Since there's only one isConnected prop I don't know how it can track them all. Knowing if your opponents are connected informs you that it may make sense to wait for them to make a move right then, or come back to the game later if they are disconnected. The server knowing if they are connected/disconnected can help force end the game (for example if they lost their internet connection for 10 minutes in a turn-based game that expects moves in under a minute).

Each client sending messages to the server onTick() per tickDelay would solve this problem though.

nicolodavis commented 6 years ago

Why wouldn't we just use the same mechanism that we use currently to determine the connection status for everyone and not conflate it with onTick?

It's quite easy for the server to just broadcast the connection status to everyone each time someone connects / disconnects.

lw1990 commented 6 years ago

Is that how the current mechanism works? If there are 10 players in a game, and player 7 disconnects, how do clients 1-6 and 8-10 know that it was player 7 who disconnected with a simple isConnected boolean and no ID? Do they have to play their turn (or wait for someone else to play their turn) before the server 'runs' and notifies them that someone is disconnected?

jorbascrumps commented 6 years ago

@lw1990 I believe the current mechanism can only be used to determine if the current client is connected to the server--not other players. What @nicolodavis meant was enhancing that mechanism to track connected status for all players and broadcast connection updates.

nicolodavis commented 6 years ago

Yep, what I'm saying is that connection status can be implemented without using onTick.

lw1990 commented 6 years ago

If the current mechanism is the client polling the server for a connection (and the server doing nothing really), and you're going to change it so that the server does stuff when clients poll it (like notify other clients that a client is still connected or is disconnected), then this becomes a real-time game framework (which is exactly what is needed), but limiting it to connected/disconnected in real time defeats the purpose. Many turn based games need pseudo-real time turn timeouts as well.

If you're going to real-time poll the server and clients anyway for connection status/notification, you might as well just make it a server-driven framework with a turn-based feel, instead of implement a truly client-input-driven framework, with several real-time server-driven features that will probably be even more effort to implement tacked ontop of the client-driven framework for connection status and timers..

So basically, if the server can be configured to fire updates to all clients every X milliseconds, as well as handle all incoming messages from clients in a message queue (event-driven), then you have the necessary framework to handle turn-based games, real-time disconnect/connection/reconnection events, and real-time timers/timeouts and server-driven events (like unit drops). The server basically has to be in control at all times, if the framework waits for the client to give the server input before it does anything EXCEPT in the case of connection/disconnection or EXCEPT in the case of ontick, you just implemented 3 different multiplayer schemas when it could be a single real-time one.

nicolodavis commented 6 years ago

I think we're talking past each other here. I'm in favor of exploring the onTick mechanism that you're proposing.

All I'm saying is that for connect / disconnects, socket.io already provides an asynchronous mechanism of handling it and we can just use that instead of polling manually.

nicolodavis commented 6 years ago

I understand your concern that it will be difficult to merge certain asynchronous mechanisms with an event driven queue on the server if we eventually go that route (in the case of connects, I would imagine that socket.io could just add another event to the message queue to be scooped up by the next tick so it is still possible).

We'll have to think about it carefully as it would change the paradigm of the framework significantly I think. For now, I think the easiest way to make some progress in that direction would be to introduce onTick and then build up from there.

In general I'm quite in favor of a message queue also for the sake of enabling horizontal scaling. If we have a global queue that server workers can just pull from, it becomes quite easy to add capacity by just increasing the number of workers. This will also become more or less necessary as we start to support real-time features.

TakesTheBiscuit commented 5 years ago

I'm keen to know if this is being roadmapped? I'm trying to work out if boardgame.io would suit a '1 hour per turn', 'total game time 24 hours', between 2 players sort of scenario? - ideally i also only want to write my game logic once and have the server enforce that logic through validation (minimise cheat vectors)

lw1990 commented 5 years ago

It seems to me there needs to be two features implemented to get the time-based features we as game developers want:

1) There needs to be a way to schedule an action on the server (like ending a turn) some time in the future when a turn starts. If a player makes a move before then, this scheduled action is discarded. If not, then the turn will end/game will end because the player didn't make a move in time.

2) There needs to be a way to update other player clients when a player triggers an Action, such as hovering over a slot in a connect4 style game, which would allow the other player to see what the other user is about to do or may do - making it more engaging. A 'turn' doesn't really get used up, and the 'phase' thing won't update both clients if I understand correctly. It makes little sense to continually send out updates like a real-time game, it should just happen when a player actually triggers the action by hovering over a different slot. Furthermore these actions shouldn't be part of the move log or move history when undoing moves etc.

nicolodavis commented 5 years ago

@lw1990 I'm not sure how #2 is related to this issue. #1 is correct.

@TakesTheBiscuit This is not on the radar for v1.

jaxony commented 5 years ago

@nicolodavis is there a simple way to end a phase after a certain period of time, triggered server-side?

nicolodavis commented 5 years ago

Not at the moment. We only support client side timers right now.

lw1990 commented 5 years ago

can someone provide a tutorial on how we can implement turn timeouts triggered on the server, even if it's not part of the boardgame.io feature set yet? this issue has been open for over a year and a half, and I'm not knowledgeable enough to figure it out, but if someone could show us how it's done in a 'hack', maybe this would get things rolling for how it should actually work in boardgame.io later

lw1990 commented 5 years ago

@nicolodavis I've been thinking about server-authoritative multiplayer games and turn timeouts and I may have come up with a solution that doesn't require server-side async timers like this issue suggests, please let me know if this is a viable strategy

If both players have client-side timers when every turn begins, then when any client side turn timer expires it sends a message to the server to try to end the turn. If the server code also says it's time to end the turn, then it does so.

Even if one player is trying to cheat and modifies the client-side code not to 'poke' the server to execute its endTurn function, the other player's non-cheating client will still do so. On the flip side, if one player tries to tell the server to end the turn prematurely for the other player, the server side code will be unaffected and won't do it.

So in theory it should be possible to have 'server-side' authoritative timers by utilizing the fact that both players are virtually never going to try to cheat by making their turns last forever during the same turn.

This would also allow for any additional features, such as pausing turns, without being abusable.

jasonharrison commented 5 years ago

@lw1990 I like that idea. We also need to consider what might happen if the client refreshes the game and resyncs (we would need to restart the client-side timer). Also we would need to account for small differences in the client timer vs. server (what happens if the client sends the 'poke' to the server 0.05 seconds early?)

nicolodavis commented 5 years ago

I'm also a bit concerned about the amount of co-ordination required to account to pull this off. There is inevitably going to be some retry logic on the client that will become complex. It might actually be simpler to just implement server-side timeouts.

lw1990 commented 5 years ago

wouldn't flow based client side timers take care of the timing issues since flow also gets executed on the server? it would be relatively simple to allow for lag by having clients continuously poke server at some interval when their client timer expires, that way if they send a little bit too early it won't matter as it will get resent again until it finally endTurns on the server.

For the client refresh, we could just store the gameTime as a variable in G or something and use that for the client side timer as a base, which should be saved on client refresh just like any other state variable in G. If we wanted implement pauses for the game, we just don't increment gameTime while the game state is paused, etc.

nicolodavis commented 5 years ago

I'm happy to work with you on this if you put up an example somewhere that we can also point others to.

If I understand you correctly, your proposal is completely client-side and doesn't require any changes to boardgame.io?

nicolodavis commented 5 years ago

wouldn't flow based client side timers take care of the timing issues since flow also gets executed on the server?

If by this you mean that an event like endTurn is processed on the server, then yes. Not sure if I parsed the question correctly.

lw1990 commented 5 years ago

I'm a noob in javascript, and I was just brainstorming for a possible solution, was hoping others could chip in on if it would work or not

The idea I had was within the existing functionality of boardgame.io, but not necessarily client-side, since the clients would be poking the server for a server-authoritative turn timeout solution.

In theory the solution would be like this, on endTurn, a custom variable gameTime which holds the number of milliseconds since the game started is used with a turnTimeout variable (such as 60,000 milliseconds) and a turnStartTime variable, also in milliseconds, to calculate when the turn should timeout inside of flow -> endTurn. However endTurn can't be automatically executed on the server after a set time so the client-side timers send turnTimeExpired moves (allowed for both players) at some interval. The interval compensates for minor lag issues of being sent a bit too soon. The move will only go through on the server, if a logic check of if (gameTime > turnTimeoutTime) { endTurn } is true.

HydraOrc commented 4 years ago

I would like to have way to change game state from master at some timeout or event fired so none of the players made a move but still something changed

vdfdev commented 3 years ago

FYI, i was discussing this with other contributors of https://github.com/freeboardgames/FreeBoardGames.org and we currently have 3 games that require timers, and they are kinda hacked together right now using frontend timers in the clients... I am willing to help with solving this at the framework level as it will be necessary as we add competitive gaming options

Shadawn commented 3 years ago

Another FYI - I'm developing a multiplayer turn-based RPG based on boardgame.io, and I'm quite in need of a server-side turn timer.

Noisycall commented 2 years ago

Hi guys, I see this issue is been last addressed in 2021, are there any updates on this? A simple tick based timer that can change the game state would be extremely useful.

Thanks!