// Copyright (c) 2016 Florian Fischer. All rights reserved. // Use of this source code is governed by a MIT license found in the LICENSE file. package net import ( "bufio" "encoding/json" "errors" "fmt" "net" "strconv" "strings" "sync" "muhq.space/go/muhq/goffel/logic" ) var ( // 0-9 Misc // 10-19 Sucess // 20-29 IO Errors // 30 Command Errors // 90-99 Session Responses responsePlain = map[int]string{ 0: "Success: ", 1: "Welcome ", 2: "Sessions:", 11: "Help:\r\n", 12: "Score:\r\n", 13: "Dice: ", 14: "Insert: ", 15: "Cancel: ", 16: "Reroll: ", 20: "I/O Error: ", 21: "Json Decoding Error: ", 30: "Invalid Command: ", 31: "Insert Error: ", 32: "Cancel Error: ", 33: "Reroll Error: ", 90: "Connected To: ", 91: "To Many Players: ", 92: "New Players: ", 93: "Starting Game: ", 94: "Round: ", 95: "", // Used to forward results of other players 96: "Remove Client: ", 97: "Winners: ", } ) // Internal structure representing a server response type sMsg struct { Response int Payload interface{} } func (msg sMsg) String() string { response := responsePlain[msg.Response] payload := msg.Payload res := "" if msg.Response == 2 { sessionsptr, _ := msg.Payload.(*map[string]*session) sessions := *sessionsptr s := "" for name, session := range sessions { s += fmt.Sprintf("\r\n%s: %v", name, session) } payload = s } if msg.Response == 11 { a, _ := msg.Payload.([]string) payload = strings.Join(a, "\r\n") } if msg.Response == 95 { a, _ := msg.Payload.([]interface{}) res, _ = a[0].(string) msg, _ = a[1].(sMsg) res += " " + responsePlain[msg.Response] } if msg.Response == 14 { a, _ := msg.Payload.([]interface{}) c, _ := a[0].(logic.Cmd) payload = fmt.Sprintf("%d -> %v", a[1], logic.ScoreNames[c.Argv[0]]) } else if msg.Response == 15 { a, _ := msg.Payload.([]interface{}) c, _ := a[0].(logic.Cmd) payload = fmt.Sprintf("%v", logic.ScoreNames[c.Argv[0]]) } return fmt.Sprintf("%s%s%v", response, res, payload) } type client struct { sock net.Conn reader *bufio.Reader writer *bufio.Writer encoder *json.Encoder decoder *json.Decoder encoding string players []logic.Player results chan sMsg } func (c *client) send(msg sMsg) error { var err error if c.encoding == "plain" { _, err = c.writer.WriteString(fmt.Sprintf("%v\r\n", msg)) if err == nil { err = c.writer.Flush() } } else { err = c.encoder.Encode(msg) } return err } func (c *client) fail(res int, pl interface{}) { c.send(sMsg{res, pl}) c.sock.Close() } func (c *client) recvCmd() (logic.Cmd, error) { var err error var cmd logic.Cmd if c.encoding == "plain" { input := "" input, err = c.reader.ReadString('\n') if err != nil { return cmd, err } cmd, err = logic.ParseCmd(input) } else { err = c.decoder.Decode(&cmd) } return cmd, err } func (c *client) round(round int, wg *sync.WaitGroup) { dice := logic.Dice{} var msg sMsg for _, p := range c.players { fin := false dice.Roll(nil) cmd := logic.Cmd{} var err error rerolls := 0 err = c.send(sMsg{94, []interface{}{round, p.Name, dice}}) if err != nil { fin = true } for !fin { // clear message msg = sMsg{} cmd, err = c.recvCmd() if err != nil { msg.Response = 30 msg.Payload = err } switch cmd.Cmd { case "h": msg.Response = 11 msg.Payload = logic.CmdHelp(cmd.Argv[0]) case "p": msg.Response = 12 msg.Payload = p.Score case "d": msg.Response = 13 msg.Payload = dice case "q": //client quits err = errors.New("Quit") c.sock.Close() case "i": i, err := p.Score.Insert(dice, cmd.Argv[0]) if err != nil { msg.Response = 31 msg.Payload = err } else { msg.Response = 14 msg.Payload = []interface{}{cmd, i, p.Score} fin = true } case "c": err := p.Score.Cancel(cmd.Argv[0]) if err != nil { msg.Response = 32 msg.Payload = err } else { msg.Response = 15 msg.Payload = []interface{}{cmd, p.Score} fin = true } case "r": var err error if rerolls < 2 { err = dice.Roll(cmd.Argv) } else { err = errors.New("Only two rerolls are allowed.") } if err != nil { msg.Response = 33 msg.Payload = err } else { msg.Response = 16 msg.Payload = dice rerolls++ } default: } if err = c.send(msg); err != nil { fin = true } } if err != nil { msg = sMsg{96, err} } else { msg = sMsg{95, []interface{}{p.Name, msg}} } c.results <- msg } wg.Done() } type session struct { clients []*client maxPlayers int curPlayers int status string } func (s session) String() (ret string) { ret += s.status ret += " max:" ret += strconv.Itoa(s.maxPlayers) ret += " p:" names := make([]string, 0, s.maxPlayers) for _, c := range s.clients { for _, p := range c.players { names = append(names, p.Name) } } return fmt.Sprintf("%s%v", ret, names) } func (s *session) terminate(reason sMsg) { for _, c := range s.clients { noreason := sMsg{} if reason != noreason { c.send(reason) } c.sock.Close() } s.status = "TERMINATED" } func (s *session) addClient(c *client) { newNames := make([]string, 0, len(c.players)) for _, n := range c.players { newNames = append(newNames, n.Name) } okClients := make([]*client, 0, len(c.players)) for _, other := range s.clients { err := other.send(sMsg{92, newNames}) if err == nil { okClients = append(okClients, other) } else { s.curPlayers-- } } s.clients = okClients s.clients = append(s.clients, c) s.curPlayers += len(c.players) if s.curPlayers == s.maxPlayers { go s.play() } } func (s *session) play() { s.status = "PLAYING" msg := sMsg{93, ""} wg := sync.WaitGroup{} for _, c := range s.clients { c.send(msg) } var reason sMsg for round := 1; round < 14; round++ { wg.Add(len(s.clients)) for _, c := range s.clients { go c.round(round, &wg) } // broadcast results wg.Wait() badClients := make(map[*client]bool) for i, c := range s.clients { for _, p := range c.players { res := <-c.results // collect broken clients if res.Response == 96 { badClients[c] = true res = sMsg{96, []interface{}{p.Name, res.Payload}} } for j, c2 := range s.clients { if i != j { // ignore error c2.send(res) } } } } // remove broken clients okClients := make([]*client, 0, len(s.clients)-len(badClients)) for _, c := range s.clients { if _, bad := badClients[c]; bad == false { okClients = append(okClients, c) } } s.clients = okClients } best := make([]logic.Player, 0, len(s.clients)) for _, c := range s.clients { b, _ := logic.FindBest(c.players) best = append(best, b...) } winners, max := logic.FindBest(best) msg = sMsg{97, []interface{}{max, winners}} for _, c := range s.clients { c.send(msg) } s.terminate(reason) } type Server struct { port string greeting string sessions map[string]*session } func NewServer(port string, greeting string) Server { return Server{port, greeting, map[string]*session{}} } func (s *Server) Serve() error { ln, err := net.Listen("tcp", ":"+s.port) if err != nil { return err } sessionLock := sync.Mutex{} for { sock, err := ln.Accept() if err != nil { return err } go func() { reader := bufio.NewReader(sock) input, err := reader.ReadString('\n') if err != nil { sock.Close() return } c := client{} c.sock = sock // is HELO json encoded ? var helo struct { Cmd string Players []string } err = json.Unmarshal([]byte(input), &helo) if err == nil { c.encoder = json.NewEncoder(sock) c.decoder = json.NewDecoder(sock) c.encoding = "json" if helo.Cmd != "HELO" { c.fail(30, "HELO expected") return } } else { c.reader = reader c.writer = bufio.NewWriter(sock) c.encoding = "plain" input = strings.TrimRight(input, " \r\n") line := strings.Split(input, " ") if line[0] != "HELO" { c.fail(30, "HELO expected") return } else if len(line) < 2 { c.fail(30, "player name expected") return } helo.Players = line[1:] } for _, name := range helo.Players { c.players = append(c.players, logic.NewPlayer(name)) } c.results = make(chan sMsg, len(c.players)) c.send(sMsg{1, s.greeting}) //names := []string{} //for name := range s.session { //names = append(names, name) //} //c.send(2, names) sessionLock.Lock() c.send(sMsg{2, &s.sessions}) sessionLock.Unlock() var reg struct { Name string MaxPlayers int } if c.encoding == "json" { err = c.decoder.Decode(®) if err != nil { c.fail(21, err) return } } else { input, err = c.reader.ReadString('\n') if err != nil { c.fail(20, err) return } input = strings.TrimRight(input, " \r\n") line := strings.Split(input, " ") reg.Name = line[0] if len(line) > 1 { reg.MaxPlayers, err = strconv.Atoi(line[1]) if err != nil { c.fail(30, err) return } } } if reg.Name == "" { c.fail(30, "Session name expected") return } if reg.MaxPlayers == 0 { reg.MaxPlayers = 2 } sessionLock.Lock() defer sessionLock.Unlock() ses, exists := s.sessions[reg.Name] if !exists || ses.status == "TERMINATED" { ses = &session{[]*client{}, reg.MaxPlayers, 0, "OPEN"} s.sessions[reg.Name] = ses } if ses.curPlayers+len(c.players) > ses.maxPlayers { c.fail(91, ses.maxPlayers-ses.curPlayers) return } ses.addClient(&c) }() } return nil }