aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/game
diff options
context:
space:
mode:
Diffstat (limited to 'backend/game')
-rw-r--r--backend/game/game.go385
-rw-r--r--backend/game/http.go56
-rw-r--r--backend/game/message.go104
-rw-r--r--backend/game/ws.go46
4 files changed, 591 insertions, 0 deletions
diff --git a/backend/game/game.go b/backend/game/game.go
new file mode 100644
index 0000000..9e63a1e
--- /dev/null
+++ b/backend/game/game.go
@@ -0,0 +1,385 @@
+package game
+
+import (
+ "context"
+ "log"
+ "time"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/nsfisis/iosdc-2024-albatross/backend/api"
+ "github.com/nsfisis/iosdc-2024-albatross/backend/db"
+)
+
+type gameState = api.GameState
+
+const (
+ gameStateClosed gameState = api.Closed
+ gameStateWaitingEntries gameState = api.WaitingEntries
+ gameStateWaitingStart gameState = api.WaitingStart
+ gameStatePrepare gameState = api.Prepare
+ gameStateStarting gameState = api.Starting
+ gameStateGaming gameState = api.Gaming
+ gameStateFinished gameState = api.Finished
+)
+
+type game struct {
+ gameID int
+ state string
+ displayName string
+ durationSeconds int
+ startedAt *time.Time
+ problem *problem
+}
+
+type problem struct {
+ problemID int
+ title string
+ description string
+}
+
+// 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)
+}
+*/
+
+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)
+}
+
+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
+ }
+ }
+ }
+}
+
+// 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
+ }
+ }
+ }
+}
+
+type GameHubs struct {
+ hubs map[int]*GameHub
+}
+
+func NewGameHubs() *GameHubs {
+ return &GameHubs{
+ hubs: make(map[int]*GameHub),
+ }
+}
+
+func (hubs *GameHubs) Close() {
+ for _, hub := range hubs.hubs {
+ hub.Close()
+ }
+}
+
+func (hubs *GameHubs) RestoreFromDB(ctx context.Context, q *db.Queries) error {
+ games, err := q.ListGames(ctx)
+ if err != nil {
+ return err
+ }
+ _ = games
+ return nil
+}
+
+func (hubs *GameHubs) SockHandler() *sockHandler {
+ return newSockHandler(hubs)
+}
diff --git a/backend/game/http.go b/backend/game/http.go
new file mode 100644
index 0000000..a5a7ded
--- /dev/null
+++ b/backend/game/http.go
@@ -0,0 +1,56 @@
+package game
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/labstack/echo/v4"
+)
+
+type sockHandler struct {
+ hubs *GameHubs
+}
+
+func newSockHandler(hubs *GameHubs) *sockHandler {
+ return &sockHandler{
+ hubs: hubs,
+ }
+}
+
+func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error {
+ gameId := c.Param("gameId")
+ gameIdInt, err := strconv.Atoi(gameId)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id")
+ }
+ var foundHub *GameHub
+ for _, hub := range h.hubs.hubs {
+ if hub.game.gameID == gameIdInt {
+ foundHub = hub
+ break
+ }
+ }
+ if foundHub == nil {
+ return echo.NewHTTPError(http.StatusNotFound, "Game not found")
+ }
+ return servePlayerWs(foundHub, c.Response(), c.Request(), "a")
+}
+
+func (h *sockHandler) HandleSockGolfWatch(c echo.Context) error {
+ gameId := c.Param("gameId")
+ gameIdInt, err := strconv.Atoi(gameId)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id")
+ }
+ var foundHub *GameHub
+ for _, hub := range h.hubs.hubs {
+ if hub.game.gameID == gameIdInt {
+ foundHub = hub
+ break
+ }
+ }
+ if foundHub == nil {
+ return echo.NewHTTPError(http.StatusNotFound, "Game not found")
+ }
+ return serveWatcherWs(foundHub, c.Response(), c.Request())
+}
diff --git a/backend/game/message.go b/backend/game/message.go
new file mode 100644
index 0000000..7d1a166
--- /dev/null
+++ b/backend/game/message.go
@@ -0,0 +1,104 @@
+package game
+
+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
+}
diff --git a/backend/game/ws.go b/backend/game/ws.go
new file mode 100644
index 0000000..2ed17af
--- /dev/null
+++ b/backend/game/ws.go
@@ -0,0 +1,46 @@
+package game
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+const (
+ writeWait = 10 * time.Second
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ maxMessageSize = 512
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+func servePlayerWs(hub *GameHub, w http.ResponseWriter, r *http.Request, team string) error {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return err
+ }
+ client := &GameClient{hub: hub, conn: conn, send: make(chan *Message), team: team}
+ client.hub.register <- client
+
+ go client.writePump()
+ go client.readPump()
+ return nil
+}
+
+func serveWatcherWs(hub *GameHub, w http.ResponseWriter, r *http.Request) error {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return err
+ }
+ watcher := &GameWatcher{hub: hub, conn: conn, send: make(chan *Message)}
+ watcher.hub.registerWatcher <- watcher
+
+ go watcher.writePump()
+ go watcher.readPump()
+ return nil
+}