Add more JSON events

This commit is contained in:
Trevor Slocum 2023-08-26 20:33:16 -07:00
parent 1c60039ed9
commit 2f09683339
7 changed files with 195 additions and 85 deletions

View file

@ -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 <message:line>`
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 <id:integer> <player:text> <name:line>`
### `joined <id:integer> <player:text>`
Sent after successfully creating or joining a game, and when another player
joins a game you are in.

View file

@ -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,
})
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

13
game.go
View file

@ -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:

View file

@ -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