aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-07-19 19:05:55 +0900
committernsfisis <nsfisis@gmail.com>2024-07-19 19:19:55 +0900
commitdf5abfc272a151c51f0e5e82214cf7aff8cfa880 (patch)
tree87395be420f16296fab56b55a03f83f87af59366 /backend
parentb0662e8add4864fed69f49a4a5cfb0d8e26523a8 (diff)
downloadphperkaigi-2025-albatross-df5abfc272a151c51f0e5e82214cf7aff8cfa880.tar.gz
phperkaigi-2025-albatross-df5abfc272a151c51f0e5e82214cf7aff8cfa880.tar.zst
phperkaigi-2025-albatross-df5abfc272a151c51f0e5e82214cf7aff8cfa880.zip
initial commit
Diffstat (limited to 'backend')
-rw-r--r--backend/game.go328
-rw-r--r--backend/go.mod9
-rw-r--r--backend/go.sum12
-rw-r--r--backend/main.go259
-rw-r--r--backend/message.go104
5 files changed, 712 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
+ }
+ }
+ }
+}
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 0000000..63e750f
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,9 @@
+module iosdc-code-battle-poc
+
+go 1.22.3
+
+require (
+ github.com/gorilla/websocket v1.5.3
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/lib/pq v1.10.9
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644
index 0000000..b60655d
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,12 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
diff --git a/backend/main.go b/backend/main.go
new file mode 100644
index 0000000..68df25b
--- /dev/null
+++ b/backend/main.go
@@ -0,0 +1,259 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+)
+
+type Config struct {
+ dbHost string
+ dbPort string
+ dbUser string
+ dbPassword string
+ dbName string
+}
+
+var config *Config
+
+var db *sqlx.DB
+
+func loadEnv() (*Config, error) {
+ dbHost, exists := os.LookupEnv("ALBATROSS_DB_HOST")
+ if !exists {
+ return nil, fmt.Errorf("ALBATROSS_DB_HOST not set")
+ }
+ dbPort, exists := os.LookupEnv("ALBATROSS_DB_PORT")
+ if !exists {
+ return nil, fmt.Errorf("ALBATROSS_DB_PORT not set")
+ }
+ dbUser, exists := os.LookupEnv("ALBATROSS_DB_USER")
+ if !exists {
+ return nil, fmt.Errorf("ALBATROSS_DB_USER not set")
+ }
+ dbPassword, exists := os.LookupEnv("ALBATROSS_DB_PASSWORD")
+ if !exists {
+ return nil, fmt.Errorf("ALBATROSS_DB_PASSWORD not set")
+ }
+ dbName, exists := os.LookupEnv("ALBATROSS_DB_NAME")
+ if !exists {
+ return nil, fmt.Errorf("ALBATROSS_DB_NAME not set")
+ }
+ return &Config{
+ dbHost: dbHost,
+ dbPort: dbPort,
+ dbUser: dbUser,
+ dbPassword: dbPassword,
+ dbName: dbName,
+ }, nil
+}
+
+const (
+ gameTypeGolf = "golf"
+ gameTypeRace = "race"
+)
+
+const (
+ gameStateWaiting = "waiting"
+ gameStateReady = "ready"
+ gameStatePlaying = "playing"
+ gameStateFinished = "finished"
+)
+
+type Game struct {
+ GameID int `db:"game_id"`
+ // "golf" or "race"
+ Type string `db:"type"`
+ CreatedAt string `db:"created_at"`
+ State string `db:"state"`
+}
+
+func initDB() error {
+ _, err := db.Exec(`
+ CREATE TABLE IF NOT EXISTS games (
+ game_id SERIAL PRIMARY KEY,
+ type VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ state VARCHAR(255) NOT NULL
+ );
+ `)
+ return err
+}
+
+var gameHubs = map[int]*GameHub{}
+
+func startGame(game *Game) {
+ if gameHubs[game.GameID] != nil {
+ return
+ }
+ gameHubs[game.GameID] = NewGameHub(game)
+ go gameHubs[game.GameID].Run()
+}
+
+func handleGolfPost(w http.ResponseWriter, r *http.Request) {
+ var yourTeam string
+ waitingGolfGames := []Game{}
+ err := db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting)
+ if err != nil {
+ http.Error(w, "Error getting games", http.StatusInternalServerError)
+ return
+ }
+ if len(waitingGolfGames) == 0 {
+ _, err = db.Exec("INSERT INTO games (type, state) VALUES ($1, $2)", gameTypeGolf, gameStateWaiting)
+ if err != nil {
+ http.Error(w, "Error creating game", http.StatusInternalServerError)
+ return
+ }
+ waitingGolfGames = []Game{}
+ err = db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting)
+ if err != nil {
+ http.Error(w, "Error getting games", http.StatusInternalServerError)
+ return
+ }
+ yourTeam = "a"
+ startGame(&waitingGolfGames[0])
+ } else {
+ yourTeam = "b"
+ db.Exec("UPDATE games SET state = $1 WHERE game_id = $2", gameStateReady, waitingGolfGames[0].GameID)
+ }
+ waitingGame := waitingGolfGames[0]
+
+ http.Redirect(w, r, fmt.Sprintf("/golf/%d/%s/", waitingGame.GameID, yourTeam), http.StatusSeeOther)
+}
+
+func handleRacePost(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/race/1/a/", http.StatusSeeOther)
+}
+
+func main() {
+ var err error
+ config, err = loadEnv()
+ if err != nil {
+ fmt.Printf("Error loading env %v", err)
+ return
+ }
+
+ for i := 0; i < 5; i++ {
+ db, err = sqlx.Connect("postgres", fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", config.dbHost, config.dbPort, config.dbUser, config.dbPassword, config.dbName))
+ if err == nil {
+ break
+ }
+ time.Sleep(5 * time.Second)
+ }
+ if err != nil {
+ log.Fatalf("Error connecting to db %v", err)
+ }
+ defer db.Close()
+
+ err = initDB()
+ if err != nil {
+ log.Fatalf("Error initializing db %v", err)
+ }
+
+ server := http.NewServeMux()
+
+ server.HandleFunc("GET /js/", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public"+r.URL.Path)
+ })
+
+ server.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/index.html")
+ })
+
+ server.HandleFunc("GET /golf/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/golf/index.html")
+ })
+
+ server.HandleFunc("POST /golf/{$}", func(w http.ResponseWriter, r *http.Request) {
+ handleGolfPost(w, r)
+ })
+
+ server.HandleFunc("GET /golf/{gameId}/watch/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/golf/watch.html")
+ })
+
+ server.HandleFunc("GET /sock/golf/{gameId}/watch/{$}", func(w http.ResponseWriter, r *http.Request) {
+ gameId := r.PathValue("gameId")
+ gameIdInt, err := strconv.Atoi(gameId)
+ if err != nil {
+ http.Error(w, "Invalid game id", http.StatusBadRequest)
+ return
+ }
+ var hub *GameHub
+ for _, h := range gameHubs {
+ if h.game.GameID == gameIdInt {
+ hub = h
+ break
+ }
+ }
+ if hub == nil {
+ http.Error(w, "Game not found", http.StatusNotFound)
+ return
+ }
+ serveWsWatcher(hub, w, r)
+ })
+
+ server.HandleFunc("GET /golf/{gameId}/{team}/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/golf/game.html")
+ })
+
+ server.HandleFunc("GET /sock/golf/{gameId}/{team}/{$}", func(w http.ResponseWriter, r *http.Request) {
+ gameId := r.PathValue("gameId")
+ gameIdInt, err := strconv.Atoi(gameId)
+ if err != nil {
+ http.Error(w, "Invalid game id", http.StatusBadRequest)
+ return
+ }
+ var hub *GameHub
+ for _, h := range gameHubs {
+ if h.game.GameID == gameIdInt {
+ hub = h
+ break
+ }
+ }
+ if hub == nil {
+ http.Error(w, "Game not found", http.StatusNotFound)
+ return
+ }
+ team := r.PathValue("team")
+ serveWs(hub, w, r, team)
+ })
+
+ server.HandleFunc("GET /race/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/race/index.html")
+ })
+
+ server.HandleFunc("POST /race/{$}", func(w http.ResponseWriter, r *http.Request) {
+ handleRacePost(w, r)
+ })
+
+ server.HandleFunc("GET /race/{gameId}/watch/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/race/watch.html")
+ })
+
+ server.HandleFunc("GET /sock/race/{gameId}/watch/{$}", func(w http.ResponseWriter, r *http.Request) {
+ // TODO
+ })
+
+ server.HandleFunc("GET /race/{gameId}/{team}/{$}", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./public/race/game.html")
+ })
+
+ server.HandleFunc("GET /sock/race/{gameId}/{team}/{$}", func(w http.ResponseWriter, r *http.Request) {
+ // TODO
+ })
+
+ defer func() {
+ for _, hub := range gameHubs {
+ hub.Close()
+ }
+ }()
+
+ http.ListenAndServe(":80", server)
+}
diff --git a/backend/message.go b/backend/message.go
new file mode 100644
index 0000000..f466a8f
--- /dev/null
+++ b/backend/message.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+type MessageWithClient struct {
+ Client *GameClient
+ Message *Message
+}
+
+type Message struct {
+ Type string `json:"type"`
+ Data MessageData `json:"data"`
+}
+
+type MessageData interface{}
+
+type MessageDataConnect struct {
+}
+
+type MessageDataPrepare struct {
+ Problem string `json:"problem"`
+}
+
+type MessageDataReady struct {
+}
+
+type MessageDataStart struct {
+ StartTime string `json:"startTime"`
+}
+
+type MessageDataCode struct {
+ Code string `json:"code"`
+}
+
+type MessageDataScore struct {
+ Score int `json:"score"`
+}
+
+type MessageDataFinish struct {
+ YourScore *int `json:"yourScore"`
+ OpponentScore *int `json:"opponentScore"`
+}
+
+type MessageDataWatch struct {
+ Problem string `json:"problem"`
+ ScoreA *int `json:"scoreA"`
+ CodeA string `json:"codeA"`
+ ScoreB *int `json:"scoreB"`
+ CodeB string `json:"codeB"`
+}
+
+func (m *Message) UnmarshalJSON(data []byte) error {
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(raw["type"], &m.Type); err != nil {
+ return err
+ }
+
+ var err error
+ switch m.Type {
+ case "connect":
+ var data MessageDataConnect
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "prepare":
+ var data MessageDataPrepare
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "ready":
+ var data MessageDataReady
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "start":
+ var data MessageDataStart
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "code":
+ var data MessageDataCode
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "score":
+ var data MessageDataScore
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "finish":
+ var data MessageDataFinish
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ case "watch":
+ var data MessageDataWatch
+ err = json.Unmarshal(raw["data"], &data)
+ m.Data = data
+ default:
+ err = fmt.Errorf("unknown message type: %s", m.Type)
+ }
+
+ return err
+}