From 5bacfe6b287b4fdf7442d28f417c66c0405faa8a Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 24 Nov 2023 19:15:50 -0800 Subject: [PATCH] Support acey-deucey Resolves #4. --- PROTOCOL.md | 6 +- board.go | 18 ++-- cmd/bgammon-server/database.go | 7 +- cmd/bgammon-server/game.go | 4 +- cmd/bgammon-server/server.go | 55 +++++++++++- game.go | 160 +++++++++++++++++++++++---------- player.go | 7 +- 7 files changed, 193 insertions(+), 64 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 909d3bf..2c20e83 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -67,8 +67,10 @@ formatted responses are more easily parsed by computers. - Reset pending checker movement. - Aliases: `r` -- `ok` - - Accept double offer or confirm checker movement and pass turn to next player. +- `ok [1-6]` + - Accept double offer or confirm checker movement. The parameter for this command only applies in acey-deucey games. + - In normal games, confirming checker movement passes the turn to the next player. + - In acey-deucey games, when confirming moves after rolling an acey-deucey, the double roll the player chooses must be specified. - Aliases: `k` - `rematch` diff --git a/board.go b/board.go index fd0b708..7a16a9a 100644 --- a/board.go +++ b/board.go @@ -21,12 +21,16 @@ const BoardSpaces = 28 // player 2's checkers. The board's space numbering is always from the // perspective of the current player (i.e. the 1 space will always be in the // current player's home board). -func NewBoard() []int { +func NewBoard(acey bool) []int { space := make([]int, BoardSpaces) - space[24], space[1] = 2, -2 - space[19], space[6] = -5, 5 - space[17], space[8] = -3, 3 - space[13], space[12] = 5, -5 + if acey { + space[SpaceHomePlayer], space[SpaceHomeOpponent] = 15, -15 + } else { + space[24], space[1] = 2, -2 + space[19], space[6] = -5, 5 + space[17], space[8] = -3, 3 + space[13], space[12] = 5, -5 + } return space } @@ -39,14 +43,14 @@ func HomeRange(player int) (from int, to int) { } // RollForMove returns the roll needed to move a checker from the provided spaces. -func RollForMove(from int, to int, player int) int { +func RollForMove(from int, to int, player int, acey bool) int { if !ValidSpace(from) || !ValidSpace(to) { return 0 } // Handle standard moves. if from >= 1 && from <= 24 && to >= 1 && to <= 24 { - return SpaceDiff(from, to) + return SpaceDiff(from, to, acey) } playerHome := SpaceHomePlayer diff --git a/cmd/bgammon-server/database.go b/cmd/bgammon-server/database.go index 3c5c5b2..80b0dd7 100644 --- a/cmd/bgammon-server/database.go +++ b/cmd/bgammon-server/database.go @@ -12,6 +12,7 @@ import ( const databaseSchema = ` CREATE TABLE game ( id serial PRIMARY KEY, + acey integer NOT NULL, started bigint NOT NULL, ended bigint NOT NULL, winner integer NOT NULL, @@ -80,7 +81,11 @@ func recordGameResult(conn *pgx.Conn, g bgammon.Game) error { } defer tx.Commit(context.Background()) - _, err = tx.Exec(context.Background(), "INSERT INTO game (started, ended, winner, player1, player2) VALUES ($1, $2, $3, $4, $5)", g.Started.Unix(), g.Ended.Unix(), g.Winner, g.Player1.Name, g.Player2.Name) + acey := 0 + if g.Acey { + acey = 1 + } + _, err = tx.Exec(context.Background(), "INSERT INTO game (acey, started, ended, winner, player1, player2) VALUES ($1, $2, $3, $4, $5, $6)", acey, g.Started.Unix(), g.Ended.Unix(), g.Winner, g.Player1.Name, g.Player2.Name) return err } diff --git a/cmd/bgammon-server/game.go b/cmd/bgammon-server/game.go index f2101d3..0d24bff 100644 --- a/cmd/bgammon-server/game.go +++ b/cmd/bgammon-server/game.go @@ -25,13 +25,13 @@ type serverGame struct { *bgammon.Game } -func newServerGame(id int) *serverGame { +func newServerGame(id int, acey bool) *serverGame { now := time.Now().Unix() return &serverGame{ id: id, created: now, lastActive: now, - Game: bgammon.NewGame(), + Game: bgammon.NewGame(acey), } } diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index f08c2c6..cfaaaf1 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -634,6 +634,16 @@ COMMANDS: continue } + var acey bool + + // Backwards-compatible acey-deucey parameter. Added in v1.1.5. + noAcey := bytes.HasPrefix(gameName, []byte("0 ")) + yesAcey := bytes.HasPrefix(gameName, []byte("1 ")) + if noAcey || yesAcey { + acey = yesAcey + gameName = gameName[2:] + } + points, err := strconv.Atoi(string(gamePoints)) if err != nil || points < 1 || points > 99 { sendUsage() @@ -650,7 +660,7 @@ COMMANDS: gameName = []byte(fmt.Sprintf("%s%s match", cmd.client.name, abbr)) } - g := newServerGame(<-s.newGameIDs) + g := newServerGame(<-s.newGameIDs, acey) g.name = gameName g.Points = points g.password = gamePassword @@ -1003,7 +1013,7 @@ COMMANDS: backgammon := bgammon.PlayerCheckers(clientGame.Board[playerBar], opponent) != 0 if !backgammon { homeStart, homeEnd := bgammon.HomeRange(clientGame.Winner) - bgammon.IterateSpaces(homeStart, homeEnd, func(space, spaceCount int) { + bgammon.IterateSpaces(homeStart, homeEnd, clientGame.Acey, func(space, spaceCount int) { if bgammon.PlayerCheckers(clientGame.Board[space], opponent) != 0 { backgammon = true } @@ -1134,7 +1144,44 @@ COMMANDS: continue } - clientGame.NextTurn() + if clientGame.Acey && ((clientGame.Roll1 == 1 && clientGame.Roll2 == 2) || (clientGame.Roll1 == 2 && clientGame.Roll2 == 1)) && len(clientGame.Moves) == 2 { + var doubles int + if len(params) > 0 { + doubles, _ = strconv.Atoi(string(params[0])) + } + if doubles < 1 || doubles > 6 { + cmd.client.sendEvent(&bgammon.EventFailedOk{ + Reason: "Choose which doubles you want for your acey-deucey.", + }) + continue + } + + clientGame.NextTurn(true) + clientGame.Roll1, clientGame.Roll2 = doubles, doubles + clientGame.Reroll = true + } else if clientGame.Acey && clientGame.Reroll { + clientGame.NextTurn(true) + clientGame.Roll1, clientGame.Roll2 = 0, 0 + if !clientGame.roll(cmd.client.playerNumber) { + cmd.client.Terminate("Server error") + opponent.Terminate("Server error") + continue + } + clientGame.Reroll = false + + clientGame.eachClient(func(client *serverClient) { + ev := &bgammon.EventRolled{ + Roll1: clientGame.Roll1, + Roll2: clientGame.Roll2, + } + ev.Player = string(cmd.client.name) + client.sendEvent(ev) + clientGame.sendBoard(client) + }) + } else { + clientGame.NextTurn(false) + } + clientGame.eachClient(func(client *serverClient) { clientGame.sendBoard(client) }) @@ -1154,7 +1201,7 @@ COMMANDS: } else if clientGame.rematch != 0 && clientGame.rematch != cmd.client.playerNumber { s.gamesLock.Lock() - newGame := newServerGame(<-s.newGameIDs) + newGame := newServerGame(<-s.newGameIDs, clientGame.Acey) newGame.name = clientGame.name newGame.Points = clientGame.Points newGame.password = clientGame.password diff --git a/game.go b/game.go index 78e4245..0c81eb4 100644 --- a/game.go +++ b/game.go @@ -15,52 +15,70 @@ var boardTopWhite = []byte("+24-23-22-21-20-19-+---+18-17-16-15-14-13-+") var boardBottomWhite = []byte("+-1--2--3--4--5--6-+---+-7--8--9-10-11-12-+") type Game struct { - Board []int - Player1 Player - Player2 Player - Turn int Started time.Time Ended time.Time - Winner int - Roll1 int - Roll2 int - Moves [][]int // Pending moves. + + Player1 Player + Player2 Player + + Acey bool // Acey-deucey. + Board []int + Turn int + Roll1 int + Roll2 int + Moves [][]int // Pending moves. + Winner 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. + Reroll bool // Used in acey-deucey. + boardStates [][]int // One board state for each move to allow undoing a move. } -func NewGame() *Game { - return &Game{ - Board: NewBoard(), +func NewGame(acey bool) *Game { + g := &Game{ + Acey: acey, + Board: NewBoard(acey), Player1: NewPlayer(1), Player2: NewPlayer(2), Points: 1, DoubleValue: 1, } + if !g.Acey { + g.Player1.Entered = true + g.Player2.Entered = true + } + return g } func (g *Game) Copy() *Game { newGame := &Game{ - Board: make([]int, len(g.Board)), - Player1: g.Player1, - Player2: g.Player2, - Turn: g.Turn, - Started: g.Started, - Ended: g.Ended, - Winner: g.Winner, - Roll1: g.Roll1, - Roll2: g.Roll2, - Moves: make([][]int, len(g.Moves)), + Started: g.Started, + Ended: g.Ended, + + Player1: g.Player1, + Player2: g.Player2, + + Acey: g.Acey, + Board: make([]int, len(g.Board)), + Turn: g.Turn, + Roll1: g.Roll1, + Roll2: g.Roll2, + Moves: make([][]int, len(g.Moves)), + Winner: g.Winner, + Points: g.Points, DoubleValue: g.DoubleValue, DoublePlayer: g.DoublePlayer, DoubleOffered: g.DoubleOffered, - boardStates: make([][]int, len(g.boardStates)), + + Reroll: g.Reroll, + + boardStates: make([][]int, len(g.boardStates)), } copy(newGame.Board, g.Board) copy(newGame.Moves, g.Moves) @@ -68,23 +86,39 @@ func (g *Game) Copy() *Game { return newGame } -func (g *Game) NextTurn() { +func (g *Game) NextTurn(replay bool) { if g.Winner != 0 { return } - nextTurn := 1 - if g.Turn == 1 { - nextTurn = 2 + if g.Acey { + if !g.Player1.Entered && PlayerCheckers(g.Board[SpaceHomePlayer], 1) == 0 { + g.Player1.Entered = true + } + if !g.Player2.Entered && PlayerCheckers(g.Board[SpaceHomeOpponent], 2) == 0 { + g.Player2.Entered = true + } } + + if !replay { + nextTurn := 1 + if g.Turn == 1 { + nextTurn = 2 + } + g.Turn = nextTurn + } + g.Roll1, g.Roll2 = 0, 0 - g.Turn = nextTurn g.Moves = g.Moves[:0] g.boardStates = g.boardStates[:0] } func (g *Game) Reset() { - g.Board = NewBoard() + if g.Acey { + g.Player1.Entered = false + g.Player2.Entered = false + } + g.Board = NewBoard(g.Acey) g.Turn = 0 g.Roll1 = 0 g.Roll2 = 0 @@ -92,6 +126,7 @@ func (g *Game) Reset() { g.DoubleValue = 1 g.DoublePlayer = 0 g.DoubleOffered = false + g.Reroll = false g.boardStates = nil } @@ -283,13 +318,23 @@ ADDMOVES: g.boardStates = gameCopy.boardStates if checkWin { + entered := g.Player1.Entered + if !local && g.Turn == 2 { + entered = g.Player2.Entered + } + var foundChecker bool - for space := 1; space <= 24; space++ { - if PlayerCheckers(g.Board[space], g.Turn) != 0 { - foundChecker = true - break + if g.Acey && !entered { + foundChecker = true + } else { + for space := 1; space <= 24; space++ { + if PlayerCheckers(g.Board[space], g.Turn) != 0 { + foundChecker = true + break + } } } + if !foundChecker { g.Winner = g.Turn } @@ -316,7 +361,7 @@ func (g *Game) LegalMoves(local bool) [][]int { } haveDiceRoll := func(from, to int) int { - diff := SpaceDiff(from, to) + diff := SpaceDiff(from, to, g.Acey) var c int for _, roll := range rolls { if roll == diff { @@ -357,7 +402,7 @@ func (g *Game) LegalMoves(local bool) [][]int { log.Panicf("no dice roll to use for %d/%d", from, to) } - diff := SpaceDiff(from, to) + diff := SpaceDiff(from, to, g.Acey) for i, roll := range rolls { if roll == diff { rolls = append(rolls[:i], rolls[i+1:]...) @@ -384,7 +429,7 @@ func (g *Game) LegalMoves(local bool) [][]int { } if mustEnter { // Must enter from bar. from, to := HomeRange(g.opponentPlayer().Number) - IterateSpaces(from, to, func(homeSpace int, spaceCount int) { + IterateSpaces(from, to, g.Acey, func(homeSpace int, spaceCount int) { if movesFound[barSpace*100+homeSpace] { return } @@ -403,8 +448,16 @@ func (g *Game) LegalMoves(local bool) [][]int { for space := range g.Board { if space == SpaceBarPlayer || space == SpaceBarOpponent { // Handled above. continue - } else if space == SpaceHomePlayer || space == SpaceHomeOpponent { // No entering from home spaces (until acey-deucey is added). - continue + } else if space == SpaceHomePlayer || space == SpaceHomeOpponent { + homeSpace := SpaceHomePlayer + entered := g.Player1.Entered + if g.Turn == 2 { + homeSpace = SpaceHomeOpponent + entered = g.Player2.Entered + } + if !g.Acey || space != homeSpace || entered { + continue + } } checkers := g.Board[space] @@ -421,7 +474,7 @@ func (g *Game) LegalMoves(local bool) [][]int { if movesFound[space*100+homeSpace] { continue } - available := haveBearOffDiceRoll(SpaceDiff(space, homeSpace)) + available := haveBearOffDiceRoll(SpaceDiff(space, homeSpace, g.Acey)) if available > 0 { ok := true if haveDiceRoll(space, homeSpace) == 0 { @@ -455,7 +508,7 @@ func (g *Game) LegalMoves(local bool) [][]int { lastSpace = 24 } - IterateSpaces(space, lastSpace, func(to int, spaceCount int) { + f := func(to int, spaceCount int) { if movesFound[space*100+to] { return } @@ -469,7 +522,14 @@ func (g *Game) LegalMoves(local bool) [][]int { moves = append(moves, []int{space, to}) movesFound[space*100+to] = true } - }) + } + if space == SpaceHomePlayer { + IterateSpaces(25, lastSpace, g.Acey, f) + } else if space == SpaceHomeOpponent { + IterateSpaces(1, lastSpace, g.Acey, f) + } else { + IterateSpaces(space, lastSpace, g.Acey, f) + } } } @@ -745,10 +805,19 @@ func (g *Game) BoardState(player int, local bool) []byte { return t.Bytes() } -func SpaceDiff(from int, to int) int { +func SpaceDiff(from int, to int, acey bool) int { if from < 0 || from > 27 || to < 0 || to > 27 { return 0 - } else if from == SpaceHomePlayer || from == SpaceHomeOpponent || to == SpaceBarPlayer || to == SpaceBarOpponent { + } else if to == SpaceBarPlayer || to == SpaceBarOpponent { + return 0 + } else if from == SpaceHomePlayer || from == SpaceHomeOpponent { + if acey { + if from == SpaceHomePlayer { + return 25 - to + } else { + return to + } + } return 0 } @@ -775,11 +844,12 @@ func SpaceDiff(from int, to int) int { return diff } -func IterateSpaces(from int, to int, f func(space int, spaceCount int)) { - if from == to || from < 1 || from > 24 || to < 1 || to > 24 { +func IterateSpaces(from int, to int, acey bool, f func(space int, spaceCount int)) { + if from == to || from < 0 || from > 25 || to < 0 || to > 25 { + return + } else if !acey && (from == 0 || from == 25 || to == 0 || to == 25) { return } - i := 1 if to > from { for space := from; space <= to; space++ { diff --git a/player.go b/player.go index 8c3ab85..29439f2 100644 --- a/player.go +++ b/player.go @@ -1,9 +1,10 @@ package bgammon type Player struct { - Number int // 1 black, 2 white - Name string - Points int + Number int // 1 black, 2 white + Name string + Points int + Entered bool // Whether all checkers have entered the board. (Acey-deucey) } func NewPlayer(number int) Player {