feat(social): per-game friend request to disguised robots + lobby/stats/tile cosmetics
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
Functional: the in-game add-friend handshake aimed at an auto-match opponent who is secretly a pooled robot now records the request per (game, seat) in a new robot_friend_requests table -- never against the shared robot account -- mirroring the robot_blocks pattern. The shared account stays out of friendships, the "requested" state is pinned to the seat (not leaked across the player's other games), the robot ignores it, and a background reaper drops the row 7 days after its game finishes. The outgoing-requests list carries these per-game rows so the seat control stays disabled across reloads. No withdraw UI, per owner decision. Cosmetics: - Lobby: an in-progress game tints the viewer's own score number green when leading or tied, red when trailing (reusing --ok/--danger); scores now render in seat-number order, matching the over-the-board scoreboard. - Stats: the per-variant best move is laid out on two lines -- the variant label, then the score (right-aligned in its column) and the word tiles (left-aligned) below it. - Dark theme: the played-tile background is a touch darker so a player-placed tile reads with more contrast (light theme unchanged). Docs (ARCHITECTURE, FUNCTIONAL + _ru mirror, backend README, UI_DESIGN) updated in the same change.
This commit is contained in:
@@ -32,9 +32,22 @@ type incomingListDTO struct {
|
||||
}
|
||||
|
||||
// outgoingListDTO is the addressees the caller has already requested (a live pending
|
||||
// request or one the addressee declined) and therefore cannot re-request.
|
||||
// request or one the addressee declined) and therefore cannot re-request, plus the
|
||||
// per-game disguised-robot requests (not real accounts), which keep the in-game
|
||||
// "request sent" state pinned to a seat.
|
||||
type outgoingListDTO struct {
|
||||
Requests []accountRefDTO `json:"requests"`
|
||||
Requests []accountRefDTO `json:"requests"`
|
||||
Robots []robotFriendRequestDTO `json:"robots"`
|
||||
}
|
||||
|
||||
// robotFriendRequestDTO is one per-game disguised-robot friend request: the row id, the
|
||||
// game name the player saw, and the game + seat it was sent in (so the in-game card can
|
||||
// re-mark that seat as already requested).
|
||||
type robotFriendRequestDTO struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
GameID string `json:"game_id"`
|
||||
Seat int `json:"seat"`
|
||||
}
|
||||
|
||||
// friendCodeDTO is a freshly issued one-time friend code (returned once).
|
||||
@@ -123,7 +136,19 @@ func (s *Server) handleFriendRequest(c *gin.Context) {
|
||||
abortBadRequest(c, "invalid account id")
|
||||
return
|
||||
}
|
||||
if err := s.social.SendFriendRequest(c.Request.Context(), uid, target); err != nil {
|
||||
// An in-game request carries the game id, so a disguised-robot opponent is recorded as a
|
||||
// per-game request (RequestInGame); a request without a game (or to a human) goes through
|
||||
// SendFriendRequest directly.
|
||||
var err error
|
||||
if strings.TrimSpace(req.GameID) == "" {
|
||||
err = s.social.SendFriendRequest(c.Request.Context(), uid, target)
|
||||
} else if gameID, ok := parseUUIDField(req.GameID); !ok {
|
||||
abortBadRequest(c, "invalid game id")
|
||||
return
|
||||
} else {
|
||||
err = s.social.RequestInGame(c.Request.Context(), uid, target, gameID)
|
||||
}
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
@@ -235,12 +260,22 @@ func (s *Server) handleOutgoingRequests(c *gin.Context) {
|
||||
abortBadRequest(c, "missing identity")
|
||||
return
|
||||
}
|
||||
ids, err := s.social.ListOutgoingRequests(c.Request.Context(), uid)
|
||||
ctx := c.Request.Context()
|
||||
ids, err := s.social.ListOutgoingRequests(ctx, uid)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, outgoingListDTO{Requests: s.accountRefs(c.Request.Context(), ids)})
|
||||
robots, err := s.social.ListRobotFriendRequests(ctx, uid)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
dto := outgoingListDTO{Requests: s.accountRefs(ctx, ids)}
|
||||
for _, r := range robots {
|
||||
dto.Robots = append(dto.Robots, robotFriendRequestDTO{ID: r.ID.String(), DisplayName: r.DisplayName, GameID: r.GameID.String(), Seat: r.Seat})
|
||||
}
|
||||
c.JSON(http.StatusOK, dto)
|
||||
}
|
||||
|
||||
// handleIssueFriendCode issues a one-time add-a-friend code for the caller.
|
||||
|
||||
Reference in New Issue
Block a user