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

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:
Ilia Denisov
2026-06-09 01:15:18 +02:00
parent 6956dad354
commit f166ff30fe
19 changed files with 657 additions and 52 deletions
+89 -25
View File
@@ -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: "Открыть",
}