From daaf81ae931654e20f882fbc6bbc4a02cbfc0273 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 28 Jul 2024 20:09:09 +0900 Subject: feat(backend): partially implement gaming --- backend/game/hub.go | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 backend/game/hub.go (limited to 'backend/game/hub.go') diff --git a/backend/game/hub.go b/backend/game/hub.go new file mode 100644 index 0000000..170f142 --- /dev/null +++ b/backend/game/hub.go @@ -0,0 +1,250 @@ +package game + +import ( + "context" + "log" + "time" + + "github.com/jackc/pgx/v5/pgtype" + + "github.com/nsfisis/iosdc-2024-albatross/backend/api" + "github.com/nsfisis/iosdc-2024-albatross/backend/db" +) + +type playerClientState int + +const ( + playerClientStateWaitingEntries playerClientState = iota + playerClientStateEntried + playerClientStateReady +) + +type gameHub struct { + ctx context.Context + game *game + q *db.Queries + players map[*playerClient]playerClientState + registerPlayer chan *playerClient + unregisterPlayer chan *playerClient + playerC2SMessages chan *playerMessageC2SWithClient + watchers map[*watcherClient]bool + registerWatcher chan *watcherClient + unregisterWatcher chan *watcherClient +} + +func newGameHub(ctx context.Context, game *game, q *db.Queries) *gameHub { + return &gameHub{ + ctx: ctx, + game: game, + q: q, + players: make(map[*playerClient]playerClientState), + 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), + } +} + +func (hub *gameHub) run() { + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + }() + + for { + select { + case player := <-hub.registerPlayer: + hub.players[player] = playerClientStateWaitingEntries + 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 message.message.(type) { + case *playerMessageC2SEntry: + log.Printf("entry: %v", message.message) + // TODO: assert state is waiting_entries + hub.players[message.client] = playerClientStateEntried + entriedPlayerCount := 0 + for _, state := range hub.players { + if playerClientStateEntried <= state { + entriedPlayerCount++ + } + } + if entriedPlayerCount == 2 { + for player := range hub.players { + player.s2cMessages <- &playerMessageS2CPrepare{ + Type: playerMessageTypeS2CPrepare, + Data: playerMessageS2CPreparePayload{ + Problem: api.Problem{ + ProblemId: 1, + Title: "the answer", + Description: "print 42", + }, + }, + } + } + err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{ + GameID: int32(hub.game.gameID), + State: string(gameStatePrepare), + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.state = gameStatePrepare + } + case *playerMessageC2SReady: + log.Printf("ready: %v", message.message) + // TODO: assert state is prepare + hub.players[message.client] = playerClientStateReady + readyPlayerCount := 0 + for _, state := range hub.players { + if playerClientStateReady <= state { + readyPlayerCount++ + } + } + if readyPlayerCount == 2 { + startAt := time.Now().Add(11 * time.Second).UTC() + for player := range hub.players { + player.s2cMessages <- &playerMessageS2CStart{ + Type: playerMessageTypeS2CStart, + Data: playerMessageS2CStartPayload{ + StartAt: int(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 + } + default: + log.Fatalf("unexpected message type: %T", message.message) + } + case <-ticker.C: + 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 *gameHub) close() { + for client := range hub.players { + hub.closePlayerClient(client) + } + 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 GameHubs struct { + hubs map[int]*gameHub + q *db.Queries +} + +func NewGameHubs(q *db.Queries) *GameHubs { + return &GameHubs{ + hubs: make(map[int]*gameHub), + q: q, + } +} + +func (hubs *GameHubs) Close() { + for _, hub := range hubs.hubs { + hub.close() + } +} + +func (hubs *GameHubs) RestoreFromDB(ctx context.Context) error { + games, err := hubs.q.ListGames(ctx) + if err != nil { + return err + } + for _, row := range games { + var startedAt *time.Time + if row.StartedAt.Valid { + startedAt = &row.StartedAt.Time + } + var problem_ *problem + if row.ProblemID.Valid { + if !row.Title.Valid || !row.Description.Valid { + panic("inconsistent data") + } + problem_ = &problem{ + problemID: int(row.ProblemID.Int32), + title: row.Title.String, + description: row.Description.String, + } + } + hubs.hubs[int(row.GameID)] = newGameHub(ctx, &game{ + gameID: int(row.GameID), + durationSeconds: int(row.DurationSeconds), + state: gameState(row.State), + displayName: row.DisplayName, + startedAt: startedAt, + problem: problem_, + }, hubs.q) + } + return nil +} + +func (hubs *GameHubs) Run() { + for _, hub := range hubs.hubs { + go hub.run() + } +} + +func (hubs *GameHubs) SockHandler() *sockHandler { + return newSockHandler(hubs) +} -- cgit v1.2.3-70-g09d2 From 161d82bee9f9e65680516a9cfd392e0cf297eadf Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 29 Jul 2024 02:58:54 +0900 Subject: feat: handle code and execresult messages --- backend/game/hub.go | 27 +++++++++++++++++++++++++-- backend/game/message.go | 23 +++++++++++++++++++---- frontend/app/components/GolfPlayApp.tsx | 17 ++++++++++++++--- 3 files changed, 58 insertions(+), 9 deletions(-) (limited to 'backend/game/hub.go') diff --git a/backend/game/hub.go b/backend/game/hub.go index 170f142..c61f2bb 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -68,7 +68,7 @@ func (hub *gameHub) run() { hub.closeWatcherClient(watcher) } case message := <-hub.playerC2SMessages: - switch message.message.(type) { + switch msg := message.message.(type) { case *playerMessageC2SEntry: log.Printf("entry: %v", message.message) // TODO: assert state is waiting_entries @@ -142,11 +142,34 @@ func (hub *gameHub) run() { } hub.game.state = gameStateStarting } + case *playerMessageC2SCode: + // TODO: assert game state is gaming + log.Printf("code: %v", message.message) + code := msg.Data.Code + score := len(code) + message.client.s2cMessages <- &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Score: &score, + Status: api.Success, + }, + } default: log.Fatalf("unexpected message type: %T", message.message) } case <-ticker.C: - if hub.game.state == gameStateGaming { + 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), diff --git a/backend/game/message.go b/backend/game/message.go index 23774ce..9116bde 100644 --- a/backend/game/message.go +++ b/backend/game/message.go @@ -8,10 +8,12 @@ import ( ) const ( - playerMessageTypeS2CPrepare = "player:s2c:prepare" - playerMessageTypeS2CStart = "player:s2c:start" - playerMessageTypeC2SEntry = "player:c2s:entry" - playerMessageTypeC2SReady = "player:c2s:ready" + playerMessageTypeS2CPrepare = "player:s2c:prepare" + playerMessageTypeS2CStart = "player:s2c:start" + playerMessageTypeS2CExecResult = "player:s2c:execreslut" + playerMessageTypeC2SEntry = "player:c2s:entry" + playerMessageTypeC2SReady = "player:c2s:ready" + playerMessageTypeC2SCode = "player:c2s:code" ) type playerMessageC2SWithClient struct { @@ -26,10 +28,14 @@ type playerMessageS2CPrepare = api.GamePlayerMessageS2CPrepare type playerMessageS2CPreparePayload = api.GamePlayerMessageS2CPreparePayload type playerMessageS2CStart = api.GamePlayerMessageS2CStart type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload +type playerMessageS2CExecResult = api.GamePlayerMessageS2CExecResult +type playerMessageS2CExecResultPayload = api.GamePlayerMessageS2CExecResultPayload type playerMessageC2S = interface{} type playerMessageC2SEntry = api.GamePlayerMessageC2SEntry type playerMessageC2SReady = api.GamePlayerMessageC2SReady +type playerMessageC2SCode = api.GamePlayerMessageC2SCode +type playerMessageC2SCodePayload = api.GamePlayerMessageC2SCodePayload func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error) { var typ string @@ -46,6 +52,15 @@ func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error return &playerMessageC2SReady{ Type: playerMessageTypeC2SReady, }, nil + case playerMessageTypeC2SCode: + var payload playerMessageC2SCodePayload + if err := json.Unmarshal(raw["data"], &payload); err != nil { + return nil, err + } + return &playerMessageC2SCode{ + Type: playerMessageTypeC2SCode, + Data: payload, + }, nil default: return nil, fmt.Errorf("unknown message type: %s", typ) } diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx index 31d1c44..c6c20d4 100644 --- a/frontend/app/components/GolfPlayApp.tsx +++ b/frontend/app/components/GolfPlayApp.tsx @@ -69,8 +69,11 @@ export default function GolfPlayApp({ game }: { game: Game }) { void setCurrentScore; const onCodeChange = useDebouncedCallback((code: string) => { - void code; - // sendJsonMessage({}); + console.log("player:c2s:code"); + sendJsonMessage({ + type: "player:c2s:code", + data: { code }, + }); }, 1000); if (readyState === ReadyState.UNINSTANTIATED) { @@ -104,6 +107,14 @@ export default function GolfPlayApp({ game }: { game: Game }) { setTimeLeftSeconds(start_at - nowSec); setGameState("starting"); } + } else if (lastJsonMessage.type === "player:s2c:execresult") { + const { score } = lastJsonMessage.data; + if ( + score !== null && + (currentScore === null || score < currentScore) + ) { + setCurrentScore(score); + } } } else { setGameState("waiting"); @@ -111,7 +122,7 @@ export default function GolfPlayApp({ game }: { game: Game }) { sendJsonMessage({ type: "player:c2s:entry" }); } } - }, [sendJsonMessage, lastJsonMessage, readyState, gameState]); + }, [sendJsonMessage, lastJsonMessage, readyState, gameState, currentScore]); if (gameState === "connecting") { return ; -- cgit v1.2.3-70-g09d2