Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 8x 22x 22x 22x 22x 22x 13x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 22x 22x 22x 22x 22x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 22x 22x 22x 22x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 22x 22x 22x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 22x 22x 22x 22x 22x 22x 1x 1x 1x 1x 1x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x | import { useCallback, useEffect, useRef, useState } from 'react'
import type { GameMove } from '@/lib/arcade/validation'
export interface PendingMove<TState> {
move: GameMove
optimisticState: TState
timestamp: number
}
export interface UseOptimisticGameStateOptions<TState> {
/**
* Initial game state
*/
initialState: TState
/**
* Apply a move to the state optimistically (client-side)
* This should be the same logic that runs on the server
*/
applyMove: (state: TState, move: GameMove) => TState
/**
* Called when server accepts a move
*/
onMoveAccepted?: (state: TState, move: GameMove) => void
/**
* Called when server rejects a move
*/
onMoveRejected?: (error: string, move: GameMove) => void
}
export interface UseOptimisticGameStateReturn<TState> {
/**
* Current game state (includes optimistic updates)
*/
state: TState
/**
* Server-confirmed version number
*/
version: number
/**
* Whether there are pending moves awaiting server confirmation
*/
hasPendingMoves: boolean
/**
* Whether authoritative server state has been received at least once.
* False until the first syncWithServer() call (initial session-state from server).
*/
hasReceivedServerState: boolean
/**
* Last error from server (move rejection)
*/
lastError: string | null
/**
* Apply a move optimistically and send to server
*/
applyOptimisticMove: (move: GameMove) => void
/**
* Handle server accepting a move
*/
handleMoveAccepted: (serverState: TState, serverVersion: number, acceptedMove: GameMove) => void
/**
* Handle server rejecting a move
*/
handleMoveRejected: (error: string, rejectedMove: GameMove) => void
/**
* Sync state with server (on reconnect or initial load)
*/
syncWithServer: (serverState: TState, serverVersion: number) => void
/**
* Clear the last error
*/
clearError: () => void
/**
* Reset to initial state
*/
reset: () => void
}
/**
* Hook for managing game state with optimistic updates
*
* This hook maintains both server state and optimistic client state,
* applying moves immediately on the client while waiting for server confirmation.
* If the server rejects a move, the state is rolled back.
*
* @param options - Configuration options
* @returns Game state and update methods
*/
export function useOptimisticGameState<TState>(
options: UseOptimisticGameStateOptions<TState>
): UseOptimisticGameStateReturn<TState> {
const { initialState, applyMove, onMoveAccepted, onMoveRejected } = options
// Server-confirmed state and version
const [serverState, setServerState] = useState<TState>(initialState)
const [serverVersion, setServerVersion] = useState(1)
// Pending moves that haven't been confirmed by server yet
const [pendingMoves, setPendingMoves] = useState<PendingMove<TState>[]>([])
// Whether we've received authoritative state from the server at least once
const [hasReceivedServerState, setHasReceivedServerState] = useState(false)
// Last error from move rejection
const [lastError, setLastError] = useState<string | null>(null)
// Track last processed version to deduplicate move-accepted events
const lastProcessedVersionRef = useRef(0)
// Ref for callbacks to avoid stale closures
const callbacksRef = useRef({ onMoveAccepted, onMoveRejected })
useEffect(() => {
callbacksRef.current = { onMoveAccepted, onMoveRejected }
}, [onMoveAccepted, onMoveRejected])
// Ref for serverState so applyOptimisticMove doesn't depend on it directly
const serverStateRef = useRef(serverState)
useEffect(() => {
serverStateRef.current = serverState
}, [serverState])
// Current state = server state + all pending moves applied
const currentState = pendingMoves.reduce(
(_state, pending) => pending.optimisticState,
serverState
)
const applyOptimisticMove = useCallback(
(move: GameMove) => {
setPendingMoves((prev) => {
const baseState =
prev.length > 0 ? prev[prev.length - 1].optimisticState : serverStateRef.current
const optimisticState = applyMove(baseState, move)
return [
...prev,
{
move,
optimisticState,
timestamp: Date.now(),
},
]
})
},
[applyMove]
)
const handleMoveAccepted = useCallback(
(newServerState: TState, newServerVersion: number, acceptedMove: GameMove) => {
// Deduplicate: skip if we've already processed this version (safety net for double delivery)
if (newServerVersion <= lastProcessedVersionRef.current) {
console.log(
`[OptimisticState] DEDUP: skipping version ${newServerVersion} (already processed ${lastProcessedVersionRef.current})`
)
return
}
lastProcessedVersionRef.current = newServerVersion
// Update server state
setServerState(newServerState)
setServerVersion(newServerVersion)
// Remove the accepted move from pending queue
setPendingMoves((prev) => {
const index = prev.findIndex(
(p) => p.move.type === acceptedMove.type && p.move.timestamp === acceptedMove.timestamp
)
if (index !== -1) {
return prev.slice(index + 1)
}
// Move not found in pending queue - might be from another tab/device
// Clear all pending moves since server state is now authoritative
return []
})
callbacksRef.current.onMoveAccepted?.(newServerState, acceptedMove)
},
[]
)
const handleMoveRejected = useCallback((error: string, rejectedMove: GameMove) => {
setLastError(error)
// Remove the rejected move and all subsequent moves from pending queue
setPendingMoves((prev) => {
const index = prev.findIndex(
(p) => p.move.type === rejectedMove.type && p.move.timestamp === rejectedMove.timestamp
)
if (index !== -1) {
// Rollback: remove rejected move and everything after it
return prev.slice(0, index)
}
return prev
})
callbacksRef.current.onMoveRejected?.(error, rejectedMove)
}, [])
const syncWithServer = useCallback((newServerState: TState, newServerVersion: number) => {
console.log('[OptimisticState] syncWithServer called:', {
newServerVersion,
gamePhase: (newServerState as any)?.gamePhase,
})
setServerState(newServerState)
setServerVersion(newServerVersion)
// Update dedup tracker so subsequent move-accepted events are correctly compared
lastProcessedVersionRef.current = newServerVersion
// Clear pending moves on sync (new authoritative state from server)
setPendingMoves([])
setHasReceivedServerState(true)
}, [])
const clearError = useCallback(() => {
setLastError(null)
}, [])
const reset = useCallback(() => {
setServerState(initialState)
setServerVersion(1)
setPendingMoves([])
setLastError(null)
setHasReceivedServerState(false)
}, [initialState])
return {
state: currentState,
version: serverVersion,
hasPendingMoves: pendingMoves.length > 0,
hasReceivedServerState,
lastError,
applyOptimisticMove,
handleMoveAccepted,
handleMoveRejected,
syncWithServer,
clearError,
reset,
}
}
|