feat(game): finished-game export as a PNG image behind a format chooser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
The history header's export button now opens a chooser — Telegram's native popup inside Telegram, the app's own modal elsewhere — offering the GCG file and a new client-rendered PNG of the final position (lib/gameimage, Canvas 2D, lazy dynamic import, zero dependencies): light theme, classic A..O/1..15 axes, label-free premium fills, and a fixed-typography per-seat scoresheet with GCG-style move coordinates, multi-word sub-lines, endgame rack-settlement row, winner trophy and a hostname + device-locale finish date footer; a long game stretches the board, never the typography. Delivery mirrors the GCG rules (Web Share with no blob fallback, else download) except on Android Telegram/VK WebViews and the desktop VK iframe, where a binary PNG has no clipboard-text fallback: those get a preview modal with a long-press/right-click save hint and a copy-image button where ClipboardItem exists.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { historyGrid, type HistoryCell } from './history';
|
||||
import { historyGrid, moveCoord, type HistoryCell } from './history';
|
||||
import type { MoveRecord } from './model';
|
||||
|
||||
function play(player: number, words: string[], score: number): MoveRecord {
|
||||
@@ -67,4 +67,26 @@ describe('historyGrid', () => {
|
||||
expect(kinds(grid)).toEqual([['play', 'empty']]);
|
||||
expect(grid[0][0]).toMatchObject({ words: ['ДОМ'] });
|
||||
});
|
||||
|
||||
it('stamps a play cell with its classic coordinate', () => {
|
||||
const grid = historyGrid([play(0, ['ДОМ'], 12)], 1, null);
|
||||
expect(grid[0][0]).toMatchObject({ kind: 'play', coord: '8H' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveCoord', () => {
|
||||
// The classic notation the GCG export uses (backend gcgPos): an across play is
|
||||
// row-then-column ("8G"), a down play column-then-row ("H8").
|
||||
it('puts the row number first for an across play', () => {
|
||||
expect(moveCoord({ dir: 'H', mainRow: 7, mainCol: 6 })).toBe('8G');
|
||||
});
|
||||
|
||||
it('puts the column letter first for a down play', () => {
|
||||
expect(moveCoord({ dir: 'V', mainRow: 7, mainCol: 7 })).toBe('H8');
|
||||
});
|
||||
|
||||
it('covers the board corners', () => {
|
||||
expect(moveCoord({ dir: 'H', mainRow: 0, mainCol: 0 })).toBe('1A');
|
||||
expect(moveCoord({ dir: 'V', mainRow: 14, mainCol: 14 })).toBe('O15');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user