Files
scrabble-game/ui/src/lib/mock/data.ts
T
Ilia Denisov 6b6baf5710
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Stage 17 round 6 (#16/#17, PR C): lobby sort + server-derived in-game friend state
Lobby: group the my-games list into your-turn / opponent-turn / finished
(empty sections hidden), ordered by last activity (your-turn oldest-first,
the other two newest-first), as a compact line-separated list. gameDTO and
FB GameView gain last_activity_unix (turn start while active, finish time
once finished); a pure lib/lobbysort.ts holds the grouping/ordering.

Friends: the in-game 'add to friends' item is now server-derived via a new
GET /user/friends/outgoing (+ friends.outgoing op), returning addressees with
a pending OR declined request (both read as 'request sent'), so it is correct
across reloads; it shows a disabled '✓ in friends' once accepted. It
live-updates when the opponent answers: RespondFriendRequest now publishes
friend_added (accept) / friend_declined (new notify sub-kind, decline) to the
original requester, whose open game re-derives its friend state.

Tests: lobbysort unit test; gateway outgoing + last_activity transcode tests;
backend integration ListOutgoingRequests + respond-publishes-to-requester;
e2e updated for the new lobby section labels + a non-friend active opponent.
Docs: ARCHITECTURE notify catalog, FUNCTIONAL(+ru) lobby/friends, PLAN.
2026-06-08 19:23:48 +02:00

239 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Seed data for the mock transport. Enough to exercise the playable slice locally
// (pnpm start) with no backend: a profile, one active mid-game whose board already
// has tiles, and two finished games. Coordinates are 0-indexed (centre 7,7). Words do
// not need to be strictly legal here — this is a visual/interaction fixture; real
// legality and scoring come from the backend.
import type {
AccountRef,
ChatMessage,
GameView,
Invitation,
MoveRecord,
Profile,
Seat,
Session,
Stats,
} from '../model';
export const ME = 'me';
export const SESSION: Session = {
token: 'mock-token',
userId: ME,
isGuest: true,
displayName: 'You',
// Both languages by default, so the mock-driven UI offers every variant.
supportedLanguages: ['en', 'ru'],
};
export const PROFILE: Profile = {
userId: ME,
displayName: 'You',
preferredLanguage: 'en',
timeZone: 'UTC',
awayStart: '00:00',
awayEnd: '07:00',
hintBalance: 3,
blockChat: false,
blockFriendRequests: false,
isGuest: false,
notificationsInAppOnly: true,
};
// Seed social/account data for the mock (pnpm start + Playwright). The mock profile
// is a durable account so the Stage 8 surfaces (friends, stats, history) are reachable.
// Ann is the active game's opponent but deliberately not a friend, so the in-game
// "add to friends" flow is demonstrable; Kaya (a finished-game opponent) is the friend.
export const MOCK_FRIENDS: AccountRef[] = [{ accountId: 'kaya', displayName: 'Kaya' }];
export const MOCK_INCOMING: AccountRef[] = [{ accountId: 'rick', displayName: 'Rick' }];
export const MOCK_STATS: Stats = { wins: 7, losses: 4, draws: 1, maxGamePoints: 421, maxWordPoints: 95 };
export function mockInvitations(): Invitation[] {
return [
{
id: 'inv1',
inviter: { accountId: 'kaya', displayName: 'Kaya' },
invitees: [{ accountId: ME, displayName: 'You', seat: 1, response: 'pending' }],
variant: 'english',
turnTimeoutSecs: 86400,
hintsAllowed: true,
hintsPerPlayer: 1,
dropoutTiles: 'remove',
status: 'pending',
gameId: '',
expiresAtUnix: Math.floor(Date.now() / 1000) + 7 * 86400,
},
];
}
function seat(s: number, accountId: string, displayName: string, score: number, isWinner = false): Seat {
return { seat: s, accountId, displayName, score, hintsUsed: 0, isWinner };
}
function play(
player: number,
dir: 'H' | 'V',
tiles: Array<[number, number, string]>,
words: string[],
score: number,
total: number,
): MoveRecord {
const ts = tiles.map(([row, col, letter]) => ({ row, col, letter, blank: false }));
return {
player,
action: 'play',
dir,
mainRow: ts[0]?.row ?? 7,
mainCol: ts[0]?.col ?? 7,
tiles: ts,
words,
count: words.length,
score,
total,
};
}
export interface MockGame {
view: GameView;
moves: MoveRecord[];
rack: string[];
bagLen: number;
hintsRemaining: number;
chat: ChatMessage[];
}
// --- active game G1: english, You (seat 0) vs Ann (seat 1), your turn ---
const G1_MOVES: MoveRecord[] = [
play(0, 'H', [
[7, 5, 'H'],
[7, 6, 'E'],
[7, 7, 'L'],
[7, 8, 'L'],
[7, 9, 'O'],
], ['HELLO'], 16, 16),
play(1, 'V', [
[6, 9, 'W'],
[8, 9, 'R'],
[9, 9, 'L'],
[10, 9, 'D'],
], ['WORLD'], 9, 9),
play(0, 'H', [
[8, 10, 'A'],
[8, 11, 'T'],
], ['RAT'], 3, 19),
play(1, 'V', [
[9, 10, 'N'],
[10, 10, 'D'],
], ['AND'], 4, 13),
];
function activeGame(): MockGame {
return {
view: {
id: 'g1',
variant: 'english',
dictVersion: 'v1',
status: 'active',
players: 2,
toMove: 0,
turnTimeoutSecs: 86400,
moveCount: G1_MOVES.length,
endReason: '',
lastActivityUnix: Math.floor(Date.now() / 1000) - 7200,
seats: [seat(0, ME, 'You', 19), seat(1, 'ann', 'Ann', 13)],
},
moves: G1_MOVES,
rack: ['R', 'E', 'T', 'I', 'N', 'A', '?'],
bagLen: 58,
hintsRemaining: 1,
chat: [
{
id: 'c1',
gameId: 'g1',
senderId: 'ann',
kind: 'message',
body: 'good luck!',
createdAtUnix: Math.floor(Date.now() / 1000) - 3600,
},
],
};
}
// --- finished games ---
function finishedG2(): MockGame {
return {
view: {
id: 'g2',
variant: 'english',
dictVersion: 'v1',
status: 'finished',
players: 2,
toMove: 0,
turnTimeoutSecs: 86400,
moveCount: 2,
endReason: 'normal',
lastActivityUnix: Math.floor(Date.now() / 1000) - 86400,
seats: [seat(0, ME, 'You', 320, true), seat(1, 'kaya', 'Kaya', 281)],
},
moves: [
play(0, 'H', [
[7, 6, 'Q'],
[7, 7, 'U'],
[7, 8, 'I'],
[7, 9, 'Z'],
], ['QUIZ'], 48, 48),
play(1, 'V', [
[6, 9, 'J'],
[8, 9, 'A'],
[9, 9, 'M'],
], ['JAZM'], 30, 30),
],
rack: [],
bagLen: 0,
hintsRemaining: 0,
chat: [],
};
}
function finishedG3(): MockGame {
return {
view: {
id: 'g3',
variant: 'russian_scrabble',
dictVersion: 'v1',
status: 'finished',
players: 2,
toMove: 0,
turnTimeoutSecs: 86400,
moveCount: 1,
endReason: 'resignation',
lastActivityUnix: Math.floor(Date.now() / 1000) - 172800,
seats: [seat(0, ME, 'You', 150), seat(1, 'rick', 'Rick', 212, true)],
},
moves: [
play(0, 'H', [
[7, 6, 'С'],
[7, 7, 'Л'],
[7, 8, 'О'],
[7, 9, 'В'],
[7, 10, 'О'],
], ['СЛОВО'], 12, 12),
],
rack: [],
bagLen: 0,
hintsRemaining: 0,
chat: [],
};
}
export function seedGames(): Map<string, MockGame> {
const m = new Map<string, MockGame>();
for (const g of [activeGame(), finishedG2(), finishedG3()]) m.set(g.view.id, g);
return m;
}