aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fl.fischer@fau.de>2017-02-16 17:50:24 +0100
committerFlorian Fischer <florian.fl.fischer@fau.de>2017-02-16 17:50:24 +0100
commitccdc8abe087b5b1a7dcd1c3fb1fec2203f36274c (patch)
tree255f3015b0e1cef085f1649944e4671ae19aaf90
parente945d998f547900461e9fa313d18ec0d29c5d2c4 (diff)
downloadgoffel-ccdc8abe087b5b1a7dcd1c3fb1fec2203f36274c.tar.gz
goffel-ccdc8abe087b5b1a7dcd1c3fb1fec2203f36274c.zip
server: add sever implementation
-rw-r--r--uis/server.go481
1 files changed, 477 insertions, 4 deletions
diff --git a/uis/server.go b/uis/server.go
index daafef9..321b507 100644
--- a/uis/server.go
+++ b/uis/server.go
@@ -3,15 +3,488 @@
package uis
+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 player struct {
+ name string
+ score logic.Score
+ client *client
+}
+
+func (p player) String() string {
+ return p.name
+}
+
+type client struct {
+ sock net.Conn
+ reader *bufio.Reader
+ writer *bufio.Writer
+ encoder *json.Encoder
+ decoder *json.Decoder
+ encoding string
+ players []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) getBest() ([]player, int) {
+ best := []player{}
+ max := 0
+ for _, p := range c.players {
+ t := p.score.Score()
+ if t > max {
+ best = []player{p}
+ max = t
+ } else if t == max {
+ best = append(best, p)
+ }
+ }
+ return best, max
+}
+
+func (c *client) round(round int, wg *sync.WaitGroup) {
+ dice := logic.Dices{}
+ 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
+ }
+ winners := []player{}
+ max := 0
+ for _, c := range s.clients {
+ best, m := c.getBest()
+ if m > max {
+ winners = best
+ max = m
+ } else if m == max {
+ winners = append(winners, best...)
+ }
+ }
+ msg = sMsg{97, []interface{}{max, winners}}
+ for _, c := range s.clients {
+ c.send(msg)
+ }
+ s.terminate(reason)
+}
+
type Server struct {
- Port string
+ port string
greeting string
+ sessions map[string]*session
}
func NewServer(port string, greeting string) Server {
- return Server{port, greeting}
+ return Server{port, greeting, map[string]*session{}}
}
-func (s *Server) Serve() {
- println("Not Implemented")
+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, player{name, logic.NewScore(), &c})
+ }
+ 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(&reg)
+ 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
}