diff --git a/PROTOCOL.md b/PROTOCOL.md index a97049e..2c49574 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -143,6 +143,12 @@ type Game struct { } ``` +### `failedok ` + +Sent after sending `ok` when there are one or more legal moves still available to the player. + +Players must make moves using all available dice rolls before ending their turn. + ### `say ` Chat message from another player. diff --git a/cmd/bgammon-server/game.go b/cmd/bgammon-server/game.go index ea02f1b..821da78 100644 --- a/cmd/bgammon-server/game.go +++ b/cmd/bgammon-server/game.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "math/rand" + "slices" "time" "code.rocket9labs.com/tslocum/bgammon" @@ -58,10 +59,26 @@ func (g *serverGame) roll(player int) bool { } func (g *serverGame) sendBoard(client *serverClient) { + playerNumber := 1 + if g.client2 == client { + playerNumber = 2 + } + if client.json { - gameState := bgammon.GameState{ - Game: g.Game, - Available: g.LegalMoves(), + gameState := ServerGameState{ + GameState: bgammon.GameState{ + Game: g.Game, + 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) } buf, err := json.Marshal(gameState) if err != nil { @@ -71,10 +88,6 @@ func (g *serverGame) sendBoard(client *serverClient) { return } - playerNumber := 1 - if g.client2 == client { - playerNumber = 2 - } scanner := bufio.NewScanner(bytes.NewReader(g.BoardState(playerNumber))) for scanner.Scan() { client.events <- append([]byte("notice "), scanner.Bytes()...) @@ -98,9 +111,11 @@ func (g *serverGame) addClient(client *serverClient) bool { } joinMessage := []byte(fmt.Sprintf("joined %d %s %s", g.id, client.name, g.name)) client.events <- joinMessage + g.sendBoard(client) opponent := g.opponent(client) if opponent != nil { opponent.events <- joinMessage + g.sendBoard(opponent) } }() switch { @@ -108,16 +123,20 @@ func (g *serverGame) addClient(client *serverClient) bool { ok = false case g.client1 != nil: g.client2 = client + g.Player2.Name = string(client.name) ok = true case g.client2 != nil: g.client1 = client + g.Player1.Name = string(client.name) ok = true default: i := rand.Intn(2) if i == 0 { g.client1 = client + g.Player1.Name = string(client.name) } else { g.client2 = client + g.Player2.Name = string(client.name) } ok = true } @@ -125,17 +144,34 @@ func (g *serverGame) addClient(client *serverClient) bool { } func (g *serverGame) removeClient(client *serverClient) { + // TODO game is considered paused when only one player is present + // once started, only the same player may join and continue the game + log.Println("remove client", client) + ok := true + defer func() { + if !ok { + return + } + opponent := g.opponent(client) + if opponent == nil { + return + } + opponent.events <- []byte(fmt.Sprintf("left %d %s %s", g.id, client.name, g.name)) + if !opponent.json { + g.sendBoard(opponent) + } + }() switch { case g.client1 == client: g.client1 = nil + g.Player1.Name = "" case g.client2 == client: g.client2 = nil + g.Player2.Name = "" default: + ok = false return } - // TODO game is considered paused when only one player is present - // once started, only the same player may join and continue the game - log.Println("removed client", client) } func (g *serverGame) opponent(client *serverClient) *serverClient { @@ -146,3 +182,8 @@ func (g *serverGame) opponent(client *serverClient) *serverClient { } return nil } + +type ServerGameState struct { + bgammon.GameState + Board []int +} diff --git a/cmd/bgammon-server/main.go b/cmd/bgammon-server/main.go index 3938ae9..c4abdbe 100644 --- a/cmd/bgammon-server/main.go +++ b/cmd/bgammon-server/main.go @@ -19,17 +19,17 @@ func main() { g := newServerGame(1) g.Board[bgammon.SpaceBarPlayer] = 3 g.Board[bgammon.SpaceBarOpponent] = -2 - g.Roll1 = 3 - g.Roll2 = 2 - g.Turn = 1 + g.Roll1 = 1 + g.Roll2 = 3 + g.Turn = 2 log.Println("initial legal moves") log.Printf("%+v", g.LegalMoves()) - g.Moves = append(g.Moves, []int{6, 4}) + //g.Moves = append(g.Moves, []int{6, 4}) log.Printf("Legal moves after %+v", g.Moves) log.Printf("%+v", g.LegalMoves()) - playerNumber := 1 + playerNumber := 2 go func() { time.Sleep(100 * time.Millisecond) diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index dc4d5b6..28f29df 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -299,8 +299,6 @@ COMMANDS: log.Panicf("failed to add client to newly created game %+v %+v", g, cmd.client) } s.games = append(s.games, g) // TODO lock - - g.sendBoard(cmd.client) case bgammon.CommandJoin, "j": if clientGame != nil { cmd.client.events <- []byte("failedjoin Please leave the game you are in before joining another game.") @@ -330,10 +328,7 @@ COMMANDS: if !g.addClient(cmd.client) { cmd.client.events <- []byte("failedjoin Game is full.") - continue COMMANDS } - - g.sendBoard(cmd.client) continue COMMANDS } } @@ -368,19 +363,45 @@ COMMANDS: } if !clientGame.roll(playerNumber) { cmd.client.events <- []byte("notice It is not your turn to roll.") - } else { - clientGame.eachClient(func(client *serverClient) { - client.events <- []byte(fmt.Sprintf("rolled %d %d", clientGame.Roll1, clientGame.Roll2)) - }) + continue + } + clientGame.eachClient(func(client *serverClient) { + roll1 := 0 + roll2 := 0 + if playerNumber == 1 { + roll1 = clientGame.Roll1 + } else { + roll2 = clientGame.Roll2 + } + 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 { + clientGame.Turn = 1 + } else if clientGame.Roll2 > clientGame.Roll1 { + clientGame.Turn = 2 + } else { + clientGame.Roll1 = 0 + clientGame.Roll2 = 0 + } } case bgammon.CommandMove, "m", "mv": if clientGame == nil { - cmd.client.events <- []byte("notice You are not currently in a game.") + cmd.client.events <- []byte("failedmove You are not currently in a game.") + continue + } + + playerNumber := 1 + if clientGame.client2 == cmd.client { + playerNumber = 2 + } + if clientGame.Turn != playerNumber { + cmd.client.events <- []byte("failedmove It is not your turn to move.") continue } sendUsage := func() { - cmd.client.events <- []byte("notice Specify one or more moves in the form FROM/TO. For example: 8/4 6/4") + cmd.client.events <- []byte("failedmove Specify one or more moves in the form FROM/TO. For example: 8/4 6/4") } if len(params) == 0 { @@ -390,7 +411,6 @@ COMMANDS: gameCopy := bgammon.Game{} gameCopy = *clientGame.Game - gameCopy.Moves = [][]int{} copy(gameCopy.Moves, clientGame.Moves) var moves [][]int @@ -411,6 +431,9 @@ COMMANDS: continue COMMANDS } + originalFrom, originalTo := from, to + from, to = bgammon.FlipSpace(from, playerNumber), bgammon.FlipSpace(to, playerNumber) + legalMoves := gameCopy.LegalMoves() var found bool for j := range legalMoves { @@ -420,7 +443,8 @@ COMMANDS: } } if !found { - cmd.client.events <- []byte(fmt.Sprintf("failedmove %d/%d Illegal move.", from, to)) + log.Printf("available legal moves: %s", bgammon.FormatMoves(legalMoves, playerNumber)) + cmd.client.events <- []byte(fmt.Sprintf("failedmove %d/%d Illegal move.", originalFrom, originalTo)) continue COMMANDS } @@ -429,25 +453,48 @@ COMMANDS: gameCopy.Moves = append(gameCopy.Moves, move) } - paramsText := bytes.Join(params, []byte(" ")) clientGame.Moves = gameCopy.Moves clientGame.eachClient(func(client *serverClient) { - client.events <- []byte(fmt.Sprintf("move %s %s", cmd.client.name, paramsText)) - clientGame.sendBoard(client) + player := 1 + if clientGame.client2 == client { + player = 2 + } + client.events <- []byte(fmt.Sprintf("move %s %s", cmd.client.name, bgammon.FormatMoves(moves, player))) + if !client.json { + clientGame.sendBoard(client) + } }) - case bgammon.CommandBoard, "b": + case bgammon.CommandOk, "k": if clientGame == nil { cmd.client.events <- []byte("notice You are not currently in a game.") - } else { + continue + } + + legalMoves := clientGame.LegalMoves() + if len(legalMoves) != 0 { playerNumber := 1 if clientGame.client2 == cmd.client { playerNumber = 2 } + cmd.client.events <- []byte(fmt.Sprintf("failedok You still have the following legal moves available: %s", bgammon.FormatMoves(legalMoves, playerNumber))) + continue + } - scanner := bufio.NewScanner(bytes.NewReader(clientGame.BoardState(playerNumber))) - for scanner.Scan() { - cmd.client.events <- append([]byte("notice "), scanner.Bytes()...) - } + log.Println("legal to pass turn") + case bgammon.CommandBoard, "b": + if clientGame == nil { + cmd.client.events <- []byte("notice You are not currently in a game.") + continue + } + + playerNumber := 1 + if clientGame.client2 == cmd.client { + playerNumber = 2 + } + + scanner := bufio.NewScanner(bytes.NewReader(clientGame.BoardState(playerNumber))) + for scanner.Scan() { + cmd.client.events <- append([]byte("notice "), scanner.Bytes()...) } case bgammon.CommandDisconnect: if clientGame != nil { diff --git a/game.go b/game.go index eb0793e..957eee7 100644 --- a/game.go +++ b/game.go @@ -284,9 +284,13 @@ func (g *Game) BoardState(player int) []byte { var playerColor = "x" var opponentColor = "o" + playerRoll := g.Roll1 + opponentRoll := g.Roll2 if white { playerColor = "o" opponentColor = "x" + playerRoll = g.Roll2 + opponentRoll = g.Roll1 } if white { @@ -365,16 +369,36 @@ func (g *Game) BoardState(player int) []byte { t.Write([]byte(fmt.Sprintf(" %d off", v))) } } else if i == 2 { - if g.Turn != player && g.Roll1 > 0 { - t.Write([]byte(fmt.Sprintf(" %d %d ", g.Roll1, g.Roll2))) - } else { - t.Write([]byte(fmt.Sprintf(" - - "))) + if g.Turn == 0 { + if g.Player1.Name != "" && g.Player2.Name != "" { + if opponentRoll != 0 { + t.Write([]byte(fmt.Sprintf(" %d", opponentRoll))) + } else { + t.Write([]byte(fmt.Sprintf(" -"))) + } + } + } else if g.Turn != player { + if g.Roll1 > 0 { + t.Write([]byte(fmt.Sprintf(" %d %d ", g.Roll1, g.Roll2))) + } else if opponentName != "" { + t.Write([]byte(fmt.Sprintf(" - - "))) + } } } else if i == 8 { - if g.Turn == player && g.Roll1 > 0 { - t.Write([]byte(fmt.Sprintf(" %d %d ", g.Roll1, g.Roll2))) - } else { - t.Write([]byte(fmt.Sprintf(" - - "))) + if g.Turn == 0 { + if g.Player1.Name != "" && g.Player2.Name != "" { + if playerRoll != 0 { + t.Write([]byte(fmt.Sprintf(" %d", playerRoll))) + } else { + t.Write([]byte(fmt.Sprintf(" -"))) + } + } + } else if g.Turn == player { + if g.Roll1 > 0 { + t.Write([]byte(fmt.Sprintf(" %d %d ", g.Roll1, g.Roll2))) + } else if playerName != "" { + t.Write([]byte(fmt.Sprintf(" - - "))) + } } } else if i == 10 { t.Write([]byte(playerColor + " " + playerName + " (" + playerRating + ")")) @@ -437,6 +461,27 @@ func numOpponentCheckers(checkers int, player int) int { } } +func FlipSpace(space int, player int) int { + if player == 1 { + return space + } + if space < 1 || space > 24 { + return space // TODO fix + } + return 24 - space + 1 +} + +func FormatMoves(moves [][]int, player int) []byte { + var out bytes.Buffer + for i := range moves { + if i != 0 { + out.WriteByte(' ') + } + out.Write([]byte(fmt.Sprintf("%d/%d", FlipSpace(moves[i][0], player), FlipSpace(moves[i][1], player)))) + } + return out.Bytes() +} + const ( VerticalBar rune = '\u2502' // │ )