// Package protocol defines the wire format of messages exchanged between // game clients and the game server. // // All messages are JSON arrays whose first element is a numeric opcode: // // [opcode, param1, param2, ...] // // Opcodes: // // 1 C->S JOIN [1, "", ""] // 2 S->C END [2, , , ""] // 3 S->C STATE [3, , ] // 4 C<->S ACK [4, ] // // Every state-changing server->client message (END, STATE) carries a // per-player monotonically increasing sequence number. The client confirms // delivery with an ACK message containing the sequence number of the last // received message (ACK is cumulative). Unacknowledged messages are kept in // a per-player queue and re-sent when the player reconnects. // // ACK in the S->C direction is reserved for confirming future client->server // messages once the protocol grows (client messages do not carry sequence // numbers yet). package protocol import ( "encoding/json" "fmt" ) // Opcodes. const ( OpJoin = 1 // C->S: join a match OpEnd = 2 // S->C: match ended OpState = 3 // S->C: game state transition OpAck = 4 // C<->S: delivery acknowledgement ) // Game state codes. States are always sent from the receiver's point of // view: a player never learns whether they are "player 1" or "player 2". const ( StatePlacingShips = 1 // both players are placing their ships StateOpponentReady = 2 // opponent finished placing ships StateWaitOpponentReady = 3 // you are ready, waiting for the opponent StateYourTurn = 4 // make your move StateWaitOpponentTurn = 5 // wait for the opponent's move ) // Match end codes. const ( EndCodeJoinTimeout = -1 // the second player never joined ) // WebSocket close codes used by the server (application range 4000+). const ( CloseCodeNormal = 1000 // normal closure CloseCodeReplaced = 4000 // connection replaced by a newer one of the same player CloseCodeExpectedJoin = 4001 // first message was not a JOIN CloseCodeBadMessage = 4002 // malformed message CloseCodeBadGameToken = 4003 // invalid/expired game token, server mismatch, invalid match CloseCodeBadUserToken = 4004 // invalid/expired user token or user is not a participant ) // ParseMessage decodes a raw message into its opcode and the remaining // arguments. func ParseMessage(data []byte) (opcode int, args []json.RawMessage, err error) { var raw []json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return 0, nil, fmt.Errorf("not a JSON array: %w", err) } if len(raw) == 0 { return 0, nil, fmt.Errorf("empty message") } if err := json.Unmarshal(raw[0], &opcode); err != nil { return 0, nil, fmt.Errorf("opcode is not a number: %w", err) } return opcode, raw[1:], nil } // ParseJoin extracts the game token and the user token from JOIN arguments. func ParseJoin(args []json.RawMessage) (gameToken, userToken string, err error) { if len(args) != 2 { return "", "", fmt.Errorf("JOIN expects 2 arguments, got %d", len(args)) } if err := json.Unmarshal(args[0], &gameToken); err != nil { return "", "", fmt.Errorf("game token is not a string: %w", err) } if err := json.Unmarshal(args[1], &userToken); err != nil { return "", "", fmt.Errorf("user token is not a string: %w", err) } return gameToken, userToken, nil } // ParseAck extracts the acknowledged sequence number from ACK arguments. func ParseAck(args []json.RawMessage) (seq int64, err error) { if len(args) != 1 { return 0, fmt.Errorf("ACK expects 1 argument, got %d", len(args)) } if err := json.Unmarshal(args[0], &seq); err != nil { return 0, fmt.Errorf("seq is not a number: %w", err) } return seq, nil } func build(v []any) []byte { data, err := json.Marshal(v) if err != nil { // Only possible with non-serializable values, which never happens // with the fixed shapes below. panic(fmt.Sprintf("protocol: marshal message: %v", err)) } return data } // BuildEnd builds an END message: [2, seq, code, reason]. func BuildEnd(seq int64, code int, reason string) []byte { return build([]any{OpEnd, seq, code, reason}) } // BuildState builds a STATE message: [3, seq, state]. func BuildState(seq int64, state int) []byte { return build([]any{OpState, seq, state}) } // BuildAck builds an ACK message: [4, seq]. func BuildAck(seq int64) []byte { return build([]any{OpAck, seq}) }