aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/game
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-03-04 22:55:01 +0900
committernsfisis <nsfisis@gmail.com>2025-03-08 10:12:44 +0900
commit1e6df136d8202c8adf65948527f4c3e7583b338c (patch)
tree7c82476f6bbbc71d72ab7e71e39559eca197fd95 /backend/game
parent54316868c3bec1ff9b04643dfe6c13cf56bf3246 (diff)
downloadphperkaigi-2025-albatross-1e6df136d8202c8adf65948527f4c3e7583b338c.tar.gz
phperkaigi-2025-albatross-1e6df136d8202c8adf65948527f4c3e7583b338c.tar.zst
phperkaigi-2025-albatross-1e6df136d8202c8adf65948527f4c3e7583b338c.zip
websocket to polling
Diffstat (limited to 'backend/game')
-rw-r--r--backend/game/client.go130
-rw-r--r--backend/game/http.go65
-rw-r--r--backend/game/hub.go679
-rw-r--r--backend/game/message.go85
-rw-r--r--backend/game/models.go38
-rw-r--r--backend/game/ws.go73
6 files changed, 77 insertions, 993 deletions
diff --git a/backend/game/client.go b/backend/game/client.go
deleted file mode 100644
index 5bcb98f..0000000
--- a/backend/game/client.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package game
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "time"
-
- "github.com/gorilla/websocket"
-)
-
-type playerClient struct {
- hub *gameHub
- conn *websocket.Conn
- s2cMessages chan playerMessageS2C
- playerID int
-}
-
-// Receives messages from the client and sends them to the hub.
-func (c *playerClient) readPump() error {
- defer func() {
- log.Printf("closing player client")
- c.hub.unregisterPlayer <- c
- c.conn.Close()
- }()
- c.conn.SetReadLimit(maxMessageSize)
- if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
- return err
- }
- c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) })
- for {
- var rawMessage map[string]json.RawMessage
- if err := c.conn.ReadJSON(&rawMessage); err != nil {
- return err
- }
- message, err := asPlayerMessageC2S(rawMessage)
- if err != nil {
- return err
- }
- c.hub.playerC2SMessages <- &playerMessageC2SWithClient{c, message}
- }
-}
-
-// Receives messages from the hub and sends them to the client.
-func (c *playerClient) writePump() error {
- ticker := time.NewTicker(pingPeriod)
- defer func() {
- ticker.Stop()
- c.conn.Close()
- }()
- for {
- select {
- case message, ok := <-c.s2cMessages:
- if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
- return err
- }
- if !ok {
- if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil {
- return err
- }
- return fmt.Errorf("closing player client")
- }
-
- err := c.conn.WriteJSON(message)
- if err != nil {
- return err
- }
- case <-ticker.C:
- if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
- return err
- }
- if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
- return err
- }
- }
- }
-}
-
-type watcherClient struct {
- hub *gameHub
- conn *websocket.Conn
- s2cMessages chan watcherMessageS2C
-}
-
-// Receives messages from the client and sends them to the hub.
-func (c *watcherClient) readPump() error {
- c.conn.SetReadLimit(maxMessageSize)
- if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
- return err
- }
- c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) })
- return nil
-}
-
-// Receives messages from the hub and sends them to the client.
-func (c *watcherClient) writePump() error {
- ticker := time.NewTicker(pingPeriod)
- defer func() {
- ticker.Stop()
- c.conn.Close()
- log.Printf("closing watcher client")
- c.hub.unregisterWatcher <- c
- }()
- for {
- select {
- case message, ok := <-c.s2cMessages:
- if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
- return err
- }
- if !ok {
- if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil {
- return err
- }
- return fmt.Errorf("closing watcher client")
- }
-
- err := c.conn.WriteJSON(message)
- if err != nil {
- return err
- }
- case <-ticker.C:
- if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
- return err
- }
- if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
- return err
- }
- }
- }
-}
diff --git a/backend/game/http.go b/backend/game/http.go
deleted file mode 100644
index 0ac7fc6..0000000
--- a/backend/game/http.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package game
-
-import (
- "net/http"
- "strconv"
-
- "github.com/labstack/echo/v4"
-
- "github.com/nsfisis/phperkaigi-2025-albatross/backend/auth"
-)
-
-type SockHandler struct {
- hubs *Hubs
-}
-
-func newSockHandler(hubs *Hubs) *SockHandler {
- return &SockHandler{
- hubs: hubs,
- }
-}
-
-func (h *SockHandler) HandleSockGolfPlay(c echo.Context) error {
- jwt := c.QueryParam("token")
- claims, err := auth.ParseJWT(jwt)
- if err != nil {
- return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
- }
- // TODO: check user permission
-
- gameID, err := strconv.Atoi(c.Param("gameID"))
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id")
- }
- hub := h.hubs.getHub(gameID)
- if hub == nil {
- return echo.NewHTTPError(http.StatusNotFound, "Game not found")
- }
- return servePlayerWs(hub, c.Response(), c.Request(), claims.UserID)
-}
-
-func (h *SockHandler) HandleSockGolfWatch(c echo.Context) error {
- jwt := c.QueryParam("token")
- claims, err := auth.ParseJWT(jwt)
- if err != nil {
- return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
- }
- if !claims.IsAdmin {
- return echo.NewHTTPError(http.StatusForbidden, "Permission denied")
- }
-
- gameID, err := strconv.Atoi(c.Param("gameID"))
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id")
- }
- hub := h.hubs.getHub(gameID)
- if hub == nil {
- return echo.NewHTTPError(http.StatusNotFound, "Game not found")
- }
-
- if hub.game.gameType != gameType1v1 {
- return echo.NewHTTPError(http.StatusBadRequest, "Only 1v1 game is supported")
- }
-
- return serveWatcherWs(hub, c.Response(), c.Request())
-}
diff --git a/backend/game/hub.go b/backend/game/hub.go
index 65f207f..0aca96c 100644
--- a/backend/game/hub.go
+++ b/backend/game/hub.go
@@ -2,461 +2,107 @@ package game
import (
"context"
- "crypto/md5"
- "errors"
- "fmt"
"log"
"regexp"
"strings"
- "time"
- "github.com/jackc/pgx/v5/pgtype"
- "github.com/oapi-codegen/nullable"
-
- "github.com/nsfisis/phperkaigi-2025-albatross/backend/api"
"github.com/nsfisis/phperkaigi-2025-albatross/backend/db"
"github.com/nsfisis/phperkaigi-2025-albatross/backend/taskqueue"
)
-type gameHub struct {
- ctx context.Context
- game *game
- q *db.Queries
- taskQueue *taskqueue.Queue
- players map[*playerClient]bool
- registerPlayer chan *playerClient
- unregisterPlayer chan *playerClient
- playerC2SMessages chan *playerMessageC2SWithClient
- watchers map[*watcherClient]bool
- registerWatcher chan *watcherClient
- unregisterWatcher chan *watcherClient
- taskResults chan taskqueue.TaskResult
+type Hub struct {
+ q *db.Queries
+ ctx context.Context
+ taskQueue *taskqueue.Queue
+ taskWorker *taskqueue.WorkerServer
}
-func newGameHub(ctx context.Context, game *game, q *db.Queries, taskQueue *taskqueue.Queue) *gameHub {
- return &gameHub{
- ctx: ctx,
- game: game,
- q: q,
- taskQueue: taskQueue,
- players: make(map[*playerClient]bool),
- registerPlayer: make(chan *playerClient),
- unregisterPlayer: make(chan *playerClient),
- playerC2SMessages: make(chan *playerMessageC2SWithClient),
- watchers: make(map[*watcherClient]bool),
- registerWatcher: make(chan *watcherClient),
- unregisterWatcher: make(chan *watcherClient),
- taskResults: make(chan taskqueue.TaskResult),
+func NewGameHub(q *db.Queries, taskQueue *taskqueue.Queue, taskWorker *taskqueue.WorkerServer) *Hub {
+ return &Hub{
+ q: q,
+ ctx: context.Background(),
+ taskQueue: taskQueue,
+ taskWorker: taskWorker,
}
}
-func (hub *gameHub) run() {
- ticker := time.NewTicker(10 * time.Second)
- defer func() {
- ticker.Stop()
- }()
-
- for {
- select {
- case player := <-hub.registerPlayer:
- hub.players[player] = true
- case player := <-hub.unregisterPlayer:
- if _, ok := hub.players[player]; ok {
- hub.closePlayerClient(player)
- }
- case watcher := <-hub.registerWatcher:
- hub.watchers[watcher] = true
- case watcher := <-hub.unregisterWatcher:
- if _, ok := hub.watchers[watcher]; ok {
- hub.closeWatcherClient(watcher)
- }
- case message := <-hub.playerC2SMessages:
- switch msg := message.message.(type) {
- case *playerMessageC2SCode:
- // TODO: assert game state is gaming
- log.Printf("code: %v", message.message)
- code := msg.Data.Code
- hub.broadcastToWatchers(&watcherMessageS2CCode{
- Type: watcherMessageTypeS2CCode,
- Data: watcherMessageS2CCodePayload{
- PlayerID: message.client.playerID,
- Code: code,
- },
- })
- case *playerMessageC2SSubmit:
- // TODO: assert game state is gaming
- log.Printf("submit: %v", message.message)
- code := msg.Data.Code
- codeSize := calcCodeSize(code)
- codeHash := calcHash(code)
- if err := hub.taskQueue.EnqueueTaskCreateSubmissionRecord(
- hub.game.gameID,
- message.client.playerID,
- code,
- codeSize,
- taskqueue.MD5HexHash(codeHash),
- ); err != nil {
- // TODO: notify failure to player
- log.Fatalf("failed to enqueue task: %v", err)
- }
- hub.broadcastToWatchers(&watcherMessageS2CSubmit{
- Type: watcherMessageTypeS2CSubmit,
- Data: watcherMessageS2CSubmitPayload{
- PlayerID: message.client.playerID,
- },
- })
- default:
- log.Printf("unexpected message type: %T", message.message)
- }
- case <-ticker.C:
- if hub.game.state == gameStateStarting {
- if time.Now().After(*hub.game.startedAt) {
- err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{
- GameID: int32(hub.game.gameID),
- State: string(gameStateGaming),
- })
- if err != nil {
- log.Fatalf("failed to set game state: %v", err)
- }
- hub.game.state = gameStateGaming
- }
- } else if hub.game.state == gameStateGaming {
- if time.Now().After(hub.game.startedAt.Add(time.Duration(hub.game.durationSeconds) * time.Second)) {
- err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{
- GameID: int32(hub.game.gameID),
- State: string(gameStateFinished),
- })
- if err != nil {
- log.Fatalf("failed to set game state: %v", err)
- }
- hub.game.state = gameStateFinished
- hub.close()
- return
- }
- }
+func (hub *Hub) Run() {
+ go func() {
+ if err := hub.taskWorker.Run(); err != nil {
+ log.Fatal(err)
}
- }
-}
+ }()
-func (hub *gameHub) sendExecResult(playerID int, testcaseID nullable.Nullable[int], status string, stdout string, stderr string) {
- hub.sendToPlayer(playerID, &playerMessageS2CExecResult{
- Type: playerMessageTypeS2CExecResult,
- Data: playerMessageS2CExecResultPayload{
- TestcaseID: testcaseID,
- Status: api.GamePlayerMessageS2CExecResultPayloadStatus(status),
- Stdout: stdout,
- Stderr: stderr,
- },
- })
- hub.broadcastToWatchers(&watcherMessageS2CExecResult{
- Type: watcherMessageTypeS2CExecResult,
- Data: watcherMessageS2CExecResultPayload{
- PlayerID: playerID,
- TestcaseID: testcaseID,
- Status: api.GameWatcherMessageS2CExecResultPayloadStatus(status),
- Stdout: stdout,
- Stderr: stderr,
- },
- })
+ go hub.processTaskResults()
}
-func (hub *gameHub) sendSubmitResult(playerID int, status string, score nullable.Nullable[int]) {
- hub.sendToPlayer(playerID, &playerMessageS2CSubmitResult{
- Type: playerMessageTypeS2CSubmitResult,
- Data: playerMessageS2CSubmitResultPayload{
- Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(status),
- Score: score,
- },
- })
- hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
- Type: watcherMessageTypeS2CSubmitResult,
- Data: watcherMessageS2CSubmitResultPayload{
- PlayerID: playerID,
- Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(status),
- Score: score,
- },
- })
+func (hub *Hub) CalcCodeSize(code string) int {
+ re := regexp.MustCompile(`\s+`)
+ return len(re.ReplaceAllString(code, ""))
}
-func (hub *gameHub) sendToPlayer(playerID int, msg playerMessageS2C) {
- for player := range hub.players {
- if player.playerID == playerID {
- player.s2cMessages <- msg
- return
- }
+func (hub *Hub) EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, code string) error {
+ rows, err := hub.q.ListTestcasesByGameID(ctx, int32(gameID))
+ if err != nil {
+ return err
}
-}
-
-func (hub *gameHub) broadcastToWatchers(msg watcherMessageS2C) {
- for watcher := range hub.watchers {
- watcher.s2cMessages <- msg
+ for _, row := range rows {
+ err := hub.taskQueue.EnqueueTaskRunTestcase(
+ gameID,
+ userID,
+ submissionID,
+ int(row.TestcaseID),
+ code,
+ row.Stdin,
+ row.Stdout,
+ )
+ if err != nil {
+ return err
+ }
}
+ return nil
}
-type codeSubmissionError struct {
- Status string
- Stdout string
- Stderr string
-}
-
-func (err *codeSubmissionError) Error() string {
- return err.Stderr
-}
-
-func (hub *gameHub) processTaskResults() {
- for taskResult := range hub.taskResults {
+func (hub *Hub) processTaskResults() {
+ for taskResult := range hub.taskWorker.Results() {
switch taskResult := taskResult.(type) {
- case *taskqueue.TaskResultCreateSubmissionRecord:
- err := hub.processTaskResultCreateSubmissionRecord(taskResult)
- if err != nil {
- hub.sendSubmitResult(
- taskResult.TaskPayload.UserID(),
- err.Status,
- nullable.NewNullNullable[int](),
- )
- }
- case *taskqueue.TaskResultCompileSwiftToWasm:
- err := hub.processTaskResultCompileSwiftToWasm(taskResult)
- if err != nil {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullNullable[int](),
- err.Status,
- err.Stdout,
- err.Stderr,
- )
- hub.sendSubmitResult(
- taskResult.TaskPayload.UserID(),
- err.Status,
- nullable.NewNullNullable[int](),
- )
- }
- case *taskqueue.TaskResultCompileWasmToNativeExecutable:
- err := hub.processTaskResultCompileWasmToNativeExecutable(taskResult)
- if err != nil {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullNullable[int](),
- err.Status,
- err.Stdout,
- err.Stderr,
- )
- hub.sendSubmitResult(
- taskResult.TaskPayload.UserID(),
- err.Status,
- nullable.NewNullNullable[int](),
- )
- } else {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullNullable[int](),
- "success",
- "",
- "",
- )
- }
case *taskqueue.TaskResultRunTestcase:
- // FIXME: error handling
- var err error
- err1 := hub.processTaskResultRunTestcase(taskResult)
- _ = err // TODO: handle err?
- aggregatedStatus, err := hub.q.AggregateTestcaseResults(hub.ctx, int32(taskResult.TaskPayload.SubmissionID))
- _ = err // TODO: handle err?
- err = hub.q.CreateSubmissionResult(hub.ctx, db.CreateSubmissionResultParams{
+ // TODO: error handling
+ _ = hub.processTaskResultRunTestcase(taskResult)
+ aggregatedStatus, _ := hub.q.AggregateTestcaseResults(hub.ctx, int32(taskResult.TaskPayload.SubmissionID))
+ if aggregatedStatus == "running" {
+ continue
+ }
+
+ // TODO: error handling
+ // TODO: transaction
+ _ = hub.q.UpdateSubmissionStatus(hub.ctx, db.UpdateSubmissionStatusParams{
SubmissionID: int32(taskResult.TaskPayload.SubmissionID),
Status: aggregatedStatus,
- Stdout: "",
- Stderr: "",
})
- if err != nil {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
- "internal_error",
- "",
- "",
- )
- hub.sendSubmitResult(
- taskResult.TaskPayload.UserID(),
- "internal_error",
- nullable.NewNullNullable[int](),
- )
+ _ = hub.q.UpdateGameStateStatus(hub.ctx, db.UpdateGameStateStatusParams{
+ GameID: int32(taskResult.TaskPayload.GameID),
+ UserID: int32(taskResult.TaskPayload.UserID),
+ Status: aggregatedStatus,
+ })
+ if aggregatedStatus != "success" {
continue
}
- if err1 != nil {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
- aggregatedStatus,
- "",
- "",
- )
- } else {
- hub.sendExecResult(
- taskResult.TaskPayload.UserID(),
- nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
- "success",
- "",
- "",
- )
- }
- if aggregatedStatus != "running" {
- var score nullable.Nullable[int]
- if aggregatedStatus == "success" {
- codeSize, err := hub.q.GetSubmissionCodeSizeByID(hub.ctx, int32(taskResult.TaskPayload.SubmissionID))
- if err == nil {
- score = nullable.NewNullableWithValue(int(codeSize))
- }
- }
- hub.sendSubmitResult(
- taskResult.TaskPayload.UserID(),
- aggregatedStatus,
- score,
- )
- }
+ _ = hub.q.SyncGameStateBestScoreSubmission(hub.ctx, db.SyncGameStateBestScoreSubmissionParams{
+ GameID: int32(taskResult.TaskPayload.GameID),
+ UserID: int32(taskResult.TaskPayload.UserID),
+ })
default:
panic("unexpected task result type")
}
}
}
-func (hub *gameHub) processTaskResultCreateSubmissionRecord(
- taskResult *taskqueue.TaskResultCreateSubmissionRecord,
-) *codeSubmissionError {
- if taskResult.Err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: taskResult.Err.Error(),
- }
- }
-
- if err := hub.taskQueue.EnqueueTaskCompileSwiftToWasm(
- taskResult.TaskPayload.GameID(),
- taskResult.TaskPayload.UserID(),
- taskResult.TaskPayload.Code,
- taskResult.TaskPayload.CodeHash(),
- taskResult.SubmissionID,
- ); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return nil
-}
-
-func (hub *gameHub) processTaskResultCompileSwiftToWasm(
- taskResult *taskqueue.TaskResultCompileSwiftToWasm,
-) *codeSubmissionError {
- if taskResult.Err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: taskResult.Err.Error(),
- }
- }
-
- if taskResult.Status != "success" {
- if err := hub.q.CreateSubmissionResult(hub.ctx, db.CreateSubmissionResultParams{
- SubmissionID: int32(taskResult.TaskPayload.SubmissionID),
- Status: taskResult.Status,
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return &codeSubmissionError{
- Status: taskResult.Status,
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }
- }
- if err := hub.taskQueue.EnqueueTaskCompileWasmToNativeExecutable(
- taskResult.TaskPayload.GameID(),
- taskResult.TaskPayload.UserID(),
- taskResult.TaskPayload.CodeHash(),
- taskResult.TaskPayload.SubmissionID,
- ); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return nil
-}
-
-func (hub *gameHub) processTaskResultCompileWasmToNativeExecutable(
- taskResult *taskqueue.TaskResultCompileWasmToNativeExecutable,
-) *codeSubmissionError {
- if taskResult.Err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: taskResult.Err.Error(),
- }
- }
-
- if taskResult.Status != "success" {
- if err := hub.q.CreateSubmissionResult(hub.ctx, db.CreateSubmissionResultParams{
- SubmissionID: int32(taskResult.TaskPayload.SubmissionID),
- Status: taskResult.Status,
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return &codeSubmissionError{
- Status: taskResult.Status,
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }
- }
-
- testcases, err := hub.q.ListTestcasesByGameID(hub.ctx, int32(taskResult.TaskPayload.GameID()))
- if err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- if len(testcases) == 0 {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: "no testcases found",
- }
- }
-
- for _, testcase := range testcases {
- if err := hub.taskQueue.EnqueueTaskRunTestcase(
- taskResult.TaskPayload.GameID(),
- taskResult.TaskPayload.UserID(),
- taskResult.TaskPayload.CodeHash(),
- taskResult.TaskPayload.SubmissionID,
- int(testcase.TestcaseID),
- testcase.Stdin,
- testcase.Stdout,
- ); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- }
- return nil
-}
-
-func (hub *gameHub) processTaskResultRunTestcase(
+func (hub *Hub) processTaskResultRunTestcase(
taskResult *taskqueue.TaskResultRunTestcase,
-) *codeSubmissionError {
+) error {
if taskResult.Err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: taskResult.Err.Error(),
- }
+ return taskResult.Err
}
if taskResult.Status != "success" {
@@ -467,202 +113,31 @@ func (hub *gameHub) processTaskResultRunTestcase(
Stdout: taskResult.Stdout,
Stderr: taskResult.Stderr,
}); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return &codeSubmissionError{
- Status: taskResult.Status,
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }
- }
- if !isTestcaseResultCorrect(taskResult.TaskPayload.Stdout, taskResult.Stdout) {
- if err := hub.q.CreateTestcaseResult(hub.ctx, db.CreateTestcaseResultParams{
- SubmissionID: int32(taskResult.TaskPayload.SubmissionID),
- TestcaseID: int32(taskResult.TaskPayload.TestcaseID),
- Status: "wrong_answer",
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }); err != nil {
- return &codeSubmissionError{
- Status: "internal_error",
- Stderr: err.Error(),
- }
- }
- return &codeSubmissionError{
- Status: "wrong_answer",
- Stdout: taskResult.Stdout,
- Stderr: taskResult.Stderr,
- }
- }
- return nil
-}
-
-func (hub *gameHub) startGame() error {
- startAt := time.Now().Add(11 * time.Second).UTC()
- for player := range hub.players {
- player.s2cMessages <- &playerMessageS2CStart{
- Type: playerMessageTypeS2CStart,
- Data: playerMessageS2CStartPayload{
- StartAt: startAt.Unix(),
- },
+ return err
}
+ return nil
}
- hub.broadcastToWatchers(&watcherMessageS2CStart{
- Type: watcherMessageTypeS2CStart,
- Data: watcherMessageS2CStartPayload{
- StartAt: startAt.Unix(),
- },
- })
- err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{
- GameID: int32(hub.game.gameID),
- StartedAt: pgtype.Timestamp{
- Time: startAt,
- InfinityModifier: pgtype.Finite,
- Valid: true,
- },
- })
- if err != nil {
- log.Fatalf("failed to set game state: %v", err)
- }
- hub.game.startedAt = &startAt
- err = hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{
- GameID: int32(hub.game.gameID),
- State: string(gameStateStarting),
- })
- if err != nil {
- log.Fatalf("failed to set game state: %v", err)
- }
- hub.game.state = gameStateStarting
- return nil
-}
-
-func (hub *gameHub) close() {
- for player := range hub.players {
- hub.closePlayerClient(player)
- }
- close(hub.registerPlayer)
- close(hub.unregisterPlayer)
- close(hub.playerC2SMessages)
- for watcher := range hub.watchers {
- hub.closeWatcherClient(watcher)
- }
- close(hub.registerWatcher)
- close(hub.unregisterWatcher)
-}
-
-func (hub *gameHub) closePlayerClient(player *playerClient) {
- delete(hub.players, player)
- close(player.s2cMessages)
-}
-
-func (hub *gameHub) closeWatcherClient(watcher *watcherClient) {
- delete(hub.watchers, watcher)
- close(watcher.s2cMessages)
-}
-
-type Hubs struct {
- hubs map[int]*gameHub
- q *db.Queries
- taskQueue *taskqueue.Queue
- taskResults chan taskqueue.TaskResult
-}
-
-func NewGameHubs(q *db.Queries, taskQueue *taskqueue.Queue, taskResults chan taskqueue.TaskResult) *Hubs {
- return &Hubs{
- hubs: make(map[int]*gameHub),
- q: q,
- taskQueue: taskQueue,
- taskResults: taskResults,
- }
-}
-func (hubs *Hubs) Close() {
- log.Println("closing all game hubs")
- for _, hub := range hubs.hubs {
- hub.close()
+ var status string
+ if isTestcaseResultCorrect(taskResult.TaskPayload.Stdout, taskResult.Stdout) {
+ status = "success"
+ } else {
+ status = "wrong_answer"
}
-}
-
-func (hubs *Hubs) getHub(gameID int) *gameHub {
- return hubs.hubs[gameID]
-}
-
-func (hubs *Hubs) RestoreFromDB(ctx context.Context) error {
- games, err := hubs.q.ListGames(ctx)
- if err != nil {
+ if err := hub.q.CreateTestcaseResult(hub.ctx, db.CreateTestcaseResultParams{
+ SubmissionID: int32(taskResult.TaskPayload.SubmissionID),
+ TestcaseID: int32(taskResult.TaskPayload.TestcaseID),
+ Status: status,
+ Stdout: taskResult.Stdout,
+ Stderr: taskResult.Stderr,
+ }); err != nil {
return err
}
- for _, row := range games {
- var startedAt *time.Time
- if row.StartedAt.Valid {
- startedAt = &row.StartedAt.Time
- }
- pr := &problem{
- problemID: int(row.ProblemID),
- title: row.Title,
- description: row.Description,
- }
- // TODO: N+1
- playerRows, err := hubs.q.ListGamePlayers(ctx, int32(row.GameID))
- if err != nil {
- return err
- }
- hubs.hubs[int(row.GameID)] = newGameHub(ctx, &game{
- gameID: int(row.GameID),
- gameType: gameType(row.GameType),
- durationSeconds: int(row.DurationSeconds),
- state: gameState(row.State),
- displayName: row.DisplayName,
- startedAt: startedAt,
- problem: pr,
- playerCount: len(playerRows),
- }, hubs.q, hubs.taskQueue)
- }
return nil
}
-func (hubs *Hubs) Run() {
- for _, hub := range hubs.hubs {
- go hub.run()
- go hub.processTaskResults()
- }
-
- for taskResult := range hubs.taskResults {
- hub := hubs.getHub(taskResult.GameID())
- if hub == nil {
- log.Printf("no such game: %d", taskResult.GameID())
- continue
- }
- hub.taskResults <- taskResult
- }
-}
-
-func (hubs *Hubs) SockHandler() *SockHandler {
- return newSockHandler(hubs)
-}
-
-func (hubs *Hubs) StartGame(gameID int) error {
- hub := hubs.getHub(gameID)
- if hub == nil {
- return errors.New("no such game")
- }
- return hub.startGame()
-}
-
func isTestcaseResultCorrect(expectedStdout, actualStdout string) bool {
expectedStdout = strings.TrimSpace(expectedStdout)
actualStdout = strings.TrimSpace(actualStdout)
return actualStdout == expectedStdout
}
-
-func calcHash(code string) string {
- return fmt.Sprintf("%x", md5.Sum([]byte(code)))
-}
-
-func calcCodeSize(code string) int {
- re := regexp.MustCompile(`\s+`)
- return len(re.ReplaceAllString(code, ""))
-}
diff --git a/backend/game/message.go b/backend/game/message.go
deleted file mode 100644
index 808561c..0000000
--- a/backend/game/message.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package game
-
-import (
- "encoding/json"
- "fmt"
-
- "github.com/nsfisis/phperkaigi-2025-albatross/backend/api"
-)
-
-const (
- playerMessageTypeS2CStart = "player:s2c:start"
- playerMessageTypeS2CExecResult = "player:s2c:execresult"
- playerMessageTypeS2CSubmitResult = "player:s2c:submitresult"
- playerMessageTypeC2SCode = "player:c2s:code"
- playerMessageTypeC2SSubmit = "player:c2s:submit"
-)
-
-type playerMessageC2SWithClient struct {
- client *playerClient
- message playerMessageC2S
-}
-
-type playerMessageS2C = interface{}
-type playerMessageS2CStart = api.GamePlayerMessageS2CStart
-type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload
-type playerMessageS2CExecResult = api.GamePlayerMessageS2CExecResult
-type playerMessageS2CExecResultPayload = api.GamePlayerMessageS2CExecResultPayload
-type playerMessageS2CSubmitResult = api.GamePlayerMessageS2CSubmitResult
-type playerMessageS2CSubmitResultPayload = api.GamePlayerMessageS2CSubmitResultPayload
-
-type playerMessageC2S = interface{}
-type playerMessageC2SCode = api.GamePlayerMessageC2SCode
-type playerMessageC2SCodePayload = api.GamePlayerMessageC2SCodePayload
-type playerMessageC2SSubmit = api.GamePlayerMessageC2SSubmit
-type playerMessageC2SSubmitPayload = api.GamePlayerMessageC2SSubmitPayload
-
-func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error) {
- var typ string
- if err := json.Unmarshal(raw["type"], &typ); err != nil {
- return nil, err
- }
-
- switch typ {
- case playerMessageTypeC2SCode:
- var payload playerMessageC2SCodePayload
- if err := json.Unmarshal(raw["data"], &payload); err != nil {
- return nil, err
- }
- return &playerMessageC2SCode{
- Type: playerMessageTypeC2SCode,
- Data: payload,
- }, nil
- case playerMessageTypeC2SSubmit:
- var payload playerMessageC2SSubmitPayload
- if err := json.Unmarshal(raw["data"], &payload); err != nil {
- return nil, err
- }
- return &playerMessageC2SSubmit{
- Type: playerMessageTypeC2SSubmit,
- Data: payload,
- }, nil
- default:
- return nil, fmt.Errorf("unknown message type: %s", typ)
- }
-}
-
-const (
- watcherMessageTypeS2CStart = "watcher:s2c:start"
- watcherMessageTypeS2CCode = "watcher:s2c:code"
- watcherMessageTypeS2CSubmit = "watcher:s2c:submit"
- watcherMessageTypeS2CExecResult = "watcher:s2c:execresult"
- watcherMessageTypeS2CSubmitResult = "watcher:s2c:submitresult"
-)
-
-type watcherMessageS2C = interface{}
-type watcherMessageS2CStart = api.GameWatcherMessageS2CStart
-type watcherMessageS2CStartPayload = api.GameWatcherMessageS2CStartPayload
-type watcherMessageS2CCode = api.GameWatcherMessageS2CCode
-type watcherMessageS2CCodePayload = api.GameWatcherMessageS2CCodePayload
-type watcherMessageS2CSubmit = api.GameWatcherMessageS2CSubmit
-type watcherMessageS2CSubmitPayload = api.GameWatcherMessageS2CSubmitPayload
-type watcherMessageS2CExecResult = api.GameWatcherMessageS2CExecResult
-type watcherMessageS2CExecResultPayload = api.GameWatcherMessageS2CExecResultPayload
-type watcherMessageS2CSubmitResult = api.GameWatcherMessageS2CSubmitResult
-type watcherMessageS2CSubmitResultPayload = api.GameWatcherMessageS2CSubmitResultPayload
diff --git a/backend/game/models.go b/backend/game/models.go
deleted file mode 100644
index 23bed12..0000000
--- a/backend/game/models.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package game
-
-import (
- "time"
-
- "github.com/nsfisis/phperkaigi-2025-albatross/backend/api"
-)
-
-type gameType = api.GameGameType
-type gameState = api.GameState
-
-const (
- gameType1v1 = api.N1V1
- gameTypeMultiplayer = api.Multiplayer
-
- gameStateClosed gameState = api.Closed
- gameStateWaiting gameState = api.Waiting
- gameStateStarting gameState = api.Starting
- gameStateGaming gameState = api.Gaming
- gameStateFinished gameState = api.Finished
-)
-
-type game struct {
- gameID int
- gameType gameType
- state gameState
- displayName string
- durationSeconds int
- startedAt *time.Time
- problem *problem
- playerCount int
-}
-
-type problem struct {
- problemID int
- title string
- description string
-}
diff --git a/backend/game/ws.go b/backend/game/ws.go
deleted file mode 100644
index 47dc7cf..0000000
--- a/backend/game/ws.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package game
-
-import (
- "log"
- "net/http"
- "time"
-
- "github.com/gorilla/websocket"
-)
-
-const (
- writeWait = 10 * time.Second
- pongWait = 60 * time.Second
- pingPeriod = (pongWait * 9) / 10
- maxMessageSize = 50 * 1024
-)
-
-var upgrader = websocket.Upgrader{
- ReadBufferSize: 1024,
- WriteBufferSize: 1024,
- CheckOrigin: func(r *http.Request) bool {
- // TODO: insecure!
- _ = r
- return true
- },
-}
-
-func servePlayerWs(hub *gameHub, w http.ResponseWriter, r *http.Request, playerID int) error {
- conn, err := upgrader.Upgrade(w, r, nil)
- if err != nil {
- return err
- }
- player := &playerClient{
- hub: hub,
- conn: conn,
- s2cMessages: make(chan playerMessageS2C),
- playerID: playerID,
- }
- hub.registerPlayer <- player
-
- go func() {
- err := player.writePump()
- log.Printf("%v", err)
- }()
- go func() {
- err := player.readPump()
- log.Printf("%v", err)
- }()
- 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 := &watcherClient{
- hub: hub,
- conn: conn,
- s2cMessages: make(chan watcherMessageS2C),
- }
- hub.registerWatcher <- watcher
-
- go func() {
- err := watcher.writePump()
- log.Printf("%v", err)
- }()
- go func() {
- err := watcher.readPump()
- log.Printf("%v", err)
- }()
- return nil
-}