Skip to content

Client authoritive multiplayer physics to get low latency gameplay#28

Merged
dkj merged 12 commits intomainfrom
features/latencywizardry
Feb 8, 2026
Merged

Client authoritive multiplayer physics to get low latency gameplay#28
dkj merged 12 commits intomainfrom
features/latencywizardry

Conversation

@dkj
Copy link
Copy Markdown
Owner

@dkj dkj commented Feb 8, 2026

Second attempt.

alternative to #27

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

polypongon/src/game/Game.js

Lines 295 to 299 in 16d6b4b

const previousState = this.gameState;
this.setGameState(state.gameState);
this.score = state.score;
this.lastScore = state.lastScore;
this.finalTime = state.finalTime || 0;

P1 Badge Keep client score from being reset to stale server value

With client-authoritative physics, the server no longer calls collision logic (see ServerGame.fixedUpdate), so this.score on the server never increments. Each gameState message then overwrites the client’s locally accumulated score via this.score = state.score, which will stay at 0. This makes the HUD score and goalConceded payload (this.score) incorrect for any online match. Unless the server also updates score from client events, this assignment should be gated (e.g., only when server is authoritative) or the server should handle score updates.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dkj dkj marked this pull request as draft February 8, 2026 14:04
@dkj dkj marked this pull request as ready for review February 8, 2026 14:06
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c70d306f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/game/Game.js Outdated
Comment on lines +255 to +259
// Client-side physics: only ignore ball updates during PLAYING
// Accept ball updates during SCORING, COUNTDOWN, etc.
if (state.gameState !== 'PLAYING') {
this.ball.x = state.ball.x;
this.ball.y = state.ball.y;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply server ball state on mid-game join

Because the client ignores gameState updates while state.gameState === 'PLAYING', a player who joins or reconnects mid-match never applies the server’s current ball position/velocity. That means their local simulation starts from stale values (e.g., whatever was left from a previous local game), so they immediately diverge from the rest of the room. This only occurs when a client joins an already-playing room or reconnects during PLAYING, but in that case it breaks synchronization for that client.

Useful? React with 👍 / 👎.

Comment thread src/game/Game.js
Comment on lines +514 to 518
updateOnlinePhysics(dt) {
if (!this.socket) return;

super.updateGameRules(dt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid client-authoritative COUNTDOWN transitions

In online mode updateOnlinePhysics calls super.updateGameRules(dt), which decrements the COUNTDOWN timer and can flip gameState to PLAYING locally. With latency or delayed server updates, the client can enter PLAYING early, run physics, and emit paddleHit/goalConceded before the server transitions. That makes client state advance ahead of the authoritative server and can desync or trigger premature scoring. This only shows up when there’s noticeable network delay or slow server state delivery.

Useful? React with 👍 / 👎.

…ion with silent hooks, and clients fully accept server ball state on first connection.
@dkj dkj marked this pull request as draft February 8, 2026 14:24
@dkj dkj marked this pull request as ready for review February 8, 2026 14:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14dc4b4913

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/game/Game.js
Comment on lines +626 to +630
// Inform server of goal
this.socket.emit('goalConceded', {
edgeIndex: i,
score: this.score,
time: Math.floor(this.timeElapsed)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent stale score in goalConceded under latency

With client-authoritative hits, this.onPaddleHit() increments this.score locally, but the next gameState update still overwrites it via this.score = state.score in the handler (see Game.js around the gameState listener). Under network delay, a stale server score can arrive between a local hit and a miss, rolling the score back; the subsequent goalConceded then reports the lower value, so the server ends the round with an undercounted final score. This only shows up when server state lags behind local hits, but it corrupts the round result when it does.

Useful? React with 👍 / 👎.

@dkj dkj marked this pull request as draft February 8, 2026 15:00
@dkj dkj marked this pull request as ready for review February 8, 2026 15:00
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94b92f6688

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/game/Game.js Outdated
Comment on lines +261 to +265
if (state.gameState !== 'PLAYING' || !this.firstStateReceived) {
this.ball.x = state.ball.x;
this.ball.y = state.ball.y;
if (state.ball.vx !== undefined) {
this.ball.vx = state.ball.vx;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset firstStateReceived before reconnects

The new firstStateReceived gate only allows server ball updates on the very first gameState or outside PLAYING. However, this flag is never reset in rejoinMultiplayer, so a player who disconnects and rejoins mid-match will still have firstStateReceived === true and will ignore the first PLAYING state from the server. That leaves the ball position/velocity stale after a reconnect, causing immediate client desync. This only happens when rejoining an in-progress room without calling stopMultiplayer/resetLocalGame first, but in that case the fix here doesn’t actually restore server truth.

Useful? React with 👍 / 👎.

@dkj dkj marked this pull request as draft February 8, 2026 15:44
@dkj dkj marked this pull request as ready for review February 8, 2026 15:46
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04fcf32602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/ServerGame.js Outdated
Comment on lines +207 to +210
console.log(`Player on edge ${edgeIndex} conceded goal. Score: ${score}, Time: ${time}`);

// Trigger game over
this.triggerScore(score, edgeIndex);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use server score when handling goalConceded

Because handleGoalConceded calls triggerScore(score, …) with the client-supplied score, any divergence in that client’s optimistic simulation (or a malicious client) can inflate the server’s final score. This can happen when the conceding client locally counted paddle hits that the server didn’t (e.g., due to latency or prediction errors), and the server will then broadcast a higher lastScore than it has actually tracked. Consider using the server’s authoritative this.score (or validating the reported score) when ending the round.

Useful? React with 👍 / 👎.

@dkj dkj marked this pull request as draft February 8, 2026 16:00
@dkj dkj marked this pull request as ready for review February 8, 2026 16:05
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f58f9be34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/ServerGame.js
Comment on lines +207 to +210
console.log(`Player on edge ${edgeIndex} conceded goal. Server Score: ${this.score}, (Client reported: ${score}), Time: ${time}`);

// Trigger game over using the server's authoritative score
this.triggerScore(this.score, edgeIndex);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard goalConceded against non-PLAYING state

handleGoalConceded always calls triggerScore once the edge matches, without checking the server’s current gameState. If a client is out of sync (e.g., still in PLAYING while the server is already in COUNTDOWN/SCORING due to latency) or is malicious, it can end the round early by sending goalConceded at any time. Please ignore/defers these events unless the server is currently in PLAYING (and not already celebrating).

Useful? React with 👍 / 👎.

Comment thread server/ServerGame.js
Comment on lines +185 to +186
// Increment server score
this.score++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore paddleHit events outside active play

onClientPaddleHit increments this.score with no guard on gameState, so late/duplicate packets or an out-of-sync client can bump the score during COUNTDOWN/SCORING. That inflates the HUD and the final score even when no active rally is in progress. Consider verifying this.gameState === 'PLAYING' (and possibly that a celebration isn’t active) before accepting the hit.

Useful? React with 👍 / 👎.

dkj added 2 commits February 8, 2026 16:18
…e on hits, refining server hit detection, and adding client-side escaped ball detection.
@dkj dkj marked this pull request as draft February 8, 2026 17:58
@dkj dkj marked this pull request as ready for review February 8, 2026 18:06
@dkj dkj merged commit 84cc28c into main Feb 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant