package fibs import ( "bytes" "context" "fmt" "io" "log" "math/rand" "regexp" "strconv" "strings" "time" "github.com/reiver/go-oi" "github.com/reiver/go-telnet" "golang.org/x/text/language" "golang.org/x/text/message" "nhooyr.io/websocket" ) const debug = 1 // TODO const whoInfoSize = 12 const ( whoInfoDataName = iota whoInfoDataOpponent whoInfoDataWatching whoInfoDataReady whoInfoDataAway whoInfoDataRating whoInfoDataExperience whoInfoDataIdleTime whoInfoDataLoginTime whoInfoDataHostname whoInfoDataClientName whoInfoDataEmail ) var DefaultProxyAddress = "" var ( TypeWelcome = []byte("1") TypeOwnInfo = []byte("2") TypeMOTD = []byte("3") TypeEndMOTD = []byte("4") TypeWhoInfo = []byte("5") TypeEndWhoInfo = []byte("6") TypeLogin = []byte("7") TypeLogout = []byte("8") TypeMsg = []byte("9") TypeMsgDelivered = []byte("10") TypeMsgSaved = []byte("11") TypeSay = []byte("12") TypeShout = []byte("13") TypeWhisper = []byte("14") TypeKibitz = []byte("15") TypeYouSay = []byte("16") TypeYouShout = []byte("17") TypeYouWhisper = []byte("18") TypeYouKibitz = []byte("19") TypeBoardState = []byte("board:") ) var numberPrinter = message.NewPrinter(language.English) type WhoInfo struct { Username string Opponent string Watching string Ready bool Away bool Rating int Experience int Idle int LoginTime int ClientName string } func (w *WhoInfo) String() string { opponent := "In the lobby" if w.Opponent != "" && w.Opponent != "-" { opponent = "playing against " + w.Opponent } clientName := "" if w.ClientName != "" && w.ClientName != "-" { clientName = " using " + w.ClientName } return fmt.Sprintf("%s (rated %d with %d exp) is %s%s", w.Username, w.Rating, w.Experience, opponent, clientName) } type Client struct { In chan []byte Out chan []byte Event chan interface{} Username string Password string loggedin bool motd []byte rawMode bool who map[string]*WhoInfo notified map[string]bool serverAddress string wsProxyAddress string // WebSocket->TCP proxy address tcpConn io.Writer wsConn *websocket.Conn Board *Board tvMode bool } func NewClient(serverAddress string, username string, password string) *Client { c := &Client{ In: make(chan []byte, 100), Out: make(chan []byte, 100), Event: make(chan interface{}, 100), serverAddress: serverAddress, wsProxyAddress: DefaultProxyAddress, Username: username, Password: password, who: make(map[string]*WhoInfo), notified: make(map[string]bool), } c.Board = NewBoard(c) go c.eventLoop() return c } func (c *Client) handleWrite() { var buffer bytes.Buffer var p []byte var crlfBuffer [2]byte = [2]byte{'\r', '\n'} crlf := crlfBuffer[:] help := []byte("help") who := []byte("who") quit := []byte("quit") bye := []byte("bye") watch := []byte("watch") tv := []byte("tv") reset := []byte("reset") about := []byte("about") show := []byte("show") average := []byte("average") dicetest := []byte("dicetest") boardstate := []byte("boardstate") stat := []byte("stat") for b := range c.Out { if bytes.Equal(bytes.ToLower(b), watch) { c.WatchRandomGame() continue } else if bytes.Equal(bytes.ToLower(b), tv) { c.tvMode = !c.tvMode if c.tvMode { l("Now watching backgammon TV") c.WatchRandomGame() } else { l("Stopped watching backgammon TV") } continue } else if bytes.Equal(bytes.ToLower(b), reset) { c.Board.ResetPreMoves() c.Event <- &EventDraw{} l("Reset pre-moves") continue } else if bytes.Equal(bytes.ToLower(b), who) { for username := range c.who { lf("%s", c.who[username]) } continue } else if bytes.HasPrefix(bytes.ToLower(b), help) || bytes.HasPrefix(bytes.ToLower(b), about) || bytes.HasPrefix(bytes.ToLower(b), average) || bytes.HasPrefix(bytes.ToLower(b), dicetest) || bytes.HasPrefix(bytes.ToLower(b), show) || bytes.HasPrefix(bytes.ToLower(b), stat) { c.rawMode = true go func() { time.Sleep(time.Second) c.rawMode = false }() } else if bytes.Equal(bytes.ToLower(b), boardstate) { homeBoardStart, homeBoardEnd := c.Board.homeBoardSpaces() lf("Board state: %s", c.Board.GetState()) lf("Player color: %d", c.Board.v[StatePlayerColor]) lf("Direction: %d", c.Board.v[StateDirection]) lf("Current turn: %d", c.Board.v[StateTurn]) lf("Player home spaces: %d-%d", homeBoardStart, homeBoardEnd) lf("Player can bear off: %t", c.Board.allPlayerPiecesInHomeBoard()) lf("Moves: %+v", c.Board.moves) lf("Pre-moves: %+v", c.Board.premove) continue } else if bytes.Equal(bytes.ToLower(b), quit) || bytes.Equal(bytes.ToLower(b), bye) { // TODO match command, not exact string c.rawMode = true } if debug > 0 { l("-> " + string(bytes.TrimSpace(b))) } buffer.Write(b) buffer.Write(crlf) p = buffer.Bytes() if c.wsProxyAddress != "" { err := c.wsConn.Write(context.Background(), websocket.MessageBinary, p) if err != nil { log.Fatalf("Transmission problem: %s", err) } buffer.Reset() continue } n, err := oi.LongWrite(c.tcpConn, p) if nil != err { break } if expected, actual := int64(len(p)), n; expected != actual { log.Fatalf("Transmission problem: tried sending %d bytes, but actually only sent %d bytes.", expected, actual) } buffer.Reset() } } func (c *Client) handleRead(r io.Reader) { var b = &bytes.Buffer{} var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up. p := buffer[:] var motd bool // Parse all messages as MOTD text until MsgEndMOTD for { // Read 1 byte. n, err := r.Read(p) if n <= 0 && nil == err { continue } else if n <= 0 && nil != err { if err.Error() != "EOF" { lf("** Disconnected: %s", err) } else { l("** Disconnected") } return } b.WriteByte(p[0]) if p[0] == '\n' { buf := make([]byte, b.Len()) copy(buf, b.Bytes()) if debug > 0 { l("<- " + string(bytes.TrimSpace(buf))) } if c.loggedin { if !motd { buf = bytes.TrimSpace(buf) if len(buf) == 0 { b.Reset() continue } } if c.rawMode { c.In <- append([]byte("** "), buf...) b.Reset() continue } if bytes.HasPrefix(b.Bytes(), TypeMOTD) && !motd { motd = true c.motd = append(c.motd, buf[1:]...) b.Reset() continue } else if bytes.HasPrefix(b.Bytes(), TypeEndMOTD) && motd { motd = false c.motd = bytes.TrimSpace(c.motd) c.In <- append([]byte("3 "), c.motd...) b.Reset() continue } else if motd { c.motd = append(c.motd, buf...) b.Reset() continue } c.In <- buf } b.Reset() } if !c.loggedin { bt := strings.TrimSpace(b.String()) if bt == "login:" { b.Reset() c.Out <- []byte(fmt.Sprintf("login bgammon 1008 %s %s", c.Username, c.Password)) c.loggedin = true } } } } // CallTELNET is called when a connection is made with the server. func (c *Client) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) { c.tcpConn = w go c.handleRead(r) c.handleWrite() } func (c *Client) keepAlive() { t := time.NewTicker(5 * time.Minute) for range t.C { c.Out <- []byte("set boardstyle 3") } } func (c *Client) updateWhoInfo(b []byte) { s := bytes.Split(b, []byte(" ")) if len(s) != whoInfoSize { return } info := &WhoInfo{ Username: string(s[whoInfoDataName]), } r := s[whoInfoDataRating] if bytes.ContainsRune(r, '.') { r = s[whoInfoDataRating][:bytes.IndexByte(s[whoInfoDataRating], '.')] } rating, err := strconv.Atoi(string(r)) if err != nil { rating = 0 } info.Rating = rating experience, err := strconv.Atoi(string(s[whoInfoDataExperience])) if err != nil { experience = 0 } info.Experience = experience opponent := "" if len(s[whoInfoDataOpponent]) > 1 || s[whoInfoDataOpponent][0] != '-' { opponent = string(s[whoInfoDataOpponent]) } info.Opponent = opponent watching := "" if len(s[whoInfoDataWatching]) > 1 || s[whoInfoDataWatching][0] != '-' { watching = string(s[whoInfoDataWatching]) } info.Watching = watching ready := false if string(s[whoInfoDataReady]) == "1" { ready = true } info.Ready = ready clientName := "" if len(s[whoInfoDataClientName]) > 1 || s[whoInfoDataClientName][0] != '-' { clientName = string(s[whoInfoDataClientName]) } info.ClientName = clientName status := "Unavailable" if info.Opponent != "" { status = "vs. " + info.Opponent } else if info.Ready { status = "Available" } itemText := info.Username + strings.Repeat(" ", 18-len(info.Username)) ratingString := numberPrinter.Sprintf("%d", info.Rating) itemText += ratingString + strings.Repeat(" ", 8-len(ratingString)) experienceString := numberPrinter.Sprintf("%d", info.Experience) itemText += experienceString + strings.Repeat(" ", 12-len(experienceString)) itemText += status c.who[string(s[whoInfoDataName])] = info // TODO who info event } func (c *Client) GetAllWhoInfo() []*WhoInfo { w := make([]*WhoInfo, len(c.who)) var i int for _, whoInfo := range c.who { w[i] = whoInfo i++ } return w } func (c *Client) LoggedIn() bool { // TODO lock return c.loggedin } func (c *Client) WatchRandomGame() { var options []string for username, whoInfo := range c.who { if username != "" && whoInfo.Opponent != "" && !strings.Contains(username, "Bot") && !strings.Contains(whoInfo.Opponent, "Bot") { options = append(options, username, whoInfo.Opponent) } } if len(options) == 0 { for username, whoInfo := range c.who { if username != "" && whoInfo.Opponent != "" { options = append(options, username, whoInfo.Opponent) } } if len(options) == 0 { return } } option := options[rand.Intn(len(options))] c.Out <- []byte("watch " + option) } func (c *Client) callWebSocket() { ctx := context.Background() var err error c.wsConn, _, err = websocket.Dial(ctx, c.wsProxyAddress, nil) if err != nil { log.Fatal("dial error", err) } defer c.wsConn.Close(websocket.StatusInternalError, "the sky is falling") //c.wsConn.Close(websocket.StatusNormalClosure, "") r, w := io.Pipe() go c.handleRead(r) go func() { for { _, data, err := c.wsConn.Read(context.Background()) if err != nil { if err.Error() != "EOF" { lf("** Disconnected: %s", err) } else { l("** Disconnected") } } w.Write(data) } }() c.handleWrite() /* TODO err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return nil } select { case <-done: case <-time.After(time.Second): }*/ } func (c *Client) Connect() error { connectionType := "Telnet" if c.wsProxyAddress != "" { connectionType = fmt.Sprintf("WebSocket proxy (%s)", c.wsProxyAddress) } l(fmt.Sprintf("Connecting via %s to %s...", connectionType, c.serverAddress)) go c.keepAlive() if c.wsProxyAddress != "" { go c.callWebSocket() return nil } err := telnet.DialToAndCall(c.serverAddress, c) if err != nil { lf("** Disconnected: %s", err) } return err } func (c *Client) eventLoop() { var setBoardStyle bool var turnRegexp = regexp.MustCompile(`^turn: (\w+)\.`) var movesRegexp = regexp.MustCompile(`^(\w+) moves (.*)`) var rollsRegexp = regexp.MustCompile(`^(\w+) rolls? (.*)`) var logInOutRegexp = regexp.MustCompile(`^\w+ logs [In|Out]\.`) var dropsConnection = regexp.MustCompile(`^\w+ drops connection\.`) var startMatchRegexp = regexp.MustCompile(`^\w+ and \w+ start a .*`) var winsMatchRegexp = regexp.MustCompile(`^\w+ wins a [0-9]+ point match against .*`) var winsThisMatchRegexp = regexp.MustCompile(`^\w+ wins the [0-9]+ point match .*`) var newGameRegexp = regexp.MustCompile(`^Starting a new game with .*`) var gameBufferRegexp = regexp.MustCompile(`^\w+ (makes|roll|rolls|rolled|move|moves) .*`) var pleaseMoveRegexp = regexp.MustCompile(`^Please move ([0-9]) pieces?.`) var cantMoveRegexp = regexp.MustCompile(`^(\w+) can't move.`) var doublesRegexp = regexp.MustCompile(`^\w+ doubles.`) var acceptDoubleRegexp = regexp.MustCompile(`^\w+ accepts the double.`) for b := range c.In { b = bytes.Replace(b, []byte{7}, []byte{}, -1) b = bytes.TrimSpace(b) bl := bytes.ToLower(b) // Select 10+ first to read prefixes correctly if bytes.HasPrefix(b, TypeMsgSaved) { lf("Message to %s saved", b[3:]) continue } else if bytes.HasPrefix(b, TypeMsgDelivered) { lf("Message to %s delivered", b[3:]) continue } else if bytes.HasPrefix(b, TypeSay) { s := bytes.SplitN(b[3:], []byte(" "), 2) lf("%s says: %s", s[0], s[1]) continue } else if bytes.HasPrefix(b, TypeShout) { s := bytes.SplitN(b[3:], []byte(" "), 2) lf("%s shouts: %s", s[0], s[1]) continue } else if bytes.HasPrefix(b, TypeWhisper) { s := bytes.SplitN(b[3:], []byte(" "), 2) lf("%s whispers: %s", s[0], s[1]) continue } else if bytes.HasPrefix(b, TypeKibitz) { s := bytes.SplitN(b[3:], []byte(" "), 2) lf("%s kibitzes: %s", s[0], s[1]) continue } else if bytes.HasPrefix(b, TypeYouSay) { s := bytes.SplitN(b[3:], []byte(" "), 2) lf("You say to %s: %s", s[0], s[1]) continue } else if bytes.HasPrefix(b, TypeYouShout) { lf("You shout: %s", b[3:]) continue } else if bytes.HasPrefix(b, TypeYouWhisper) { lf("You whisper: %s", b[3:]) continue } else if bytes.HasPrefix(b, TypeYouKibitz) { lf("You kibitz: %s", b[3:]) continue } else if bytes.HasPrefix(b, TypeWelcome) { s := bytes.Split(b[2:], []byte(" ")) loginTimestamp, err := strconv.Atoi(string(s[1])) if err != nil { loginTimestamp = 0 } loginTime := time.Unix(int64(loginTimestamp), 0) lf("Welcome, %s! Last login at %s", s[0], loginTime) continue } else if bytes.HasPrefix(b, TypeOwnInfo) { // TODO Print own info continue } else if bytes.HasPrefix(b, TypeMOTD) { for _, line := range bytes.Split(c.motd, []byte("\n")) { l(string(line)) } if !setBoardStyle { c.Out <- []byte("set boardstyle 3") setBoardStyle = true } continue } else if bytes.HasPrefix(b, TypeWhoInfo) { c.updateWhoInfo(b[2:]) // TODO who window continue } else if bytes.HasPrefix(b, TypeEndWhoInfo) { // TODO draw who info event continue } else if bytes.HasPrefix(b, TypeLogin) || bytes.HasPrefix(b, TypeLogout) { b = b[2:] b = b[bytes.IndexByte(b, ' ')+1:] l(string(b)) // TODO enable showing log In and Out messages // TODO remove from who continue } else if bytes.HasPrefix(b, TypeMsg) { lf("Received message: %s", b[3:]) continue } else if bytes.HasPrefix(b, TypeBoardState) { c.Board.SetState(string(bytes.SplitN(b, []byte(" "), 2)[0][6:])) c.Event <- &EventBoardState{S: c.Board.s, V: c.Board.v} continue } else if turnRegexp.Match(b) { turn := turnRegexp.FindSubmatch(b) if string(turn[1]) == c.Username || string(turn[1]) == c.Board.s[0] || string(turn[1]) == "You" { c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] } else { c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1 } } else if rollsRegexp.Match(b) { roll := rollsRegexp.FindSubmatch(b) periodIndex := bytes.IndexRune(roll[2], '.') if periodIndex > -1 { roll[2] = roll[2][:periodIndex] } s := bytes.Split(roll[2], []byte(" ")) var dice [2]int var i int for _, m := range s { v, err := strconv.Atoi(string(m)) if err == nil { dice[i] = v i++ } } if string(roll[1]) == c.Board.s[0] || string(roll[1]) == "You" { c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] c.Board.v[StatePlayerDice1] = dice[0] c.Board.v[StatePlayerDice2] = dice[1] } else { c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1 c.Board.v[StateOpponentDice1] = dice[0] c.Board.v[StateOpponentDice2] = dice[1] } c.Event <- &EventBoardState{S: c.Board.s, V: c.Board.v} c.Board.ResetMoves() c.Board.Draw() } else if movesRegexp.Match(b) { match := movesRegexp.FindSubmatch(b) player := c.Board.v[StatePlayerColor] if string(match[1]) == c.Board.s[1] { player *= -1 } c.Board.ResetMoves() from := -1 to := -1 s := bytes.Split(match[2], []byte(" ")) for _, m := range s { move := bytes.Split(m, []byte("-")) if len(move) == 2 { from = c.Board.parseMoveString(player, string(move[0])) to = c.Board.parseMoveString(player, string(move[1])) if from >= 0 && to >= 0 { c.Event <- &EventMove{ Player: player, From: from, To: to, } } c.Board.Move(player, string(move[0]), string(move[1])) } } c.Board.SimplifyMoves() c.Board.Draw() bs := string(b) if strings.HasSuffix(bs, " .") { bs = bs[:len(bs)-2] + "." } lg(bs) continue } else if string(b) == "Value of 'boardstyle' set to 3." { continue } else if string(b) == "It's your turn to move." || strings.TrimSpace(string(b)) == "It's your turn to roll or double." || strings.TrimSpace(string(b)) == "It's your turn. Please roll or double" { c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] c.Board.Draw() if strings.TrimSpace(string(b)) == "It's your turn to roll or double." || strings.TrimSpace(string(b)) == "It's your turn. Please roll or double" { c.Out <- []byte("roll") // TODO Delay and allow configuring } } else if cantMoveRegexp.Match(b) { match := cantMoveRegexp.FindSubmatch(b) u := string(match[1]) if u == c.Username || u == c.Board.s[0] || u == "You" { //c.Board.opponentDice[0] = 0 //c.Board.opponentDice[1] = 0 c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1 } else if u == c.Board.s[1] { //c.Board.playerDice[0] = 0 //c.Board.playerDice[1] = 0 c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] } } else if pleaseMoveRegexp.Match(b) { match := pleaseMoveRegexp.FindSubmatch(b) n, err := strconv.Atoi(string(match[1])) if err == nil { c.Board.v[StateMovablePieces] = n } } else if bytes.HasPrefix(bl, []byte("you're now watching")) { // Board state is not sent automatically when watching c.Out <- []byte("board") } else if logInOutRegexp.Match(b) { continue } else if dropsConnection.Match(b) { continue } else if startMatchRegexp.Match(b) { continue } else if winsThisMatchRegexp.Match(b) { if c.tvMode { go func() { time.Sleep(5 * time.Second) if !c.tvMode { return } c.WatchRandomGame() }() } continue } else if winsMatchRegexp.Match(b) { continue } else if newGameRegexp.Match(b) { c.Board.ResetMoves() c.Board.resetSelection() // TODO reset premove continue } if gameBufferRegexp.Match(b) || cantMoveRegexp.Match(b) || doublesRegexp.Match(b) || acceptDoubleRegexp.Match(b) || bytes.HasPrefix(bl, []byte("you're now watching")) || bytes.HasPrefix(bl, []byte("** you stop watching")) || strings.HasPrefix(string(b), "Bearing off:") || strings.HasPrefix(string(b), "The only possible move") { lg(string(b)) if !bytes.HasPrefix(bl, []byte("you're now watching")) && !bytes.HasPrefix(bl, []byte("** you stop watching")) { continue } } l(string(b)) } }