Stage 17 #4: enrich the out-of-app your-turn push + add game-over
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The Telegram 'your turn' notification now names the opponent and recaps their last
move (voiced as the opponent: «{name}: my move — «WORD». Score 120:95» for a scoring
play; a short 'swapped / passed, your turn' otherwise), and a new game-over
notification reports the result + final score when a game ends by any path (closing
play, all-pass, resign, timeout). Scores are recipient-first (the reader's score
leads), 2-4 players (120:95:80).
- schema: YourTurnEvent gains opponent_name/last_action/last_word/score_line
(appended, backward-compatible); new GameOverEvent{result, score_line}. Go + UI
bindings regenerated (flatc 23.5.26 + pnpm codegen).
- backend: notify.YourTurn enriched + notify.GameOver; emitMove resolves the mover's
name and emits per-recipient (your_turn to the next mover, game_over to every seat),
with recipient-first score lines built in one place.
- gateway: game_over joins the out-of-app whitelist (routing.go).
- connector: render builds the enriched your_turn + game_over text per language (en/ru).
- tests: notify round-trip (enriched + game_over), emit (enriched fields + game_over to
all seats / per-seat result), connector render (en/ru), routing; integration replay
(play → your_turn with real name; resign → game_over) green.
- docs: ARCHITECTURE push catalog + out-of-app set, FUNCTIONAL (+ _ru), PLAN tracker.
This commit is contained in:
@@ -1425,8 +1425,18 @@ provided cert) at the contour caddy; prod VPN; rollback.
|
||||
`GameActionRequest` → an `Ack`); the lobby drops the card optimistically and keeps the cache
|
||||
in sync. Covered by an integration test (active→`ErrGameActive`, outsider→`ErrNotAPlayer`,
|
||||
per-account visibility, idempotent), a gateway transcode test, and a mock e2e (kebab → ❌).
|
||||
- **Deferred to the next PR (agreed):** #4 enrich the out-of-app "your turn" / game-end push
|
||||
with the opponent's name, last word and score.
|
||||
- **Enriched out-of-app push (#4, shipped):** the "your turn" Telegram notification now names
|
||||
the opponent and recaps their last move — voiced as the opponent, `«{name}: my move —
|
||||
«WORD». Score 120:95»` for a scoring play, or a short "swapped / passed, your turn" — and a
|
||||
new **game-over** notification reports the result + final score when a game ends (any path:
|
||||
closing play, all-pass, resign, timeout). Scores are **recipient-first** (the reader's
|
||||
score leads), 2- to 4-player (`120:95:80`). `YourTurnEvent` gained `opponent_name`/
|
||||
`last_action`/`last_word`/`score_line` (appended, backward-compatible) and a new
|
||||
`GameOverEvent` carries `result`/`score_line`; both emit per-recipient from the game commit
|
||||
(`emitMove`), join the out-of-app whitelist, and render per language (en/ru) in the Telegram
|
||||
connector. The backend resolves the mover's display name (the score line and result are
|
||||
built per recipient). Covered by notify round-trip, emit, connector-render (en/ru) and
|
||||
routing tests.
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/notify"
|
||||
fb "scrabble/pkg/fbs/scrabblefb"
|
||||
)
|
||||
|
||||
// recordingPublisher captures every published intent for assertions.
|
||||
@@ -29,13 +31,17 @@ func TestEmitMoveNotifiesActor(t *testing.T) {
|
||||
ToMove: 1,
|
||||
TurnStartedAt: time.Now(),
|
||||
TurnTimeout: time.Hour,
|
||||
Seats: []Seat{{Seat: 0, AccountID: actor}, {Seat: 1, AccountID: opp}},
|
||||
Seats: []Seat{{Seat: 0, AccountID: actor, Score: 19}, {Seat: 1, AccountID: opp, Score: 13}},
|
||||
}
|
||||
svc.emitMove(g, engine.MoveRecord{Player: 0, Action: engine.ActionPlay, Score: 10, Total: 10})
|
||||
svc.emitMove(context.Background(), g, engine.MoveRecord{Player: 0, Action: engine.ActionPlay, Words: []string{"HELLO"}, Score: 10, Total: 19})
|
||||
|
||||
kinds := map[uuid.UUID][]string{}
|
||||
var yourTurn notify.Intent
|
||||
for _, in := range pub.intents {
|
||||
kinds[in.UserID] = append(kinds[in.UserID], in.Kind)
|
||||
if in.UserID == opp && in.Kind == notify.KindYourTurn {
|
||||
yourTurn = in
|
||||
}
|
||||
}
|
||||
if !slices.Contains(kinds[actor], notify.KindOpponentMoved) {
|
||||
t.Errorf("actor should get opponent_moved, got %v", kinds[actor])
|
||||
@@ -49,4 +55,51 @@ func TestEmitMoveNotifiesActor(t *testing.T) {
|
||||
if slices.Contains(kinds[actor], notify.KindYourTurn) {
|
||||
t.Errorf("actor is not next to move, should not get your_turn")
|
||||
}
|
||||
// The your_turn push is enriched: the last move's action and word, and a recipient-first
|
||||
// score line (the next mover, seat 1, first). The opponent name needs the account store and
|
||||
// is left empty by this store-less unit (covered at the render layer).
|
||||
yt := fb.GetRootAsYourTurnEvent(yourTurn.Payload, 0)
|
||||
if got := string(yt.LastAction()); got != "play" {
|
||||
t.Errorf("your_turn last_action = %q, want play", got)
|
||||
}
|
||||
if got := string(yt.LastWord()); got != "HELLO" {
|
||||
t.Errorf("your_turn last_word = %q, want HELLO", got)
|
||||
}
|
||||
if got := string(yt.ScoreLine()); got != "13:19" { // seat 1 (recipient) first, then seat 0
|
||||
t.Errorf("your_turn score_line = %q, want 13:19", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmitMoveAnnouncesGameOver checks the closing move sends a game_over push to every seat,
|
||||
// each with its own outcome and a recipient-first final score.
|
||||
func TestEmitMoveAnnouncesGameOver(t *testing.T) {
|
||||
winner, loser := uuid.New(), uuid.New()
|
||||
pub := &recordingPublisher{}
|
||||
svc := &Service{pub: pub}
|
||||
g := Game{
|
||||
ID: uuid.New(),
|
||||
Status: StatusFinished,
|
||||
Players: 2,
|
||||
EndReason: "out_of_tiles",
|
||||
Seats: []Seat{{Seat: 0, AccountID: winner, Score: 120, IsWinner: true}, {Seat: 1, AccountID: loser, Score: 95}},
|
||||
}
|
||||
svc.emitMove(context.Background(), g, engine.MoveRecord{Player: 0, Action: engine.ActionPlay, Total: 120})
|
||||
|
||||
over := map[uuid.UUID]notify.Intent{}
|
||||
for _, in := range pub.intents {
|
||||
if in.Kind == notify.KindGameOver {
|
||||
over[in.UserID] = in
|
||||
}
|
||||
}
|
||||
if len(over) != 2 {
|
||||
t.Fatalf("game_over should reach both seats, got %d", len(over))
|
||||
}
|
||||
w := fb.GetRootAsGameOverEvent(over[winner].Payload, 0)
|
||||
if string(w.Result()) != "won" || string(w.ScoreLine()) != "120:95" {
|
||||
t.Errorf("winner game_over = %q / %q, want won / 120:95", w.Result(), w.ScoreLine())
|
||||
}
|
||||
l := fb.GetRootAsGameOverEvent(over[loser].Payload, 0)
|
||||
if string(l.Result()) != "lost" || string(l.ScoreLine()) != "95:120" {
|
||||
t.Errorf("loser game_over = %q / %q, want lost / 95:120", l.Result(), l.ScoreLine())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -350,7 +351,7 @@ func (svc *Service) commit(ctx context.Context, gameID uuid.UUID, g *engine.Game
|
||||
if err != nil {
|
||||
return Game{}, err
|
||||
}
|
||||
svc.emitMove(post, rec)
|
||||
svc.emitMove(ctx, post, rec)
|
||||
return post, nil
|
||||
}
|
||||
|
||||
@@ -361,20 +362,92 @@ func (svc *Service) commit(ctx context.Context, gameID uuid.UUID, g *engine.Game
|
||||
// out-of-app push), so the actor is not notified out of band about their own move.
|
||||
// Delivery is best-effort (notify.Publisher never blocks) and the gateway fans each
|
||||
// event out to all of the recipient's live streams.
|
||||
func (svc *Service) emitMove(post Game, rec engine.MoveRecord) {
|
||||
intents := make([]notify.Intent, 0, len(post.Seats)+1)
|
||||
func (svc *Service) emitMove(ctx context.Context, post Game, rec engine.MoveRecord) {
|
||||
intents := make([]notify.Intent, 0, 2*len(post.Seats))
|
||||
for _, s := range post.Seats {
|
||||
intents = append(intents, notify.OpponentMoved(s.AccountID, post.ID, rec.Player, rec.Action.String(), rec.Score, rec.Total))
|
||||
}
|
||||
if post.Status == StatusActive {
|
||||
switch post.Status {
|
||||
case StatusActive:
|
||||
if next, ok := seatAccount(post.Seats, post.ToMove); ok {
|
||||
deadline := post.TurnStartedAt.Add(post.TurnTimeout)
|
||||
intents = append(intents, notify.YourTurn(next, post.ID, deadline))
|
||||
action := rec.Action.String()
|
||||
word := ""
|
||||
if action == "play" && len(rec.Words) > 0 {
|
||||
word = rec.Words[0]
|
||||
}
|
||||
opponent := svc.displayName(ctx, post.Seats, rec.Player)
|
||||
intents = append(intents, notify.YourTurn(next, post.ID, deadline, opponent, action, word, scoreLine(post, post.ToMove)))
|
||||
}
|
||||
case StatusFinished:
|
||||
// The game just ended (any path: a closing play, all-pass, resign or timeout). Tell every
|
||||
// seat, each with their own perspective + recipient-first score, so an offline player gets
|
||||
// an out-of-app "game over" push (online players take it from the in-app refresh).
|
||||
for _, s := range post.Seats {
|
||||
intents = append(intents, notify.GameOver(s.AccountID, post.ID, seatResult(post.Seats, s.Seat), scoreLine(post, s.Seat)))
|
||||
}
|
||||
}
|
||||
svc.pub.Publish(intents...)
|
||||
}
|
||||
|
||||
// displayName resolves the display name of the account at the given seat, or "" when the seat
|
||||
// is absent or the lookup fails (the enriched push then falls back to its plain text).
|
||||
func (svc *Service) displayName(ctx context.Context, seats []Seat, seat int) string {
|
||||
if svc.accounts == nil {
|
||||
return ""
|
||||
}
|
||||
id, ok := seatAccount(seats, seat)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
acc, err := svc.accounts.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return acc.DisplayName
|
||||
}
|
||||
|
||||
// scoreLine formats the running scores with recipientSeat's score first, then the remaining
|
||||
// seats in seat order, colon-joined (e.g. "120:95:80") — the recipient-first form used in the
|
||||
// out-of-app notifications.
|
||||
func scoreLine(g Game, recipientSeat int) string {
|
||||
n := len(g.Seats)
|
||||
bySeat := make([]int, n)
|
||||
for _, s := range g.Seats {
|
||||
if s.Seat >= 0 && s.Seat < n {
|
||||
bySeat[s.Seat] = s.Score
|
||||
}
|
||||
}
|
||||
parts := make([]string, 0, n)
|
||||
if recipientSeat >= 0 && recipientSeat < n {
|
||||
parts = append(parts, strconv.Itoa(bySeat[recipientSeat]))
|
||||
}
|
||||
for seat := 0; seat < n; seat++ {
|
||||
if seat != recipientSeat {
|
||||
parts = append(parts, strconv.Itoa(bySeat[seat]))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
// seatResult reports the finished-game outcome from recipientSeat's perspective: "draw" when no
|
||||
// seat is flagged the winner, "won" when recipientSeat is, otherwise "lost".
|
||||
func seatResult(seats []Seat, recipientSeat int) string {
|
||||
winner := false
|
||||
for _, s := range seats {
|
||||
if s.IsWinner {
|
||||
winner = true
|
||||
if s.Seat == recipientSeat {
|
||||
return "won"
|
||||
}
|
||||
}
|
||||
}
|
||||
if !winner {
|
||||
return "draw"
|
||||
}
|
||||
return "lost"
|
||||
}
|
||||
|
||||
// seatAccount returns the account seated at the given seat index, or false when
|
||||
// no seat matches (the slice is not assumed to be ordered by seat).
|
||||
func seatAccount(seats []Seat, seat int) (uuid.UUID, bool) {
|
||||
|
||||
@@ -13,18 +13,45 @@ import (
|
||||
// the payload with the shared scrabblefb schema. Keeping the encoding here lets
|
||||
// the game/social/lobby services emit events without importing the wire schema.
|
||||
|
||||
// YourTurn announces to userID that it is their turn in game gameID, with the
|
||||
// turn's nominal deadline.
|
||||
func YourTurn(userID, gameID uuid.UUID, deadline time.Time) Intent {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
// YourTurn announces to userID that it is their turn in game gameID, with the turn's nominal
|
||||
// deadline. opponentName, lastAction, lastWord and scoreLine enrich the out-of-app push (Stage
|
||||
// 17): the player who just moved, their move kind, the main word of a scoring play (empty
|
||||
// otherwise) and the recipient-first running score line. Empty strings render the plain "your
|
||||
// turn" text.
|
||||
func YourTurn(userID, gameID uuid.UUID, deadline time.Time, opponentName, lastAction, lastWord, scoreLine string) Intent {
|
||||
b := flatbuffers.NewBuilder(128)
|
||||
gid := b.CreateString(gameID.String())
|
||||
name := b.CreateString(opponentName)
|
||||
action := b.CreateString(lastAction)
|
||||
word := b.CreateString(lastWord)
|
||||
score := b.CreateString(scoreLine)
|
||||
fb.YourTurnEventStart(b)
|
||||
fb.YourTurnEventAddGameId(b, gid)
|
||||
fb.YourTurnEventAddDeadlineUnix(b, deadline.Unix())
|
||||
fb.YourTurnEventAddOpponentName(b, name)
|
||||
fb.YourTurnEventAddLastAction(b, action)
|
||||
fb.YourTurnEventAddLastWord(b, word)
|
||||
fb.YourTurnEventAddScoreLine(b, score)
|
||||
b.Finish(fb.YourTurnEventEnd(b))
|
||||
return Intent{UserID: userID, Kind: KindYourTurn, Payload: b.FinishedBytes(), EventID: eventID()}
|
||||
}
|
||||
|
||||
// GameOver announces to userID that game gameID finished. result is the outcome from userID's
|
||||
// own perspective ("won"/"lost"/"draw") and scoreLine is the recipient-first final score; both
|
||||
// feed the out-of-app "game over" push (Stage 17).
|
||||
func GameOver(userID, gameID uuid.UUID, result, scoreLine string) Intent {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
gid := b.CreateString(gameID.String())
|
||||
res := b.CreateString(result)
|
||||
score := b.CreateString(scoreLine)
|
||||
fb.GameOverEventStart(b)
|
||||
fb.GameOverEventAddGameId(b, gid)
|
||||
fb.GameOverEventAddResult(b, res)
|
||||
fb.GameOverEventAddScoreLine(b, score)
|
||||
b.Finish(fb.GameOverEventEnd(b))
|
||||
return Intent{UserID: userID, Kind: KindGameOver, Payload: b.FinishedBytes(), EventID: eventID()}
|
||||
}
|
||||
|
||||
// OpponentMoved tells userID that seat just committed a move in game gameID,
|
||||
// summarising it (the client refetches the full state).
|
||||
func OpponentMoved(userID, gameID uuid.UUID, seat int, action string, score, total int) Intent {
|
||||
|
||||
@@ -27,6 +27,9 @@ const (
|
||||
// KindNotification is a lightweight "re-poll your lobby counters" signal
|
||||
// (incoming friend requests, invitations) that drives the lobby badge.
|
||||
KindNotification = "notify"
|
||||
// KindGameOver announces a finished game to each seated player, driving the
|
||||
// out-of-app "game over" push (Stage 17).
|
||||
KindGameOver = "game_over"
|
||||
)
|
||||
|
||||
// Notification sub-kinds carried in a KindNotification event payload; the client
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestNopPublisherDiscards(t *testing.T) {
|
||||
|
||||
func TestYourTurnPayloadRoundTrips(t *testing.T) {
|
||||
uid, gid := uuid.New(), uuid.New()
|
||||
in := notify.YourTurn(uid, gid, time.Unix(1717000000, 0))
|
||||
in := notify.YourTurn(uid, gid, time.Unix(1717000000, 0), "Ann", "play", "STOOL", "120:95")
|
||||
if in.UserID != uid || in.Kind != notify.KindYourTurn || in.EventID == "" {
|
||||
t.Fatalf("intent metadata wrong: %+v", in)
|
||||
}
|
||||
@@ -72,6 +72,23 @@ func TestYourTurnPayloadRoundTrips(t *testing.T) {
|
||||
if got := ev.DeadlineUnix(); got != 1717000000 {
|
||||
t.Fatalf("deadline = %d, want 1717000000", got)
|
||||
}
|
||||
if string(ev.OpponentName()) != "Ann" || string(ev.LastAction()) != "play" ||
|
||||
string(ev.LastWord()) != "STOOL" || string(ev.ScoreLine()) != "120:95" {
|
||||
t.Fatalf("enriched fields wrong: name=%q action=%q word=%q score=%q",
|
||||
ev.OpponentName(), ev.LastAction(), ev.LastWord(), ev.ScoreLine())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameOverPayloadRoundTrips(t *testing.T) {
|
||||
uid, gid := uuid.New(), uuid.New()
|
||||
in := notify.GameOver(uid, gid, "won", "120:95:80")
|
||||
if in.UserID != uid || in.Kind != notify.KindGameOver || in.EventID == "" {
|
||||
t.Fatalf("intent metadata wrong: %+v", in)
|
||||
}
|
||||
ev := fb.GetRootAsGameOverEvent(in.Payload, 0)
|
||||
if string(ev.GameId()) != gid.String() || string(ev.Result()) != "won" || string(ev.ScoreLine()) != "120:95:80" {
|
||||
t.Fatalf("game_over fields wrong: game=%q result=%q score=%q", ev.GameId(), ev.Result(), ev.ScoreLine())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpponentMovedPayloadRoundTrips(t *testing.T) {
|
||||
|
||||
@@ -485,7 +485,12 @@ in-app only, so the actor gets no out-of-app push for their own move), **chat-me
|
||||
friend-added, friend-declined, invitation or game-started; emitted on a friend-request,
|
||||
on answering one (accept → friend-added, decline → friend-declined — to the original
|
||||
requester, so a game screen watching that opponent re-derives its "add to friends" state,
|
||||
Stage 17), and on an invitation create or its game start). Event payloads are FlatBuffers-encoded by
|
||||
Stage 17), and on an invitation create or its game start). Stage 17 added **game-over** (emitted to every
|
||||
seat from the same game commit when a game finishes — any path: a closing play, all-pass,
|
||||
resign or timeout) and **enriched your-turn** so the out-of-app push reads in full: it now
|
||||
also carries the mover's display name, their last action and the main word of a scoring play,
|
||||
and a **recipient-first** running score line (e.g. `120:95:80`, the reader's score first).
|
||||
Event payloads are FlatBuffers-encoded by
|
||||
the backend and forwarded verbatim. A client that is not currently streaming falls
|
||||
back to the matchmaker's `Poll` for match-found and, for the lobby **notification
|
||||
badge** (incoming friend requests + open invitations), the client polls on lobby
|
||||
@@ -499,7 +504,7 @@ back to the interface language — and the `notifications_in_app_only` flag) and
|
||||
button — only when the recipient has a Telegram identity and has not confined
|
||||
notifications to the app, so the two channels never duplicate. The connector routes by
|
||||
that language to the matching bot and renders the message in it. The out-of-app set is
|
||||
your-turn, nudge, match-found and the invitation / friend-request notify sub-kinds;
|
||||
your-turn, game-over, nudge, match-found and the invitation / friend-request notify sub-kinds;
|
||||
the connector renders the message and skips the rest. Operator broadcasts
|
||||
(`SendToUser` / `SendToGameChannel`, §10 admin) instead pick the bot by an
|
||||
**operator-chosen** language in the console, unrelated to the recipient's login. Session-revocation events and
|
||||
|
||||
+6
-3
@@ -41,9 +41,12 @@ sets their offered languages and is the bot their out-of-app notifications come
|
||||
joined a game and has been idle past the retention window is garbage-collected. While the app is open the client
|
||||
keeps a live stream and receives in-app updates in real time — the opponent's move,
|
||||
your turn, chat, nudges and a found match. When the app is **closed**, the chosen
|
||||
out-of-app events (your turn, nudge, a found match, an invitation or friend request)
|
||||
arrive as a **Telegram notification** instead — unless the player keeps notifications
|
||||
in the app only (a profile setting, **on by default**).
|
||||
out-of-app events (your turn, game over, nudge, a found match, an invitation or friend
|
||||
request) arrive as a **Telegram notification** instead — unless the player keeps
|
||||
notifications in the app only (a profile setting, **on by default**). The "your turn"
|
||||
notification names the opponent and recaps their last move — the word and the running score
|
||||
for a scoring play, or that they swapped or passed — and a finished game sends a "game over"
|
||||
notification with your result and the final score (scores read with yours first).
|
||||
|
||||
### Accounts, linking & merge *(Stage 1 / 11)*
|
||||
First platform contact auto-provisions a durable account. From the profile a player
|
||||
|
||||
@@ -42,9 +42,13 @@ Mini App** авторизует по подписанным `initData` плат
|
||||
в одну игру и простаивавший дольше окна удержания, удаляется сборщиком. Пока приложение открыто, клиент
|
||||
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,
|
||||
чат, nudge и найденный матч. Когда приложение **закрыто**, выбранные внеприложенческие
|
||||
события (ваш ход, nudge, найденный матч, приглашение или заявка в друзья) приходят
|
||||
вместо этого **уведомлением в Telegram** — если только игрок не оставил уведомления
|
||||
только в приложении (настройка профиля, **включена по умолчанию**).
|
||||
события (ваш ход, конец партии, nudge, найденный матч, приглашение или заявка в друзья)
|
||||
приходят вместо этого **уведомлением в Telegram** — если только игрок не оставил
|
||||
уведомления только в приложении (настройка профиля, **включена по умолчанию**).
|
||||
Уведомление «ваш ход» называет соперника и пересказывает его последний ход — слово и
|
||||
текущий счёт для результативного хода либо что он поменял фишки или пропустил, — а по
|
||||
завершении партии приходит уведомление «конец партии» с твоим результатом и финальным
|
||||
счётом (счёт читается, твой первым).
|
||||
|
||||
### Аккаунты, привязка и слияние *(Stage 1 / 11)*
|
||||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||||
|
||||
@@ -8,6 +8,7 @@ var outOfAppKinds = map[string]bool{
|
||||
"nudge": true,
|
||||
"match_found": true,
|
||||
"notify": true,
|
||||
"game_over": true,
|
||||
}
|
||||
|
||||
// OutOfAppKind reports whether a push kind is eligible for out-of-app delivery.
|
||||
|
||||
@@ -3,7 +3,7 @@ package connector
|
||||
import "testing"
|
||||
|
||||
func TestOutOfAppKind(t *testing.T) {
|
||||
out := []string{"your_turn", "nudge", "match_found", "notify"}
|
||||
out := []string{"your_turn", "game_over", "nudge", "match_found", "notify"}
|
||||
for _, k := range out {
|
||||
if !OutOfAppKind(k) {
|
||||
t.Errorf("OutOfAppKind(%q) = false, want true", k)
|
||||
|
||||
+18
-1
@@ -472,10 +472,27 @@ table GcgExport {
|
||||
|
||||
// --- push event payloads ---
|
||||
|
||||
// YourTurnEvent signals that it is now the recipient's turn.
|
||||
// YourTurnEvent signals that it is now the recipient's turn. The trailing fields enrich the
|
||||
// out-of-app push (Stage 17): opponent_name is the player who just moved, last_action is their
|
||||
// move kind ("play"/"pass"/"exchange"/...), last_word is the main word of a scoring play (empty
|
||||
// otherwise), and score_line is the recipient-first running score (e.g. "120:95:80"). They are
|
||||
// appended (FlatBuffers-optional), so an older reader that only needs game_id/deadline is unaffected.
|
||||
table YourTurnEvent {
|
||||
game_id:string;
|
||||
deadline_unix:long;
|
||||
opponent_name:string;
|
||||
last_action:string;
|
||||
last_word:string;
|
||||
score_line:string;
|
||||
}
|
||||
|
||||
// GameOverEvent signals that a game the recipient is seated in has finished, driving the
|
||||
// out-of-app "game over" push (Stage 17). result is the outcome from the recipient's own
|
||||
// perspective ("won"/"lost"/"draw"); score_line is the recipient-first final score.
|
||||
table GameOverEvent {
|
||||
game_id:string;
|
||||
result:string;
|
||||
score_line:string;
|
||||
}
|
||||
|
||||
// OpponentMovedEvent summarises a move another seat just committed; the client
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package scrabblefb
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type GameOverEvent struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsGameOverEvent(buf []byte, offset flatbuffers.UOffsetT) *GameOverEvent {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &GameOverEvent{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishGameOverEventBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsGameOverEvent(buf []byte, offset flatbuffers.UOffsetT) *GameOverEvent {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &GameOverEvent{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedGameOverEventBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *GameOverEvent) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *GameOverEvent) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *GameOverEvent) GameId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *GameOverEvent) Result() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *GameOverEvent) ScoreLine() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GameOverEventStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(3)
|
||||
}
|
||||
func GameOverEventAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func GameOverEventAddResult(builder *flatbuffers.Builder, result flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(result), 0)
|
||||
}
|
||||
func GameOverEventAddScoreLine(builder *flatbuffers.Builder, scoreLine flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(scoreLine), 0)
|
||||
}
|
||||
func GameOverEventEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -61,8 +61,40 @@ func (rcv *YourTurnEvent) MutateDeadlineUnix(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(6, n)
|
||||
}
|
||||
|
||||
func (rcv *YourTurnEvent) OpponentName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *YourTurnEvent) LastAction() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *YourTurnEvent) LastWord() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *YourTurnEvent) ScoreLine() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func YourTurnEventStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
builder.StartObject(6)
|
||||
}
|
||||
func YourTurnEventAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
@@ -70,6 +102,18 @@ func YourTurnEventAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOf
|
||||
func YourTurnEventAddDeadlineUnix(builder *flatbuffers.Builder, deadlineUnix int64) {
|
||||
builder.PrependInt64Slot(1, deadlineUnix, 0)
|
||||
}
|
||||
func YourTurnEventAddOpponentName(builder *flatbuffers.Builder, opponentName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(opponentName), 0)
|
||||
}
|
||||
func YourTurnEventAddLastAction(builder *flatbuffers.Builder, lastAction flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(lastAction), 0)
|
||||
}
|
||||
func YourTurnEventAddLastWord(builder *flatbuffers.Builder, lastWord flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(lastWord), 0)
|
||||
}
|
||||
func YourTurnEventAddScoreLine(builder *flatbuffers.Builder, scoreLine flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(scoreLine), 0)
|
||||
}
|
||||
func YourTurnEventEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Package render turns a backend push event into a localized Telegram message with
|
||||
// a Mini App deep-link. Only the out-of-app push set is rendered (your_turn, nudge,
|
||||
// match_found, and the invitation / friend_request notify sub-kinds); every other
|
||||
// a Mini App deep-link. Only the out-of-app push set is rendered (your_turn, game_over,
|
||||
// nudge, match_found, and the invitation / friend_request notify sub-kinds); every other
|
||||
// kind returns ok=false so the connector skips it (the in-app stream still carries
|
||||
// it).
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"scrabble/pkg/fbs/scrabblefb"
|
||||
"scrabble/platform/telegram/internal/deeplink"
|
||||
)
|
||||
@@ -29,7 +31,10 @@ func Render(kind string, payload []byte, lang string) (Message, bool) {
|
||||
switch kind {
|
||||
case "your_turn":
|
||||
ev := scrabblefb.GetRootAsYourTurnEvent(payload, 0)
|
||||
return Message{Text: p.yourTurn, ButtonText: p.openGame, StartParam: deeplink.Game(string(ev.GameId()))}, true
|
||||
return Message{Text: yourTurnText(ev, p), ButtonText: p.openGame, StartParam: deeplink.Game(string(ev.GameId()))}, true
|
||||
case "game_over":
|
||||
ev := scrabblefb.GetRootAsGameOverEvent(payload, 0)
|
||||
return Message{Text: gameOverText(ev, p), ButtonText: p.openGame, StartParam: deeplink.Game(string(ev.GameId()))}, true
|
||||
case "nudge":
|
||||
ev := scrabblefb.GetRootAsNudgeEvent(payload, 0)
|
||||
return Message{Text: p.nudge, ButtonText: p.openGame, StartParam: deeplink.Game(string(ev.GameId()))}, true
|
||||
@@ -48,33 +53,92 @@ func Render(kind string, payload []byte, lang string) (Message, bool) {
|
||||
return Message{}, false
|
||||
}
|
||||
|
||||
// phrases is one language's message catalog.
|
||||
// yourTurnText renders the enriched "your turn" body (Stage 17), voiced as the opponent who
|
||||
// just moved ("{name}: my move — «WORD». Score 120:95"). It falls back to the plain phrase when
|
||||
// the opponent name is missing (an older backend, or an unresolved name).
|
||||
func yourTurnText(ev *scrabblefb.YourTurnEvent, p phrases) string {
|
||||
name := string(ev.OpponentName())
|
||||
if name == "" {
|
||||
return p.yourTurn
|
||||
}
|
||||
switch string(ev.LastAction()) {
|
||||
case "play":
|
||||
if word := string(ev.LastWord()); word != "" {
|
||||
return fmt.Sprintf(p.yourTurnPlay, name, word, string(ev.ScoreLine()))
|
||||
}
|
||||
return fmt.Sprintf(p.yourTurnMoved, name)
|
||||
case "exchange":
|
||||
return fmt.Sprintf(p.yourTurnExchange, name)
|
||||
case "pass":
|
||||
return fmt.Sprintf(p.yourTurnPass, name)
|
||||
default:
|
||||
return p.yourTurn
|
||||
}
|
||||
}
|
||||
|
||||
// gameOverText renders the "game over" body (Stage 17) from the recipient's own perspective.
|
||||
func gameOverText(ev *scrabblefb.GameOverEvent, p phrases) string {
|
||||
score := string(ev.ScoreLine())
|
||||
switch string(ev.Result()) {
|
||||
case "won":
|
||||
return fmt.Sprintf(p.gameOverWon, score)
|
||||
case "lost":
|
||||
return fmt.Sprintf(p.gameOverLost, score)
|
||||
default:
|
||||
return fmt.Sprintf(p.gameOverDraw, score)
|
||||
}
|
||||
}
|
||||
|
||||
// phrases is one language's message catalog. The yourTurn*/gameOver* entries are fmt format
|
||||
// strings: yourTurnPlay takes (name, word, scoreLine); yourTurnExchange/Pass/Moved take (name);
|
||||
// the gameOver* entries take (scoreLine).
|
||||
type phrases struct {
|
||||
yourTurn string
|
||||
nudge string
|
||||
matchFound string
|
||||
invitation string
|
||||
friendRequest string
|
||||
openGame string
|
||||
open string
|
||||
yourTurn string
|
||||
yourTurnPlay string
|
||||
yourTurnExchange string
|
||||
yourTurnPass string
|
||||
yourTurnMoved string
|
||||
gameOverWon string
|
||||
gameOverLost string
|
||||
gameOverDraw string
|
||||
nudge string
|
||||
matchFound string
|
||||
invitation string
|
||||
friendRequest string
|
||||
openGame string
|
||||
open string
|
||||
}
|
||||
|
||||
var english = phrases{
|
||||
yourTurn: "It's your turn.",
|
||||
nudge: "You were nudged — it's your turn.",
|
||||
matchFound: "Your game is ready.",
|
||||
invitation: "You have a new game invitation.",
|
||||
friendRequest: "You have a new friend request.",
|
||||
openGame: "Open game",
|
||||
open: "Open",
|
||||
yourTurn: "It's your turn.",
|
||||
yourTurnPlay: "%s: my move — «%s». Score %s",
|
||||
yourTurnExchange: "%s: swapping tiles, your turn.",
|
||||
yourTurnPass: "%s: passing, your turn.",
|
||||
yourTurnMoved: "%s moved, your turn.",
|
||||
gameOverWon: "Game over — you won! Score %s",
|
||||
gameOverLost: "Game over — you lost. Score %s",
|
||||
gameOverDraw: "Game over — a draw. Score %s",
|
||||
nudge: "You were nudged — it's your turn.",
|
||||
matchFound: "Your game is ready.",
|
||||
invitation: "You have a new game invitation.",
|
||||
friendRequest: "You have a new friend request.",
|
||||
openGame: "Open game",
|
||||
open: "Open",
|
||||
}
|
||||
|
||||
var russian = phrases{
|
||||
yourTurn: "Ваш ход.",
|
||||
nudge: "Вас поторопили — ваш ход.",
|
||||
matchFound: "Игра найдена.",
|
||||
invitation: "Вас пригласили в игру.",
|
||||
friendRequest: "Вам пришла заявка в друзья.",
|
||||
openGame: "Открыть игру",
|
||||
open: "Открыть",
|
||||
yourTurn: "Ваш ход.",
|
||||
yourTurnPlay: "%s: мой ход — «%s». Счёт %s",
|
||||
yourTurnExchange: "%s: меняю фишки, ваш ход.",
|
||||
yourTurnPass: "%s: пропускаю ход, ваш ход.",
|
||||
yourTurnMoved: "%s сходил(а), ваш ход.",
|
||||
gameOverWon: "Игра окончена — вы выиграли! Счёт %s",
|
||||
gameOverLost: "Игра окончена — вы проиграли. Счёт %s",
|
||||
gameOverDraw: "Игра окончена — ничья. Счёт %s",
|
||||
nudge: "Вас поторопили — ваш ход.",
|
||||
matchFound: "Игра найдена.",
|
||||
invitation: "Вас пригласили в игру.",
|
||||
friendRequest: "Вам пришла заявка в друзья.",
|
||||
openGame: "Открыть игру",
|
||||
open: "Открыть",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
@@ -46,6 +47,86 @@ func notifyPayload(kind string) []byte {
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
func enrichedYourTurnPayload(id, name, action, word, score string) []byte {
|
||||
b := flatbuffers.NewBuilder(0)
|
||||
gid := b.CreateString(id)
|
||||
n := b.CreateString(name)
|
||||
a := b.CreateString(action)
|
||||
w := b.CreateString(word)
|
||||
s := b.CreateString(score)
|
||||
scrabblefb.YourTurnEventStart(b)
|
||||
scrabblefb.YourTurnEventAddGameId(b, gid)
|
||||
scrabblefb.YourTurnEventAddOpponentName(b, n)
|
||||
scrabblefb.YourTurnEventAddLastAction(b, a)
|
||||
scrabblefb.YourTurnEventAddLastWord(b, w)
|
||||
scrabblefb.YourTurnEventAddScoreLine(b, s)
|
||||
b.Finish(scrabblefb.YourTurnEventEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
func gameOverPayload(id, result, score string) []byte {
|
||||
b := flatbuffers.NewBuilder(0)
|
||||
gid := b.CreateString(id)
|
||||
r := b.CreateString(result)
|
||||
s := b.CreateString(score)
|
||||
scrabblefb.GameOverEventStart(b)
|
||||
scrabblefb.GameOverEventAddGameId(b, gid)
|
||||
scrabblefb.GameOverEventAddResult(b, r)
|
||||
scrabblefb.GameOverEventAddScoreLine(b, s)
|
||||
b.Finish(scrabblefb.GameOverEventEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// TestRenderYourTurnEnriched checks the enriched "your turn" body names the opponent, their word
|
||||
// and the recipient-first score for a scoring play, and uses the shorter phrase for an exchange.
|
||||
func TestRenderYourTurnEnriched(t *testing.T) {
|
||||
en, ok := Render("your_turn", enrichedYourTurnPayload(gameID, "Ann", "play", "STOOL", "120:95"), "en")
|
||||
if !ok {
|
||||
t.Fatal("expected ok")
|
||||
}
|
||||
for _, want := range []string{"Ann", "STOOL", "120:95"} {
|
||||
if !strings.Contains(en.Text, want) {
|
||||
t.Errorf("en play text %q missing %q", en.Text, want)
|
||||
}
|
||||
}
|
||||
ru, _ := Render("your_turn", enrichedYourTurnPayload(gameID, "Аня", "play", "СТОЛ", "120:95"), "ru")
|
||||
for _, want := range []string{"Аня", "СТОЛ", "120:95", "мой ход"} {
|
||||
if !strings.Contains(ru.Text, want) {
|
||||
t.Errorf("ru play text %q missing %q", ru.Text, want)
|
||||
}
|
||||
}
|
||||
ex, _ := Render("your_turn", enrichedYourTurnPayload(gameID, "Ann", "exchange", "", ""), "en")
|
||||
if !strings.Contains(ex.Text, "Ann") || strings.Contains(ex.Text, "«") {
|
||||
t.Errorf("en exchange text %q should name the opponent and carry no word", ex.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderGameOver checks the game_over body reads from the recipient's perspective in both
|
||||
// languages and carries the game deep-link.
|
||||
func TestRenderGameOver(t *testing.T) {
|
||||
cases := []struct{ result, en, ru string }{
|
||||
{"won", "won", "выиграли"},
|
||||
{"lost", "lost", "проиграли"},
|
||||
{"draw", "draw", "ничья"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
en, ok := Render("game_over", gameOverPayload(gameID, tc.result, "120:95"), "en")
|
||||
if !ok {
|
||||
t.Fatalf("%s: expected ok", tc.result)
|
||||
}
|
||||
if en.StartParam != "g"+gameID {
|
||||
t.Errorf("%s StartParam = %q, want g%s", tc.result, en.StartParam, gameID)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(en.Text), tc.en) || !strings.Contains(en.Text, "120:95") {
|
||||
t.Errorf("en %s text %q", tc.result, en.Text)
|
||||
}
|
||||
ru, _ := Render("game_over", gameOverPayload(gameID, tc.result, "120:95"), "ru")
|
||||
if !strings.Contains(ru.Text, tc.ru) {
|
||||
t.Errorf("ru %s text %q missing %q", tc.result, ru.Text, tc.ru)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderGameEvents(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, kind string
|
||||
|
||||
@@ -23,6 +23,7 @@ export { FriendList } from './scrabblefb/friend-list.js';
|
||||
export { FriendRespondRequest } from './scrabblefb/friend-respond-request.js';
|
||||
export { GameActionRequest } from './scrabblefb/game-action-request.js';
|
||||
export { GameList } from './scrabblefb/game-list.js';
|
||||
export { GameOverEvent } from './scrabblefb/game-over-event.js';
|
||||
export { GameView } from './scrabblefb/game-view.js';
|
||||
export { GcgExport } from './scrabblefb/gcg-export.js';
|
||||
export { GuestLoginRequest } from './scrabblefb/guest-login-request.js';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
export class GameOverEvent {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):GameOverEvent {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsGameOverEvent(bb:flatbuffers.ByteBuffer, obj?:GameOverEvent):GameOverEvent {
|
||||
return (obj || new GameOverEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsGameOverEvent(bb:flatbuffers.ByteBuffer, obj?:GameOverEvent):GameOverEvent {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new GameOverEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId():string|null
|
||||
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
result():string|null
|
||||
result(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
result(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
scoreLine():string|null
|
||||
scoreLine(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
scoreLine(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startGameOverEvent(builder:flatbuffers.Builder) {
|
||||
builder.startObject(3);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addResult(builder:flatbuffers.Builder, resultOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, resultOffset, 0);
|
||||
}
|
||||
|
||||
static addScoreLine(builder:flatbuffers.Builder, scoreLineOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, scoreLineOffset, 0);
|
||||
}
|
||||
|
||||
static endGameOverEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createGameOverEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, resultOffset:flatbuffers.Offset, scoreLineOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
GameOverEvent.startGameOverEvent(builder);
|
||||
GameOverEvent.addGameId(builder, gameIdOffset);
|
||||
GameOverEvent.addResult(builder, resultOffset);
|
||||
GameOverEvent.addScoreLine(builder, scoreLineOffset);
|
||||
return GameOverEvent.endGameOverEvent(builder);
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,36 @@ deadlineUnix():bigint {
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
opponentName():string|null
|
||||
opponentName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
opponentName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
lastAction():string|null
|
||||
lastAction(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
lastAction(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
lastWord():string|null
|
||||
lastWord(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
lastWord(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 12);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
scoreLine():string|null
|
||||
scoreLine(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
scoreLine(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 14);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startYourTurnEvent(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
builder.startObject(6);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
@@ -44,15 +72,35 @@ static addDeadlineUnix(builder:flatbuffers.Builder, deadlineUnix:bigint) {
|
||||
builder.addFieldInt64(1, deadlineUnix, BigInt('0'));
|
||||
}
|
||||
|
||||
static addOpponentName(builder:flatbuffers.Builder, opponentNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, opponentNameOffset, 0);
|
||||
}
|
||||
|
||||
static addLastAction(builder:flatbuffers.Builder, lastActionOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(3, lastActionOffset, 0);
|
||||
}
|
||||
|
||||
static addLastWord(builder:flatbuffers.Builder, lastWordOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(4, lastWordOffset, 0);
|
||||
}
|
||||
|
||||
static addScoreLine(builder:flatbuffers.Builder, scoreLineOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(5, scoreLineOffset, 0);
|
||||
}
|
||||
|
||||
static endYourTurnEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createYourTurnEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, deadlineUnix:bigint):flatbuffers.Offset {
|
||||
static createYourTurnEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, deadlineUnix:bigint, opponentNameOffset:flatbuffers.Offset, lastActionOffset:flatbuffers.Offset, lastWordOffset:flatbuffers.Offset, scoreLineOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
YourTurnEvent.startYourTurnEvent(builder);
|
||||
YourTurnEvent.addGameId(builder, gameIdOffset);
|
||||
YourTurnEvent.addDeadlineUnix(builder, deadlineUnix);
|
||||
YourTurnEvent.addOpponentName(builder, opponentNameOffset);
|
||||
YourTurnEvent.addLastAction(builder, lastActionOffset);
|
||||
YourTurnEvent.addLastWord(builder, lastWordOffset);
|
||||
YourTurnEvent.addScoreLine(builder, scoreLineOffset);
|
||||
return YourTurnEvent.endYourTurnEvent(builder);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user