package notify import ( "time" flatbuffers "github.com/google/flatbuffers/go" "github.com/google/uuid" "scrabble/backend/internal/engine" fb "scrabble/pkg/fbs/scrabblefb" ) // The constructors below build one Intent per live event, FlatBuffers-encoding // 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. 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. moveCount is the post-move count, which the client compares against its cached // game to detect a missed in-app move and fall back to a refetch (R4). func YourTurn(userID, gameID uuid.UUID, deadline time.Time, opponentName, lastAction, lastWord, scoreLine string, moveCount int) 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) fb.YourTurnEventAddMoveCount(b, int32(moveCount)) 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). game is the final post-game summary (the // adjusted scores after rack penalties and the winner flag), so an in-app client settles the // finished game from the event without a refetch (R4). func GameOver(userID, gameID uuid.UUID, result, scoreLine string, game GameSummary) Intent { b := flatbuffers.NewBuilder(512) gid := b.CreateString(gameID.String()) res := b.CreateString(result) score := b.CreateString(scoreLine) gameOff := buildGameView(b, game) fb.GameOverEventStart(b) fb.GameOverEventAddGameId(b, gid) fb.GameOverEventAddResult(b, res) fb.GameOverEventAddScoreLine(b, score) fb.GameOverEventAddGame(b, gameOff) b.Finish(fb.GameOverEventEnd(b)) return Intent{UserID: userID, Kind: KindGameOver, Payload: b.FinishedBytes(), EventID: eventID()} } // OpponentMoved tells userID that move was just committed in game gameID, carrying it as a delta // the client applies to its cached game without a refetch (R4): move is the decoded play/pass/ // exchange, game is the post-move summary (per-seat scores, to_move, move_count, status) and // bagLen is the bag size after the draw. The seat/action/score/total scalars repeat the move's // summary for pre-R4 wire back-compat. func OpponentMoved(userID, gameID uuid.UUID, move engine.MoveRecord, game GameSummary, bagLen int) Intent { b := flatbuffers.NewBuilder(512) gid := b.CreateString(gameID.String()) act := b.CreateString(move.Action.String()) moveOff := buildMoveRecord(b, move) gameOff := buildGameView(b, game) fb.OpponentMovedEventStart(b) fb.OpponentMovedEventAddGameId(b, gid) fb.OpponentMovedEventAddSeat(b, int32(move.Player)) fb.OpponentMovedEventAddAction(b, act) fb.OpponentMovedEventAddScore(b, int32(move.Score)) fb.OpponentMovedEventAddTotal(b, int32(move.Total)) fb.OpponentMovedEventAddMove(b, moveOff) fb.OpponentMovedEventAddGame(b, gameOff) fb.OpponentMovedEventAddBagLen(b, int32(bagLen)) b.Finish(fb.OpponentMovedEventEnd(b)) return Intent{UserID: userID, Kind: KindOpponentMoved, Payload: b.FinishedBytes(), EventID: eventID()} } // ChatMessage delivers a stored chat message (or nudge) to userID. func ChatMessage(userID, gameID, senderID uuid.UUID, id, kind, body string, createdAt time.Time) Intent { b := flatbuffers.NewBuilder(128) idOff := b.CreateString(id) gid := b.CreateString(gameID.String()) sid := b.CreateString(senderID.String()) kindOff := b.CreateString(kind) bodyOff := b.CreateString(body) fb.ChatMessageStart(b) fb.ChatMessageAddId(b, idOff) fb.ChatMessageAddGameId(b, gid) fb.ChatMessageAddSenderId(b, sid) fb.ChatMessageAddKind(b, kindOff) fb.ChatMessageAddBody(b, bodyOff) fb.ChatMessageAddCreatedAtUnix(b, createdAt.Unix()) b.Finish(fb.ChatMessageEnd(b)) return Intent{UserID: userID, Kind: KindChatMessage, Payload: b.FinishedBytes(), EventID: eventID()} } // Nudge tells userID that fromUserID nudged them in game gameID. func Nudge(userID, gameID, fromUserID uuid.UUID) Intent { b := flatbuffers.NewBuilder(64) gid := b.CreateString(gameID.String()) from := b.CreateString(fromUserID.String()) fb.NudgeEventStart(b) fb.NudgeEventAddGameId(b, gid) fb.NudgeEventAddFromUserId(b, from) b.Finish(fb.NudgeEventEnd(b)) return Intent{UserID: userID, Kind: KindNudge, Payload: b.FinishedBytes(), EventID: eventID()} } // MatchFound tells userID that game gameID, which they are seated in, has started (an auto-match // pairing or a robot substitution). state is the recipient's full initial view of the new game, // so the client navigates straight in from the event with no follow-up fetch (R4). func MatchFound(userID, gameID uuid.UUID, state PlayerState) Intent { b := flatbuffers.NewBuilder(512) gid := b.CreateString(gameID.String()) stateOff := buildStateView(b, state) fb.MatchFoundEventStart(b) fb.MatchFoundEventAddGameId(b, gid) fb.MatchFoundEventAddState(b, stateOff) b.Finish(fb.MatchFoundEventEnd(b)) return Intent{UserID: userID, Kind: KindMatchFound, Payload: b.FinishedBytes(), EventID: eventID()} } // Notification is a lightweight "re-poll" signal to userID that something in their lobby // changed. kind is a sub-discriminator (NotifyFriendRequest, NotifyFriendAdded, // NotifyFriendDeclined, NotifyInvitation, NotifyGameStarted). It carries no payload; prefer the // enriched constructors below, which let the client update its lobby without a refetch (R4). func Notification(userID uuid.UUID, kind string) Intent { b := flatbuffers.NewBuilder(32) k := b.CreateString(kind) fb.NotificationEventStart(b) fb.NotificationEventAddKind(b, k) b.Finish(fb.NotificationEventEnd(b)) return Intent{UserID: userID, Kind: KindNotification, Payload: b.FinishedBytes(), EventID: eventID()} } // NotificationAccount builds a lobby notification of one of the friend_* kinds carrying the // account it concerns (the requester, the new friend or the decliner), so the client updates its // requests/friends lists and the in-game "add friend" state without a refetch (R4). func NotificationAccount(userID uuid.UUID, kind string, acc AccountRef) Intent { b := flatbuffers.NewBuilder(128) k := b.CreateString(kind) accOff := buildAccountRef(b, acc) fb.NotificationEventStart(b) fb.NotificationEventAddKind(b, k) fb.NotificationEventAddAccount(b, accOff) b.Finish(fb.NotificationEventEnd(b)) return Intent{UserID: userID, Kind: KindNotification, Payload: b.FinishedBytes(), EventID: eventID()} } // NotificationGameStarted builds the NotifyGameStarted notification carrying the recipient's // initial view of the just-started invited game, so the client seeds its game cache and the // lobby list without a refetch (R4). func NotificationGameStarted(userID uuid.UUID, state PlayerState) Intent { b := flatbuffers.NewBuilder(512) k := b.CreateString(NotifyGameStarted) stateOff := buildStateView(b, state) fb.NotificationEventStart(b) fb.NotificationEventAddKind(b, k) fb.NotificationEventAddState(b, stateOff) b.Finish(fb.NotificationEventEnd(b)) return Intent{UserID: userID, Kind: KindNotification, Payload: b.FinishedBytes(), EventID: eventID()} } // NotificationInvitation builds the NotifyInvitation notification carrying the new invitation, // so the client adds it to its lobby invitations list without a refetch (R4). func NotificationInvitation(userID uuid.UUID, inv InvitationSummary) Intent { b := flatbuffers.NewBuilder(512) k := b.CreateString(NotifyInvitation) invOff := buildInvitation(b, inv) fb.NotificationEventStart(b) fb.NotificationEventAddKind(b, k) fb.NotificationEventAddInvitation(b, invOff) b.Finish(fb.NotificationEventEnd(b)) return Intent{UserID: userID, Kind: KindNotification, Payload: b.FinishedBytes(), EventID: eventID()} } // eventID returns a best-effort correlation id for one emitted event. func eventID() string { if id, err := uuid.NewV7(); err == nil { return id.String() } return "" }