package fibs import ( "bytes" "fmt" "log" "strconv" "strings" "sync" ) const ( BoxDrawingsLightVertical = '|' ) const ( StateLength = iota StatePlayerScore StateOpponentScore StateBoardSpace0 ) const ( StatePlayerName = iota StateOpponentName ) const ( StateTurn = 29 + iota StatePlayerDice1 StatePlayerDice2 StateOpponentDice1 StateOpponentDice2 StateDoublingValue StatePlayerMayDouble StateOpponentMayDouble StateWasDoubled StatePlayerColor StateDirection StateObsoleteHome StateObsoleteBar StatePlayerHome StateOpponentHome StatePlayerBar StateOpponentBar StateMovablePieces StateObsoletePlayerForced StateObsoleteOpponentForced StateRedoubles ) const ( SpaceUnknown = -1 ) const initialState = "FIBS:Welcome:5:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:-1:0:0:0:0:1:1:1:0:1:-1:0:25:0:0:0:0:4:0:0:0" var boardTopWhite = []byte("+13-14-15-16-17-18-+---+19-20-21-22-23-24-+") var boardBottomWhite = []byte("+12-11-10--9--8--7-+---+-6--5--4--3--2--1-+") var boardTopBlack = []byte("+-1--2--3--4--5--6-+---+-7--8--9-10-11-12-+") var boardBottomBlack = []byte("+24-23-22-21-20-19-+---+18-17-16-15-14-13-+") type Board struct { client *Client state string s []string v []int moves [][2]int movesColor int validMoves map[int][][]int from map[int]int to map[int]int selectedNum int selectedSpace int premove [][2]int Premovefrom map[int]int Premoveto map[int]int dragFromX int dragFromY int sync.Mutex } func NewBoard(client *Client) *Board { b := &Board{ client: client, s: make([]string, 52), v: make([]int, 50), } b.ResetMoves() b.ResetPreMoves() b.SetState(initialState) // TODO /* b.v[StatePlayerColor] = -1 b.v[StateBoardSpace0+11] = 12 b.v[StateBoardSpace0+9] = 7 b.v[StateBoardSpace0+13] = -13 b.v[StateBoardSpace0+24] = -3 b.v[StatePlayerBar] = 3 b.Update() */ return b } // TODO refactor func (b *Board) GetStringState() []string { b.Lock() defer b.Unlock() return b.s } func (b *Board) GetIntState() []int { b.Lock() defer b.Unlock() return b.v } func (b *Board) resetSelection() { b.selectedSpace = 0 b.selectedNum = 0 } func (b *Board) autoSendMoves() { movable := 2 if b.v[StatePlayerDice1] > 0 && b.v[StatePlayerDice1] == b.v[StatePlayerDice2] { movable = 4 } if b.v[StateMovablePieces] > 0 { movable = b.v[StateMovablePieces] } if len(b.premove) < movable { return } moveCommand := []byte("move") for j := 0; j < 2; j++ { for i := range b.premove { var from string if b.premove[i][0] == 0 || b.premove[i][0] == 25 { from = "bar" } else { from = strconv.Itoa(b.premove[i][0]) } if (j == 0) != (from == "bar") { continue // Always send bar moves first } var to string if b.premove[i][1] == b.PlayerBearOffSpace() { to = "off" } else { to = strconv.Itoa(b.premove[i][1]) } moveCommand = append(moveCommand, []byte(" "+from+"-"+to)...) } } b.client.Out <- moveCommand } func (b *Board) GetState() string { var s = strings.Join(b.s, ":") for i := range b.v { s += ":" + strconv.Itoa(b.v[i]) } return s } func (b *Board) SetState(state string) { b.Lock() s := strings.Split(state, ":") newPlayers := s[StatePlayerName] != b.s[StatePlayerName] || s[StateOpponentName] != b.s[StateOpponentName] copy(b.s, s) v := make([]int, 50) var err error for i := 0; i < 50; i++ { v[i], err = strconv.Atoi(b.s[i+2]) if err != nil { log.Fatal(err) } } newTurn := v[StateTurn] != b.v[StateTurn] // Retain dice rolls if !newPlayers && !newTurn { copyDice := []int{ StatePlayerDice1, StatePlayerDice2, StateOpponentDice1, StateOpponentDice2, } for _, vi := range copyDice { if v[vi] == 0 { v[vi] = b.v[vi] } } } copy(b.v, v) b.ResetPreMoves() b.Unlock() b.Draw() } func (b *Board) Draw() { b.client.Event <- &EventDraw{} } func (b *Board) renderSpace(index int, spaceValue int) []byte { var playerColor = "x" var opponentColor = "o" if b.v[StatePlayerColor] == 1 { playerColor = "o" opponentColor = "x" } var pieceColor string value := b.v[StateBoardSpace0+index] if index == b.PlayerBarSpace() { value = b.v[StatePlayerBar] pieceColor = playerColor } else if index == 25-b.PlayerBarSpace() { value = b.v[StateOpponentBar] pieceColor = opponentColor } else { if value < 0 { pieceColor = "x" } else if value > 0 { pieceColor = "o" } else { pieceColor = playerColor } } abs := value if value < 0 { abs = value * -1 } top := index <= 12 if b.v[StatePlayerColor] == 1 { top = !top } firstDigit := 4 secondDigit := 5 if !top { firstDigit = 5 secondDigit = 4 } var firstNumeral string var secondNumeral string if abs > 5 { if abs > 9 { firstNumeral = "1" } else { firstNumeral = strconv.Itoa(abs) } if abs > 9 { secondNumeral = strconv.Itoa(abs - 10) } if spaceValue == firstDigit && (!top || abs > 9) { pieceColor = firstNumeral } else if spaceValue == secondDigit && abs > 9 { pieceColor = secondNumeral } else if top && spaceValue == secondDigit { pieceColor = firstNumeral } } if abs > 5 { abs = 5 } var r []byte foregroundColor := "#FFFFFF" backgroundColor := "#000000" if index != 0 && index != 25 { if true { // default theme if index%2 == 0 { backgroundColor = "#303030" } else { backgroundColor = "#101010" } } else { // rainbow foregroundColor = "#000000" switch index % 6 { case 1: backgroundColor = "#FF0000" case 2: backgroundColor = "#FFA500" case 3: backgroundColor = "#FFFF00" case 4: backgroundColor = "#008000" case 5: backgroundColor = "#0000FF" case 0: backgroundColor = "#4B0082" } } } // Highlight legal moves highlightSpace := b.ValidMove(b.selectedSpace, index) highlightSpace = false // TODO Make configurable, disable by default if b.selectedNum > 0 && highlightSpace && index != 25 && index != 0 { foregroundColor = "black" backgroundColor = "yellow" } if abs > 0 && spaceValue <= abs { r = []byte(pieceColor) } else { r = []byte(" ") } rightArrowFrom := (b.v[StateDirection] == b.movesColor) == (index > 12) if b.selectedSpace == index && b.selectedNum > 0 && spaceValue <= abs && spaceValue > abs-b.selectedNum { r = []byte("*") } else if b.Premovefrom[index] > 0 && spaceValue > (abs+b.Premoveto[index])-b.Premovefrom[index] && spaceValue <= abs+b.Premoveto[index] { if index == 25-b.PlayerBarSpace() { r = []byte("▾") } else if index == b.PlayerBarSpace() { r = []byte("▴") } else if rightArrowFrom { r = []byte("▸") } else { r = []byte("◂") } foregroundColor = "yellow" } else if b.Premoveto[index] > 0 && spaceValue > abs && spaceValue <= abs+(b.Premoveto[index]) { r = []byte(playerColor) foregroundColor = "yellow" } else if b.from[index] > 0 && spaceValue > abs && spaceValue <= abs+b.from[index] { if rightArrowFrom { r = []byte("▸") } else { r = []byte("◂") } if b.movesColor == b.v[StatePlayerColor] { foregroundColor = "yellow" } else { foregroundColor = "green" } } else if b.to[index] > 0 && spaceValue > abs-(b.to[index]+b.from[index]) { if b.movesColor == b.v[StatePlayerColor] { foregroundColor = "yellow" } else { foregroundColor = "green" } } return append(append([]byte(fmt.Sprintf("[\"space-%d\"][%s:%s:b] ", index, foregroundColor, backgroundColor)), r...), []byte(" [-:-:-][\"\"]")...) } func (b *Board) ResetMoves() { b.moves = nil b.movesColor = 0 b.validMoves = make(map[int][][]int) b.from = make(map[int]int) b.to = make(map[int]int) } func (b *Board) ResetPreMoves() { b.premove = nil b.Premovefrom = make(map[int]int) b.Premoveto = make(map[int]int) } func (b *Board) PlayerHomeSpaces() (int, int) { homeBoardStart := 1 homeBoardEnd := 6 if (b.v[StateDirection] == -1) == (b.v[StatePlayerColor] == -1) { homeBoardStart = 19 homeBoardEnd = 24 } return homeBoardStart, homeBoardEnd } func (b *Board) PlayerPieceAreHome() bool { homeBoardStart, homeBoardEnd := b.PlayerHomeSpaces() hasPlayerPiece := func(index int) bool { if index < 0 || index > 25 { return false } value := b.v[StateBoardSpace0+index] // Include pre-moves mod := b.v[StatePlayerColor] value -= b.client.Board.Premovefrom[index] * mod if b.v[StatePlayerColor] == -1 { return value < 0 } return value > 0 } for i := 1; i < 24; i++ { if i >= homeBoardStart && i <= homeBoardEnd { continue } if hasPlayerPiece(i) { return false } } return true } func (b *Board) spaceAvailable(index int) bool { if index < 0 || index > 25 { return false } if index == 0 || index == 25 { return b.PlayerPieceAreHome() } return (b.v[StatePlayerColor] == 1 && b.v[StateBoardSpace0+index] >= -1) || (b.v[StatePlayerColor] == -1 && b.v[StateBoardSpace0+index] <= 1) } func (b *Board) GetValidMoves(from int) [][]int { if validMoves, ok := b.validMoves[from]; ok { return validMoves } var validMoves [][]int defer func() { b.validMoves[from] = validMoves }() if b.v[StateTurn] != b.v[StatePlayerColor] || b.v[StatePlayerDice1] == 0 || b.v[StatePlayerDice2] == 0 { return validMoves } trySpaces := [][]int{ {b.v[StatePlayerDice1]}, {b.v[StatePlayerDice2]}, {b.v[StatePlayerDice1], b.v[StatePlayerDice2]}, {b.v[StatePlayerDice2], b.v[StatePlayerDice1]}, } if b.v[StatePlayerDice1] == b.v[StatePlayerDice2] { trySpaces = append(trySpaces, []int{b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1]}, []int{b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1]}) } if b.PlayerPieceAreHome() { homeSpace := b.PlayerBearOffSpace() spacesHome := from - homeSpace if spacesHome < 0 { spacesHome *= -1 } if spacesHome <= b.v[StatePlayerDice1] || spacesHome <= b.v[StatePlayerDice2] { trySpaces = append(trySpaces, []int{spacesHome}) } } foundMoves := make(map[int]bool) CHECKSPACES: for i := range trySpaces { checkSpace := 0 for _, space := range trySpaces[i] { checkSpace += space if !b.spaceAvailable(from + (checkSpace * b.v[StateDirection])) { continue CHECKSPACES } } space := from + (checkSpace * b.v[StateDirection]) if _, value := foundMoves[space]; !value { foundMoves[space] = true validMoves = append(validMoves, trySpaces[i]) } } return validMoves } func (b *Board) PlayerBarSpace() int { return 25 - b.PlayerBearOffSpace() } func (b *Board) PlayerBearOffSpace() int { if b.v[StateDirection] == -1 { return 0 } return 25 } func (b *Board) ValidMove(f int, t int) bool { if b.v[StateTurn] != b.v[StatePlayerColor] || b.v[StatePlayerDice1] == 0 || b.v[StatePlayerDice2] == 0 { return false } if t == b.PlayerBearOffSpace() { // TODO bear off logic, only allow high roll return b.PlayerPieceAreHome() } validMoves := b.GetValidMoves(f) CHECKVALID: for i := range validMoves { checkSpace := 0 for _, space := range validMoves[i] { checkSpace += space if !b.spaceAvailable(f + (checkSpace * b.v[StateDirection])) { continue CHECKVALID } } if f+(checkSpace*b.v[StateDirection]) == t { return true } } return false } func (b *Board) parseMoveString(player int, s string) int { space, err := strconv.Atoi(s) if err != nil { space = SpaceUnknown if s == "bar" { barSpace := b.PlayerBarSpace() if b.v[StatePlayerColor] == player { space = barSpace } else { space = 25 - barSpace } } else if s == "off" { space = b.PlayerBearOffSpace() } } return space } func (b *Board) Move(player int, f string, t string) { from := b.parseMoveString(player, f) to := b.parseMoveString(player, t) if from == SpaceUnknown || to == SpaceUnknown { lf("WARNING: Unknown move %s-%s", f, t) return } b.moves = append(b.moves, [2]int{from, to}) b.movesColor = player b.from[from]++ b.to[to]++ spaceValue := b.v[StateBoardSpace0+to] // Hit. if (spaceValue == -1 && player == 1) || (spaceValue == 1 && player == -1) { bar := 25 - b.PlayerBarSpace() if player == b.v[StatePlayerColor] { bar = b.PlayerBarSpace() } b.v[StateBoardSpace0+bar] -= player } b.v[StateBoardSpace0+from] -= player b.v[StateBoardSpace0+to] += player b.v[StateTurn] = player * -1 b.validMoves = make(map[int][][]int) b.ResetPreMoves() } func (b *Board) SimplifyMoves() { for i := range b.moves { for j := range b.moves { if b.moves[i][1] == b.moves[j][0] { // Same to space as from space b.moves[j][0] = b.moves[i][0] // Set from space to earlier from space b.moves = append(b.moves[:i], b.moves[i+1:]...) b.SimplifyMoves() return } else if b.moves[i][0] == b.moves[j][1] { // Same to space as from space b.moves[j][1] = b.moves[i][1] // Set to space to earlier to space b.moves = append(b.moves[:i], b.moves[i+1:]...) b.SimplifyMoves() return } } } } func (b *Board) GetSelection() (num int, space int) { return b.selectedNum, b.selectedSpace } func (b *Board) SetSelection(num int, space int) { b.selectedNum, b.selectedSpace = num, space } func (b *Board) ResetSelection() { b.selectedNum, b.selectedSpace = 0, 0 } func (b *Board) addPreMove(from int, to int, num int) bool { // Allow bearing off when the player moves their own pieces on to the bar if to == 0 || to == 25 { to = b.PlayerBearOffSpace() } // Expand combined move moves := b.client.Board.GetValidMoves(from) CHECKPREMOVES: for i := range moves { checkSpace := 0 for _, space := range moves[i] { checkSpace += space lf("CHECK %d %d", checkSpace, from+(checkSpace*b.v[StateDirection])) if !b.spaceAvailable(from + (checkSpace * b.v[StateDirection])) { continue CHECKPREMOVES } } lf("SECOND PHASE %+v %d", moves[i], num) if (from+(checkSpace*b.v[StateDirection]) == to) && len(moves[i]) > 1 { lf("SECOND.5 PHASE %+v %d", moves[i], num) for j := 0; j < num; j++ { checkSpace = 0 lastSpace := 0 for _, space := range moves[i] { checkSpace += space lf("THIRD PHASE %d %d", from+(lastSpace*b.v[StateDirection]), from+(checkSpace*b.v[StateDirection])) if !b.addPreMove(from+(lastSpace*b.v[StateDirection]), from+(checkSpace*b.v[StateDirection]), 1) { return false } lastSpace = checkSpace } } return true } } if !b.ValidMove(from, to) { return false } for i := 0; i < num; i++ { b.premove = append(b.premove, [2]int{from, to}) b.Premovefrom[from]++ b.Premoveto[to]++ } lf("ADD %+v", b.premove) return true } func (b *Board) AddPreMove(from int, to int) bool { if !b.addPreMove(from, to, b.selectedNum) { return false } lf("FINAL %+v", b.premove) b.resetSelection() b.autoSendMoves() return true } func (b *Board) GetPreMoves() [][2]int { return b.premove } func (b *Board) Render() []byte { b.Lock() var white bool if b.v[StatePlayerColor] == 1 { white = true } var opponentName = b.s[1] var playerName = b.s[0] var playerColor = "x" var opponentColor = "o" if white { playerColor = "o" opponentColor = "x" } var t bytes.Buffer t.WriteString("[\"space-off\"] [\"\"]\n") t.WriteString("[\"space-off\"] [\"\"]\n") t.WriteString("[\"space-off\"] ") if white { t.Write(boardTopWhite) } else { t.Write(boardTopBlack) } t.WriteString("[\"\"] ") t.WriteByte('\n') space := func(i int, j int) []byte { spaceValue := i + 1 if i > 5 { spaceValue = 5 - (i - 6) } if j == -1 { if i <= 4 { return b.renderSpace(25-b.PlayerBarSpace(), spaceValue) } return b.renderSpace(b.PlayerBarSpace(), spaceValue) } var index int if !white { if i < 6 { j = 12 - j } else { j = 11 - j } index = 12 + j if i > 5 { index = 12 - j } } else { index = 12 + j if i > 5 { index = 11 - j } } if !white { index = 24 - index } index++ // increment to get actual space number (0 is bar) if i == 5 { return []byte("[-:#000000] [-:-]") } return b.renderSpace(index, spaceValue) } for i := 0; i < 11; i++ { t.Write([]byte("[\"space-off\"]")) if i == 5 && b.v[StateDoublingValue] > 1 { t.WriteString(fmt.Sprintf("%2d ", b.v[StateDoublingValue])) if b.v[StatePlayerMayDouble] == 1 { t.WriteByte('v') } else { t.WriteByte('^') } } else { t.WriteByte(' ') t.WriteByte(' ') t.WriteByte(' ') t.WriteByte(' ') } t.WriteRune(BoxDrawingsLightVertical) t.Write([]byte("[\"\"]")) for j := 0; j < 12; j++ { t.Write(space(i, j)) if j == 5 { t.WriteRune(BoxDrawingsLightVertical) t.Write(space(i, -1)) t.WriteRune(BoxDrawingsLightVertical) } } t.Write([]byte("[\"space-off\"]" + string(BoxDrawingsLightVertical) + " ")) playerRollColor := "yellow" playerBold := "b" opponentRollColor := "white" opponentBold := "" if b.v[StateTurn] != b.v[StatePlayerColor] { playerRollColor = "white" opponentRollColor = "green" playerBold = "" opponentBold = "b" } if i == 0 { t.Write([]byte("[" + opponentRollColor + "::" + opponentBold + "]" + opponentColor + " " + opponentName + " (" + b.s[4] + ")")) if b.v[StateOpponentHome] > 0 { t.Write([]byte(fmt.Sprintf(" %d off", b.v[StateOpponentHome]))) } t.Write([]byte("[-::-]")) } else if i == 2 { if b.v[StateOpponentDice1] > 0 { t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", opponentRollColor, opponentBold, b.v[StateOpponentDice1], b.v[StateOpponentDice2]))) } else { t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", opponentRollColor))) } } else if i == 8 { if b.v[StatePlayerDice1] > 0 { t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", playerRollColor, playerBold, b.v[StatePlayerDice1], b.v[StatePlayerDice2]))) } else { t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", playerRollColor))) } } else if i == 10 { t.Write([]byte("[" + playerRollColor + "::" + playerBold + "]" + playerColor + " " + playerName + " (" + b.s[3] + ")")) if b.v[StatePlayerHome] > 0 { t.Write([]byte(fmt.Sprintf(" %d off", b.v[StatePlayerHome]))) } t.Write([]byte("[-::-]")) } t.Write([]byte("[\"\"] ")) t.WriteByte('\n') } t.WriteString("[\"space-off\"] ") if white { t.Write(boardBottomWhite) } else { t.Write(boardBottomBlack) } t.WriteString(" [\"\"]\n") t.WriteString("[\"space-off\"] [\"\"]") b.Unlock() return t.Bytes() }