feat(login): randomize Russian welcome headlines
This commit is contained in:
@@ -1114,7 +1114,7 @@ manner of Docker's random container names.
|
|||||||
|
|
||||||
## Milestone 013 — Random humorous login headlines
|
## Milestone 013 — Random humorous login headlines
|
||||||
|
|
||||||
**Status:** `NOT_STARTED`
|
**Status:** `COMPLETE`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -1155,7 +1155,21 @@ headline at random whenever a player opens the page.
|
|||||||
|
|
||||||
### Evidence
|
### Evidence
|
||||||
|
|
||||||
Completion evidence is not yet available. Expected evidence includes targeted
|
- The browser test `login headline selection is randomizable, Russian, and has
|
||||||
browser-test output, `bash scripts/run_host_tests.sh`, and the reproducible
|
a fallback` passed. It verifies eight locally authored, appropriate Russian
|
||||||
web-asset size and hash report from
|
two-line headlines, deterministic injected selection, fallback to the first
|
||||||
`/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1 --target buildfs`.
|
valid headline for an invalid source, and initialization-time rendering into
|
||||||
|
the existing heading structure.
|
||||||
|
- `node --check web/player.js` and `bash scripts/run_host_tests.sh` passed. The
|
||||||
|
full host suite reported ten browser tests and web-asset reproducibility with
|
||||||
|
SHA-256 `ca5a637087748707367bd6ccec7885d0ff6ad20bd4713df528403e13f26fa540`
|
||||||
|
using 20,281 of 1,572,864 allowed compressed bytes.
|
||||||
|
- `pio run -e esp32-c6-devkitm-1 --target buildfs` passed; its LittleFS image
|
||||||
|
uses 2,031,616 of 2,031,616 partition bytes. The layout preserves the
|
||||||
|
existing `h1`, line break, and highlighted span, so no responsive or
|
||||||
|
accessibility structure changed.
|
||||||
|
- Each login-page initialization selects its headline through the bounded
|
||||||
|
browser-random helper. Strong browser randomness is preferred, and the
|
||||||
|
helper falls back safely when unavailable. This satisfies every Milestone 013
|
||||||
|
acceptance criterion. Milestone 013 is `COMPLETE`; Milestone 014 remains
|
||||||
|
`NOT_STARTED`.
|
||||||
|
|||||||
@@ -48,6 +48,21 @@ test("frontend name suggestions randomize both words and remain bounded", () =>
|
|||||||
assert.doesNotMatch(script, /suggest_name|name_suggestion/);
|
assert.doesNotMatch(script, /suggest_name|name_suggestion/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("login headline selection is randomizable, Russian, and has a fallback", () => {
|
||||||
|
assert.ok(player.LOGIN_HEADLINES.length >= 8);
|
||||||
|
const selected = player.randomLoginHeadline(() => 5);
|
||||||
|
assert.deepEqual(selected, {first: "Крот сказал", highlight: "что это срез"});
|
||||||
|
assert.equal(player.randomLoginHeadline(() => -1), player.LOGIN_HEADLINES[0]);
|
||||||
|
for (const headline of player.LOGIN_HEADLINES) {
|
||||||
|
assert.match(headline.first, /[А-Яа-яЁё]/u);
|
||||||
|
assert.match(headline.highlight, /[А-Яа-яЁё]/u);
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||||
|
assert.match(script, /renderWelcomeHeadline\(randomLoginHeadline\(\)\)/);
|
||||||
|
assert.match(script, /title\.replaceChildren/);
|
||||||
|
});
|
||||||
|
|
||||||
test("command envelopes contain intentions rather than client-owned state", () => {
|
test("command envelopes contain intentions rather than client-owned state", () => {
|
||||||
const move = player.createEnvelope("move", 7, "move-7", {direction: "west"});
|
const move = player.createEnvelope("move", 7, "move-7", {direction: "west"});
|
||||||
assert.deepEqual(move, {v: 1, type: "move", seq: 7, requestId: "move-7", payload: {direction: "west"}});
|
assert.deepEqual(move, {v: 1, type: "move", seq: 7, requestId: "move-7", payload: {direction: "west"}});
|
||||||
|
|||||||
+25
-2
@@ -7,8 +7,18 @@
|
|||||||
const MAX_VIEW_OBJECTS = 30;
|
const MAX_VIEW_OBJECTS = 30;
|
||||||
const OBSERVER_CELL_SIZE = 8;
|
const OBSERVER_CELL_SIZE = 8;
|
||||||
const NAME_ADJECTIVES = Object.freeze(["Бодрый", "Хитрый", "Тихий", "Смелый", "Ловкий", "Сонный", "Шустрый", "Лунный", "Яркий", "Дикий", "Вечный", "Храбрый"]);
|
const NAME_ADJECTIVES = Object.freeze(["Бодрый", "Хитрый", "Тихий", "Смелый", "Ловкий", "Сонный", "Шустрый", "Лунный", "Яркий", "Дикий", "Вечный", "Храбрый"]);
|
||||||
const NAME_NOUNS = Object.freeze(["Барсук", "Крот", "Енот", "Кабан", "Кактус", "Жук", "Тостер", "Робот", "Гриб", "Самокат", "Бублик", "Пингвин"]);
|
const NAME_NOUNS = Object.freeze(["Барсук", "Крот", "Енот", "Кабан", "Кактус", "Жук", "Тостер", "Тюлень", "Гриб", "Ежик", "Бублик", "Пингвин"]);
|
||||||
const NAME_COMBINATION_COUNT = NAME_ADJECTIVES.length * NAME_NOUNS.length;
|
const NAME_COMBINATION_COUNT = NAME_ADJECTIVES.length * NAME_NOUNS.length;
|
||||||
|
const LOGIN_HEADLINES = Object.freeze([
|
||||||
|
Object.freeze({first: "Кусты уже", highlight: "потеряли выход"}),
|
||||||
|
Object.freeze({first: "Лабиринт снова", highlight: "подстригли криво"}),
|
||||||
|
Object.freeze({first: "Пиксели дружно", highlight: "ушли в кусты"}),
|
||||||
|
Object.freeze({first: "Карта молчит", highlight: "но смотрит косо"}),
|
||||||
|
Object.freeze({first: "Выход рядом", highlight: "в другой вселенной"}),
|
||||||
|
Object.freeze({first: "Крот сказал", highlight: "что это срез"}),
|
||||||
|
Object.freeze({first: "Тупик машет", highlight: "обеими стенами"}),
|
||||||
|
Object.freeze({first: "Компас уверен", highlight: "почти наверняка"})
|
||||||
|
]);
|
||||||
const KEY_DIRECTIONS = Object.freeze({
|
const KEY_DIRECTIONS = Object.freeze({
|
||||||
ArrowUp: "north", KeyW: "north",
|
ArrowUp: "north", KeyW: "north",
|
||||||
ArrowRight: "east", KeyD: "east",
|
ArrowRight: "east", KeyD: "east",
|
||||||
@@ -55,6 +65,10 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomLoginHeadline(randomSource) {
|
||||||
|
return LOGIN_HEADLINES[randomIndex(LOGIN_HEADLINES.length, randomSource)];
|
||||||
|
}
|
||||||
|
|
||||||
function directionFromKey(code) {
|
function directionFromKey(code) {
|
||||||
return KEY_DIRECTIONS[code] || null;
|
return KEY_DIRECTIONS[code] || null;
|
||||||
}
|
}
|
||||||
@@ -152,12 +166,21 @@
|
|||||||
byId("observer-screen").hidden = name !== "observer";
|
byId("observer-screen").hidden = name !== "observer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderWelcomeHeadline(headline) {
|
||||||
|
const title = byId("welcome-title");
|
||||||
|
const highlight = documentObject.createElement("span");
|
||||||
|
highlight.textContent = headline.highlight;
|
||||||
|
title.replaceChildren(documentObject.createTextNode(headline.first), documentObject.createElement("br"), highlight);
|
||||||
|
}
|
||||||
|
|
||||||
function setConnection(connectionState, text) {
|
function setConnection(connectionState, text) {
|
||||||
byId("connection").dataset.state = connectionState;
|
byId("connection").dataset.state = connectionState;
|
||||||
byId("connection-text").textContent = text;
|
byId("connection-text").textContent = text;
|
||||||
byId("details-text").textContent = connectionState === "online" ? "WebSocket подключён к локальному ESP." : "Команды временно не отправляются. Повторяем подключение автоматически.";
|
byId("details-text").textContent = connectionState === "online" ? "WebSocket подключён к локальному ESP." : "Команды временно не отправляются. Повторяем подключение автоматически.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderWelcomeHeadline(randomLoginHeadline());
|
||||||
|
|
||||||
function beginFeedback() {
|
function beginFeedback() {
|
||||||
if (state.feedbackReady) return;
|
if (state.feedbackReady) return;
|
||||||
state.feedbackReady = true;
|
state.feedbackReady = true;
|
||||||
@@ -493,7 +516,7 @@
|
|||||||
return {state, handleMessage, move};
|
return {state, handleMessage, move};
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, directionFromKey, directionFromSwipe, randomIndex, randomSuggestedName, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, directionFromKey, directionFromSwipe, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
||||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||||
root.Maze32Player = api;
|
root.Maze32Player = api;
|
||||||
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
||||||
|
|||||||
Reference in New Issue
Block a user