fix(battleship): no shots until both fleets are deployed - #33
Merged
Merged
Conversation
The first player to deploy could fire on an opponent who was still shuffling
their board.
Nothing server-side knew what "ready" meant. The session was created with
turn = sides[1] the instant the lobby started, and the only gate on a move
was `g.turn ~= mySide`. Board placement lived entirely in the client:
confirmPlacement() flipped its own phase to 'firing' and set
myTurn = humanSide === '1' without telling anyone, so player 1 could shoot
immediately, and player 2 ate a buffered shot the moment they finished
shuffling - with their layout fixed after player 1's.
Move the readiness handshake to the server, which is the only place both
clients agree:
- Sessions carry `ready = {}`, and games opt in with `requiresSetup`
(battleship only - chess/connectfour have no setup phase, and wordle
bypasses turn logic via freeRelay, so both are unaffected).
- New games:setupReady callback marks a side ready, tells the other side
('oppReady'), and once BOTH are in, picks who moves first and pushes
'begin' to both clients.
- The move handler rejects everything before that with "Opponent is still
deploying", so the client gate can no longer be bypassed.
Client: confirmPlacement() now reports ready and enters a new 'waiting'
phase instead of starting the match itself; firing begins only on 'begin',
which is also what decides myTurn. The waiting state shows "Waiting for X to
deploy…". The pre-existing pendingShot buffer is kept purely as a safety net
- with the gate in place a shot can no longer arrive early.
Not verified in-game (no FiveM server here); reasoned through the engine and
typechecked. Worth a two-player smoke test before merge.
Github-Samuel
pushed a commit
that referenced
this pull request
Sep 15, 2026
Readiness was client-only, so the first player to deploy could fire on an opponent still arranging their board. The ready handshake now lives server-side: games opt in with requiresSetup, a setupReady callback marks each side ready, and firing only begins once both are in and the server has picked who moves first. The move handler rejects shots before that, so the client gate can't be bypassed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The first player to deploy could fire on an opponent who was still shuffling
their board.
Nothing server-side knew what "ready" meant. The session was created with
turn = sides[1] the instant the lobby started, and the only gate on a move
was
g.turn ~= mySide. Board placement lived entirely in the client:confirmPlacement() flipped its own phase to 'firing' and set
myTurn = humanSide === '1' without telling anyone, so player 1 could shoot
immediately, and player 2 ate a buffered shot the moment they finished
shuffling - with their layout fixed after player 1's.
Move the readiness handshake to the server, which is the only place both
clients agree:
ready = {}, and games opt in withrequiresSetup(battleship only - chess/connectfour have no setup phase, and wordle
bypasses turn logic via freeRelay, so both are unaffected).
('oppReady'), and once BOTH are in, picks who moves first and pushes
'begin' to both clients.
deploying", so the client gate can no longer be bypassed.
Client: confirmPlacement() now reports ready and enters a new 'waiting'
phase instead of starting the match itself; firing begins only on 'begin',
which is also what decides myTurn. The waiting state shows "Waiting for X to
deploy…". The pre-existing pendingShot buffer is kept purely as a safety net
Not verified in-game (no FiveM server here); reasoned through the engine and
typechecked. Worth a two-player smoke test before merge.
Fixes #23