Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@

**Play chess right on GitHub. Take lessons free.** No install, no account, no ads, no tracking.

Open **[wsams.github.io/acnab/web](https://wsams.github.io/acnab/web/)** and the board is already there. Type SAN/PGN, or click pieces. Two players can each turn on a coach before the first move, and both of them see that choice. The page is static: notation, saves, the CPU, and the coach all stay in your browser.
Open **[wsams.github.io/acnab/web](https://wsams.github.io/acnab/web/)** and the board is already there. Choose a clock (or turn it off), turn on a coach, then press **Start game**. With the CPU on, **New game** tosses a coin and still waits for **Start game** before Stockfish moves. The page is static: notation, saves, the CPU, and the coach all stay in your browser.

**acnab** renders chess positions from standard chess notation in the terminal and in the browser.

![acnab web UI — Midnight theme with Blush & Magenta pieces, dual clocks, live notation, and local saves](docs/web-ui.png)

The browser UI shows the live board beside a Moves panel: type SAN/PGN, hit **Render**, and the position updates with FEN, material, and move history. Dual clocks, theme/piece-color presets, and browser-local saves sit alongside the board. Turn on **CPU player** to face Stockfish at a chosen strength. **Lessons** is a second engine that never moves a piece: before the first move, each player chooses a coach, both players see who has one, and the choice locks for that game. It explains candidate moves in chess language and lets you preview a line before you commit. Type SAN, or use **Click to move**.
The browser UI shows the live board beside a Moves panel: type SAN/PGN, hit **Render**, and the position updates with FEN, material, and move history. Dual clocks, theme/piece-color presets, and browser-local saves sit alongside the board. The clock can be Live, Notation pause, or Off, and that choice locks when the game starts. **Pause** stops the clock and covers the board until you resume. Turn on **CPU player** to face Stockfish at a chosen strength. **Lessons** is a second engine that never moves a piece: before Start game, each player chooses a coach, both players see who has one, and the choice locks for that game. It explains candidate moves in chess language and lets you preview a line before you commit. Type SAN, or use **Click to move**.

## Play free, and take a lesson

The whole app is published on GitHub Pages from this repository. You do not download a client, create an account, or sit through an ad.

- **Play:** [https://wsams.github.io/acnab/web/](https://wsams.github.io/acnab/web/)
- **Lessons:** on a new board, each side turns a coach on before the first move. Both players see the choice, and it cannot be turned on once the game has started. Against the CPU, choose your own coach before the match starts.
- **Lessons:** on a new board, each side turns a coach on before Start game. Both players see the choice, and it locks when the game starts. Against the CPU, toss a coin, choose your coach, then press Start game. Stockfish does not move until then.
- **Privacy:** acnab does not include analytics, accounts, or ads. Games and the lesson setting are stored in your browser only. Stockfish is vendored under `web/vendor/stockfish/` (GPLv3) and runs locally.

### What the coach does

Lessons are a second Stockfish. With two players at the board, each side can take a coach. The buttons stay visible, so you always know when the other player is using one. That choice is made before the first move. Against the CPU, you choose your own coach before the match starts. The coach does not play a move.
Lessons are a second Stockfish. With two players at the board, each side can take a coach. The buttons stay visible, so you always know when the other player is using one. That choice is made before Start game. Against the CPU, you choose your own coach after the coin toss and before Start game. The coach does not play a move.

- It offers a few tries, scored from White’s side of the evaluation, and says whether each one is the first choice, a sound alternative, or a concession.
- The write-up uses real chess ideas it can actually see on the board: development, the center, king safety, absolute and relative pins, forks, discovered check, skewers, outposts, passed pawns, pawn breaks, open files, prophylaxis, and opposition. A Ruy Lopez bishop is described as pressure, not a pin, while the d-pawn still blocks the king.
Expand Down Expand Up @@ -72,7 +72,7 @@ python3 chess.py --list-games

### Free on GitHub Pages

Open **[https://wsams.github.io/acnab/web/](https://wsams.github.io/acnab/web/)** to play in the browser for free. On a new board, turn a coach on before the first move. No account, no ads, no tracking.
Open **[https://wsams.github.io/acnab/web/](https://wsams.github.io/acnab/web/)** to play in the browser for free. On a new board, set the clock and a coach, then press Start game. No account, no ads, no tracking.

GitHub Pages publishes the static UI from `web/` (the site root redirects there). The build is fully static and client-side:

Expand All @@ -83,8 +83,9 @@ GitHub Pages publishes the static UI from `web/` (the site root redirects there)
- shareable board links (notation encoded in the URL hash; opening a link resets to the start and autoplays)
- play controls to step through or autoplay a game with a gliding piece animation
- click to lift a piece and click to place it; the moves box fills in SAN as each move lands
- optional CPU opponent (Stockfish.js 18 lite WASM) — off by default; coin toss for who plays White. Limited levels use Stockfish’s UCI Elo scale (1320–2500). Beginner and Casual are approximate mixes below that floor.
- optional **Lessons** coach — off by default; a second, full-strength Stockfish that never moves. Two players each choose a coach before the first move, and both can see who has one. It cannot be turned on in the middle of a game. It explains candidate moves (pins, forks, outposts, pawn breaks, opposition, and the opening you just entered), lets you preview or watch the line, and only writes a move if you press Play.
- optional CPU opponent (Stockfish.js 18 lite WASM) — off by default; **New game** tosses a coin for who plays White, and **Start game** begins the match. The CPU does not move before that. Limited levels use Stockfish’s UCI Elo scale (1320–2500). Beginner and Casual are approximate mixes below that floor.
- optional **Lessons** coach — off by default; a second, full-strength Stockfish that never moves. Two players each choose a coach before Start game, and both can see who has one. It cannot be turned on in the middle of a game. It explains candidate moves (pins, forks, outposts, pawn breaks, opposition, and the opening you just entered), lets you preview or watch the line, and only writes a move if you press Play.
- chess clock with Live, Notation pause, or Off, chosen before Start game. **Pause** stops the clock and covers the board.
- no CDN runtime dependency for the chess engine (Stockfish is vendored under `web/vendor/stockfish/`, GPLv3). The page does not load analytics or ads.

Example shared game (opens at move 1 and plays through to mate):
Expand Down
33 changes: 33 additions & 0 deletions tests/clock-mode.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { CLOCK_MODES, ChessClock } from '../web/clock.js';

test('off mode never runs and shows Off', () => {
const clock = new ChessClock({ mode: 'off', baseMs: 60_000, incrementMs: 1000 });
clock.press('white');
clock.setActive('black', { start: true });
clock.afterMove('black');
clock.ensureRunning();
const snap = clock.snapshot();
assert.equal(snap.mode, 'off');
assert.equal(snap.running, false);
assert.equal(snap.active, null);
assert.equal(snap.display.white, 'Off');
assert.equal(snap.display.black, 'Off');
assert.equal(CLOCK_MODES.off.label, 'Off');
});

test('pause stops a running clock until it is resumed', () => {
const clock = new ChessClock({ mode: 'live', baseMs: 60_000 });
clock.press('white');
assert.equal(clock.snapshot().running, true);
assert.equal(clock.snapshot().active, 'white');
clock.pause();
const paused = clock.snapshot();
assert.equal(paused.running, false);
assert.equal(paused.active, 'white');
clock.ensureRunning();
assert.equal(clock.snapshot().running, true);
assert.equal(clock.snapshot().active, 'white');
});
28 changes: 27 additions & 1 deletion tests/lessons-agreement.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,39 @@ test('stepping through history does not change the agreement', () => {
assert.deepEqual(reviewing.enrolled, { white: false, black: true });
});

test('starting the game freezes each coach before any move', () => {
const playing = advanceCoaching(setup, {
live: true,
offerCoaching: true,
gameStarted: true,
sans: [],
}, { white: true, black: false });
assert.equal(playing.phase, 'playing');
assert.deepEqual(playing.enrolled, { white: true, black: false });

const moved = advanceCoaching(playing, {
live: true,
offerCoaching: true,
gameStarted: true,
sans: ['e4'],
}, { white: true, black: true });
assert.equal(moved.phase, 'playing');
assert.deepEqual(moved.enrolled, { white: true, black: false });
});

test('two players are told who has a coach', () => {
const names = { white: 'Ivory', black: 'Ebony' };
assert.match(agreementText({
phase: 'setup',
enrolled: { white: false, black: false },
sideNames: names,
}), /Before the first move/);
}), /Before Start game/);
assert.match(agreementText({
phase: 'setup',
enrolled: { white: false, black: false },
cpu: { enabled: true, humanSide: 'black', tossing: false },
sideNames: names,
}), /Start game/);
assert.match(agreementText({
phase: 'playing',
enrolled: { white: true, black: false },
Expand Down
104 changes: 100 additions & 4 deletions web/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -543,10 +543,27 @@ code {
}

.board-toolbar {
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: 0.45rem;
}

.game-actions {
justify-content: flex-end;
}

.button:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
}

.lessons-seats .button:disabled,
.lessons-depth .button:disabled {
opacity: 1;
}

.compact-heading {
flex-direction: column;
align-items: flex-start;
Expand Down Expand Up @@ -1144,10 +1161,74 @@ textarea {
line-height: 1.35;
}

.clock-panel.is-typing-paused .clock-face.is-active {
.clock-panel.is-typing-paused .clock-face.is-active,
.clock-panel.is-paused .clock-face.is-active {
opacity: 0.72;
}

.clock-panel.is-off .clock-time {
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 1.15rem;
opacity: 0.72;
}

.pause-veil {
position: fixed;
inset: 0;
z-index: 70;
display: grid;
place-items: center;
padding: 1.25rem;
background: color-mix(in srgb, #0c0a09 42%, transparent);
}

.pause-veil[hidden] {
display: none;
}

.pause-card {
display: grid;
justify-items: center;
gap: 0.65rem;
width: min(100%, 22rem);
padding: 1.15rem 1.2rem 1.2rem;
border-radius: 1.1rem;
background: var(--panel);
border: 1px solid var(--border);
box-shadow: var(--shadow);
text-align: center;
}

.pause-kicker {
font-family: "Syne", sans-serif;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}

.pause-card p {
color: var(--muted);
line-height: 1.4;
}

.pause-card-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}

body.is-game-paused .clock-panel,
body.is-game-paused .board-layout,
body.is-game-paused .panel-notation,
body.is-game-paused .panel-board-meta {
filter: blur(16px);
pointer-events: none;
user-select: none;
}

.board.is-settling .board-square .piece {
animation: piece-settle 0.35s ease both;
}
Expand Down Expand Up @@ -2295,11 +2376,25 @@ textarea {
font-size: 1rem;
}

.clock-controls,
.clock-hint {
body.is-game-live .clock-controls,
body.is-game-live .clock-hint,
body.is-game-paused .clock-controls,
body.is-game-paused .clock-hint {
display: none;
}

body.is-game-setup .panel-notation .lessons-panel {
order: 1;
}

body.is-game-setup .panel-notation .cpu-panel {
order: 2;
}

body.is-game-setup .panel-notation .controls-stack {
order: 3;
}

.clock-faces {
gap: 0.45rem;
}
Expand Down Expand Up @@ -2503,7 +2598,8 @@ textarea {
width: 100%;
}

.board-actions .button {
.board-actions .button,
.game-actions .button {
flex: 1 1 auto;
}

Expand Down
Loading
Loading