mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-06 22:15:13 +00:00
The board, the home screen and the game-over panel, in Vietnamese, served by the Go binary as a static single-page app. The store is a reducer over ServerMessage and computes nothing. Validity, turn order, scores and the result are read from the wire, which is what lets one screen serve the bot now and online play later. Every Vietnamese string lives in one module, including the map from RejectReason to a message, so the server can send UI keys instead of prose. The word field is uncontrolled. A Telex or VNI input method composes a diacritic across several keystrokes, and writing the value back on each one cancels the composition. It is read on submit and cleared only there. The countdown is drawn against the server's clock, estimated from the ping round trip, and settles 300ms early so the ring never claims more time than the server allows. The screen owns the socket and the game while it is mounted, and gives the current game up on the way out. Asking for a game is stored intent rather than a condition inferred from the board being empty: clearing the board for a rematch is that same condition, so the inference sent a second StartBotGame and the server built two rooms that then destroyed each other. A test greps the built bundle for dictionary words and holds a size budget, so the wordlist cannot reach the browser unnoticed. Another reads the error codes out of the Go transport and fails when one has no Vietnamese message.
61 lines
1.9 KiB
JavaScript
61 lines
1.9 KiB
JavaScript
// The countdown is the one place the client shows a number the server owns, so
|
|
// the rule under test is that it never claims more time than the server allows.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { SETTLE_MS, fraction, remainingMs, seconds } from '../src/lib/countdown.js';
|
|
|
|
describe('remainingMs', () => {
|
|
it('reaches zero before the server deadline, never after it', () => {
|
|
const deadline = 100_000;
|
|
expect(remainingMs(deadline, deadline - SETTLE_MS)).toBe(0);
|
|
expect(remainingMs(deadline, deadline - SETTLE_MS - 1)).toBe(1);
|
|
});
|
|
|
|
it('never goes negative once the deadline has passed', () => {
|
|
expect(remainingMs(100_000, 200_000)).toBe(0);
|
|
});
|
|
|
|
it('is zero when no turn is running', () => {
|
|
expect(remainingMs(0, 50_000)).toBe(0);
|
|
});
|
|
|
|
it('counts against the server clock, so a wrong device clock is corrected', () => {
|
|
// The client believes it is 80_000; the estimated server clock is
|
|
// 90_000. The ring has to follow the server.
|
|
expect(remainingMs(100_000, 90_000)).toBe(10_000 - SETTLE_MS);
|
|
});
|
|
});
|
|
|
|
describe('fraction', () => {
|
|
it('is full at the start of a turn and empty at the end', () => {
|
|
expect(fraction(20_000, 20_000)).toBe(1);
|
|
expect(fraction(0, 20_000)).toBe(0);
|
|
});
|
|
|
|
it('is half way through a half-spent turn', () => {
|
|
expect(fraction(10_000, 20_000)).toBe(0.5);
|
|
});
|
|
|
|
it('shows a full ring when no turn is timed, not an expired one', () => {
|
|
expect(fraction(0, 0)).toBe(1);
|
|
});
|
|
|
|
it('clamps rather than overflowing the arc', () => {
|
|
expect(fraction(30_000, 20_000)).toBe(1);
|
|
expect(fraction(-5, 20_000)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('seconds', () => {
|
|
it('shows the last second for its whole duration', () => {
|
|
expect(seconds(1)).toBe(1);
|
|
expect(seconds(999)).toBe(1);
|
|
expect(seconds(1000)).toBe(1);
|
|
expect(seconds(1001)).toBe(2);
|
|
});
|
|
|
|
it('shows zero only when the time is actually gone', () => {
|
|
expect(seconds(0)).toBe(0);
|
|
});
|
|
});
|