Révision UX : portrait stable, bandeaux, menu, paysage plateau = écran, vibrations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2be5bdcb4f
commit
c6649f94c0
@@ -19,6 +19,7 @@ import {
|
||||
} from "../../src/domain/game/position.js";
|
||||
import { fixedDice } from "../../src/domain/game/roll.js";
|
||||
import type { GameState } from "../../src/domain/game/state.js";
|
||||
import { boardModel, pipLead } from "../../src/ui/board-model.js";
|
||||
import { loadNodeEngine } from "../support/node-engine.js";
|
||||
|
||||
// Le chargement du moteur prend une à deux secondes sur le PC de 2011.
|
||||
@@ -283,7 +284,7 @@ Then("c'est toujours à vous", () => {
|
||||
assert.equal(lastEvent().type, "DiceRolled");
|
||||
});
|
||||
|
||||
When("vous continuez", async () => {
|
||||
When("vous laissez passer votre tour", async () => {
|
||||
await game.skipTurn();
|
||||
const event = lastEvent();
|
||||
assert.ok(event.type === "MovePlayed" && event.side === "player");
|
||||
@@ -344,6 +345,27 @@ Then("vous pouvez revoir son coup", async () => {
|
||||
assert.deepEqual(await game.lastMoveSteps(), computerMove);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- Compte de points
|
||||
|
||||
// Les bandeaux lisent le compte de points du plateau donné par le moteur ;
|
||||
// qui mène se calcule dans l'interface (board-model.ts).
|
||||
|
||||
async function pips() {
|
||||
return boardModel(await game.displayedBoard()).pipCount;
|
||||
}
|
||||
|
||||
Then("chaque camp a un compte de points de {int}", async (n: number) => {
|
||||
assert.deepEqual(await pips(), { player: n, computer: n });
|
||||
});
|
||||
|
||||
Then("personne ne mène la course", async () => {
|
||||
assert.equal(pipLead(await pips()).leader, null);
|
||||
});
|
||||
|
||||
Then("vous menez la course de {int}", async (n: number) => {
|
||||
assert.deepEqual(pipLead(await pips()), { leader: "player", by: n });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ Videau
|
||||
|
||||
// La confirmation elle-même est une question de l'interface (une fenêtre
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// Étapes du scénario docs/scenarios/regler-l-application.feature (ADR 0008) :
|
||||
// la partie reprise passe par le domaine et le vrai moteur, et par le même
|
||||
// enregistrement que l'application ; les réglages et les vibrations par les
|
||||
// modules de l'interface qui les portent (settings.ts, haptics.ts).
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { BeforeAll, Given, setDefaultTimeout, Then, When } from "@cucumber/cucumber";
|
||||
import type { Engine } from "../../src/domain/game/engine.js";
|
||||
import type { GameJournal } from "../../src/domain/game/events.js";
|
||||
import { Game } from "../../src/domain/game/game.js";
|
||||
import { parseHalfMoves, START_POSITION } from "../../src/domain/game/position.js";
|
||||
import { fixedDice } from "../../src/domain/game/roll.js";
|
||||
import { createGameStore } from "../../src/storage/game-store.js";
|
||||
import { BANNER_MS } from "../../src/ui/app.js";
|
||||
import { createHaptics } from "../../src/ui/haptics.js";
|
||||
import { nextTheme, parseSettings, type Settings } from "../../src/ui/settings.js";
|
||||
import { loadNodeEngine } from "../support/node-engine.js";
|
||||
|
||||
setDefaultTimeout(30_000);
|
||||
|
||||
let engine: Engine;
|
||||
let game: Game;
|
||||
let oldJournal: GameJournal;
|
||||
let oldBoard: unknown;
|
||||
let settings: Settings;
|
||||
let vibrations: number[][] = [];
|
||||
|
||||
BeforeAll(async () => {
|
||||
engine = await loadNodeEngine();
|
||||
});
|
||||
|
||||
// -------------------------------------------------- Nouvelle partie annulable
|
||||
|
||||
// Comme dans l'application : chaque partie est enregistrée (localStorage,
|
||||
// ici un stockage en mémoire), et l'ancienne est reprise depuis son journal.
|
||||
const data = new Map<string, string>();
|
||||
const store = createGameStore({
|
||||
getItem: (key) => data.get(key) ?? null,
|
||||
setItem: (key, value) => void data.set(key, value),
|
||||
});
|
||||
|
||||
Given("une partie est en cours depuis quelques coups", async () => {
|
||||
game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4, 5, 2]) });
|
||||
for (const halfMove of parseHalfMoves("8/5 6/5")) {
|
||||
assert.ok(await game.playHalfMove(halfMove));
|
||||
}
|
||||
await game.confirmMove();
|
||||
await game.playComputerTurn();
|
||||
store.save(game.journal);
|
||||
assert.equal(game.state.turn, "player");
|
||||
});
|
||||
|
||||
When("vous commencez une nouvelle partie depuis le menu", async () => {
|
||||
oldJournal = game.journal;
|
||||
oldBoard = await game.displayedBoard();
|
||||
game = Game.start(engine);
|
||||
store.save(game.journal);
|
||||
});
|
||||
|
||||
Then("une nouvelle partie commence aussitôt", () => {
|
||||
assert.equal(game.state.position, START_POSITION);
|
||||
assert.ok(game.events.every((e) => e.type === "GameStarted" || e.type === "DiceRolled"));
|
||||
});
|
||||
|
||||
Then("l'ancienne partie peut être reprise pendant {int} secondes", (seconds: number) => {
|
||||
assert.equal(BANNER_MS, seconds * 1000);
|
||||
});
|
||||
|
||||
When("vous reprenez l'ancienne partie", () => {
|
||||
game = Game.resume(engine, oldJournal);
|
||||
store.save(game.journal);
|
||||
});
|
||||
|
||||
Then("vous retrouvez exactement la partie quittée", async () => {
|
||||
assert.deepEqual(store.load(), oldJournal);
|
||||
assert.deepEqual(game.journal, oldJournal);
|
||||
assert.deepEqual(await game.displayedBoard(), oldBoard);
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------- Réglages
|
||||
|
||||
Given("vous ouvrez l'application pour la première fois", () => {
|
||||
settings = parseSettings(null);
|
||||
vibrations = [];
|
||||
});
|
||||
|
||||
const haptic = createHaptics(
|
||||
(pattern) => {
|
||||
vibrations.push(pattern);
|
||||
return true;
|
||||
},
|
||||
() => settings.vibrations,
|
||||
);
|
||||
|
||||
When("un pion est frappé", () => {
|
||||
vibrations = [];
|
||||
haptic("checkerHit");
|
||||
});
|
||||
|
||||
Then("le téléphone vibre deux fois, brièvement", () => {
|
||||
assert.deepEqual(vibrations, [[25, 40, 25]]);
|
||||
});
|
||||
|
||||
When("vous coupez les vibrations dans le menu", () => {
|
||||
settings = { ...settings, vibrations: false };
|
||||
});
|
||||
|
||||
Then("le téléphone ne vibre pas", () => {
|
||||
assert.deepEqual(vibrations, []);
|
||||
});
|
||||
|
||||
Then("l'écran reste allumé pendant une partie", () => {
|
||||
assert.equal(settings.keepScreenOn, true);
|
||||
});
|
||||
|
||||
When("vous coupez « Écran allumé pendant la partie » dans le menu", () => {
|
||||
settings = parseSettings(JSON.stringify({ ...settings, keepScreenOn: false }));
|
||||
});
|
||||
|
||||
Then("l'écran peut se mettre en veille comme d'habitude", () => {
|
||||
assert.equal(settings.keepScreenOn, false);
|
||||
});
|
||||
|
||||
Then("le thème suit celui du téléphone", () => {
|
||||
assert.equal(settings.theme, "auto");
|
||||
});
|
||||
|
||||
When("vous changez de thème dans le menu", () => {
|
||||
// Rien : l'étape suivante fait le tour des thèmes.
|
||||
});
|
||||
|
||||
Then("il passe à clair, puis à sombre, puis revient à automatique", () => {
|
||||
const seen = [settings.theme];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
seen.push(nextTheme(seen.at(-1) ?? "auto"));
|
||||
}
|
||||
assert.deepEqual(seen, ["auto", "light", "dark", "auto"]);
|
||||
});
|
||||
Reference in New Issue
Block a user