package fibs import ( "bytes" "fmt" "log" "sort" "strconv" "strings" "sync" ) // TODO Add PlayerName, etc const ( BoxDrawingsLightVertical = '|' ) const ( StateLength = iota StatePlayerScore StateOpponentScore StateBoardSpace0 ) 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 playerDice [2]int opponentDice [2]int selected [2]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, } 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) GetIntState() []int { return b.v } func (b *Board) resetSelection() { b.selected[0] = 0 b.selected[1] = 0 } func (b *Board) autoSendMoves() { movable := 2 if b.playerDice[0] > 0 && b.playerDice[0] == b.playerDice[1] { movable = 4 } if b.v[StateMovablePieces] > 0 { movable = b.v[StateMovablePieces] } if len(b.premove) == 1 { abs := b.premove[0][1] - b.premove[0][0] direction := 1 if abs < 0 { abs *= -1 direction = -1 } if b.playerDice[0] == b.playerDice[1] { for expandDoubles := 4; expandDoubles >= 2; expandDoubles-- { if abs != b.playerDice[0]*expandDoubles { continue } from, _ := b.premove[0][0], b.premove[0][1] b.premove = nil for i := 1; i <= expandDoubles; i++ { b.premove = append(b.premove, [2]int{from + ((b.playerDice[0]*i - 1) * direction), from + ((b.playerDice[0] * i) * direction)}) } break } } } 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.playerHomeSpace() { to = "off" } else { to = strconv.Itoa(b.premove[i][1]) } moveCommand = append(moveCommand, []byte(" "+from+"-"+to)...) } } b.ResetPreMoves() 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() lastTurn := 0 if len(b.v) > 0 { lastTurn = b.v[StateTurn] } b.s = strings.Split(state, ":") b.v = make([]int, len(b.s)-2) var err error for i := 0; i < 46; i++ { b.v[i], err = strconv.Atoi(b.s[i+2]) if err != nil { log.Fatal(err) } } if b.v[StateTurn] != lastTurn { if lastTurn == b.v[StatePlayerColor] { b.playerDice = [2]int{0, 0} } else { b.opponentDice = [2]int{0, 0} } } if b.v[StatePlayerDice1] > 0 { b.playerDice = [2]int{b.v[StatePlayerDice1], b.v[StatePlayerDice2]} } if b.v[StateOpponentDice1] > 0 { b.opponentDice = [2]int{b.v[StateOpponentDice1], b.v[StateOpponentDice2]} } b.Unlock() b.Draw() } func (b *Board) Draw() { // TODO } 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 } } // TODO 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.selected[0], index) highlightSpace = false // TODO Make configurable, disable by default //+(b.playerDice[0]*b.v[StatePlayerColor]) ||b.selected[0] == index+(b.playerDice[1]*b.v[StatePlayerColor])) && b.selected[1] > 0 if b.selected[1] > 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.selected[0] == index && b.selected[1] > 0 && spaceValue <= abs && spaceValue > abs-b.selected[1] { 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) allPlayerPiecesInHomeBoard() bool { homeBoardStart := 1 homeBoardEnd := 6 if b.v[StateDirection] == -1 { homeBoardStart = 19 homeBoardEnd = 24 } hasPlayerPiece := func(index int) bool { if index < 0 || index > 25 { return false } return (b.v[StatePlayerColor] == 1 && b.v[StateBoardSpace0+index] > 0) || (b.v[StatePlayerColor] == -1 && b.v[StateBoardSpace0+index] < 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.allPlayerPiecesInHomeBoard() } 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.playerDice[0] == 0 || b.playerDice[1] == 0 { return validMoves } // TODO consider opponent blocking midway In full move trySpaces := [][]int{ {b.playerDice[0]}, {b.playerDice[1]}, {b.playerDice[0], b.playerDice[1]}, {b.playerDice[1], b.playerDice[0]}, } if b.playerDice[0] == b.playerDice[1] { trySpaces = append(trySpaces, []int{b.playerDice[0], b.playerDice[0], b.playerDice[0]}, []int{b.playerDice[0], b.playerDice[0], b.playerDice[0], b.playerDice[0]}) } if b.allPlayerPiecesInHomeBoard() { homeSpace := b.playerHomeSpace() spacesHome := from - homeSpace if spacesHome < 0 { spacesHome *= -1 } if spacesHome <= b.playerDice[0] || spacesHome <= b.playerDice[1] { 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, space) } } sort.Ints(validMoves) return validMoves } func (b *Board) playerBarSpace() int { return 25 - b.playerHomeSpace() } func (b *Board) playerHomeSpace() 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.playerDice[0] == 0 || b.playerDice[1] == 0 { return false } if t == b.playerHomeSpace() { // TODO bear off logic, only allow high roll return b.allPlayerPiecesInHomeBoard() } validMoves := b.GetValidMoves(f) for i := range validMoves { if validMoves[i] == 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.playerHomeSpace() } } 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 { // TODO debug print ("error: failed to parse move: player %d, from %s, to %s", player, f, t) return } b.moves = append(b.moves, [2]int{from, to}) b.movesColor = player b.from[from]++ b.to[to]++ 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) addPreMove(from int, to int) bool { if !b.validMove(from, to) { return false } b.premove = append(b.premove, [2]int{from, to}) b.premovefrom[from]++ b.premoveto[to]++ return true } 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.opponentDice[0] > 0 { t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", opponentRollColor, opponentBold, b.opponentDice[0], b.opponentDice[1]))) } else { t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", opponentRollColor))) } } else if i == 8 { if b.playerDice[0] > 0 { t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", playerRollColor, playerBold, b.playerDice[0], b.playerDice[1]))) } 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() }