Stage 7 (wip): wire remaining ops (backend REST, FBS, gateway transcode) + real UI transport
backend: REST handlers for pass/exchange/resign/hint/evaluate/check_word/complaint/history/chat-list/nudge + new game.ListForAccount (my games) + seat display_name resolution pkg/fbs: GameActionRequest/ExchangeRequest/EvalRequest/EvalResult/CheckWordRequest/WordCheckResult/ComplaintRequest/HintResult/History/GameList/ChatList + SeatView.display_name; committed Go regenerated (flatc 23.5.26) gateway: 11 new transcode ops + backendclient methods + FB encoders ui: edge TS codegen (flatc --ts + protoc-gen-es, committed), FlatBuffers<->model codec, real connect-web transport (binary, bearer auth, Subscribe). prod bundle ~69KB gzip JS
This commit is contained in:
@@ -100,9 +100,8 @@ func encodeMatch(m backendclient.MatchResp) []byte {
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeChat builds a ChatMessage payload.
|
||||
func encodeChat(c backendclient.ChatResp) []byte {
|
||||
b := flatbuffers.NewBuilder(192)
|
||||
// buildChatMessage builds a ChatMessage table and returns its offset.
|
||||
func buildChatMessage(b *flatbuffers.Builder, c backendclient.ChatResp) flatbuffers.UOffsetT {
|
||||
id := b.CreateString(c.ID)
|
||||
gid := b.CreateString(c.GameID)
|
||||
sid := b.CreateString(c.SenderID)
|
||||
@@ -115,7 +114,103 @@ func encodeChat(c backendclient.ChatResp) []byte {
|
||||
fb.ChatMessageAddKind(b, kind)
|
||||
fb.ChatMessageAddBody(b, body)
|
||||
fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix)
|
||||
b.Finish(fb.ChatMessageEnd(b))
|
||||
return fb.ChatMessageEnd(b)
|
||||
}
|
||||
|
||||
// encodeChat builds a ChatMessage payload.
|
||||
func encodeChat(c backendclient.ChatResp) []byte {
|
||||
b := flatbuffers.NewBuilder(192)
|
||||
b.Finish(buildChatMessage(b, c))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeHintResult builds a HintResult payload.
|
||||
func encodeHintResult(r backendclient.HintResultResp) []byte {
|
||||
b := flatbuffers.NewBuilder(512)
|
||||
move := buildMoveRecord(b, r.Move)
|
||||
fb.HintResultStart(b)
|
||||
fb.HintResultAddMove(b, move)
|
||||
fb.HintResultAddHintsRemaining(b, int32(r.HintsRemaining))
|
||||
b.Finish(fb.HintResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeEvalResult builds an EvalResult payload.
|
||||
func encodeEvalResult(r backendclient.EvalResultResp) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
words := buildStringVector(b, r.Words, fb.EvalResultStartWordsVector)
|
||||
fb.EvalResultStart(b)
|
||||
fb.EvalResultAddLegal(b, r.Legal)
|
||||
fb.EvalResultAddScore(b, int32(r.Score))
|
||||
fb.EvalResultAddWords(b, words)
|
||||
b.Finish(fb.EvalResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeWordCheck builds a WordCheckResult payload.
|
||||
func encodeWordCheck(r backendclient.WordCheckResp) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
word := b.CreateString(r.Word)
|
||||
fb.WordCheckResultStart(b)
|
||||
fb.WordCheckResultAddWord(b, word)
|
||||
fb.WordCheckResultAddLegal(b, r.Legal)
|
||||
b.Finish(fb.WordCheckResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeHistory builds a History payload (the decoded move journal).
|
||||
func encodeHistory(r backendclient.HistoryResp) []byte {
|
||||
b := flatbuffers.NewBuilder(1024)
|
||||
moveOffs := make([]flatbuffers.UOffsetT, len(r.Moves))
|
||||
for i, m := range r.Moves {
|
||||
moveOffs[i] = buildMoveRecord(b, m)
|
||||
}
|
||||
fb.HistoryStartMovesVector(b, len(moveOffs))
|
||||
for i := len(moveOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(moveOffs[i])
|
||||
}
|
||||
moves := b.EndVector(len(moveOffs))
|
||||
gid := b.CreateString(r.GameID)
|
||||
fb.HistoryStart(b)
|
||||
fb.HistoryAddGameId(b, gid)
|
||||
fb.HistoryAddMoves(b, moves)
|
||||
b.Finish(fb.HistoryEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeGameList builds a GameList payload (the caller's games).
|
||||
func encodeGameList(r backendclient.GameListResp) []byte {
|
||||
b := flatbuffers.NewBuilder(1024)
|
||||
gameOffs := make([]flatbuffers.UOffsetT, len(r.Games))
|
||||
for i, g := range r.Games {
|
||||
gameOffs[i] = buildGameView(b, g)
|
||||
}
|
||||
fb.GameListStartGamesVector(b, len(gameOffs))
|
||||
for i := len(gameOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(gameOffs[i])
|
||||
}
|
||||
games := b.EndVector(len(gameOffs))
|
||||
fb.GameListStart(b)
|
||||
fb.GameListAddGames(b, games)
|
||||
b.Finish(fb.GameListEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeChatList builds a ChatList payload (a game's chat history).
|
||||
func encodeChatList(r backendclient.ChatListResp) []byte {
|
||||
b := flatbuffers.NewBuilder(512)
|
||||
msgOffs := make([]flatbuffers.UOffsetT, len(r.Messages))
|
||||
for i, m := range r.Messages {
|
||||
msgOffs[i] = buildChatMessage(b, m)
|
||||
}
|
||||
fb.ChatListStartMessagesVector(b, len(msgOffs))
|
||||
for i := len(msgOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(msgOffs[i])
|
||||
}
|
||||
msgs := b.EndVector(len(msgOffs))
|
||||
fb.ChatListStart(b)
|
||||
fb.ChatListAddMessages(b, msgs)
|
||||
b.Finish(fb.ChatListEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
@@ -124,12 +219,14 @@ func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers
|
||||
seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
|
||||
for i, s := range g.Seats {
|
||||
aid := b.CreateString(s.AccountID)
|
||||
dname := b.CreateString(s.DisplayName)
|
||||
fb.SeatViewStart(b)
|
||||
fb.SeatViewAddSeat(b, int32(s.Seat))
|
||||
fb.SeatViewAddAccountId(b, aid)
|
||||
fb.SeatViewAddScore(b, int32(s.Score))
|
||||
fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
|
||||
fb.SeatViewAddIsWinner(b, s.IsWinner)
|
||||
fb.SeatViewAddDisplayName(b, dname)
|
||||
seatOffs[i] = fb.SeatViewEnd(b)
|
||||
}
|
||||
fb.GameViewStartSeatsVector(b, len(seatOffs))
|
||||
|
||||
@@ -26,6 +26,17 @@ const (
|
||||
MsgLobbyEnqueue = "lobby.enqueue"
|
||||
MsgLobbyPoll = "lobby.poll"
|
||||
MsgChatPost = "chat.post"
|
||||
MsgGamesList = "games.list"
|
||||
MsgGamePass = "game.pass"
|
||||
MsgGameExchange = "game.exchange"
|
||||
MsgGameResign = "game.resign"
|
||||
MsgGameHint = "game.hint"
|
||||
MsgGameEvaluate = "game.evaluate"
|
||||
MsgGameCheckWord = "game.check_word"
|
||||
MsgGameComplaint = "game.complaint"
|
||||
MsgGameHistory = "game.history"
|
||||
MsgChatList = "chat.list"
|
||||
MsgChatNudge = "chat.nudge"
|
||||
)
|
||||
|
||||
// Request is one decoded Execute call.
|
||||
@@ -69,6 +80,17 @@ func NewRegistry(backend *backendclient.Client, tg auth.TelegramValidator) *Regi
|
||||
r.ops[MsgLobbyEnqueue] = Op{Handler: enqueueHandler(backend), Auth: true}
|
||||
r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true}
|
||||
r.ops[MsgChatPost] = Op{Handler: chatPostHandler(backend), Auth: true}
|
||||
r.ops[MsgGamesList] = Op{Handler: gamesListHandler(backend), Auth: true}
|
||||
r.ops[MsgGamePass] = Op{Handler: passHandler(backend), Auth: true}
|
||||
r.ops[MsgGameExchange] = Op{Handler: exchangeHandler(backend), Auth: true}
|
||||
r.ops[MsgGameResign] = Op{Handler: resignHandler(backend), Auth: true}
|
||||
r.ops[MsgGameHint] = Op{Handler: hintHandler(backend), Auth: true}
|
||||
r.ops[MsgGameEvaluate] = Op{Handler: evaluateHandler(backend), Auth: true}
|
||||
r.ops[MsgGameCheckWord] = Op{Handler: checkWordHandler(backend), Auth: true}
|
||||
r.ops[MsgGameComplaint] = Op{Handler: complaintHandler(backend), Auth: true}
|
||||
r.ops[MsgGameHistory] = Op{Handler: historyHandler(backend), Auth: true}
|
||||
r.ops[MsgChatList] = Op{Handler: chatListHandler(backend), Auth: true}
|
||||
r.ops[MsgChatNudge] = Op{Handler: nudgeHandler(backend), Auth: true}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -219,3 +241,150 @@ func decodeTiles(in *fb.SubmitPlayRequest) []backendclient.TileJSON {
|
||||
}
|
||||
return tiles
|
||||
}
|
||||
|
||||
// decodeEvalTiles reads the tentative tiles from an EvalRequest.
|
||||
func decodeEvalTiles(in *fb.EvalRequest) []backendclient.TileJSON {
|
||||
n := in.TilesLength()
|
||||
tiles := make([]backendclient.TileJSON, 0, n)
|
||||
var t fb.TileRecord
|
||||
for i := 0; i < n; i++ {
|
||||
if in.Tiles(&t, i) {
|
||||
tiles = append(tiles, backendclient.TileJSON{
|
||||
Row: int(t.Row()),
|
||||
Col: int(t.Col()),
|
||||
Letter: string(t.Letter()),
|
||||
Blank: t.Blank(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return tiles
|
||||
}
|
||||
|
||||
// decodeStringVector reads the exchange tiles from an ExchangeRequest.
|
||||
func decodeStringVector(in *fb.ExchangeRequest) []string {
|
||||
n := in.TilesLength()
|
||||
out := make([]string, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out = append(out, string(in.Tiles(i)))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func gamesListHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
res, err := backend.GamesList(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeGameList(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func passHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.Pass(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeMoveResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func resignHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.Resign(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeMoveResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsExchangeRequest(req.Payload, 0)
|
||||
res, err := backend.Exchange(ctx, req.UserID, string(in.GameId()), decodeStringVector(in))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeMoveResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func hintHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.Hint(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeHintResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsEvalRequest(req.Payload, 0)
|
||||
res, err := backend.Evaluate(ctx, req.UserID, string(in.GameId()), string(in.Dir()), decodeEvalTiles(in))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeEvalResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkWordHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsCheckWordRequest(req.Payload, 0)
|
||||
res, err := backend.CheckWord(ctx, req.UserID, string(in.GameId()), string(in.Word()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeWordCheck(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func complaintHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsComplaintRequest(req.Payload, 0)
|
||||
if err := backend.Complaint(ctx, req.UserID, string(in.GameId()), string(in.Word()), string(in.Note())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeAck(true), nil
|
||||
}
|
||||
}
|
||||
|
||||
func historyHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.History(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeHistory(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func chatListHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.ChatList(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeChatList(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func nudgeHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
||||
res, err := backend.Nudge(ctx, req.UserID, string(in.GameId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeChat(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user