From 2f09683339c25039627cadecd5f500d14e6102c2 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Sat, 26 Aug 2023 20:33:16 -0700 Subject: [PATCH] Add more JSON events --- PROTOCOL.md | 14 ++++-- cmd/bgammon-server/client.go | 95 ++++++++++++++++++++++++++++++++---- cmd/bgammon-server/game.go | 39 ++++++++------- cmd/bgammon-server/server.go | 80 ++++++++++++++---------------- event.go | 31 +++++++++--- game.go | 13 +++++ gamestate.go | 8 +-- 7 files changed, 195 insertions(+), 85 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 4d8c77c..ff0feb8 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -1,4 +1,8 @@ -# Secification of bgammon protocol +# Specification of bgammon.org protocol + +Connect to `bgammon.org:1337` via TCP. + +All commands and events are separated by newlines. ## User commands @@ -69,7 +73,7 @@ This command is not normally used, as the game state is provided in JSON format. Disconnect from the server. -## Server responses +## Events (server responses) Data types: @@ -78,6 +82,10 @@ Data types: - `text` - alphanumeric without spaces - `line` - alphanumeric with spaces +All events are sent in either JSON or human-readable format. The human-readable +format is documented here. The structure of each JSON message is available by referencing +[this file](https://code.rocket9labs.com/tslocum/bgammon/src/branch/main/event.go). + ### `hello ` Initial welcome message sent by the server. It provides instructions on how to log in. @@ -104,7 +112,7 @@ Game description. End of games list. -### `joined ` +### `joined ` Sent after successfully creating or joining a game, and when another player joins a game you are in. diff --git a/cmd/bgammon-server/client.go b/cmd/bgammon-server/client.go index 3c4d2c7..da4ff0d 100644 --- a/cmd/bgammon-server/client.go +++ b/cmd/bgammon-server/client.go @@ -1,16 +1,91 @@ package main -import "code.rocket9labs.com/tslocum/bgammon" +import ( + "encoding/json" + "fmt" + + "code.rocket9labs.com/tslocum/bgammon" +) type serverClient struct { - id int - json bool - name []byte - account int - connected int64 - lastActive int64 - lastPing int64 - commands <-chan []byte - events chan<- []byte + id int + json bool + name []byte + account int + connected int64 + lastActive int64 + lastPing int64 + commands <-chan []byte + events chan<- []byte + playerNumber int bgammon.Client } + +func (c *serverClient) sendEvent(e interface{}) { + if c.json { + switch ev := e.(type) { + case *bgammon.EventWelcome: + ev.Type = "welcome" + case *bgammon.EventNotice: + ev.Type = "notice" + case *bgammon.EventSay: + ev.Type = "say" + case *bgammon.EventList: + ev.Type = "list" + case *bgammon.EventJoined: + ev.Type = "joined" + case *bgammon.EventFailedJoin: + ev.Type = "failedjoin" + case *bgammon.EventBoard: + ev.Type = "board" + case *bgammon.EventRolled: + ev.Type = "rolled" + case *bgammon.EventMoved: + ev.Type = "moved" + } + + buf, err := json.Marshal(e) + if err != nil { + panic(err) + } + c.events <- buf + return + } + + switch ev := e.(type) { + case *bgammon.EventWelcome: + c.events <- []byte(fmt.Sprintf("welcome %s there are %d clients playing %d games.", ev.PlayerName, ev.Clients, ev.Games)) + case *bgammon.EventNotice: + c.events <- []byte(fmt.Sprintf("notice %s", ev.Message)) + case *bgammon.EventSay: + c.events <- []byte(fmt.Sprintf("say %s %s", ev.Player, ev.Message)) + case *bgammon.EventList: + c.events <- []byte("liststart Games list:") + for _, g := range ev.Games { + password := 0 + if g.Password { + password = 1 + } + name := "Game" + if g.Name != "" { + name = g.Name + } + c.events <- []byte(fmt.Sprintf("game %d %d %d %s", g.ID, password, g.Players, name)) + } + c.events <- []byte("listend End of games list.") + case *bgammon.EventJoined: + c.events <- []byte(fmt.Sprintf("joined %d %s", ev.GameID, ev.Player)) + case *bgammon.EventFailedJoin: + c.events <- []byte(fmt.Sprintf("failedjoin %s", ev.Reason)) + case *bgammon.EventRolled: + c.events <- []byte(fmt.Sprintf("rolled %s %d %d", ev.Player, ev.Roll1, ev.Roll2)) + case *bgammon.EventMoved: + c.events <- []byte(fmt.Sprintf("moved %s %s", ev.Player, bgammon.FormatMoves(ev.Moves, c.playerNumber))) + } +} + +func (c *serverClient) sendNotice(message string) { + c.sendEvent(&bgammon.EventNotice{ + Message: message, + }) +} diff --git a/cmd/bgammon-server/game.go b/cmd/bgammon-server/game.go index 3484d6a..e1cbdf4 100644 --- a/cmd/bgammon-server/game.go +++ b/cmd/bgammon-server/game.go @@ -3,7 +3,6 @@ package main import ( "bufio" "bytes" - "encoding/json" "fmt" "log" "math/rand" @@ -65,26 +64,21 @@ func (g *serverGame) sendBoard(client *serverClient) { } if client.json { - gameState := ServerGameState{ + ev := &bgammon.EventBoard{ GameState: bgammon.GameState{ - Game: g.Game, + Game: g.Game.Copy(), Available: g.LegalMoves(), }, - Board: g.Game.Board, } if playerNumber == 2 { - log.Println(gameState.Board) - log.Println(g.Game.Board) - slices.Reverse(gameState.Board) + /*log.Println(gameState.Board) + log.Println(g.Game.Board)*/ + slices.Reverse(ev.Board) - log.Println(gameState.Board) - log.Println(g.Game.Board) + /*log.Println(gameState.Board) + log.Println(g.Game.Board)*/ } - buf, err := json.Marshal(gameState) - if err != nil { - log.Fatalf("failed to marshal json for %+v: %s", gameState, err) - } - client.events <- []byte(fmt.Sprintf("board %s", buf)) + client.sendEvent(ev) return } @@ -120,12 +114,18 @@ func (g *serverGame) addClient(client *serverClient) bool { if !ok { return } - joinMessage := []byte(fmt.Sprintf("joined %d %s %s", g.id, client.name, g.name)) - client.events <- joinMessage + + ev := &bgammon.EventJoined{ + GameID: g.id, + } + ev.Player = string(client.name) + + client.sendEvent(ev) g.sendBoard(client) + opponent := g.opponent(client) if opponent != nil { - opponent.events <- joinMessage + opponent.sendEvent(ev) g.sendBoard(opponent) } }() @@ -135,19 +135,23 @@ func (g *serverGame) addClient(client *serverClient) bool { case g.client1 != nil: g.client2 = client g.Player2.Name = string(client.name) + client.playerNumber = 2 ok = true case g.client2 != nil: g.client1 = client g.Player1.Name = string(client.name) + client.playerNumber = 1 ok = true default: i := rand.Intn(2) if i == 0 { g.client1 = client g.Player1.Name = string(client.name) + client.playerNumber = 1 } else { g.client2 = client g.Player2.Name = string(client.name) + client.playerNumber = 2 } ok = true } @@ -163,6 +167,7 @@ func (g *serverGame) removeClient(client *serverClient) { if !ok { return } + client.playerNumber = 0 opponent := g.opponent(client) if opponent == nil { return diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index 5d0ad1a..6edbea9 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -3,7 +3,6 @@ package main import ( "bufio" "bytes" - "encoding/json" "fmt" "log" "math/rand" @@ -141,7 +140,6 @@ func (s *server) sendHello(c *serverClient) { } func (s *server) sendWelcome(c *serverClient) { - c.events <- []byte(fmt.Sprintf("welcome %s there are %d clients playing %d games.", c.name, len(s.clients), len(s.games))) } func (s *server) gameByClient(c *serverClient) *serverGame { @@ -209,7 +207,11 @@ COMMANDS: cmd.client.json = true } - s.sendWelcome(cmd.client) + cmd.client.sendEvent(&bgammon.EventWelcome{ + PlayerName: string(cmd.client.name), + Clients: len(s.clients), + Games: len(s.games), + }) log.Printf("login as %s - %s", username, password) continue @@ -268,31 +270,16 @@ COMMANDS: } opponent.events <- []byte(fmt.Sprintf("say %s %s", cmd.client.name, bytes.Join(params, []byte(" ")))) case bgammon.CommandList, "ls": - if cmd.client.json { - ev := bgammon.EventList{} - for _, g := range s.games { - ev.Games = append(ev.Games, bgammon.GameListing{ - ID: g.id, - Password: len(g.password) != 0, - Players: g.playerCount(), - Name: string(g.name), - }) - } - buf, err := json.Marshal(ev) - if err != nil { - panic(err) - } - cmd.client.events <- buf - continue - } - cmd.client.events <- []byte("liststart Games list:") - players := 0 - password := 0 - name := "game name" + ev := &bgammon.EventList{} for _, g := range s.games { - cmd.client.events <- []byte(fmt.Sprintf("game %d %d %d %s", g.id, password, players, name)) + ev.Games = append(ev.Games, bgammon.GameListing{ + ID: g.id, + Password: len(g.password) != 0, + Players: g.playerCount(), + Name: string(g.name), + }) } - cmd.client.events <- []byte("listend End of games list.") + cmd.client.sendEvent(ev) case bgammon.CommandCreate, "c": sendUsage := func() { cmd.client.events <- []byte("notice To create a public game specify whether it is public or private. When creating a private game, a password must also be provided.") @@ -319,8 +306,6 @@ COMMANDS: continue } - log.Printf("create game (password %s) name: %s", gamePassword, gameName) - g := newServerGame(<-s.newGameIDs) g.name = gameName g.password = gamePassword @@ -330,12 +315,14 @@ COMMANDS: s.games = append(s.games, g) // TODO lock case bgammon.CommandJoin, "j": if clientGame != nil { - cmd.client.events <- []byte("failedjoin Please leave the game you are in before joining another game.") + cmd.client.sendEvent(&bgammon.EventFailedJoin{ + Reason: "Please leave the game you are in before joining another game.", + }) continue } sendUsage := func() { - cmd.client.events <- []byte("notice To join a public game specify its game ID. To join a private game, a password must also be specified.") + cmd.client.sendNotice("To join a public game specify its game ID. To join a private game, a password must also be specified.") } if len(params) == 0 { @@ -351,12 +338,16 @@ COMMANDS: for _, g := range s.games { if g.id == gameID { if len(g.password) != 0 && (len(params) < 2 || !bytes.Equal(g.password, bytes.Join(params[2:], []byte(" ")))) { - cmd.client.events <- []byte("failedjoin Invalid password.") + cmd.client.sendEvent(&bgammon.EventFailedJoin{ + Reason: "Invalid password.", + }) continue COMMANDS } if !g.addClient(cmd.client) { - cmd.client.events <- []byte("failedjoin Game is full.") + cmd.client.sendEvent(&bgammon.EventFailedJoin{ + Reason: "Game is full.", + }) } continue COMMANDS } @@ -390,15 +381,16 @@ COMMANDS: cmd.client.events <- []byte("notice It is not your turn to roll.") continue } + ev := &bgammon.EventRolled{ + Roll1: clientGame.Roll1, + Roll2: clientGame.Roll2, + } + ev.Player = string(cmd.client.name) clientGame.eachClient(func(client *serverClient) { - roll1 := 0 - roll2 := 0 - if clientNumber == 1 { - roll1 = clientGame.Roll1 - } else { - roll2 = clientGame.Roll2 + client.sendEvent(ev) + if !client.json { + clientGame.sendBoard(client) } - client.events <- []byte(fmt.Sprintf("rolled %s %d %d", cmd.client.name, roll1, roll2)) }) if clientGame.Turn == 0 && clientGame.Roll1 != 0 && clientGame.Roll2 != 0 { if clientGame.Roll1 > clientGame.Roll2 { @@ -475,12 +467,12 @@ COMMANDS: } clientGame.Moves = gameCopy.Moves + ev := &bgammon.EventMoved{ + Moves: moves, + } + ev.Player = string(cmd.client.name) clientGame.eachClient(func(client *serverClient) { - player := 1 - if clientGame.client2 == client { - player = 2 - } - client.events <- []byte(fmt.Sprintf("move %s %s", cmd.client.name, bgammon.FormatMoves(moves, player))) + client.sendEvent(ev) if !client.json { clientGame.sendBoard(client) } diff --git a/event.go b/event.go index 7522bbb..b924cc1 100644 --- a/event.go +++ b/event.go @@ -13,17 +13,24 @@ type Event struct { } type EventWelcome struct { + Event PlayerName string Clients int Games int } -type EventJoined struct { - GameID int - PlayerName string +type EventNotice struct { + Event + Message string +} + +type EventSay struct { + Event + Message string } type GameListing struct { + Event ID int Password bool Players int @@ -31,23 +38,33 @@ type GameListing struct { } type EventList struct { + Event Games []GameListing } -type EventSay struct { - Message string +type EventJoined struct { + Event + GameID int +} + +type EventFailedJoin struct { + Event + Reason string } type EventBoard struct { + Event GameState } -type EventRoll struct { +type EventRolled struct { + Event Roll1 int Roll2 int } -type EventMove struct { +type EventMoved struct { + Event Moves [][]int } diff --git a/game.go b/game.go index 957eee7..60a9c12 100644 --- a/game.go +++ b/game.go @@ -31,6 +31,19 @@ func NewGame() *Game { } } +func (g *Game) Copy() *Game { + newGame := &Game{ + Player1: g.Player1, + Player2: g.Player2, + Turn: g.Turn, + Roll1: g.Roll1, + Roll2: g.Roll2, + } + copy(newGame.Board, g.Board) + copy(newGame.Moves, g.Moves) + return newGame +} + func (g *Game) turnPlayer() Player { switch g.Turn { case 2: diff --git a/gamestate.go b/gamestate.go index 99f81de..84a52fd 100644 --- a/gamestate.go +++ b/gamestate.go @@ -2,19 +2,19 @@ package bgammon type GameState struct { *Game - Player int - Available [][]int // Legal moves. + PlayerNumber int + Available [][]int // Legal moves. } func (g *GameState) OpponentPlayer() Player { - if g.Player == 1 { + if g.PlayerNumber == 1 { return g.Player2 } return g.Player1 } func (g *GameState) LocalPlayer() Player { - if g.Player == 1 { + if g.PlayerNumber == 1 { return g.Player1 } return g.Player2