R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Localised "About" / landing copy, shared by the About screen and the public landing
|
||||
// page (Stage 17). Kept out of the flat i18n catalog because it is structured (a heading,
|
||||
// page. Kept out of the flat i18n catalog because it is structured (a heading,
|
||||
// a rules link, two bulleted sections) and only used in these two long-form places.
|
||||
|
||||
import type { Locale } from './i18n/index.svelte';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from './alphabet';
|
||||
|
||||
// The cache module is per-file-isolated by vitest, so only what these tests seed exists.
|
||||
describe('alphabet cache (Stage 13)', () => {
|
||||
describe('alphabet cache', () => {
|
||||
it('upper-cases letters for display and maps indices and values case-insensitively', () => {
|
||||
setAlphabet('scrabble_en', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Per-variant alphabet table cache (Stage 13). The client is alphabet-agnostic: it caches
|
||||
// Per-variant alphabet table cache. The client is alphabet-agnostic: it caches
|
||||
// each variant's (index, letter, value) table — sent by the server on a per-variant cache
|
||||
// miss, behind game.state's include_alphabet flag — and renders the rack and the blank
|
||||
// chooser with it while live play exchanges bare alphabet indices on the wire. Letters are
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface Toast {
|
||||
|
||||
export const app = $state<{
|
||||
ready: boolean;
|
||||
/** Whether the live-event stream is connected; drives the matchmaking poll fallback (R4). */
|
||||
/** Whether the live-event stream is connected; drives the matchmaking poll fallback. */
|
||||
streamAlive: boolean;
|
||||
session: Session | null;
|
||||
profile: Profile | null;
|
||||
@@ -154,12 +154,12 @@ function openStream(): void {
|
||||
showToast(t('game.yourTurn'), 'info');
|
||||
} else if (e.kind === 'match_found') {
|
||||
// Seed the cache from the event's initial state so the game renders instantly on arrival,
|
||||
// then navigate (R4).
|
||||
// then navigate.
|
||||
if (e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
navigate(`/game/${e.gameId}`);
|
||||
} else if (e.kind === 'notify') {
|
||||
// A started invited game seeds its cache so opening it is instant; the lobby badge stays
|
||||
// on the authoritative refresh (R4).
|
||||
// on the authoritative refresh.
|
||||
if (e.sub === 'game_started' && e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
void refreshNotifications();
|
||||
}
|
||||
@@ -231,7 +231,7 @@ async function adoptSession(s: Session): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* applyLinkResult applies a completed account link or merge (Stage 11): it adopts a
|
||||
* applyLinkResult applies a completed account link or merge: it adopts a
|
||||
* switched session (a guest initiator whose durable counterpart won, so the active
|
||||
* account changed) or, otherwise, refreshes the current profile in place.
|
||||
*/
|
||||
@@ -261,7 +261,7 @@ function syncTelegramChrome(): void {
|
||||
* syncTelegramSafeArea mirrors Telegram's content-safe-area top inset (the height its native
|
||||
* nav overlays the viewport in fullscreen) into the --tg-content-top CSS var and toggles a
|
||||
* `tg-fullscreen` class, so the header can drop below the nav and lift the menu into its
|
||||
* band (Stage 17). Called on launch and on Telegram's safe-area / fullscreen change events.
|
||||
* band. Called on launch and on Telegram's safe-area / fullscreen change events.
|
||||
*/
|
||||
function syncTelegramSafeArea(): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
@@ -275,7 +275,7 @@ function syncTelegramSafeArea(): void {
|
||||
* syncViewportHeight mirrors the visual-viewport height into the --vvh CSS var so a screen can
|
||||
* fit the visible area above an open soft keyboard (iOS does not shrink dvh for the keyboard).
|
||||
* On a screen whose input sits at the bottom (chat, word-check) this keeps the input visible
|
||||
* without the page scrolling, so the layout no longer jumps when the keyboard appears (Stage 17).
|
||||
* without the page scrolling, so the layout no longer jumps when the keyboard appears.
|
||||
*/
|
||||
function syncViewportHeight(): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
@@ -69,7 +69,7 @@ export interface GatewayClient {
|
||||
lobbyCancel(): Promise<void>;
|
||||
|
||||
// --- game ---
|
||||
// Stage 13: the play loop exchanges alphabet indices, so submit/evaluate/exchange/
|
||||
// The play loop exchanges alphabet indices, so submit/evaluate/exchange/
|
||||
// check-word take the game's variant (to map letters<->indices via the cached alphabet
|
||||
// table), and gameState's includeAlphabet asks the server to embed that table.
|
||||
gameState(gameId: string, includeAlphabet: boolean): Promise<StateView>;
|
||||
@@ -82,10 +82,10 @@ export interface GatewayClient {
|
||||
evaluate(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[], variant: Variant): Promise<EvalResult>;
|
||||
checkWord(gameId: string, word: string, variant: Variant): Promise<WordCheckResult>;
|
||||
complaint(gameId: string, word: string, note: string): Promise<void>;
|
||||
/** Hide a finished game from the caller's own lobby list (Stage 17); per-account, irreversible. */
|
||||
/** Hide a finished game from the caller's own lobby list; per-account, irreversible. */
|
||||
hideGame(gameId: string): Promise<void>;
|
||||
|
||||
// --- draft (Stage 17) ---
|
||||
// --- draft ---
|
||||
/** The player's server-persisted client-side composition (rack order + board tiles), so a
|
||||
* reload or a second device resumes the same arrangement. The JSON is opaque to the
|
||||
* gateway; the client owns the {rack_order, board_tiles} shape. */
|
||||
@@ -97,7 +97,7 @@ export interface GatewayClient {
|
||||
chatList(gameId: string): Promise<ChatMessage[]>;
|
||||
nudge(gameId: string): Promise<ChatMessage>;
|
||||
|
||||
// --- friends (Stage 8) ---
|
||||
// --- friends ---
|
||||
friendsList(): Promise<AccountRef[]>;
|
||||
friendsIncoming(): Promise<AccountRef[]>;
|
||||
/** Addressees the caller has already requested (pending or declined); cannot re-request. */
|
||||
@@ -109,24 +109,24 @@ export interface GatewayClient {
|
||||
friendCodeIssue(): Promise<FriendCode>;
|
||||
friendCodeRedeem(code: string): Promise<AccountRef>;
|
||||
|
||||
// --- blocks (Stage 8) ---
|
||||
// --- blocks ---
|
||||
blocksList(): Promise<AccountRef[]>;
|
||||
block(accountId: string): Promise<void>;
|
||||
unblock(accountId: string): Promise<void>;
|
||||
|
||||
// --- invitations (Stage 8) ---
|
||||
// --- invitations ---
|
||||
invitationsList(): Promise<Invitation[]>;
|
||||
invitationCreate(inviteeIds: string[], settings: InvitationSettings): Promise<Invitation>;
|
||||
invitationAccept(invitationId: string): Promise<Invitation>;
|
||||
invitationDecline(invitationId: string): Promise<Invitation>;
|
||||
invitationCancel(invitationId: string): Promise<void>;
|
||||
|
||||
// --- profile / stats / history (Stage 8) ---
|
||||
// --- profile / stats / history ---
|
||||
profileUpdate(p: ProfileUpdate): Promise<Profile>;
|
||||
statsGet(): Promise<Stats>;
|
||||
exportGcg(gameId: string): Promise<GcgExport>;
|
||||
|
||||
// --- account linking & merge (Stage 11) ---
|
||||
// --- account linking & merge ---
|
||||
linkEmailRequest(email: string): Promise<void>;
|
||||
linkEmailConfirm(email: string, code: string): Promise<LinkResult>;
|
||||
linkEmailMerge(email: string, code: string): Promise<LinkResult>;
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from './codec';
|
||||
|
||||
describe('codec', () => {
|
||||
it('round-trips a draft save request and view (Stage 17)', () => {
|
||||
it('round-trips a draft save request and view', () => {
|
||||
const json = '{"rack_order":"1,0","board_tiles":[]}';
|
||||
const req = fb.DraftRequest.getRootAsDraftRequest(new ByteBuffer(encodeDraftSave('g1', json)));
|
||||
expect(req.gameId()).toBe('g1');
|
||||
@@ -35,7 +35,7 @@ describe('codec', () => {
|
||||
expect(decodeDraftView(b.asUint8Array())).toBe('{"x":1}');
|
||||
});
|
||||
|
||||
it('encodes a SubmitPlayRequest with alphabet indices (Stage 13)', () => {
|
||||
it('encodes a SubmitPlayRequest with alphabet indices', () => {
|
||||
setAlphabet('scrabble_en', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
{ index: 1, letter: 'b', value: 3 },
|
||||
@@ -268,10 +268,10 @@ describe('codec', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Stage 13: the live play loop exchanges alphabet indices, mapped through the per-variant
|
||||
// The live play loop exchanges alphabet indices, mapped through the per-variant
|
||||
// table cached in lib/alphabet. Each test seeds the cache it needs (setAlphabet replaces
|
||||
// the whole table), so they are independent of order.
|
||||
describe('codec — alphabet on the wire (Stage 13)', () => {
|
||||
describe('codec — alphabet on the wire', () => {
|
||||
it('encodes an ExchangeRequest as alphabet indices, blank as the sentinel', () => {
|
||||
setAlphabet('scrabble_en', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
|
||||
+7
-7
@@ -38,7 +38,7 @@ import type {
|
||||
|
||||
// --- request encoders ---
|
||||
|
||||
// buildPlayTile encodes one to-place tile by its alphabet index (Stage 13); a placed blank
|
||||
// buildPlayTile encodes one to-place tile by its alphabet index; a placed blank
|
||||
// carries its designated letter's index with blank set.
|
||||
function buildPlayTile(b: Builder, t: PlacedTile, variant: Variant): Offset {
|
||||
fb.PlayTile.startPlayTile(b);
|
||||
@@ -73,7 +73,7 @@ export function encodeStateRequest(gameId: string, includeAlphabet: boolean): Ui
|
||||
return finish(b, fb.StateRequest.endStateRequest(b));
|
||||
}
|
||||
|
||||
// encodeDraftSave wraps the player's composition JSON (Stage 17). The string is opaque on the
|
||||
// encodeDraftSave wraps the player's composition JSON. The string is opaque on the
|
||||
// wire — the gateway forwards it verbatim and only the client reads {rack_order, board_tiles}.
|
||||
export function encodeDraftSave(gameId: string, json: string): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
@@ -324,7 +324,7 @@ export function decodeProfile(buf: Uint8Array): Profile {
|
||||
|
||||
// decodeStateViewTable projects a StateView table (a root or one nested in an event) to the
|
||||
// model. It caches the alphabet when present (a per-variant cache miss) and decodes the index
|
||||
// rack to display letters with it (Stage 13).
|
||||
// rack to display letters with it.
|
||||
function decodeStateViewTable(v: fb.StateView): StateView {
|
||||
const g = v.game();
|
||||
const variant = (g ? s(g.variant()) : 'scrabble_en') as Variant;
|
||||
@@ -355,7 +355,7 @@ export function decodeMoveResult(buf: Uint8Array): MoveResult {
|
||||
const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf));
|
||||
const m = r.move();
|
||||
const g = r.game();
|
||||
// The actor's refilled rack rides back as alphabet indices (R4); decode it with the game's variant.
|
||||
// The actor's refilled rack rides back as alphabet indices; decode it with the game's variant.
|
||||
const variant = (g ? s(g.variant()) : 'scrabble_en') as Variant;
|
||||
const rack: string[] = [];
|
||||
for (let i = 0; i < r.rackLength(); i++) rack.push(letterForIndex(variant, r.rack(i) ?? 0));
|
||||
@@ -493,7 +493,7 @@ export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stage 8 encoders ---
|
||||
// --- social encoders ---
|
||||
|
||||
export function encodeTarget(accountId: string): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
@@ -563,7 +563,7 @@ export function encodeUpdateProfile(p: ProfileUpdate): Uint8Array {
|
||||
return finish(b, fb.UpdateProfileRequest.endUpdateProfileRequest(b));
|
||||
}
|
||||
|
||||
// --- account linking & merge (Stage 11) ---
|
||||
// --- account linking & merge ---
|
||||
|
||||
export function encodeLinkEmailRequest(email: string): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
@@ -604,7 +604,7 @@ export function decodeLinkResult(buf: Uint8Array): LinkResult {
|
||||
};
|
||||
}
|
||||
|
||||
// --- Stage 8 decoders ---
|
||||
// --- social decoders ---
|
||||
|
||||
function decodeAccountRef(r: fb.AccountRef): AccountRef {
|
||||
return { accountId: s(r.accountId()), displayName: s(r.displayName()) };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Global connectivity signal (Stage 17). `online` is false while the app is actively failing to
|
||||
// Global connectivity signal. `online` is false while the app is actively failing to
|
||||
// reach the gateway — a unary call retrying after a transport/rate-limit failure, or the live
|
||||
// stream dropped. The transport and the live-stream owner report transitions; the UI reads
|
||||
// `connection.online` to show the "Connecting…" indicator and to softly disable proactive
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Draft (client-side composition) serialization, kept pure for unit tests. The server stores
|
||||
// the JSON opaquely (Stage 17); only the client interprets {rack_order, board_tiles}. The rack
|
||||
// the JSON opaquely; only the client interprets {rack_order, board_tiles}. The rack
|
||||
// order is a comma-joined permutation of the server rack's indices, in the player's visual
|
||||
// order; the board tiles are the tiles laid but not yet submitted.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('applyMoveDelta', () => {
|
||||
expect(applyMoveDelta(undefined, delta(1, 1))).toEqual({ refetch: false });
|
||||
});
|
||||
|
||||
it('refetches when the payload carries no delta (pre-R4 peer / dropped payload)', () => {
|
||||
it('refetches when the payload carries no delta (older peer / dropped payload)', () => {
|
||||
expect(applyMoveDelta(cache(3), { bagLen: 0 })).toEqual({ refetch: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Pure reducers that advance the per-game cache from live events (R4), so the UI renders a move
|
||||
// Pure reducers that advance the per-game cache from live events, so the UI renders a move
|
||||
// from the event without a follow-up game.state + game.history fetch. They never touch the network
|
||||
// or the cache store — the stream handler applies the returned cache and the game screen acts on
|
||||
// `refetch` — which keeps the gap / own-move / idempotency logic unit-testable in isolation.
|
||||
@@ -42,7 +42,7 @@ export function applyMoveDelta(cached: CachedGame | undefined, d: MoveDelta): De
|
||||
// Nothing cached to advance (the game was never opened on this device): ignore it; the next open
|
||||
// cold-loads the game.
|
||||
if (!cached) return { refetch: false };
|
||||
// A pre-R4 peer, or a dropped payload, carried no delta: the open game must refetch.
|
||||
// An older peer, or a dropped payload, carried no delta: the open game must refetch.
|
||||
if (!d.move || !d.game) return { refetch: true };
|
||||
const have = cached.view.game.moveCount;
|
||||
const next = d.game.moveCount;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Pure helpers for the public landing page (Stage 17), kept out of the Svelte component so
|
||||
// Pure helpers for the public landing page, kept out of the Svelte component so
|
||||
// the per-language Telegram-channel link selection is unit-testable.
|
||||
|
||||
import type { Locale } from './i18n/index.svelte';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Pure grouping + ordering of the lobby's game list (Stage 17). The lobby shows three
|
||||
// Pure grouping + ordering of the lobby's game list. The lobby shows three
|
||||
// sections — games awaiting the caller's move, games awaiting the opponent, and finished
|
||||
// games — each ordered by last activity: your-turn oldest-first (the longest-neglected on
|
||||
// top), the other two newest-first.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Mock alphabet fixtures (Stage 13). In production the per-variant (index, letter, value)
|
||||
// Mock alphabet fixtures. In production the per-variant (index, letter, value)
|
||||
// table comes from the server; the mock seeds the same client cache from a local copy so
|
||||
// the rack, the blank chooser and the mock's scoring work with no backend. The data is the
|
||||
// solver's value tables (scrabble-solver/rules/rules.go), in alphabet-index order, so a
|
||||
|
||||
@@ -98,7 +98,7 @@ export class MockGateway implements GatewayClient {
|
||||
|
||||
constructor() {
|
||||
// Seed the per-variant alphabet cache the rack, blank chooser and scoring read, so the
|
||||
// mock-driven UI is alphabet-agnostic without a backend (Stage 13).
|
||||
// mock-driven UI is alphabet-agnostic without a backend.
|
||||
seedMockAlphabets();
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ export class MockGateway implements GatewayClient {
|
||||
}
|
||||
async complaint(): Promise<void> {}
|
||||
|
||||
// Hide a finished game from the caller's list (Stage 17): drop it from the in-memory store so a
|
||||
// Hide a finished game from the caller's list: drop it from the in-memory store so a
|
||||
// subsequent gamesList omits it, mirroring the backend's per-account, finished-only rule.
|
||||
async hideGame(gameId: string): Promise<void> {
|
||||
const g = this.game(gameId);
|
||||
@@ -333,7 +333,7 @@ export class MockGateway implements GatewayClient {
|
||||
this.drafts.delete(gameId);
|
||||
}
|
||||
|
||||
// --- draft (Stage 17): an in-memory composition store, so the reload/off-turn flow is
|
||||
// --- draft: an in-memory composition store, so the reload/off-turn flow is
|
||||
// exercised without a backend. A committed move clears the actor's own draft, as on the server.
|
||||
async draftGet(gameId: string): Promise<string> {
|
||||
return this.drafts.get(gameId) ?? '';
|
||||
@@ -470,7 +470,7 @@ export class MockGateway implements GatewayClient {
|
||||
Object.assign(this.profile, p);
|
||||
return { ...this.profile };
|
||||
}
|
||||
// --- account linking & merge (Stage 11) ---
|
||||
// --- account linking & merge ---
|
||||
async linkEmailRequest(_email: string): Promise<void> {}
|
||||
async linkEmailConfirm(email: string, _code: string): Promise<LinkResult> {
|
||||
// An address containing "merge" stands in for one already owned by another
|
||||
|
||||
@@ -42,7 +42,7 @@ export const PROFILE: Profile = {
|
||||
};
|
||||
|
||||
// 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.
|
||||
// is a durable account so the social 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' }];
|
||||
|
||||
+4
-4
@@ -70,7 +70,7 @@ export interface StateView {
|
||||
export interface MoveResult {
|
||||
move: MoveRecord;
|
||||
game: GameView;
|
||||
/** The actor's refilled rack after the move (R4), so the mover renders the next state without a refetch. */
|
||||
/** The actor's refilled rack after the move, so the mover renders the next state without a refetch. */
|
||||
rack: string[];
|
||||
bagLen: number;
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export interface Session {
|
||||
supportedLanguages: string[];
|
||||
}
|
||||
|
||||
// LinkResult is the outcome of an account link/merge step (Stage 11). status is
|
||||
// LinkResult is the outcome of an account link/merge step. status is
|
||||
// 'linked' (bound to the current account), 'merge_required' (the identity belongs to
|
||||
// another account — the secondary* fields summarise it for the irreversible
|
||||
// confirmation) or 'merged'. session is set only when the active account switched
|
||||
@@ -230,8 +230,8 @@ export interface GameList {
|
||||
* A live event delivered over the Subscribe stream. The game events carry the move as a
|
||||
* delta — move plus the post-move summary (and the bag size) — the client applies to its
|
||||
* cached game without a refetch; match_found / game_started carry the recipient's initial
|
||||
* StateView; notify carries the changed lobby payload (R4). The enriched fields are optional
|
||||
* so a client falls back to a refetch when a payload is absent (a gap, or a pre-R4 peer).
|
||||
* StateView; notify carries the changed lobby payload. The enriched fields are optional
|
||||
* so a client falls back to a refetch when a payload is absent (a gap, or an older peer).
|
||||
*/
|
||||
export type PushEvent =
|
||||
| { kind: 'your_turn'; gameId: string; deadlineUnix: number; moveCount: number }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BOARD_SIZE, centre, premiumGrid } from './premiums';
|
||||
// Premium-square geometry parity with scrabble-solver/rules/rules.go: scrabble_en/scrabble_ru
|
||||
// share standardBoard (centre is a double word); erudit_ru shares the geometry but a
|
||||
// non-doubling centre. Tile-value and alphabet parity moved to the Go engine test
|
||||
// (backend/internal/engine AlphabetTable) in Stage 13 — the server now owns that table.
|
||||
// (backend/internal/engine AlphabetTable) — the server now owns that table.
|
||||
describe('premium layout', () => {
|
||||
it('is a 15x15 grid with TW corners', () => {
|
||||
const g = premiumGrid('scrabble_en');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// of truth, scrabble-solver/rules/rules.go (standardBoard / eruditBoard). The board is not
|
||||
// transmitted on the wire (StateView has no board), so the client renders the premiums
|
||||
// locally; only the centre differs by variant. A Vitest parity test pins the geometry.
|
||||
// Tile values and the alphabet moved to the server-sent per-variant table in Stage 13 (see
|
||||
// Tile values and the alphabet come from the server-sent per-variant table (see
|
||||
// lib/alphabet.ts), so this file is geometry only.
|
||||
|
||||
import type { Variant } from './model';
|
||||
@@ -85,5 +85,5 @@ export function centre(variant: Variant): { row: number; col: number } {
|
||||
return { row: 7, col: 7 };
|
||||
}
|
||||
|
||||
// Tile values and the per-variant alphabet now arrive from the server (lib/alphabet.ts,
|
||||
// Stage 13); the board geometry above is all this module owns.
|
||||
// Tile values and the per-variant alphabet now arrive from the server (lib/alphabet.ts);
|
||||
// the board geometry above is all this module owns.
|
||||
|
||||
@@ -14,7 +14,7 @@ export const maxAwayMinutes = 12 * 60;
|
||||
|
||||
// Unicode letters joined by single space / "." / "_" separators, where a "." or "_"
|
||||
// may be followed by a single space. No leading separator and no adjacent separators
|
||||
// except "<dot|underscore> <space>"; a single trailing "." is allowed (Stage 17). Same
|
||||
// except "<dot|underscore> <space>"; a single trailing "." is allowed. Same
|
||||
// rule as the Go displayNameRe.
|
||||
const displayNameRe = /^\p{L}+(?:(?:[._] ?| )\p{L}+)*\.?$/u;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('resultBadge', () => {
|
||||
|
||||
it('finished two-player: a 0-0 resignation is a defeat, not a score-tied win', () => {
|
||||
// The opponent won by resignation (isWinner) although neither side scored — the lobby
|
||||
// must read this as a loss, matching the game-detail screen (Stage 17 regression).
|
||||
// must read this as a loss, matching the game-detail screen (regression).
|
||||
expect(resultBadge(game([seat(0, 'me', 0), seat(1, 'a', 0, true)]), 'me')).toEqual({
|
||||
key: 'result.defeat',
|
||||
emoji: '🥈',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Retry policy + error classification for the gateway transport (Stage 17). When a unary call
|
||||
// Retry policy + error classification for the gateway transport. When a unary call
|
||||
// fails at the transport level the app retries it with capped exponential backoff while showing
|
||||
// the "Connecting…" indicator, instead of flashing a red toast each time.
|
||||
//
|
||||
|
||||
@@ -192,7 +192,7 @@ export function onTelegramPath(): boolean {
|
||||
return location.pathname.startsWith('/telegram/');
|
||||
}
|
||||
|
||||
// --- Login Widget (web sign-in for account linking, Stage 11) ---
|
||||
// --- Login Widget (web sign-in for account linking) ---
|
||||
|
||||
// The Login Widget is the web (non-Mini-App) Telegram sign-in. It is used only to
|
||||
// attach a Telegram identity to an existing account from a browser; inside the Mini
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Game variants offered on New Game, and the Stage 15 gating of that choice by the
|
||||
// Game variants offered on New Game, and the gating of that choice by the
|
||||
// languages the sign-in service supports. Kept out of the .svelte screen so the
|
||||
// gating is unit-testable (the project's node-env Vitest layer).
|
||||
|
||||
@@ -14,8 +14,7 @@ export interface VariantOption {
|
||||
// ALL_VARIANTS lists every variant in display order. The labels are display names keyed by
|
||||
// the game's alphabet, not the interface language: the English-alphabet game is always
|
||||
// "Scrabble" and the Russian-alphabet Scrabble always "Скрэббл" (both unlocalized, so the
|
||||
// two never collide whatever the UI language); Erudit is localized "Erudite"/"Эрудит"
|
||||
// (Stage 17).
|
||||
// two never collide whatever the UI language); Erudit is localized "Erudite"/"Эрудит".
|
||||
export const ALL_VARIANTS: VariantOption[] = [
|
||||
{ id: 'scrabble_en', label: 'new.english' },
|
||||
{ id: 'scrabble_ru', label: 'new.russian' },
|
||||
|
||||
Reference in New Issue
Block a user