From 8da2829b6ad829f872303b04e509de71f2a21a4e Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 20 Oct 2023 13:51:32 -0700 Subject: [PATCH] Add doubling cube --- PROTOCOL.md | 9 +++- cmd/bgammon-server/server.go | 97 ++++++++++++++++++++++++++++++++++++ command.go | 2 + game.go | 47 ++++++++++++----- gamestate.go | 27 ++++++++++ player.go | 1 + 6 files changed, 169 insertions(+), 14 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index b7826e5..fcb032a 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -45,6 +45,13 @@ formatted responses are more easily parsed by computers. - Join match by match ID or by player. - Aliases: `j` +- `double` + - Offer double to opponent. + - Aliases: `d` + +- `resign` + - Decline double offer and resign game. + - `roll` - Roll dice. - Aliases: `r` @@ -58,7 +65,7 @@ formatted responses are more easily parsed by computers. - Aliases: `r` - `ok` - - Confirm checker movement and pass turn to next player. + - Accept double offer or confirm checker movement and pass turn to next player. - Aliases: `k` - `rematch` diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index 21e83d6..f6e54f5 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -617,6 +617,88 @@ COMMANDS: } clientGame.removeClient(cmd.client) + case bgammon.CommandDouble, "d": + if clientGame == nil { + cmd.client.sendNotice("You are not currently in a match.") + continue + } + + if clientGame.Turn != cmd.client.playerNumber { + cmd.client.sendNotice("It is not your turn.") + continue + } + + gameState := &bgammon.GameState{ + Game: clientGame.Game, + PlayerNumber: cmd.client.playerNumber, + Available: clientGame.LegalMoves(), + } + if !gameState.MayDouble() { + cmd.client.sendNotice("You may not double at this time.") + continue + } + + clientGame.DoubleOffered = true + + cmd.client.sendNotice(fmt.Sprintf("Double offered to opponent (%d points).", clientGame.Points*2)) + clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s offers a double (%d points).", cmd.client.name, clientGame.Points*2)) + + clientGame.eachClient(func(client *serverClient) { + if client.json { + clientGame.sendBoard(client) + } + }) + case bgammon.CommandResign: + if clientGame == nil { + cmd.client.sendNotice("You are not currently in a match.") + continue + } + + gameState := &bgammon.GameState{ + Game: clientGame.Game, + PlayerNumber: cmd.client.playerNumber, + Available: clientGame.LegalMoves(), + } + if !gameState.MayResign() { + cmd.client.sendNotice("You may not resign at this time.") + continue + } + + cmd.client.sendNotice("Declined double offer") + clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s declined double offer.", cmd.client.name)) + + log.Println("RESIGN VALUE: ", clientGame.DoubleValue) // TODO + if cmd.client.playerNumber == 1 { + clientGame.Player2.Points += clientGame.DoubleValue + if clientGame.Player2.Points >= clientGame.Points { + clientGame.Winner = 2 + } else { + clientGame.Reset() + } + } else { + clientGame.Player1.Points += clientGame.DoubleValue + if clientGame.Player1.Points >= clientGame.Points { + clientGame.Winner = 1 + } else { + clientGame.Reset() + } + } + + var winEvent *bgammon.EventWin + if clientGame.Winner != 0 { + winEvent = &bgammon.EventWin{} + if clientGame.Winner == 1 { + winEvent.Player = clientGame.Player1.Name + } else { + winEvent.Player = clientGame.Player2.Name + } + } + clientGame.eachClient(func(client *serverClient) { + clientGame.sendBoard(client) + if winEvent != nil { + client.sendEvent(winEvent) + } + }) case bgammon.CommandRoll, "r": if clientGame == nil { cmd.client.sendEvent(&bgammon.EventFailedRoll{ @@ -783,6 +865,21 @@ COMMANDS: continue } + if clientGame.DoubleOffered && clientGame.Turn != cmd.client.playerNumber { + opponent := clientGame.opponent(cmd.client) + if opponent == nil { + cmd.client.sendNotice("You may not accept the double until your opponent rejoins the match.") + continue + } + + clientGame.DoubleOffered = false + clientGame.DoubleValue *= 2 + clientGame.DoublePlayer = opponent.playerNumber + cmd.client.sendNotice("Accepted double.") + opponent.sendNotice(fmt.Sprintf("%s accepted double.", cmd.client.name)) + continue + } + legalMoves := clientGame.LegalMoves() if len(legalMoves) != 0 { available := bgammon.FlipMoves(legalMoves, cmd.client.playerNumber) diff --git a/command.go b/command.go index 5893ada..f60ad59 100644 --- a/command.go +++ b/command.go @@ -14,6 +14,8 @@ const ( CommandCreate = "create" // Create match. CommandJoin = "join" // Join match. CommandLeave = "leave" // Leave match. + CommandDouble = "double" // Offer double to opponent. + CommandResign = "resign" // Decline double offer and resign game. CommandRoll = "roll" // Roll dice. CommandMove = "move" // Move checkers. CommandReset = "reset" // Reset checker movement. diff --git a/game.go b/game.go index 1b59081..d6770d9 100644 --- a/game.go +++ b/game.go @@ -22,30 +22,39 @@ type Game struct { Roll1 int Roll2 int Moves [][]int // Pending moves. - Points int + + Points int // Points required to win the match. + DoubleValue int // Doubling cube value. + DoublePlayer int // Player that currently posesses the doubling cube. + DoubleOffered bool // Whether the current player is offering a double. boardStates [][]int // One board state for each move to allow undoing a move. } func NewGame() *Game { return &Game{ - Board: NewBoard(), - Player1: NewPlayer(1), - Player2: NewPlayer(2), + Board: NewBoard(), + Player1: NewPlayer(1), + Player2: NewPlayer(2), + Points: 1, + DoubleValue: 1, } } func (g *Game) Copy() *Game { newGame := &Game{ - Board: make([]int, len(g.Board)), - Player1: g.Player1, - Player2: g.Player2, - Turn: g.Turn, - Winner: g.Winner, - Roll1: g.Roll1, - Roll2: g.Roll2, - Moves: make([][]int, len(g.Moves)), - boardStates: make([][]int, len(g.boardStates)), + Board: make([]int, len(g.Board)), + Player1: g.Player1, + Player2: g.Player2, + Turn: g.Turn, + Winner: g.Winner, + Roll1: g.Roll1, + Roll2: g.Roll2, + Moves: make([][]int, len(g.Moves)), + Points: g.Points, + DoubleValue: g.DoubleValue, + DoublePlayer: g.DoublePlayer, + boardStates: make([][]int, len(g.boardStates)), } copy(newGame.Board, g.Board) copy(newGame.Moves, g.Moves) @@ -68,6 +77,18 @@ func (g *Game) NextTurn() { g.boardStates = g.boardStates[:0] } +func (g *Game) Reset() { + g.Board = NewBoard() + g.Turn = 0 + g.Roll1 = 0 + g.Roll2 = 0 + g.Moves = nil + g.DoubleValue = 1 + g.DoublePlayer = 0 + g.DoubleOffered = false + g.boardStates = nil +} + func (g *Game) turnPlayer() Player { switch g.Turn { case 2: diff --git a/gamestate.go b/gamestate.go index b42ac4d..99ab05a 100644 --- a/gamestate.go +++ b/gamestate.go @@ -56,8 +56,19 @@ func (g *GameState) SpaceAt(x int, y int) int { return space } +// MayDouble returns whether the player may send the 'double' command. +func (g *GameState) MayDouble() bool { + if g.Winner != 0 { + return false + } + return g.Turn != 0 && g.Turn == g.PlayerNumber && g.Roll1 == 0 && !g.DoubleOffered && (g.DoublePlayer == 0 || g.DoublePlayer == g.PlayerNumber) +} + // MayRoll returns whether the player may send the 'roll' command. func (g *GameState) MayRoll() bool { + if g.Winner != 0 || g.DoubleOffered { + return false + } switch g.Turn { case 0: if g.PlayerNumber == 1 { @@ -78,10 +89,26 @@ func (g *GameState) MayRoll() bool { // MayOK returns whether the player may send the 'ok' command. func (g *GameState) MayOK() bool { + if g.Winner != 0 { + return false + } else if g.Turn != 0 && g.Turn != g.PlayerNumber && g.DoubleOffered { + return true + } return g.Turn != 0 && g.Turn == g.PlayerNumber && g.Roll1 != 0 && len(g.Available) == 0 } +// MayResign returns whether the player may send the 'resign' command. +func (g *GameState) MayResign() bool { + if g.Winner != 0 { + return false + } + return g.Turn != 0 && g.Turn != g.PlayerNumber && g.DoubleOffered +} + // MayReset returns whether the player may send the 'reset' command. func (g *GameState) MayReset() bool { + if g.Winner != 0 { + return false + } return g.Turn != 0 && g.Turn == g.PlayerNumber && len(g.Moves) > 0 } diff --git a/player.go b/player.go index 5cdd5c3..8c3ab85 100644 --- a/player.go +++ b/player.go @@ -3,6 +3,7 @@ package bgammon type Player struct { Number int // 1 black, 2 white Name string + Points int } func NewPlayer(number int) Player {