create a websocket game server
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// 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, "<game_token>", "<user_token>"]
|
||||
// 2 S->C END [2, <seq>, <code>, "<reason>"]
|
||||
// 3 S->C STATE [3, <seq>, <state_code>]
|
||||
// 4 C<->S ACK [4, <seq>]
|
||||
//
|
||||
// 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})
|
||||
}
|
||||
Reference in New Issue
Block a user