aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/game.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/game.go')
-rw-r--r--backend/game.go328
1 files changed, 328 insertions, 0 deletions
diff --git a/backend/game.go b/backend/game.go
new file mode 100644
index 0000000..bc2069a
--- /dev/null
+++ b/backend/game.go
@@ -0,0 +1,328 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type GameHub struct {
+ game *Game
+ clients map[*GameClient]bool
+ receive chan *MessageWithClient
+ register chan *GameClient
+ unregister chan *GameClient
+ watchers map[*GameWatcher]bool
+ registerWatcher chan *GameWatcher
+ unregisterWatcher chan *GameWatcher
+ state int
+ finishTime time.Time
+}
+
+func NewGameHub(game *Game) *GameHub {
+ return &GameHub{
+ game: game,
+ clients: make(map[*GameClient]bool),
+ receive: make(chan *MessageWithClient),
+ register: make(chan *GameClient),
+ unregister: make(chan *GameClient),
+ watchers: make(map[*GameWatcher]bool),
+ registerWatcher: make(chan *GameWatcher),
+ unregisterWatcher: make(chan *GameWatcher),
+ state: 0,
+ }
+}
+
+func (h *GameHub) Run() {
+ ticker := time.NewTicker(10 * time.Second)
+ defer func() {
+ ticker.Stop()
+ }()
+
+ for {
+ select {
+ case client := <-h.register:
+ h.clients[client] = true
+ log.Printf("client registered: %d", len(h.clients))
+ case client := <-h.unregister:
+ if _, ok := h.clients[client]; ok {
+ h.closeClient(client)
+ }
+ log.Printf("client unregistered: %d", len(h.clients))
+ if len(h.clients) == 0 {
+ h.Close()
+ return
+ }
+ case watcher := <-h.registerWatcher:
+ h.watchers[watcher] = true
+ log.Printf("watcher registered: %d", len(h.watchers))
+ case watcher := <-h.unregisterWatcher:
+ if _, ok := h.watchers[watcher]; ok {
+ h.closeWatcher(watcher)
+ }
+ log.Printf("watcher unregistered: %d", len(h.watchers))
+ case message := <-h.receive:
+ log.Printf("received message: %s", message.Message.Type)
+ switch message.Message.Type {
+ case "connect":
+ if h.state == 0 {
+ h.state = 1
+ } else if h.state == 1 {
+ h.state = 2
+ for client := range h.clients {
+ client.send <- &Message{Type: "prepare", Data: MessageDataPrepare{Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。"}}
+ }
+ } else {
+ log.Printf("invalid state: %d", h.state)
+ h.closeClient(message.Client)
+ }
+ case "ready":
+ if h.state == 2 {
+ h.state = 3
+ } else if h.state == 3 {
+ h.state = 4
+ for client := range h.clients {
+ client.send <- &Message{Type: "start", Data: MessageDataStart{StartTime: time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339)}}
+ }
+ h.finishTime = time.Now().Add(3 * time.Minute)
+ } else {
+ log.Printf("invalid state: %d", h.state)
+ h.closeClient(message.Client)
+ }
+ case "code":
+ if h.state == 4 {
+ code := message.Message.Data.(MessageDataCode).Code
+ message.Client.code = code
+ message.Client.send <- &Message{Type: "score", Data: MessageDataScore{Score: 100}}
+ if message.Client.score == nil {
+ message.Client.score = new(int)
+ }
+ *message.Client.score = 100
+
+ var scoreA, scoreB *int
+ var codeA, codeB string
+ for client := range h.clients {
+ if client.team == "a" {
+ scoreA = client.score
+ codeA = client.code
+ } else {
+ scoreB = client.score
+ codeB = client.code
+ }
+ }
+ for watcher := range h.watchers {
+ watcher.send <- &Message{
+ Type: "watch",
+ Data: MessageDataWatch{
+ Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。",
+ ScoreA: scoreA,
+ CodeA: codeA,
+ ScoreB: scoreB,
+ CodeB: codeB,
+ },
+ }
+ }
+ } else {
+ log.Printf("invalid state: %d", h.state)
+ h.closeClient(message.Client)
+ }
+ default:
+ log.Printf("unknown message type: %s", message.Message.Type)
+ h.closeClient(message.Client)
+ }
+ case <-ticker.C:
+ log.Printf("state: %d", h.state)
+ if h.state == 4 {
+ if time.Now().After(h.finishTime) {
+ h.state = 5
+ clientAndScores := make(map[*GameClient]*int)
+ for client := range h.clients {
+ clientAndScores[client] = client.score
+ }
+ for client, score := range clientAndScores {
+ var opponentScore *int
+ for c2, s2 := range clientAndScores {
+ if c2 != client {
+ opponentScore = s2
+ break
+ }
+ }
+ client.send <- &Message{Type: "finish", Data: MessageDataFinish{YourScore: score, OpponentScore: opponentScore}}
+ }
+ }
+ }
+ }
+ }
+}
+
+func (h *GameHub) Close() {
+ for client := range h.clients {
+ h.closeClient(client)
+ }
+ close(h.receive)
+ close(h.register)
+ close(h.unregister)
+ for watcher := range h.watchers {
+ h.closeWatcher(watcher)
+ }
+ close(h.registerWatcher)
+ close(h.unregisterWatcher)
+}
+
+func (h *GameHub) closeClient(client *GameClient) {
+ delete(h.clients, client)
+ close(client.send)
+}
+
+func (h *GameHub) closeWatcher(watcher *GameWatcher) {
+ delete(h.watchers, watcher)
+ close(watcher.send)
+}
+
+const (
+ writeWait = 10 * time.Second
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ maxMessageSize = 512
+)
+
+var (
+ newline = []byte{'\n'}
+ space = []byte{' '}
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+type GameClient struct {
+ hub *GameHub
+ conn *websocket.Conn
+ send chan *Message
+ score *int
+ code string
+ team string
+}
+
+type GameWatcher struct {
+ hub *GameHub
+ conn *websocket.Conn
+ send chan *Message
+}
+
+// Receives messages from the client and sends them to the hub.
+func (c *GameClient) readPump() {
+ defer func() {
+ log.Printf("closing client")
+ c.hub.unregister <- c
+ c.conn.Close()
+ }()
+ c.conn.SetReadLimit(maxMessageSize)
+ c.conn.SetReadDeadline(time.Now().Add(pongWait))
+ c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+ for {
+ var message Message
+ err := c.conn.ReadJSON(&message)
+ if err != nil {
+ log.Printf("error: %v", err)
+ return
+ }
+ c.hub.receive <- &MessageWithClient{c, &message}
+ }
+}
+
+// Receives messages from the hub and sends them to the client.
+func (c *GameClient) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer func() {
+ ticker.Stop()
+ c.conn.Close()
+ }()
+ for {
+ select {
+ case message, ok := <-c.send:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if !ok {
+ c.conn.WriteMessage(websocket.CloseMessage, []byte{})
+ return
+ }
+
+ err := c.conn.WriteJSON(message)
+ if err != nil {
+ return
+ }
+ case <-ticker.C:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ return
+ }
+ }
+ }
+}
+
+func serveWs(hub *GameHub, w http.ResponseWriter, r *http.Request, team string) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ client := &GameClient{hub: hub, conn: conn, send: make(chan *Message), team: team}
+ client.hub.register <- client
+
+ go client.writePump()
+ go client.readPump()
+}
+
+func serveWsWatcher(hub *GameHub, w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ watcher := &GameWatcher{hub: hub, conn: conn, send: make(chan *Message)}
+ watcher.hub.registerWatcher <- watcher
+
+ go watcher.writePump()
+ go watcher.readPump()
+}
+
+// Receives messages from the client and sends them to the hub.
+func (c *GameWatcher) readPump() {
+ c.conn.SetReadLimit(maxMessageSize)
+ c.conn.SetReadDeadline(time.Now().Add(pongWait))
+ c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+}
+
+// Receives messages from the hub and sends them to the client.
+func (c *GameWatcher) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer func() {
+ ticker.Stop()
+ c.conn.Close()
+ log.Printf("closing watcher")
+ c.hub.unregisterWatcher <- c
+ }()
+ for {
+ select {
+ case message, ok := <-c.send:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if !ok {
+ c.conn.WriteMessage(websocket.CloseMessage, []byte{})
+ return
+ }
+
+ err := c.conn.WriteJSON(message)
+ if err != nil {
+ return
+ }
+ case <-ticker.C:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ return
+ }
+ }
+ }
+}