├── .npmrc
├── src
├── lib
│ ├── index.js
│ ├── engine.ts
│ ├── Chess.svelte
│ ├── PromotionDialog.svelte
│ └── api.ts
├── routes
│ ├── +layout.ts
│ ├── css
│ │ └── +page.svelte
│ ├── undo
│ │ └── +page.svelte
│ ├── +page.svelte
│ ├── screenshot
│ │ └── +page.svelte
│ ├── reactive
│ │ └── +page.svelte
│ ├── fen
│ │ └── +page.svelte
│ ├── events
│ │ └── +page.svelte
│ └── stockfish
│ │ └── +page.svelte
├── app.d.ts
└── app.html
├── static
├── favicon.png
├── screenshot.png
└── style-paper.css
├── .github
├── dependabot.yml
└── workflows
│ ├── run-tests.yml
│ └── pages.yml
├── vite.config.ts
├── .gitignore
├── .eslintignore
├── tsconfig.json
├── vitest.config.ts
├── .eslintrc.cjs
├── svelte.config.js
├── vitest.config.githubaction.ts
├── test
├── Chess.test.ts
├── engine.test.ts
├── PromotionDialog.test.ts
├── Chess.engine.test.ts
└── api.test.ts
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | export { default as Chess } from './Chess.svelte'
2 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gtim/svelte-chess/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | // Examples are fully prerenderable
2 | export const prerender = true;
3 |
--------------------------------------------------------------------------------
/static/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gtim/svelte-chess/HEAD/static/screenshot.png
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()]
6 | });
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 | /coverage
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 |
11 | # Ignore files for PNPM, NPM and YARN
12 | pnpm-lock.yaml
13 | package-lock.json
14 | yarn.lock
15 |
--------------------------------------------------------------------------------
/src/routes/css/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "NodeNext"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { sveltekit } from '@sveltejs/kit/vite';
3 |
4 | export default defineConfig({
5 | plugins: [
6 | sveltekit(),
7 | ],
8 | test: {
9 | globals: true,
10 | environment: 'jsdom',
11 | // workaround for vitest bug: https://github.com/vitest-dev/vitest/issues/2834
12 | alias: [ { find: /^svelte$/, replacement: 'svelte/internal' } ],
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/src/routes/undo/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 | chess?.reset()}>Reset board
9 | chess?.undo()}>Undo
10 | chess?.toggleOrientation()}>Flip board
11 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@main
17 | with:
18 | persist-credentials: false
19 |
20 | - run: npm install
21 |
22 | - run: npm run build
23 |
24 | - run: npm run check
25 |
26 | - run: npm run test -- run --config vitest.config.githubaction.ts
27 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
12 | chess?.reset()}>Reset board
13 | chess?.undo()}>Undo
14 | chess?.playEngineMove()}>Play engine move
15 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/src/routes/screenshot/+page.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy demo to Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@main
15 | with:
16 | persist-credentials: false
17 |
18 | - name: Install
19 | run: npm install
20 |
21 | - name: Build
22 | run: npm run build
23 |
24 | - name: Deploy to Pages
25 | uses: peaceiris/actions-gh-pages@v3.9.3
26 | with:
27 | github_token: ${{ secrets.GITHUB_TOKEN }}
28 | publish_dir: build
29 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:svelte/recommended'
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | plugins: ['@typescript-eslint'],
10 | parserOptions: {
11 | sourceType: 'module',
12 | ecmaVersion: 2020,
13 | extraFileExtensions: ['.svelte']
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true
19 | },
20 | overrides: [
21 | {
22 | files: ['*.svelte'],
23 | parser: 'svelte-eslint-parser',
24 | parserOptions: {
25 | parser: '@typescript-eslint/parser'
26 | }
27 | }
28 | ]
29 | };
30 |
--------------------------------------------------------------------------------
/src/routes/reactive/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | {#if inCheck}
11 | Check!
12 | {/if}
13 | Move {moveNumber}, {turn == 'w' ? 'White' : 'Black'} to move.
14 | {#if history?.length > 0}
15 | Moves: {history.join(' ')}
16 | {/if}
17 | FEN: {fen}
18 |
19 |
25 |
--------------------------------------------------------------------------------
/src/routes/fen/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 | {#each fens as fen, i}
18 | {chess.load(fen)}}>{i}
19 | {/each}
20 |
21 |
22 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-static';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter()
15 | },
16 | compilerOptions: {
17 | accessors: process.env.TEST,
18 | },
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/vitest.config.githubaction.ts:
--------------------------------------------------------------------------------
1 | // This config excludes stockfish/webworker tests, for use when running test suite in Github actions.
2 | // Those stopped working between 2023-07-24 and 2023-08-13, but work fine locally.
3 | //
4 | // Example failed job: https://github.com/gtim/svelte-chess/actions/runs/5847212483/job/15853245035
5 | // FAIL test/Chess.engine.test.ts [ test/Chess.engine.test.ts ]
6 | // FAIL test/engine.test.ts [ test/engine.test.ts ]
7 | //TypeError: The "path" argument must be of type string or an instance of Buffer or URL. Received an instance of URL
8 | // ❯ Object.openSync node:fs:595:10
9 | // ❯ readFileSync node:fs:471:35
10 | // ❯ node_modules/vite/dist/node/constants.js:5:32
11 |
12 |
13 | import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'
14 | import viteConfig from './vitest.config.ts'
15 |
16 | export default mergeConfig(viteConfig, defineConfig({
17 | test: {
18 | exclude: [...configDefaults.exclude, 'test/Chess.engine.test.ts', 'test/engine.test.ts'],
19 | },
20 | }))
21 |
--------------------------------------------------------------------------------
/test/Chess.test.ts:
--------------------------------------------------------------------------------
1 | import Chess from '../src/lib/Chess.svelte';
2 | import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
3 |
4 | describe("Chess Component basic usage", () => {
5 |
6 | test("Play a simple game", async () => {
7 | const { component } = render( Chess );
8 | component.move( 'e4' );
9 | component.move( 'e5' );
10 | component.move( 'Bc4' );
11 | component.move( 'Nc6' );
12 | component.move( 'Qh5' );
13 | component.move( 'Nf6' );
14 | component.move( 'Qxf7' );
15 | });
16 | });
17 |
18 | test.todo( 'each bindable prop (moveNumber, turn, inCheck, history, isGameOver)' );
19 | test.todo( 'class prop' );
20 | test.todo( 'initial board orientation' );
21 | test.todo( 'initial FEN' );
22 | test.todo( 'promotion' );
23 | test.todo( 'en passant' );
24 | test.todo( 'events emitted: move, checkmate' );
25 | test.todo( 'load()' );
26 | test.todo( 'getHistory()' );
27 | test.todo( 'getBoard()' );
28 | test.todo( 'undo()' );
29 | test.todo( 'reset()' );
30 | test.todo( 'toggleOrientation()' );
31 | test.todo( 'game ends on checkmate' );
32 | test.todo( 'game ends on various draw conditions' );
33 | test.todo( 'board displayed on screen' );
34 | test.todo( 'pieces displayed on screen' );
35 | test.todo( 'promotion dialog displayed on screen' );
36 | test.todo( 'non-programmatic moves (click board)' );
37 | test.todo( 'class attribute applies style ' );
38 | test.todo( 'dispatches move event' );
39 | test.todo( 'dispatches gameOver event' );
40 | test.todo( 'dispatches uci event' );
41 |
--------------------------------------------------------------------------------
/src/routes/events/+page.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
This example listens for move and gameOver events.
32 |
33 |
34 |
35 | {#each messages as message (message.details)}
36 |
37 | {message.title}
38 |
39 | {/each}
40 |
41 |
42 |
56 |
--------------------------------------------------------------------------------
/src/routes/stockfish/+page.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 | chess?.playEngineMove()}>Play engine move
26 |
27 |
28 |
29 | {#each uciMessages as message}
30 |
{message.text}
31 | {/each}
32 |
33 |
34 |
35 |
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-chess",
3 | "description": "Fully playable chess component for Svelte. Powered by Chess.js logic, Chessground chessboard and optionally Stockfish chess AI.",
4 | "keywords": [
5 | "chess",
6 | "svelte",
7 | "chessground",
8 | "chess.js",
9 | "stockfish",
10 | "chess-engine",
11 | "chessboard",
12 | "lichess",
13 | "ui",
14 | "typescript"
15 | ],
16 | "version": "0.11.1",
17 | "license": "GPL-3.0",
18 | "homepage": "https://github.com/gtim/svelte-chess#readme",
19 | "bugs": "https://github.com/gtim/svelte-chess/issues",
20 | "scripts": {
21 | "dev": "vite dev",
22 | "build": "vite build && npm run package",
23 | "preview": "vite preview",
24 | "package": "svelte-kit sync && svelte-package && publint",
25 | "prepublishOnly": "npm run package",
26 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
27 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
28 | "lint": "eslint .",
29 | "test": "vitest",
30 | "coverage": "vitest run --coverage"
31 | },
32 | "exports": {
33 | ".": {
34 | "types": "./dist/index.d.ts",
35 | "svelte": "./dist/index.js"
36 | }
37 | },
38 | "files": [
39 | "dist",
40 | "!dist/**/*.test.*",
41 | "!dist/**/*.spec.*"
42 | ],
43 | "peerDependencies": {
44 | "svelte": "^3.54.0"
45 | },
46 | "devDependencies": {
47 | "@sveltejs/adapter-static": "^2.0.3",
48 | "@sveltejs/kit": "^1.22.5",
49 | "@sveltejs/package": "^2.2.1",
50 | "@testing-library/svelte": "^4.0.0",
51 | "@testing-library/user-event": "^14.4.3",
52 | "@typescript-eslint/eslint-plugin": "^5.62.0",
53 | "@typescript-eslint/parser": "^5.45.0",
54 | "@vitest/coverage-v8": "^0.34.1",
55 | "@vitest/web-worker": "^0.33.0",
56 | "eslint": "^8.47.0",
57 | "eslint-plugin-svelte": "^2.31.1",
58 | "jsdom": "^22.1.0",
59 | "publint": "^0.2.0",
60 | "svelte": "^3.54.0",
61 | "svelte-check": "^3.5.0",
62 | "tslib": "^2.6.1",
63 | "typescript": "^5.0.0",
64 | "vite": "^4.3.0",
65 | "vitest": "^0.33.0"
66 | },
67 | "svelte": "./dist/index.js",
68 | "types": "./dist/index.d.ts",
69 | "type": "module",
70 | "dependencies": {
71 | "chess.js": "^1.0.0-beta.6",
72 | "svelte-chessground": "^2.0.2"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/lib/engine.ts:
--------------------------------------------------------------------------------
1 | // TODO:
2 | // default opening book
3 | // skill level
4 | // setMoveTime, setDepth, setSkill
5 | // expose a bindable engine-is-searching boolean from the Chess component? or from this class?
6 | import type { Color } from '$lib/api.js';
7 |
8 | export interface EngineOptions {
9 | //skill?: number, // 1-20
10 | moveTime?: number, // Maximum time in ms to spend on a move
11 | depth?: number, // Maximum depth to search per move
12 | color?: Color | 'both' | 'none',
13 | stockfishPath?: string,
14 | };
15 |
16 | enum State {
17 | Uninitialised = 'uninitialised',
18 | Initialising = 'initialising',
19 | Waiting = 'waiting',
20 | Searching = 'searching', // searching for the best move
21 | };
22 |
23 | export class Engine {
24 | private stockfish: Worker | undefined;
25 | private state = State.Uninitialised;
26 | private moveTime: number;
27 | private depth: number;
28 | private color: Color | 'both' | 'none';
29 | private stockfishPath: string;
30 | private externalUciCallback: ( (uci:string) => void ) | undefined = undefined;
31 | // Callbacks used when waiting for specific UCI messages
32 | private onUciOk: ( () => void ) | undefined = undefined; // "uciok" marks end of initialisation
33 | private onBestMove: ( (uci:string) => void ) | undefined = undefined; // "uciok", used during initialisation
34 | // Constructor
35 | constructor( options: EngineOptions = {} ) {
36 | this.moveTime = options.moveTime || 2000;
37 | this.depth = options.depth || 40;
38 | this.color = options.color || 'b';
39 | this.stockfishPath = options.stockfishPath || 'stockfish.js';
40 | }
41 |
42 | // Initialise Stockfish. Resolve promise after receiving uciok.
43 | init(): Promise {
44 | return new Promise((resolve) => {
45 | this.state = State.Initialising;
46 | // NOTE: stockfish.js is not part of the npm package due to its size (1-2 MB).
47 | // You can find the file here: https://github.com/gtim/svelte-chess/tree/main/static
48 | this.stockfish = new Worker(this.stockfishPath);
49 | this.stockfish.addEventListener('message', (e)=>this._onUci(e) );
50 | this.onUciOk = () => {
51 | if ( this.state === State.Initialising ) {
52 | this.state = State.Waiting;
53 | this.onUciOk = undefined;
54 | resolve();
55 | }
56 | };
57 | this.stockfish.postMessage('uci');
58 | });
59 | }
60 |
61 | // Callback when receiving UCI messages from Stockfish.
62 | private _onUci( { data }: { data: string } ): void {
63 | const uci = data;
64 | if ( this.onUciOk && uci === 'uciok' ) {
65 | this.onUciOk();
66 | }
67 | if ( this.onBestMove && uci.slice(0,8) === 'bestmove' ) {
68 | this.onBestMove( uci );
69 | }
70 | if ( this.externalUciCallback ) {
71 | this.externalUciCallback( uci );
72 | }
73 | }
74 | setUciCallback( callback: (uci:string)=>void ) {
75 | this.externalUciCallback = callback;
76 | }
77 |
78 | getMove( fen: string ): Promise {
79 | return new Promise((resolve) => {
80 | if ( ! this.stockfish )
81 | throw new Error('Engine not initialised');
82 | if ( this.state !== State.Waiting )
83 | throw new Error('Engine not ready (state: ' + this.state + ')');
84 | this.state = State.Searching;
85 | this.stockfish.postMessage('position fen ' + fen);
86 | this.stockfish.postMessage(`go depth ${this.depth} movetime ${this.moveTime}`);
87 | this.onBestMove = ( uci: string ) => {
88 | const uciArray = uci.split(' ');
89 | const bestMoveLan = uciArray[1];
90 | this.state = State.Waiting;
91 | this.onBestMove = undefined;
92 | resolve( bestMoveLan );
93 | };
94 | });
95 | }
96 |
97 | getColor() {
98 | return this.color;
99 | }
100 |
101 | isSearching() {
102 | return this.state === State.Searching;
103 | }
104 |
105 | async stopSearch() {
106 | return new Promise((resolve) => {
107 | if ( ! this.stockfish )
108 | throw new Error('Engine not initialised');
109 | if ( this.state !== State.Searching )
110 | resolve();
111 | this.onBestMove = ( uci: string ) => {
112 | this.state = State.Waiting;
113 | this.onBestMove = undefined;
114 | resolve();
115 | }
116 | this.stockfish.postMessage('stop');
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/lib/Chess.svelte:
--------------------------------------------------------------------------------
1 |
8 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/test/engine.test.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from '../src/lib/engine.js';
2 | import { Chess as ChessJS } from 'chess.js';
3 | import '@vitest/web-worker';
4 |
5 | const INITIAL_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
6 |
7 | describe("basic engine tests", () => {
8 |
9 | const engine = new Engine({
10 | stockfishPath: 'static/stockfish.js',
11 | moveTime: 500,
12 | });
13 |
14 | test( "getMove throws when engine uninitialised", async () => {
15 | await expect( ()=>engine.getMove( INITIAL_FEN ) ).rejects.toThrow(/init/);
16 | });
17 |
18 | test("init finishes initialising", async () => {
19 | const initSpy = vi.spyOn( engine, 'init' );
20 | await engine.init();
21 | await expect( initSpy ).toHaveReturned();
22 | }, 10e3);
23 |
24 | test.each([
25 | { fen: INITIAL_FEN, note: 'initial position' },
26 | { fen: 'rnbqkb1r/pp2pppp/3p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R b KQkq - 2 5', note: 'black to move' },
27 | { fen: '5k2/p1p4R/1pr5/3p1pP1/P2P1P2/2P2K2/8/8 w - - 0 35' },
28 | { fen: '8/6P1/7k/5K2/8/8/8/8 w - - 0 1', note: 'promotion' },
29 | { fen: '8/8/3q3B/8/8/3k2P1/7b/R3K3 w Q - 0 1', note: 'castle'},
30 | ])( 'getMove returns a valid move for position $fen ($note)', async ({fen}) => {
31 | const move = await engine.getMove(fen);
32 | const chess = new ChessJS(fen);
33 | await chess.move( move, { strict: false } ); // throws on invalid move
34 | } );
35 |
36 | test("engine spends approx. moveTime ms on a move (500ms)", async () => {
37 | const start = new Date();
38 | await engine.getMove( INITIAL_FEN );
39 | const timeTakenMs = new Date() - start;
40 | expect( timeTakenMs ).toBeGreaterThan( 400 );
41 | expect( timeTakenMs ).toBeLessThan( 600 );
42 | });
43 |
44 | test("isSearching is true when searching, false before and after", async () => {
45 | expect( engine.isSearching() ).toBeFalsy();
46 | const movePromise = engine.getMove( INITIAL_FEN );
47 | expect( engine.isSearching() ).toBeTruthy();
48 | await new Promise(resolve => setTimeout(resolve, 100));
49 | expect( engine.isSearching() ).toBeTruthy();
50 | await movePromise;
51 | expect( engine.isSearching() ).toBeFalsy();
52 | });
53 |
54 | test( "stopSearch() stops search", async () => {
55 | const movePromise = engine.getMove( INITIAL_FEN );
56 | await new Promise(resolve => setTimeout(resolve, 100));
57 | expect( engine.isSearching() ).toBeTruthy();
58 | expect( engine['state'] ).toEqual( 'searching' ); // test private prop
59 | await engine.stopSearch();
60 | expect( engine.isSearching() ).toBeFalsy();
61 | expect( engine['state'] ).toEqual( 'waiting' ); // test private prop
62 | });
63 |
64 | test.each([
65 | { fen: '2r2rk1/p5pp/1P2p3/6q1/1P2Qn2/P7/2R2PPP/3B1RK1 b - - 2 27', answer: 'c8c2' },
66 | { fen: 'r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4', answer: 'h5f7' },
67 | { fen: '8/8/8/4k3/8/8/8/2BQKB2 w - - 0 1', answer: 'f1c4' },
68 | { fen: '7B/8/8/8/8/N7/pp1K4/k7 w - - 0 1', answer: 'd2c3' },
69 | ])( 'engine solves puzzle: $fen', async ({fen,answer}) => {
70 | await expect( engine.getMove( fen ) ).resolves.toEqual( answer );
71 | await new Promise(resolve => setTimeout(resolve, 20));
72 | });
73 | });
74 |
75 | test.each(['w','b','both','none'])("getColor returns %s", (color) => {
76 | const engine = new Engine({ color });
77 | expect( engine.getColor() ).toEqual( color );
78 | });
79 |
80 | test("engine does not solve hard puzzle at low depth", async () => {
81 | const engine = new Engine({
82 | stockfishPath: 'static/stockfish.js',
83 | depth: 8,
84 | });
85 | await engine.init();
86 | await expect( engine.getMove( 'r2q1r2/1b2bpkp/n3p1p1/2ppP1P1/p6R/1PN1BQR1/NPP2P1P/4K3 w - - 0 1' ) ).resolves.not.toEqual( 'f3f6' );
87 | await expect( engine.getMove( '8/8/4kpp1/3p1b2/p6P/2B5/6P1/6K1 b - - 0 1' ) ).resolves.not.toEqual( 'f5h3' );
88 | }, 10e3);
89 |
90 | test( "getMove throws if initialisation hasn't finished", async () => {
91 | const engine = new Engine({ stockfishPath: 'static/stockfish.js' });
92 | const initPromise = engine.init();
93 | await expect( ()=>engine.getMove( INITIAL_FEN ) ).rejects.toThrow(/not ready/);
94 | await initPromise;
95 | });
96 |
97 | test( "uci callback is called on init and move", async () => {
98 | const engine = new Engine({ stockfishPath: 'static/stockfish.js' });
99 | const uciCallback = vi.fn();
100 | engine.setUciCallback( uciCallback );
101 | expect( uciCallback ).not.toHaveBeenCalled();
102 | await engine.init();
103 | expect( uciCallback ).toHaveBeenCalled();
104 | const numCallsBeforeMove = uciCallback.mock.calls.length;
105 | await engine.getMove( INITIAL_FEN );
106 | const numCallsAfterMove = uciCallback.mock.calls.length;
107 | expect( numCallsAfterMove ).toBeGreaterThan( numCallsBeforeMove );
108 | } );
109 |
--------------------------------------------------------------------------------
/test/PromotionDialog.test.ts:
--------------------------------------------------------------------------------
1 | import PromotionDialog from '../src/lib/PromotionDialog.svelte';
2 | import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
3 |
4 | describe("PromotionDialog Component", () => {
5 |
6 | test("all four piece options displayed", () => {
7 | render( PromotionDialog, {
8 | square: 'a1',
9 | callback: (piece: PieceSymbol) => {},
10 | });
11 | const queen = screen.getByRole( 'button', { name: /queen/ } );
12 | const knight = screen.getByRole( 'button', { name: /knight/ } );
13 | const rook = screen.getByRole( 'button', { name: /rook/ } );
14 | const bishop = screen.getByRole( 'button', { name: /bishop/ } );
15 | expect( queen.className.split(' ') ).toContain( 'q' );
16 | expect( knight.className.split(' ') ).toContain( 'n' );
17 | expect( rook.className.split(' ') ).toContain( 'r' );
18 | expect( bishop.className.split(' ') ).toContain( 'b' );
19 | expect( queen.className.split(' ') ).toContain( 'piece' );
20 | expect( knight.className.split(' ') ).toContain( 'piece' );
21 | expect( rook.className.split(' ') ).toContain( 'piece' );
22 | expect( bishop.className.split(' ') ).toContain( 'piece' );
23 | });
24 |
25 | test("1st rank promotion shows black pieces", () => {
26 | render( PromotionDialog, {
27 | square: 'a1',
28 | callback: (piece: PieceSymbol) => {},
29 | });
30 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'black' );
31 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'black' );
32 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'black' );
33 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'black' );
34 | });
35 |
36 | test("8th rank promotion shows white pieces", () => {
37 | render( PromotionDialog, {
38 | square: 'a8',
39 | callback: (piece: PieceSymbol) => {},
40 | });
41 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'white' );
42 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'white' );
43 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'white' );
44 | expect( screen.getByRole( 'button', { name: /queen/ } ).className.split(' ') ).toContain( 'white' );
45 | });
46 |
47 | test.each([
48 | { piece: 'queen', symbol: 'q' },
49 | { piece: 'knight', symbol: 'n' },
50 | { piece: 'rook', symbol: 'r' },
51 | { piece: 'bishop', symbol: 'b' },
52 | ])( 'clicking $piece calls back with $symbol', async ({piece,symbol}) => {
53 | const callback = vi.fn();
54 | render( PromotionDialog, {
55 | square: 'a1',
56 | callback
57 | });
58 | const button = screen.getByRole( 'button', { name: new RegExp(piece) } );
59 | fireEvent.click( button );
60 | await waitFor( () => expect(callback).toHaveBeenLastCalledWith(symbol) );
61 | });
62 |
63 | });
64 |
65 | describe("button positions", () => {
66 |
67 | // Tests marginTop and marginLeft on specific elements.
68 | // Would preferably test div screen position directly.
69 |
70 | test.each([
71 | { square: 'c8', orientation: 'w', marginLeft: '25%', marginsTop: ['0%','12.5%','25%','37.5%'] },
72 | { square: 'c1', orientation: 'w', marginLeft: '25%', marginsTop: ['87.5%','75%','62.5%','50%'] },
73 | { square: 'c8', orientation: 'b', marginLeft: '62.5%', marginsTop: ['87.5%','75%','62.5%','50%'] },
74 | { square: 'c1', orientation: 'b', marginLeft: '62.5%', marginsTop: ['0%','12.5%','25%','37.5%'] },
75 | { square: 'h8', orientation: 'w', marginLeft: '87.5%', marginsTop: ['0%','12.5%','25%','37.5%'] },
76 | { square: 'h1', orientation: 'w', marginLeft: '87.5%', marginsTop: ['87.5%','75%','62.5%','50%'] },
77 | { square: 'h8', orientation: 'b', marginLeft: '0%', marginsTop: ['87.5%','75%','62.5%','50%'] },
78 | { square: 'h1', orientation: 'b', marginLeft: '0%', marginsTop: ['0%','12.5%','25%','37.5%'] },
79 | ])( 'button positions for promotion $square, $orientation orientation', async ({square,orientation,marginLeft,marginsTop}) => {
80 | render( PromotionDialog, {
81 | square,
82 | orientation,
83 | callback: (piece: PieceSymbol) => {},
84 | });
85 | const queenStyle = screen.getByRole( 'button', { name: /queen/ } ).parentElement.style;
86 | const knightStyle = screen.getByRole( 'button', { name: /knight/ } ).parentElement.style;
87 | const rookStyle = screen.getByRole( 'button', { name: /rook/ } ).parentElement.style;
88 | const bishopStyle = screen.getByRole( 'button', { name: /bishop/ } ).parentElement.style;
89 | expect( queenStyle.marginTop ).toEqual( marginsTop[0] );
90 | expect( knightStyle.marginTop ).toEqual( marginsTop[1] );
91 | expect( rookStyle.marginTop ).toEqual( marginsTop[2] );
92 | expect( bishopStyle.marginTop ).toEqual( marginsTop[3] );
93 | expect( queenStyle.marginLeft ).toEqual( marginLeft );
94 | expect( knightStyle.marginLeft ).toEqual( marginLeft );
95 | expect( rookStyle.marginLeft ).toEqual( marginLeft );
96 | expect( bishopStyle.marginLeft ).toEqual( marginLeft );
97 | } );
98 | });
99 |
--------------------------------------------------------------------------------
/test/Chess.engine.test.ts:
--------------------------------------------------------------------------------
1 | // Tests for the engine glue part of the Chess component.
2 | // Very slow since the engine is loaded many times.
3 | // This file and engine.test.ts should probably be split up into
4 | // a) actual unit tests with mocked web worker, and
5 | // b) integration tests with actual web worker.
6 |
7 | import Chess from '../src/lib/Chess.svelte';
8 | import type { Move } from '../src/lib/Chess.svelte';
9 | import { Engine } from '../src/lib/engine.js';
10 | import { render, screen, waitFor, act } from '@testing-library/svelte';
11 | import '@vitest/web-worker';
12 |
13 | describe("Engine auto-plays moves", async () => {
14 | test( 'Auto-plays first and third half-moves as White', async () => {
15 | const engine = new Engine({
16 | stockfishPath: 'static/stockfish.js',
17 | color: 'w',
18 | moveTime: 50,
19 | });
20 | const { component } = render( Chess, { props: { engine } } );
21 | component.$on( 'move', (event) => {
22 | if ( component.getHistory().length == 1 ) {
23 | component.move('g6');
24 | }
25 | } );
26 | await waitFor( () => expect( component.getHistory() ).toHaveLength(3), { timeout: 10e3 } );
27 | });
28 | test( 'Auto-plays second and fourth half-moves as Black', async () => {
29 | const engine = new Engine({
30 | stockfishPath: 'static/stockfish.js',
31 | color: 'b',
32 | moveTime: 50,
33 | });
34 | const { component } = render( Chess, { props: { engine } } );
35 | component.$on( 'ready', () => component.move('g3') );
36 | component.$on( 'move', (event) => {
37 | if ( component.getHistory().length == 2 ) {
38 | component.move('Bg2');
39 | }
40 | } );
41 | await waitFor( () => expect( component.getHistory() ).toHaveLength(4), { timeout: 10e3 } );
42 | });
43 | test( 'Auto-plays no moves as color "none"', async () => {
44 | const engine = new Engine({
45 | stockfishPath: 'static/stockfish.js',
46 | color: 'none',
47 | moveTime: 50,
48 | });
49 | const { component } = render( Chess, { props: { engine } } );
50 | // Expect history.length == 0 after 5 seconds (which includes engine init)
51 | await expect(
52 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(0), { timeout: 5e3 } )
53 | ).rejects.toThrow(/expected \+0 to/);
54 | // Play move
55 | component.move('e4');
56 | // Expect history.length == 1 after another second
57 | await expect(
58 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(1), { timeout: 1e3 } )
59 | ).rejects.toThrow(/expected 1 to /);
60 | });
61 | test( 'Auto-plays properly as White when reset or position loaded', async () => {
62 | const engine = new Engine({
63 | stockfishPath: 'static/stockfish.js',
64 | color: 'w',
65 | moveTime: 50,
66 | });
67 | const { component } = render( Chess, { props: { engine } } );
68 | await waitFor( () => expect( component.getHistory() ).toHaveLength(1), { timeout: 10e3 } );
69 |
70 | component.reset();
71 | expect( component.getHistory() ).toHaveLength(0);
72 | await waitFor( () => expect( component.getHistory() ).toHaveLength(1), { timeout: 10e3 } );
73 |
74 | component.load('q3nr1k/4bppp/3p4/4nPP1/r2BP2P/Np2Q3/1P6/1K1R1B1R w - - 5 25');
75 | expect( component.getHistory() ).toHaveLength(0);
76 | await waitFor( () => expect( component.getHistory() ).toHaveLength(1), { timeout: 10e3 } );
77 |
78 | component.load('q3nr1k/4bppp/3p4/1B2nPP1/r2BP2P/Np2Q3/1P6/1K1R3R b - - 6 25');
79 | expect( component.getHistory() ).toHaveLength(0);
80 | await expect( // Expect history.length == 0 after another second
81 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(0), { timeout: 1e3 } )
82 | ).rejects.toThrow(/expected \+0 to /);
83 | });
84 | test( 'Auto-plays properly as Black when reset or position loaded', async () => {
85 | const engine = new Engine({
86 | stockfishPath: 'static/stockfish.js',
87 | color: 'b',
88 | moveTime: 50,
89 | });
90 | const { component } = render( Chess, { props: { engine } } );
91 | // Expect history.length == 0 after 5 seconds (which includes engine init)
92 | await expect(
93 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(0), { timeout: 5e3 } )
94 | ).rejects.toThrow(/expected \+0 to/);
95 |
96 | component.reset();
97 | await expect( // Expect history.length == 0 after another second
98 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(0), { timeout: 1e3 } )
99 | ).rejects.toThrow(/expected \+0 to/);
100 |
101 | component.load('q3nr1k/4bppp/3p4/4nPP1/r2BP2P/Np2Q3/1P6/1K1R1B1R w - - 5 25');
102 | await expect( // Expect history.length == 0 after another second
103 | () => waitFor( () => expect( component.getHistory().length ).not.toEqual(0), { timeout: 1e3 } )
104 | ).rejects.toThrow(/expected \+0 to/);
105 |
106 | component.load('q3nr1k/4bppp/3p4/1B2nPP1/r2BP2P/Np2Q3/1P6/1K1R3R b - - 6 25');
107 | expect( component.getHistory() ).toHaveLength(0);
108 | await waitFor( () => expect( component.getHistory() ).toHaveLength(1), { timeout: 10e3 } );
109 | });
110 | test.each( [
111 | '8/8/8/4k3/R7/5R2/8/1K6 w - - 0 1', // K+2R vs K
112 | '8/4b3/8/4k3/2K5/5b2/8/8 w - - 0 1', // K vs K+2B
113 | ] )('engine-vs-engine game plays until mate (FEN %s)', async (fen) => {
114 | const engine = new Engine({
115 | stockfishPath: 'static/stockfish.js',
116 | color: 'both',
117 | moveTime: 100,
118 | });
119 | const { component } = render( Chess, { props: { fen, engine } } );
120 | // wait until mate
121 | await waitFor( () => expect( component.getHistory().slice(-1)[0].slice(-1) ).toEqual('#'), { timeout: 30e3 } );
122 | }, 30e3);
123 | }, 120e3);
124 |
125 |
126 | describe("move / playEngineMove", async () => {
127 | test( "move()/playEngineMove() throw if called before ready-event" , async () => {
128 | const engine = new Engine({
129 | stockfishPath: 'static/stockfish.js',
130 | color: 'none',
131 | moveTime: 50,
132 | });
133 | const { component, container } = render( Chess, { props: { engine } } );
134 | const onReady = vi.fn();
135 | component.$on( 'ready', onReady );
136 | expect( () => component.move('d4') ).toThrow();
137 | expect( () => component.playEngineMove() ).rejects.toThrow();
138 | await waitFor( () => expect(onReady).toHaveReturned(), { timeout: 10e3 } );
139 | });
140 | test( "playEngineMove() plays an engine move", async () => {
141 | const engine = new Engine({
142 | stockfishPath: 'static/stockfish.js',
143 | color: 'none',
144 | moveTime: 50,
145 | });
146 | const { component } = render( Chess, { props: { engine } } );
147 | component.$on( 'ready', () => {component.playEngineMove()} );
148 | await waitFor( () => expect( component.getHistory() ).toHaveLength(1), { timeout: 10e3 } );
149 | }, 10e3 );
150 | test( "move() while engine is searching stops search and performs move", async () => {
151 | const engine = new Engine({
152 | stockfishPath: 'static/stockfish.js',
153 | color: 'none',
154 | moveTime: 300,
155 | });
156 | const { component, container } = render( Chess, { props: { engine } } );
157 | const onReady = vi.fn( async () => {
158 | expect( engine.isSearching() ).toBeFalsy();
159 | component.playEngineMove();
160 | expect( engine.isSearching() ).toBeTruthy();
161 | component.move('d4');
162 | });
163 | const onMove = vi.fn();
164 | component.$on( 'ready', onReady );
165 | component.$on( 'move', onMove );
166 | expect( onMove ).toHaveBeenCalledTimes(0);
167 | await waitFor( () => expect(onReady).toHaveReturned(), { timeout: 10e3 } );
168 | expect( onMove ).toHaveBeenCalledTimes(1);
169 | await new Promise(resolve => setTimeout(resolve, 500));
170 | expect( onMove ).toHaveBeenCalledTimes(1);
171 | expect( component.fen ).toEqual( 'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1' );
172 | }, 15e3);
173 | }, 30e3);
174 |
175 |
176 |
177 | test.todo( "engine moves (or does not move) correctly after undo()" );
178 | test.todo( "on:uci forwards UCI messages");
179 |
180 | // How to test these?
181 | describe.todo("Chessground interaction", () => {
182 | // access the chessground instance?
183 | test.todo( "interactions are disabled before engine is loaded");
184 | test.todo( "interactions are enabled after engine is loaded" );
185 | test.todo( "interactions are disabled while engine is searching" );
186 | });
187 | test.todo( "correct king is hilighted on check"); // chessground adds translated to the king's square
188 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte-chess: Playable chess component
2 |
3 | Fully playable chess component for Svelte.
4 | Powered by
5 | [Chess.js](https://github.com/jhlywa/chess.js) logic,
6 | [Chessground](https://github.com/lichess-org/chessground) chessboard
7 | and optionally [Stockfish](https://github.com/official-stockfish/Stockfish) chess AI.
8 |
9 | 
10 |
11 | ## Features
12 |
13 | * Track game state via props or detailed events
14 | * Play against Stockfish
15 | * Undo moves
16 | * Pawn promotion dialog
17 | * Fully restylable
18 | * Move history
19 | * Typed
20 |
21 | ## Usage
22 |
23 | Installation:
24 |
25 | npm install svelte-chess
26 |
27 | Basic playable chessboard ([REPL](https://svelte.dev/repl/b1a489538165489aa2720a65b476a58b?version=3.59.1)):
28 |
29 |
32 |
33 |
34 | Interact with the game via [props](#props), [methods](#methods) or [events](#events).
35 |
36 | ### Props
37 |
38 | Game state can be observed by binding to props.
39 |
40 | | Prop | Bindable and readable | Writable | Value |
41 | | ------------ | :-------------------: | :------: | ------------------------------------------------------------------------------------ |
42 | | `turn` | ✓ | | Current color to move: `w` or `b` |
43 | | `moveNumber` | ✓ | | Current move number (whole moves) |
44 | | `history` | ✓ | | Array of all moves as SAN strings, e.g. `['d4','Nf6']` |
45 | | `inCheck` | ✓ | | True if the player to move is in check. |
46 | | `isGameOver` | ✓ | | True if the game is over. See also the [gameOver event](#events). |
47 | | `fen` | ✓ | ✓ | Current position in [FEN](https://www.chessprogramming.org/Forsyth-Edwards_Notation) |
48 | | `orientation`| ✓ | ✓ | Orientation of the board: `w` or `b`. |
49 | | `engine` | | ✓ | Options for the Stockfish chess AI. See [Engine](#engine--stockfish). |
50 | | `class` | | ✓ | CSS class applied to children instead of default (see [Styling](#styling)). |
51 |
52 | All readable props are bindable and updated whenever the game state changes.
53 | Writable props are only used when the component is created.
54 |
55 | Example using bindable props to monitor state ([REPL](https://svelte.dev/repl/d0ec69dde1f84390ac8b4d5746db9505?version=3.59.1)):
56 |
57 |
61 |
62 |
63 | It's move {moveNumber}, with {turn} to move.
64 | Moves played: {history?.join(' ')}.
65 |
66 |
67 | Starting from a specific FEN ([REPL](https://svelte.dev/repl/ebce18a71d774b2db987abc71f45648a?version=3.59.1)):
68 |
69 |
70 |
71 | ### Methods
72 |
73 | The board state can be read and manipulated via method calls to the Chess component itself.
74 |
75 | Methods for reading game/board state:
76 |
77 | * `getHistory()`: Same as the `history` prop. All moves played in the game, as an array of SAN strings, e.g. `['d4','Nf6','Bg5']`.
78 | * `getHistory({verbose: true})`: All moves played in the game, as an array of [Move objects](#move).
79 | * `getBoard()`: An 8x8 array of the current position. Each element is null (empty square) or an object on the form `{ square: 'd8', type: 'q', color: 'b' }`.
80 |
81 | Methods for manipulating game/board state:
82 |
83 | * `move( san )`: Make a move programmatically. Argument is the move in [short algebraic notation](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)), e.g. `Nf3`. Throws an error if the move is illegal or malformed.
84 | * `load( fen )`: Loads a position from FEN. Throws an error if the FEN could not be parsed.
85 | * `reset()`: Resets the game to the initial position.
86 | * `undo()`: Undoes the last move and returns it.
87 | * `toggleOrientation()`: Flips the board.
88 | * `makeEngineMove()`: Make the best move according to the engine. See [Engine / Stockfish](#engine--stockfish) for loading the engine.
89 |
90 | Example implementing undo/reset buttons ([REPL](https://svelte.dev/repl/7dd7b6454b12466e90ac78a842151311?version=3.59.1)):
91 |
92 |
96 |
97 | chess?.reset()}>Reset
98 | chess?.undo()}>Undo
99 |
100 | ### Events
101 |
102 | A `ready` event is dispatched when the Chess component is ready for interaction,
103 | which is generally immediately on mount. If an [engine](#engine--stockfish) was
104 | specified, the event is dispatched after engine initialisation, which might take
105 | a second.
106 |
107 | A `move` event is dispatched after every move, containing the corresponding [Move object](#move).
108 |
109 | A `gameOver` event is emitted after a move that ends the game. The GameOver object has two keys:
110 | * `reason`: `checkmate`, `stalemate`, `repetition`, `insufficient material` or `fifty-move rule`.
111 | * `result`: 1 for White win, 0 for Black win, or 0.5 for a draw.
112 |
113 | A `uci` event is emitted when Stockfish, if enabled, sends a UCI message.
114 |
115 | Example listening for `move` and `gameOver` events ([REPL](https://svelte.dev/repl/6fc2874d1a594d76aede4834722e4f83?version=3.59.1)):
116 |
117 |
127 |
128 |
129 | Svelte-chess exports the MoveEvent, GameOverEvent, ReadyEvent and UciEvent types.
130 |
131 | ### Engine / Stockfish
132 |
133 | Svelte-chess can be used to play against the chess AI Stockfish 14. You need to download the Stockfish web worker script separately: [stockfish.js web worker (1.6MB)](https://raw.githubusercontent.com/gtim/svelte-chess/stockfish/static/stockfish.js) and serve it at `/stockfish.js`. If you're using SvelteKit, do this by putting it in the static folder.
134 |
135 | Example playing Black versus Stockfish ([live](https://gtim.github.io/svelte-chess/stockfish)):
136 |
137 |
141 |
142 |
143 | The `engine` prop is an object with the following keys, all optional:
144 |
145 | | Key | Default | Description |
146 | | ----------- | ------- | --------------------------------------------------------------------------- |
147 | | `color` | `b` | Color the engine plays: `w` or `b`, or `both` for an engine-vs-engine game, or `none` if the engine should only make a move when `makeEngineMove()` is called. |
148 | | `moveTime` | 2000 | Max time in milliseconds for the engine to spend on a move. |
149 | | `depth` | 40 | Max depth in ply for the engine to search. |
150 |
151 | To inspect Stockfish's current evaluation and other engine details, you can listen to `uci` events from the Chess component to read all [UCI](https://www.chessprogramming.org/UCI) messages sent by Stockfish.
152 |
153 | ### Styling
154 |
155 | The stylesheet shipped with Chessground is used by default. To restyle the
156 | board, pass the `class` prop and import a stylesheet.
157 |
158 | Example with custom stylesheet:
159 |
160 |
163 |
164 |
165 |
166 | A sample stylesheet can be found in [/static/style-paper.css](https://github.com/gtim/svelte-chess/blob/main/static/style-paper.css).
167 |
168 | ## Types
169 |
170 | ### Move
171 |
172 | A `Move` describes a chess move. Properties:
173 | - `color`: `w` for White move or `b` for Black move.
174 | - `from` and `to`: Origin and destination squares, e.g. `g1` and `f3`.
175 | - `piece`: Piece symbol, one of `pnbrqk` (pawn, knight, bishop, rook, queen, king).
176 | - `captured` and `promotion`: Piece symbol of a capture or promotion, if applicable.
177 | - `san`: Standard algebraic notation, e.g. `Nf3`.
178 | - `lan`: Long algebraic notation, e.g. `g1f3`.
179 | - `before` and `after`: FEN of positions before and after the move.
180 | - `flags`: String of letters for each flag that applies to the move: `c` for standard capture, `e` for en passant capture, `n` for non-capture, `b` for two-square pawn move, `p` for promotion, `k` for kingside castling and `q` for queenside castling.
181 | - `check`: True if the move put the opponent in check (or checkmate).
182 | - `checkmate`: True if the move put the opponent in checkmate.
183 |
184 |
185 | ## Future
186 |
187 | * Programmatically draw arrows/circles on the board
188 |
--------------------------------------------------------------------------------
/src/lib/PromotionDialog.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 | {#each pieces as piece, i}
32 | {@const putPiecesFromTop = white && orientation === 'w' || black && orientation === 'b'}
33 | {@const marginTop = putPiecesFromTop ? i * 12.5 : 100 - 12.5*(i+1)}
34 |
35 |
callback(piece)}
38 | on:keydown={(e)=>keyboardCallback(e,piece)}
39 | role="button" tabindex="0" aria-label="Promote to {pieceNames[piece]}"
40 | >
41 |
42 | {/each}
43 |
44 |
45 |
105 |
--------------------------------------------------------------------------------
/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | import type { Chessground } from 'svelte-chessground';
2 | import { Chess as ChessJS, SQUARES } from 'chess.js';
3 | import type { Square, PieceSymbol, Color, Move as CjsMove } from 'chess.js';
4 | export type { Square, PieceSymbol, Color };
5 | import type { Engine } from '$lib/engine.js';
6 |
7 | export type Move = CjsMove & {
8 | check: boolean,
9 | checkmate: boolean,
10 | };
11 |
12 | export type GameOver = {
13 | reason: "checkmate" | "stalemate" | "repetition" | "insufficient material" | "fifty-move rule",
14 | result: 1 | 0 | 0.5,
15 | };
16 |
17 | export class Api {
18 | private chessJS: ChessJS;
19 | private gameIsOver = false;
20 | private initialised = false;
21 | constructor(
22 | private cg: Chessground,
23 | fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
24 | private stateChangeCallback: (api:Api) => void = (api)=>{}, // called when the game state (not visuals) changes
25 | private promotionCallback: (sq:Square) => Promise = async (sq)=>'q', // called before promotion
26 | private moveCallback: (move:Move) => void = (m)=>{}, // called after move
27 | private gameOverCallback: ( gameOver:GameOver ) => void = (go)=>{}, // called after game-ending move
28 | private _orientation: Color = 'w',
29 | private engine: Engine | undefined = undefined,
30 | ) {
31 | this.cg.set( {
32 | fen,
33 | orientation: Api._colorToCgColor( _orientation ),
34 | movable: { free: false },
35 | premovable: { enabled: false },
36 | } );
37 | this.chessJS = new ChessJS( fen );
38 | }
39 |
40 | async init() {
41 | if ( this.engine ) {
42 | await this.engine.init();
43 | this.load( this.chessJS.fen() );
44 | if ( this._enginePlaysNextMove() ) {
45 | this.playEngineMove();
46 | }
47 | } else {
48 | this.load( this.chessJS.fen() );
49 | }
50 | this.initialised = true;
51 | }
52 |
53 | // Load FEN. Throws exception on invalid FEN.
54 | load( fen: string ) {
55 | let engineStopSearchPromise;
56 | if ( this.initialised && this.engine?.isSearching() )
57 | engineStopSearchPromise = this.engine.stopSearch();
58 | this.chessJS.load( fen );
59 | this._checkForGameOver();
60 | this.cg.set( { animation: { enabled: false } } );
61 | const cgColor = Api._colorToCgColor( this.chessJS.turn() );
62 | const enginePlaysNextMove = this._enginePlaysNextMove();
63 | this.cg.set( {
64 | fen: fen,
65 | turnColor: cgColor,
66 | check: this.chessJS.inCheck(),
67 | lastMove: undefined,
68 | selected: undefined,
69 | movable: {
70 | free: false,
71 | color: cgColor,
72 | dests: enginePlaysNextMove ? new Map() : this.possibleMovesDests(),
73 | events: {
74 | after: (orig, dest) => { this._chessgroundMoveCallback(orig,dest) },
75 | },
76 | },
77 | } );
78 | this.cg.set( { animation: { enabled: true } } );
79 | if ( this.initialised && enginePlaysNextMove ) {
80 | // Play immediate engine move, but wait until stopSearch has finished
81 | if ( engineStopSearchPromise ) {
82 | engineStopSearchPromise.then( () => {
83 | this.playEngineMove();
84 | });
85 | } else {
86 | this.playEngineMove();
87 | }
88 | }
89 | this.stateChangeCallback(this);
90 | }
91 |
92 | /*
93 | * Making a move
94 | */
95 |
96 | // called after a move is played on Chessground
97 | async _chessgroundMoveCallback( orig: Square|'a0', dest: Square|'a0' ) {
98 | if ( orig === 'a0' || dest === 'a0' ) {
99 | // the Chessground square type (Key) includes a0
100 | throw Error('invalid square');
101 | }
102 | if ( this.engine && this.engine.isSearching() )
103 | this.engine.stopSearch();
104 | let cjsMove: CjsMove;
105 | if ( this._moveIsPromotion( orig, dest ) ) {
106 | const promotion = await this.promotionCallback( dest );
107 | cjsMove = this.chessJS.move({ from: orig, to: dest, promotion });
108 | } else {
109 | cjsMove = this.chessJS.move({ from: orig, to: dest });
110 | }
111 | const move = Api._cjsMoveToMove( cjsMove );
112 | this._postMoveAdmin( move );
113 | }
114 |
115 | private _moveIsPromotion( orig: Square, dest: Square ): boolean {
116 | return this.chessJS.get(orig).type === 'p' && ( dest.charAt(1) == '1' || dest.charAt(1) == '8' );
117 | }
118 |
119 | // Make a move programmatically
120 | // argument is either a short algebraic notation (SAN) string
121 | // or an object with from/to/promotion (see chess.js move())
122 | move( moveSanOrObj: string | { from: string, to: string, promotion?: string } ) {
123 | if ( ! this.initialised )
124 | throw new Error('Called move before initialisation finished.');
125 | if ( this.gameIsOver )
126 | throw new Error('Invalid move: Game is over.');
127 | if ( this.engine && this.engine.isSearching() )
128 | this.engine.stopSearch();
129 | const cjsMove = this.chessJS.move( moveSanOrObj ); // throws on illegal move
130 | const move = Api._cjsMoveToMove( cjsMove );
131 | this.cg.move( move.from, move.to );
132 | this.cg.set({ turnColor: Api._colorToCgColor( this.chessJS.turn() ) });
133 | this._postMoveAdmin( move );
134 | }
135 | // Make a move programmatically from long algebraic notation (LAN) string,
136 | // as returned by UCI engines.
137 | moveLan( moveLan: string ) {
138 | const from = moveLan.slice(0,2);
139 | const to = moveLan.slice(2,4);
140 | const promotion = moveLan.charAt(4) || undefined;
141 | this.move( { from, to, promotion } );
142 | }
143 |
144 | // Called after a move (chess.js or chessground) to:
145 | // - update chess-logic details Chessground doesn't handle
146 | // - dispatch events
147 | // - play engine move
148 | private _postMoveAdmin( move: Move ) {
149 |
150 | const enginePlaysNextMove = this._enginePlaysNextMove();
151 |
152 | // reload FEN after en-passant or promotion. TODO make promotion smoother
153 | if ( move.flags.includes('e') || move.flags.includes('p') ) {
154 | this.cg.set({ fen: this.chessJS.fen() });
155 | }
156 | // highlight king if in check
157 | if ( move.check ) {
158 | this.cg.set({ check: true });
159 | }
160 | // dispatch move event
161 | this.moveCallback( move );
162 | // dispatch gameOver event if applicable
163 | this._checkForGameOver();
164 | // set legal moves
165 | if ( enginePlaysNextMove ) {
166 | this.cg.set({ movable: { dests: new Map() } }); // no legal moves
167 | } else {
168 | this._updateChessgroundWithPossibleMoves();
169 | }
170 | // update state props
171 | this.stateChangeCallback(this);
172 |
173 | // engine move
174 | if ( ! this.gameIsOver && enginePlaysNextMove ) {
175 | this.playEngineMove();
176 | }
177 |
178 | }
179 |
180 | async playEngineMove() {
181 | if ( ! this.engine )
182 | throw new Error('playEngineMove called without initialised engine');
183 | return this.engine.getMove( this.chessJS.fen() ).then( (lan) => {
184 | this.moveLan(lan);
185 | });
186 | }
187 |
188 | private _enginePlaysNextMove() {
189 | return this.engine && ( this.engine.getColor() === 'both' || this.engine.getColor() === this.chessJS.turn() );
190 | }
191 |
192 | private _updateChessgroundWithPossibleMoves() {
193 | const cgColor = Api._colorToCgColor( this.chessJS.turn() );
194 | this.cg.set({
195 | turnColor: cgColor,
196 | movable: {
197 | color: cgColor,
198 | dests: this.possibleMovesDests(),
199 | },
200 | });
201 | }
202 | private _checkForGameOver() {
203 | if ( this.chessJS.isCheckmate() ) {
204 | const result = this.chessJS.turn() == 'w' ? 0 : 1;
205 | this.gameOverCallback( { reason: 'checkmate', result } );
206 | this.gameIsOver = true;
207 | } else if ( this.chessJS.isStalemate() ) {
208 | this.gameOverCallback( { reason: 'stalemate', result: 0.5 } );
209 | this.gameIsOver = true;
210 | } else if ( this.chessJS.isInsufficientMaterial() ) {
211 | this.gameOverCallback( { reason: 'insufficient material', result: 0.5 } );
212 | this.gameIsOver = true;
213 | } else if ( this.chessJS.isThreefoldRepetition() ) {
214 | this.gameOverCallback( { reason: 'repetition', result: 0.5 } );
215 | this.gameIsOver = true;
216 | } else if ( this.chessJS.isDraw() ) {
217 | // use isDraw until chess.js exposes isFiftyMoveDraw()
218 | this.gameOverCallback( { reason: 'fifty-move rule', result: 0.5 } );
219 | this.gameIsOver = true;
220 | } else {
221 | this.gameIsOver = false;
222 | }
223 | }
224 |
225 |
226 | /*
227 | *
228 | */
229 |
230 | // Find all legal moves in chessground "dests" format
231 | possibleMovesDests() {
232 | const dests = new Map();
233 | if ( ! this.gameIsOver ) {
234 | SQUARES.forEach(s => {
235 | const ms = this.chessJS.moves({square: s, verbose: true});
236 | if (ms.length) dests.set(s, ms.map(m => m.to));
237 | });
238 | }
239 | return dests;
240 | }
241 |
242 | // Reset board to the starting position
243 | reset(): void {
244 | this.load( 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' );
245 | }
246 |
247 | // Undo last move
248 | undo(): Move | null {
249 | const cjsMove = this.chessJS.undo();
250 | const move = cjsMove ? Api._cjsMoveToMove( cjsMove ) : null;
251 | const turnColor = Api._colorToCgColor( this.chessJS.turn() );
252 | this.cg.set({
253 | fen: this.chessJS.fen(),
254 | check: this.chessJS.inCheck() ? turnColor : undefined,
255 | turnColor: turnColor,
256 | lastMove: undefined,
257 | });
258 | this.gameIsOver = false;
259 | this._updateChessgroundWithPossibleMoves();
260 | this.stateChangeCallback(this);
261 | return move;
262 | }
263 |
264 | // Board orientation
265 | toggleOrientation(): void {
266 | this._orientation = this._orientation === 'w' ? 'b' : 'w';
267 | this.cg.set({
268 | orientation: Api._colorToCgColor( this._orientation ),
269 | });
270 | this.stateChangeCallback(this);
271 | }
272 | orientation(): Color {
273 | return this._orientation;
274 | }
275 |
276 | // Check if game is over (checkmate, stalemate, repetition, insufficient material, fifty-move rule)
277 | isGameOver(): boolean {
278 | return this.gameIsOver;
279 | }
280 |
281 |
282 | /*
283 | * Methods passed through to chess.js
284 | */
285 |
286 | fen(): string {
287 | return this.chessJS.fen();
288 | }
289 | turn() {
290 | return this.chessJS.turn();
291 | }
292 | moveNumber() {
293 | return this.chessJS.moveNumber();
294 | }
295 | inCheck() {
296 | return this.chessJS.inCheck();
297 | }
298 | history(): string[]
299 | history({ verbose }: { verbose: true }): Move[]
300 | history({ verbose }: { verbose: false }): string[]
301 | history({ verbose }: { verbose: boolean }): string[] | Move[]
302 | history({ verbose = false }: { verbose?: boolean } = {}) {
303 | if ( verbose ) {
304 | return this.chessJS.history({ verbose }).map( Api._cjsMoveToMove );
305 | } else {
306 | return this.chessJS.history({ verbose });
307 | }
308 | }
309 | board() {
310 | return this.chessJS.board();
311 | }
312 |
313 | // Convert between chess.js color (w/b) and chessground color (white/black).
314 | // Chess.js color is always used internally.
315 | static _colorToCgColor( chessjsColor: Color ): 'white' | 'black' {
316 | return chessjsColor === 'w' ? 'white' : 'black';
317 | }
318 | static _cgColorToColor( chessgroundColor: 'white' | 'black' ): Color {
319 | return chessgroundColor === 'white' ? 'w' : 'b';
320 | }
321 |
322 | // Convert chess.js move (CjsMove) to svelte-chess Move.
323 | // Only difference is check:boolean and checkmate:boolean in the latter.
324 | static _cjsMoveToMove( cjsMove: CjsMove ): Move {
325 | const lastSanChar = cjsMove.san.slice(-1);
326 | const checkmate = lastSanChar === '#';
327 | const check = lastSanChar === '+' || checkmate;
328 | return { ...cjsMove, check, checkmate };
329 | }
330 |
331 | }
332 |
--------------------------------------------------------------------------------
/test/api.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, expect} from 'vitest';
2 | import { Api } from '../src/lib/api.js';
3 | import { Chessground } from 'svelte-chessground';
4 |
5 | // Mock svelte-chessground: Chessground changes are *not* tested here
6 | vi.mock('svelte-chessground', () => {
7 | const Chessground = vi.fn();
8 | Chessground.prototype.move = vi.fn();
9 | Chessground.prototype.set = vi.fn();
10 | return { Chessground };
11 | });
12 |
13 | let api: Api;
14 | beforeEach(async () => {
15 | api = new Api( new Chessground() );
16 | await api.init();
17 | } );
18 |
19 | describe("possibleMovesDests", () => {
20 | test("correct moves from initial position", () => {
21 | expect(api.possibleMovesDests()).toEqual(
22 | new Map( Object.entries( {
23 | a2: ['a3','a4'],
24 | b2: ['b3','b4'],
25 | c2: ['c3','c4'],
26 | d2: ['d3','d4'],
27 | e2: ['e3','e4'],
28 | f2: ['f3','f4'],
29 | g2: ['g3','g4'],
30 | h2: ['h3','h4'],
31 | b1: ['a3','c3'],
32 | g1: ['f3','h3'],
33 | } ) )
34 | );
35 | });
36 | });
37 |
38 | describe("move", () => {
39 | test("play 1. e4 e5 2. Nf3", () => {
40 | api.move('e4');
41 | api.move('e5');
42 | api.move('Nf3');
43 | expect( api.fen() ).toEqual( 'rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2' );
44 | });
45 | test("1. d5 is illegal", () => {
46 | expect( () => api.move('d5') ).toThrowError();
47 | });
48 | });
49 |
50 | describe("board manipulation", () => {
51 | test("reset board", () => {
52 | const initialFen = api.fen();
53 | api.reset();
54 | expect( api.fen() ).toEqual( initialFen );
55 | api.move('e4');
56 | expect( api.fen() ).not.toEqual( initialFen );
57 | api.reset();
58 | expect( api.fen() ).toEqual( initialFen );
59 | } );
60 | test("undo last move", () => {
61 | let move;
62 | api.move('e4');
63 | api.move('e5');
64 | api.move('Bc4');
65 | expect( api.fen() ).toEqual( 'rnbqkbnr/pppp1ppp/8/4p3/2B1P3/8/PPPP1PPP/RNBQK1NR b KQkq - 1 2' );
66 | move = api.undo();
67 | expect( move.san ).toEqual( 'Bc4' );
68 | expect( api.fen() ).toEqual( 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2' );
69 | api.move('Qh5');
70 | expect( api.fen() ).toEqual( 'rnbqkbnr/pppp1ppp/8/4p2Q/4P3/8/PPPP1PPP/RNB1KBNR b KQkq - 1 2' );
71 | move = api.undo();
72 | expect( move.san ).toEqual( 'Qh5' );
73 | move = api.undo();
74 | expect( move.san ).toEqual( 'e5' );
75 | expect( api.fen() ).toEqual( 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1' );
76 | } );
77 | });
78 |
79 | describe("board state", () => {
80 | test("turn color", () => {
81 | expect( api.turn() ).toEqual( 'w' );
82 | api.move('e4');
83 | expect( api.turn() ).toEqual( 'b' );
84 | api.move('e5');
85 | expect( api.turn() ).toEqual( 'w' );
86 | api.move('Bc4');
87 | expect( api.turn() ).toEqual( 'b' );
88 | api.move('d6');
89 | expect( api.turn() ).toEqual( 'w' );
90 | api.undo();
91 | expect( api.turn() ).toEqual( 'b' );
92 | api.reset();
93 | expect( api.turn() ).toEqual( 'w' );
94 | } );
95 | test("move number", () => {
96 | expect( api.moveNumber() ).toEqual( 1 );
97 | api.move('e4');
98 | expect( api.moveNumber() ).toEqual( 1 );
99 | api.move('e5');
100 | expect( api.moveNumber() ).toEqual( 2 );
101 | api.move('Bc4');
102 | expect( api.moveNumber() ).toEqual( 2 );
103 | api.move('d6');
104 | expect( api.moveNumber() ).toEqual( 3 );
105 | api.undo();
106 | expect( api.moveNumber() ).toEqual( 2 );
107 | api.reset();
108 | expect( api.moveNumber() ).toEqual( 1 );
109 | } );
110 | test("move history, verbose", () => {
111 | expect( api.history({verbose:true}) ).toEqual( [] );
112 | api.move('e4');
113 | api.move('e5');
114 | expect( api.history({verbose:true}) ).toEqual( [
115 | {
116 | after: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1',
117 | before: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
118 | color: 'w',
119 | flags: 'b',
120 | from: 'e2',
121 | lan: 'e2e4',
122 | piece: 'p',
123 | san: 'e4',
124 | to: 'e4',
125 | check: false,
126 | checkmate: false,
127 | },
128 | {
129 | after: 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2',
130 | before: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1',
131 | color: 'b',
132 | flags: 'b',
133 | from: 'e7',
134 | lan: 'e7e5',
135 | piece: 'p',
136 | san: 'e5',
137 | to: 'e5',
138 | check: false,
139 | checkmate: false,
140 | }
141 | ] );
142 | expect( () => api.move('a6') ).toThrowError();
143 | api.move('Bc4');
144 | expect( api.history({verbose:true}) ).toHaveLength(3);
145 | expect( (api.history({verbose:true}))[2] ).toEqual( {
146 | before: 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2',
147 | after: 'rnbqkbnr/pppp1ppp/8/4p3/2B1P3/8/PPPP1PPP/RNBQK1NR b KQkq - 1 2',
148 | color: 'w',
149 | piece: 'b',
150 | from: 'f1',
151 | to: 'c4',
152 | san: 'Bc4',
153 | flags: 'n',
154 | lan: 'f1c4',
155 | check: false,
156 | checkmate: false,
157 | });
158 | api.reset();
159 | expect( api.history({verbose:true}) ).toEqual( [] );
160 | } );
161 | test("move history, verbose, check/checkmate", () => {
162 | api.move('e4');
163 | api.move('e5');
164 | api.move('Bc4');
165 | api.move('Bc5');
166 | api.move('Qf3');
167 | api.move('Bxf2');
168 | api.move('Qxf2');
169 | api.move('d6');
170 | api.move('Qxf7');
171 | const history = api.history({verbose:true});
172 | expect( history ).toHaveLength(9);
173 | expect( history[0] ).toContain( { check: false, checkmate: false } );
174 | expect( history[1] ).toContain( { check: false, checkmate: false } );
175 | expect( history[2] ).toContain( { check: false, checkmate: false } );
176 | expect( history[3] ).toContain( { check: false, checkmate: false } );
177 | expect( history[4] ).toContain( { check: false, checkmate: false } );
178 | expect( history[5] ).toContain( { check: true, checkmate: false } );
179 | expect( history[6] ).toContain( { check: false, checkmate: false } );
180 | expect( history[7] ).toContain( { check: false, checkmate: false } );
181 | expect( history[8] ).toContain( { check: true, checkmate: true } );
182 | } );
183 |
184 | test("board()", () => {
185 | api.move('e4');
186 | const board = api.board();
187 | expect( board ).toHaveLength(8);
188 | expect( board[0] ).toEqual( [
189 | { square: 'a8', type: 'r', color: 'b' },
190 | { square: 'b8', type: 'n', color: 'b' },
191 | { square: 'c8', type: 'b', color: 'b' },
192 | { square: 'd8', type: 'q', color: 'b' },
193 | { square: 'e8', type: 'k', color: 'b' },
194 | { square: 'f8', type: 'b', color: 'b' },
195 | { square: 'g8', type: 'n', color: 'b' },
196 | { square: 'h8', type: 'r', color: 'b' }
197 | ]);
198 | expect( board[1] ).toEqual( [
199 | { square: 'a7', type: 'p', color: 'b' },
200 | { square: 'b7', type: 'p', color: 'b' },
201 | { square: 'c7', type: 'p', color: 'b' },
202 | { square: 'd7', type: 'p', color: 'b' },
203 | { square: 'e7', type: 'p', color: 'b' },
204 | { square: 'f7', type: 'p', color: 'b' },
205 | { square: 'g7', type: 'p', color: 'b' },
206 | { square: 'h7', type: 'p', color: 'b' }
207 | ] );
208 | expect( board[2] ).toEqual( [ null, null, null, null, null, null, null, null ] );
209 | expect( board[3] ).toEqual( [ null, null, null, null, null, null, null, null ] );
210 | expect( board[4] ).toEqual( [ null, null, null, null, { square: 'e4', type: 'p', color: 'w' }, null, null, null ] );
211 | expect( board[5] ).toEqual( [ null, null, null, null, null, null, null, null ] );
212 | expect( board[6] ).toEqual( [
213 | { square: 'a2', type: 'p', color: 'w' },
214 | { square: 'b2', type: 'p', color: 'w' },
215 | { square: 'c2', type: 'p', color: 'w' },
216 | { square: 'd2', type: 'p', color: 'w' },
217 | null,
218 | { square: 'f2', type: 'p', color: 'w' },
219 | { square: 'g2', type: 'p', color: 'w' },
220 | { square: 'h2', type: 'p', color: 'w' }
221 | ] );
222 | expect( board[7] ).toEqual( [
223 | { square: 'a1', type: 'r', color: 'w' },
224 | { square: 'b1', type: 'n', color: 'w' },
225 | { square: 'c1', type: 'b', color: 'w' },
226 | { square: 'd1', type: 'q', color: 'w' },
227 | { square: 'e1', type: 'k', color: 'w' },
228 | { square: 'f1', type: 'b', color: 'w' },
229 | { square: 'g1', type: 'n', color: 'w' },
230 | { square: 'h1', type: 'r', color: 'w' }
231 | ] );
232 | } );
233 | } );
234 |
235 | describe("game end", () => {
236 | test("game ends after repetition", () => {
237 | api.move('e4');
238 | expect( api.isGameOver() ).toBeFalsy();
239 | api.move('e5');
240 | expect( api.isGameOver() ).toBeFalsy();
241 | api.move('Nf3');
242 | expect( api.isGameOver() ).toBeFalsy();
243 | api.move('Nf6');
244 | expect( api.isGameOver() ).toBeFalsy();
245 | api.move('Ng1');
246 | expect( api.isGameOver() ).toBeFalsy();
247 | api.move('Ng8');
248 | expect( api.isGameOver() ).toBeFalsy();
249 | api.move('Nf3');
250 | expect( api.isGameOver() ).toBeFalsy();
251 | api.move('Nf6');
252 | expect( api.isGameOver() ).toBeFalsy();
253 | api.move('Ng1');
254 | expect( api.isGameOver() ).toBeFalsy();
255 | expect( api.possibleMovesDests() ).not.toEqual( new Map() );
256 | api.move('Ng8');
257 | expect( api.isGameOver() ).toBeTruthy();
258 | expect( () => api.move('Nf3') ).toThrowError();
259 | expect( api.isGameOver() ).toBeTruthy();
260 | expect( api.possibleMovesDests() ).toEqual( new Map() );
261 | } );
262 | test("game ends after checkmate", () => {
263 | api.move('f4');
264 | expect( api.isGameOver() ).toBeFalsy();
265 | api.move('e6');
266 | expect( api.isGameOver() ).toBeFalsy();
267 | api.move('g4');
268 | expect( api.isGameOver() ).toBeFalsy();
269 | expect( api.possibleMovesDests() ).not.toEqual( new Map( ));
270 | api.move('Qh4');
271 | expect( api.isGameOver() ).toBeTruthy();
272 | expect( api.possibleMovesDests() ).toEqual( new Map( ));
273 | } );
274 | } );
275 |
276 | describe("start from FEN", () => {
277 | test( "start from FEN", async () => {
278 | api = new Api( new Chessground(), 'rnbqkb1r/1p2pppp/p2p1n2/8/3NP3/2N1B3/PPP2PPP/R2QKB1R b KQkq - 1 6' );
279 | await api.init();
280 | expect( api.moveNumber() ).toEqual(6);
281 | expect( api.turn() ).toEqual('b');
282 | expect( () => api.move('d6') ).toThrowError();
283 | api.move('Ng4');
284 | } );
285 | } );
286 |
287 | describe("callbacks are called", async () => {
288 | const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
289 | let api, stateChangeCallback, promotionCallback, moveCallback, gameOverCallback;
290 | beforeEach( async () => {
291 | stateChangeCallback = vi.fn();
292 | promotionCallback = vi.fn();
293 | moveCallback = vi.fn();
294 | gameOverCallback = vi.fn();
295 | api = new Api( new Chessground(), fen, stateChangeCallback, promotionCallback, moveCallback, gameOverCallback );
296 | await api.init();
297 | });
298 | test( 'moveCallback is called with move object', () => {
299 | expect( moveCallback ).not.toHaveBeenCalled();
300 | api.move( 'f4' );
301 | expect( moveCallback ).toHaveBeenLastCalledWith({
302 | after: "rnbqkbnr/pppppppp/8/8/5P2/8/PPPPP1PP/RNBQKBNR b KQkq - 0 1",
303 | before: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
304 | san: "f4",
305 | lan: "f2f4",
306 | from: "f2",
307 | to: "f4",
308 | color: "w",
309 | flags: "b",
310 | piece: "p",
311 | check: false,
312 | checkmate: false,
313 | });
314 | expect( moveCallback ).toHaveBeenCalledTimes(1);
315 | });
316 | test( 'stateChangeCallback is called on move/undo/reset/toggleOrientation', () => {
317 | expect( stateChangeCallback ).toHaveBeenCalled();
318 |
319 | let numCalls = stateChangeCallback.mock.calls.length;
320 | api.move('d4');
321 | expect( stateChangeCallback.mock.calls.length ).toBeGreaterThan(numCalls);
322 | api.move('e6');
323 |
324 | numCalls = stateChangeCallback.mock.calls.length;
325 | api.undo();
326 | expect( stateChangeCallback.mock.calls.length ).toBeGreaterThan(numCalls);
327 |
328 | numCalls = stateChangeCallback.mock.calls.length;
329 | api.reset();
330 | expect( stateChangeCallback.mock.calls.length ).toBeGreaterThan(numCalls);
331 |
332 | numCalls = stateChangeCallback.mock.calls.length;
333 | api.toggleOrientation();
334 | expect( stateChangeCallback.mock.calls.length ).toBeGreaterThan(numCalls);
335 | });
336 | test( 'gameOverCallback is called on mate', () => {
337 | api.move('f4');
338 | api.move('e6');
339 | api.move('g4');
340 | expect( gameOverCallback ).not.toHaveBeenCalled();
341 | api.move('Qh4');
342 | expect( gameOverCallback ).toHaveBeenCalledWith({ reason:'checkmate', result: 0 });
343 | });
344 | test( 'gameOverCallback is called on stalemate', () => {
345 | api.load('8/7k/4K3/8/8/6Q1/8/8 b - - 0 1');
346 | api.move('Kh8');
347 | expect( gameOverCallback ).not.toHaveBeenCalled();
348 | api.move('Qg6');
349 | expect( gameOverCallback ).toHaveBeenCalledWith({ reason:'stalemate', result: 0.5 });
350 | } );
351 | test( 'gameOverCallback is called on insufficient material', () => {
352 | api.load('7b/7k/4K3/8/8/6Q1/8/8 w - - 0 1');
353 | api.move('Qg7');
354 | expect( gameOverCallback ).not.toHaveBeenCalled();
355 | api.move('Kxg7');
356 | expect( gameOverCallback ).toHaveBeenCalledWith({ reason:'insufficient material', result: 0.5 });
357 | } );
358 | test( 'gameOverCallback is called on repetition', () => {
359 | api.move('a3');
360 | api.move('Na6'); api.move('Nh3'); api.move('Nb8'); api.move('Ng1');
361 | api.move('Na6'); api.move('Nh3'); api.move('Nb8');
362 | expect( gameOverCallback ).not.toHaveBeenCalled();
363 | api.move('Ng1');
364 | expect( gameOverCallback ).toHaveBeenCalledWith({ reason:'repetition', result: 0.5 });
365 | } );
366 | test( 'gameOverCallback is called on fifty-move rule', () => {
367 | api.load('R7/r7/8/8/8/k7/8/K7 w - - 0 1');
368 | const path = ['a8','b8','c8','d8','e8','f8','g8','h8','h7','g7','f7','e7','d7','c7','b7','b6','c6','d6','e6','f6','g6','h6','h5','g5','f5','e5','d5','c5','b5','b4','c4','d4','e4','f4','g4','h4','h2','g2','f2','e2','d2','c2','b2','b8','c8','d8','e8','f8','g8','h8'];
369 | for ( let i = 1; i < path.length; i++ ) {
370 | api.move('R' + path[i]); // white move
371 | api.move('R' + path[i-1]); // black move
372 | }
373 | api.move('Rh7');
374 | expect( gameOverCallback ).not.toHaveBeenCalled();
375 | api.move('Rg7');
376 | expect( gameOverCallback ).toHaveBeenCalledWith({ reason:'fifty-move rule', result: 0.5 });
377 | } );
378 | test.todo( 'promotionCallback is called' ); // must play move through chessground
379 | });
380 |
--------------------------------------------------------------------------------
/static/style-paper.css:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * Base
4 | *
5 | */
6 |
7 | .cg-paper.cg-wrap {
8 | box-sizing: content-box;
9 | position: relative;
10 | display: block;
11 | }
12 |
13 | .cg-paper cg-container {
14 | position: absolute;
15 | width: 100%;
16 | height: 100%;
17 | display: block;
18 | top: 0;
19 | }
20 |
21 | .cg-paper cg-board {
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | width: 100%;
26 | height: 100%;
27 | -webkit-user-select: none;
28 | -moz-user-select: none;
29 | -ms-user-select: none;
30 | user-select: none;
31 | line-height: 0;
32 | background-size: cover;
33 | }
34 |
35 | .cg-paper.cg-wrap.manipulable cg-board {
36 | cursor: pointer;
37 | }
38 |
39 | .cg-paper cg-board square {
40 | position: absolute;
41 | top: 0;
42 | left: 0;
43 | width: 12.5%;
44 | height: 12.5%;
45 | pointer-events: none;
46 | }
47 |
48 | .cg-paper cg-board square.move-dest {
49 | pointer-events: auto;
50 | }
51 |
52 | .cg-paper cg-board square.last-move {
53 | will-change: transform;
54 | }
55 |
56 | .cg-paper.cg-wrap piece {
57 | position: absolute;
58 | top: 0;
59 | left: 0;
60 | width: 12.5%;
61 | height: 12.5%;
62 | background-size: cover;
63 | z-index: 2;
64 | will-change: transform;
65 | pointer-events: none;
66 | }
67 |
68 | .cg-paper cg-board piece.dragging {
69 | cursor: move;
70 | /* !important to override z-index from 3D piece inline style */
71 | z-index: 11 !important;
72 | }
73 |
74 | .cg-paper piece.anim {
75 | z-index: 8;
76 | }
77 |
78 | .cg-paper piece.fading {
79 | z-index: 1;
80 | opacity: 0.5;
81 | }
82 |
83 | .cg-paper.cg-wrap piece.ghost {
84 | opacity: 0.3;
85 | }
86 |
87 | .cg-paper.cg-wrap piece svg {
88 | overflow: hidden;
89 | position: relative;
90 | top: 0px;
91 | left: 0px;
92 | width: 100%;
93 | height: 100%;
94 | pointer-events: none;
95 | z-index: 2;
96 | opacity: 0.6;
97 | }
98 |
99 | .cg-paper.cg-wrap cg-auto-pieces,
100 | .cg-paper.cg-wrap .cg-shapes,
101 | .cg-paper.cg-wrap .cg-custom-svgs {
102 | overflow: visible;
103 | position: absolute;
104 | top: 0px;
105 | left: 0px;
106 | width: 100%;
107 | height: 100%;
108 | pointer-events: none;
109 | }
110 |
111 | .cg-paper.cg-wrap cg-auto-pieces {
112 | z-index: 2;
113 | }
114 |
115 | .cg-paper.cg-wrap cg-auto-pieces piece {
116 | opacity: 0.3;
117 | }
118 |
119 | .cg-paper.cg-wrap .cg-shapes {
120 | overflow: hidden;
121 | opacity: 0.6;
122 | z-index: 2;
123 | }
124 |
125 | .cg-paper.cg-wrap .cg-custom-svgs {
126 | /* over piece.anim = 8, but under piece.dragging = 11 */
127 | z-index: 9;
128 | }
129 |
130 | .cg-paper.cg-wrap .cg-custom-svgs svg {
131 | overflow: visible;
132 | }
133 |
134 | .cg-paper.cg-wrap coords {
135 | position: absolute;
136 | display:none;
137 | }
138 |
139 | /*
140 | *
141 | * Board
142 | *
143 | */
144 |
145 | .cg-paper cg-board {
146 | outline:solid 1px #93877E;
147 | background-image: url("data:image/svg+xml;base64,PCEtLSBnZW5lcmF0ZWQgYnkgZ2VuLXNjaGVzc2dyb3VuZC1iZy5wbCAtLT4KPHN2ZyB3aWR0aD0nMjQwJyBoZWlnaHQ9JzI0MCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz4KCTxnIHN0cm9rZT0nIzkzODc3RScgc3Ryb2tlLXdpZHRoPScxLjYzMTc4NDg3OTY2MTI2Jz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNS4wMDAwMDAnIHgyPSc1LjAwMDAwMCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nOS42MTUzODUnIHgyPSc5LjYxNTM4NScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTQuMjMwNzY5JyB4Mj0nMTQuMjMwNzY5JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxOC44NDYxNTQnIHgyPScxOC44NDYxNTQnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzIzLjQ2MTUzOCcgeDI9JzIzLjQ2MTUzOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjguMDc2OTIzJyB4Mj0nMjguMDc2OTIzJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczMi42OTIzMDgnIHgyPSczMi42OTIzMDgnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzM3LjMwNzY5MicgeDI9JzM3LjMwNzY5MicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDEuOTIzMDc3JyB4Mj0nNDEuOTIzMDc3JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0Ni41Mzg0NjInIHgyPSc0Ni41Mzg0NjInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzUxLjE1Mzg0NicgeDI9JzUxLjE1Mzg0NicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNTUuNzY5MjMxJyB4Mj0nNTUuNzY5MjMxJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc2MC4zODQ2MTUnIHgyPSc2MC4zODQ2MTUnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzY1LjAwMDAwMCcgeDI9JzY1LjAwMDAwMCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNjkuNjE1Mzg1JyB4Mj0nNjkuNjE1Mzg1JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc3NC4yMzA3NjknIHgyPSc3NC4yMzA3NjknIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9Jzc4Ljg0NjE1NCcgeDI9Jzc4Ljg0NjE1NCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nODMuNDYxNTM4JyB4Mj0nODMuNDYxNTM4JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc4OC4wNzY5MjMnIHgyPSc4OC4wNzY5MjMnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzkyLjY5MjMwOCcgeDI9JzkyLjY5MjMwOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nOTcuMzA3NjkyJyB4Mj0nOTcuMzA3NjkyJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxMDEuOTIzMDc3JyB4Mj0nMTAxLjkyMzA3NycgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTA2LjUzODQ2MicgeDI9JzEwNi41Mzg0NjInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzExMS4xNTM4NDYnIHgyPScxMTEuMTUzODQ2JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxMTUuNzY5MjMxJyB4Mj0nMTE1Ljc2OTIzMScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTIwLjM4NDYxNScgeDI9JzEyMC4zODQ2MTUnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzEyNS4wMDAwMDAnIHgyPScxMjUuMDAwMDAwJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxMjkuNjE1Mzg1JyB4Mj0nMTI5LjYxNTM4NScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTM0LjIzMDc2OScgeDI9JzEzNC4yMzA3NjknIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzEzOC44NDYxNTQnIHgyPScxMzguODQ2MTU0JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxNDMuNDYxNTM4JyB4Mj0nMTQzLjQ2MTUzOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTQ4LjA3NjkyMycgeDI9JzE0OC4wNzY5MjMnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzE1Mi42OTIzMDgnIHgyPScxNTIuNjkyMzA4JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxNTcuMzA3NjkyJyB4Mj0nMTU3LjMwNzY5MicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTYxLjkyMzA3NycgeDI9JzE2MS45MjMwNzcnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzE2Ni41Mzg0NjInIHgyPScxNjYuNTM4NDYyJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxNzEuMTUzODQ2JyB4Mj0nMTcxLjE1Mzg0NicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTc1Ljc2OTIzMScgeDI9JzE3NS43NjkyMzEnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzE4MC4zODQ2MTUnIHgyPScxODAuMzg0NjE1JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxODUuMDAwMDAwJyB4Mj0nMTg1LjAwMDAwMCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMTg5LjYxNTM4NScgeDI9JzE4OS42MTUzODUnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzE5NC4yMzA3NjknIHgyPScxOTQuMjMwNzY5JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScxOTguODQ2MTU0JyB4Mj0nMTk4Ljg0NjE1NCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjAzLjQ2MTUzOCcgeDI9JzIwMy40NjE1MzgnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzIwOC4wNzY5MjMnIHgyPScyMDguMDc2OTIzJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyMTIuNjkyMzA4JyB4Mj0nMjEyLjY5MjMwOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjE3LjMwNzY5MicgeDI9JzIxNy4zMDc2OTInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzIyMS45MjMwNzcnIHgyPScyMjEuOTIzMDc3JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyMjYuNTM4NDYyJyB4Mj0nMjI2LjUzODQ2MicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjMxLjE1Mzg0NicgeDI9JzIzMS4xNTM4NDYnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzIzNS43NjkyMzEnIHgyPScyMzUuNzY5MjMxJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyNDAuMzg0NjE1JyB4Mj0nMjQwLjM4NDYxNScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjQ1LjAwMDAwMCcgeDI9JzI0NS4wMDAwMDAnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzI0OS42MTUzODUnIHgyPScyNDkuNjE1Mzg1JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyNTQuMjMwNzY5JyB4Mj0nMjU0LjIzMDc2OScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjU4Ljg0NjE1NCcgeDI9JzI1OC44NDYxNTQnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzI2My40NjE1MzgnIHgyPScyNjMuNDYxNTM4JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyNjguMDc2OTIzJyB4Mj0nMjY4LjA3NjkyMycgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjcyLjY5MjMwOCcgeDI9JzI3Mi42OTIzMDgnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzI3Ny4zMDc2OTInIHgyPScyNzcuMzA3NjkyJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyODEuOTIzMDc3JyB4Mj0nMjgxLjkyMzA3NycgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMjg2LjUzODQ2MicgeDI9JzI4Ni41Mzg0NjInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzI5MS4xNTM4NDYnIHgyPScyOTEuMTUzODQ2JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPScyOTUuNzY5MjMxJyB4Mj0nMjk1Ljc2OTIzMScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzAwLjM4NDYxNScgeDI9JzMwMC4zODQ2MTUnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzMwNS4wMDAwMDAnIHgyPSczMDUuMDAwMDAwJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczMDkuNjE1Mzg1JyB4Mj0nMzA5LjYxNTM4NScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzE0LjIzMDc2OScgeDI9JzMxNC4yMzA3NjknIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzMxOC44NDYxNTQnIHgyPSczMTguODQ2MTU0JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczMjMuNDYxNTM4JyB4Mj0nMzIzLjQ2MTUzOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzI4LjA3NjkyMycgeDI9JzMyOC4wNzY5MjMnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzMzMi42OTIzMDgnIHgyPSczMzIuNjkyMzA4JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczMzcuMzA3NjkyJyB4Mj0nMzM3LjMwNzY5MicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzQxLjkyMzA3NycgeDI9JzM0MS45MjMwNzcnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzM0Ni41Mzg0NjInIHgyPSczNDYuNTM4NDYyJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczNTEuMTUzODQ2JyB4Mj0nMzUxLjE1Mzg0NicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzU1Ljc2OTIzMScgeDI9JzM1NS43NjkyMzEnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzM2MC4zODQ2MTUnIHgyPSczNjAuMzg0NjE1JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczNjUuMDAwMDAwJyB4Mj0nMzY1LjAwMDAwMCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzY5LjYxNTM4NScgeDI9JzM2OS42MTUzODUnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzM3NC4yMzA3NjknIHgyPSczNzQuMjMwNzY5JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczNzguODQ2MTU0JyB4Mj0nMzc4Ljg0NjE1NCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzgzLjQ2MTUzOCcgeDI9JzM4My40NjE1MzgnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzM4OC4wNzY5MjMnIHgyPSczODguMDc2OTIzJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSczOTIuNjkyMzA4JyB4Mj0nMzkyLjY5MjMwOCcgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nMzk3LjMwNzY5MicgeDI9JzM5Ny4zMDc2OTInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQwMS45MjMwNzcnIHgyPSc0MDEuOTIzMDc3JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0MDYuNTM4NDYyJyB4Mj0nNDA2LjUzODQ2MicgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDExLjE1Mzg0NicgeDI9JzQxMS4xNTM4NDYnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQxNS43NjkyMzEnIHgyPSc0MTUuNzY5MjMxJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0MjAuMzg0NjE1JyB4Mj0nNDIwLjM4NDYxNScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDI1LjAwMDAwMCcgeDI9JzQyNS4wMDAwMDAnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQyOS42MTUzODUnIHgyPSc0MjkuNjE1Mzg1JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0MzQuMjMwNzY5JyB4Mj0nNDM0LjIzMDc2OScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDM4Ljg0NjE1NCcgeDI9JzQzOC44NDYxNTQnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQ0My40NjE1MzgnIHgyPSc0NDMuNDYxNTM4JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0NDguMDc2OTIzJyB4Mj0nNDQ4LjA3NjkyMycgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDUyLjY5MjMwOCcgeDI9JzQ1Mi42OTIzMDgnIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQ1Ny4zMDc2OTInIHgyPSc0NTcuMzA3NjkyJyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0NjEuOTIzMDc3JyB4Mj0nNDYxLjkyMzA3NycgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDY2LjUzODQ2MicgeDI9JzQ2Ni41Mzg0NjInIHkyPSctNS4wMDAwMDAnIC8+CgkJPGxpbmUgeDE9Jy01LjAwMDAwMCcgeTE9JzQ3MS4xNTM4NDYnIHgyPSc0NzEuMTUzODQ2JyB5Mj0nLTUuMDAwMDAwJyAvPgoJCTxsaW5lIHgxPSctNS4wMDAwMDAnIHkxPSc0NzUuNzY5MjMxJyB4Mj0nNDc1Ljc2OTIzMScgeTI9Jy01LjAwMDAwMCcgLz4KCQk8bGluZSB4MT0nLTUuMDAwMDAwJyB5MT0nNDgwLjM4NDYxNScgeDI9JzQ4MC4zODQ2MTUnIHkyPSctNS4wMDAwMDAnIC8+Cgk8L2c+Cgk8ZyBmaWxsPScjRkFGMEU2Jz4KCQk8cmVjdCB4PScwJyB5PScwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nNjAnIHk9JzAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxMjAnIHk9JzAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxODAnIHk9JzAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PSczMCcgeT0nMzAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PSc5MCcgeT0nMzAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxNTAnIHk9JzMwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMjEwJyB5PSczMCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzAnIHk9JzYwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nNjAnIHk9JzYwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMTIwJyB5PSc2MCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzE4MCcgeT0nNjAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PSczMCcgeT0nOTAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PSc5MCcgeT0nOTAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxNTAnIHk9JzkwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMjEwJyB5PSc5MCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzAnIHk9JzEyMCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzYwJyB5PScxMjAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxMjAnIHk9JzEyMCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzE4MCcgeT0nMTIwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMzAnIHk9JzE1MCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzkwJyB5PScxNTAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PScxNTAnIHk9JzE1MCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzIxMCcgeT0nMTUwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMCcgeT0nMTgwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nNjAnIHk9JzE4MCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzEyMCcgeT0nMTgwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMTgwJyB5PScxODAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCQk8cmVjdCB4PSczMCcgeT0nMjEwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nOTAnIHk9JzIxMCcgd2lkdGg9JzMwJyBoZWlnaHQ9JzMwJyAvPgoJCTxyZWN0IHg9JzE1MCcgeT0nMjEwJyB3aWR0aD0nMzAnIGhlaWdodD0nMzAnIC8+CgkJPHJlY3QgeD0nMjEwJyB5PScyMTAnIHdpZHRoPSczMCcgaGVpZ2h0PSczMCcgLz4KCTwvZz4KPC9zdmc+Cg==");
148 | }
149 |
150 | /** Interactive board square colors */
151 | .cg-paper cg-board square.move-dest {
152 | background: radial-gradient(rgba(164, 97, 91, 0.8) 22%, rgba(0, 0, 0, 0) 0);
153 | }
154 | .cg-paper cg-board square.move-dest:hover {
155 | background: radial-gradient(rgba(164, 97, 91, 0.8) 44%, rgba(0, 0, 0, 0) 0);
156 | }
157 | .cg-paper cg-board square.oc.move-dest {
158 | background: radial-gradient(transparent 0%, transparent 80%, rgba(164, 97, 91, 0.8) 80%);
159 | }
160 | .cg-paper cg-board square.oc.move-dest:hover {
161 | background: radial-gradient(transparent 0%, transparent 70%, rgba(164, 97, 91, 0.8) 75%);
162 | }
163 | .cg-paper cg-board square.selected {
164 | background-color: rgba(164, 97, 91, 0.8);
165 | }
166 |
167 | /** Alternating colors in rank/file labels */
168 | .cg-paper.cg-wrap.orientation-white coords.ranks coord:nth-child(2n),
169 | .cg-paper.cg-wrap.orientation-white coords.files coord:nth-child(2n),
170 | .cg-paper.cg-wrap.orientation-black coords.ranks coord:nth-child(2n + 1),
171 | .cg-paper.cg-wrap.orientation-black coords.files coord:nth-child(2n + 1) {
172 | color: rgba(72, 72, 72, 0.8);
173 | }
174 |
175 | .cg-paper.cg-wrap.orientation-black coords.ranks coord:nth-child(2n),
176 | .cg-paper.cg-wrap.orientation-black coords.files coord:nth-child(2n),
177 | .cg-paper.cg-wrap.orientation-white coords.ranks coord:nth-child(2n + 1),
178 | .cg-paper.cg-wrap.orientation-white coords.files coord:nth-child(2n + 1) {
179 | color: rgba(255, 255, 255, 0.8);
180 | }
181 |
182 |
183 | /*
184 | *
185 | * Pieces
186 | *
187 | **/
188 |
189 |
190 | .cg-paper.cg-wrap piece.pawn.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8cGF0aCBkPSJNMjIuNSA5Yy0yLjIxIDAtNCAxLjc5LTQgNCAwIC44OS4yOSAxLjcxLjc4IDIuMzhDMTcuMzMgMTYuNSAxNiAxOC41OSAxNiAyMWMwIDIuMDMuOTQgMy44NCAyLjQxIDUuMDMtMyAxLjA2LTcuNDEgNS41NS03LjQxIDEzLjQ3aDIzYzAtNy45Mi00LjQxLTEyLjQxLTcuNDEtMTMuNDcgMS40Ny0xLjE5IDIuNDEtMyAyLjQxLTUuMDMgMC0yLjQxLTEuMzMtNC41LTMuMjgtNS42Mi40OS0uNjcuNzgtMS40OS43OC0yLjM4IDAtMi4yMS0xLjc5LTQtNC00eiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkFGMEU2IiBzdHJva2Utd2lkdGg9IjYuMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KCTxwYXRoIGQ9Ik0yMi41IDljLTIuMjEgMC00IDEuNzktNCA0IDAgLjg5LjI5IDEuNzEuNzggMi4zOEMxNy4zMyAxNi41IDE2IDE4LjU5IDE2IDIxYzAgMi4wMy45NCAzLjg0IDIuNDEgNS4wMy0zIDEuMDYtNy40MSA1LjU1LTcuNDEgMTMuNDdoMjNjMC03LjkyLTQuNDEtMTIuNDEtNy40MS0xMy40NyAxLjQ3LTEuMTkgMi40MS0zIDIuNDEtNS4wMyAwLTIuNDEtMS4zMy00LjUtMy4yOC01LjYyLjQ5LS42Ny43OC0xLjQ5Ljc4LTIuMzggMC0yLjIxLTEuNzktNC00LTR6IiBmaWxsPSIjRkFGMEU2IiBzdHJva2U9IiMyODJCMjgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+'); }
191 | .cg-paper.cg-wrap piece.pawn.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8cGF0aCBkPSJNMjIuNSA5Yy0yLjIxIDAtNCAxLjc5LTQgNCAwIC44OS4yOSAxLjcxLjc4IDIuMzhDMTcuMzMgMTYuNSAxNiAxOC41OSAxNiAyMWMwIDIuMDMuOTQgMy44NCAyLjQxIDUuMDMtMyAxLjA2LTcuNDEgNS41NS03LjQxIDEzLjQ3aDIzYzAtNy45Mi00LjQxLTEyLjQxLTcuNDEtMTMuNDcgMS40Ny0xLjE5IDIuNDEtMyAyLjQxLTUuMDMgMC0yLjQxLTEuMzMtNC41LTMuMjgtNS42Mi40OS0uNjcuNzgtMS40OS43OC0yLjM4IDAtMi4yMS0xLjc5LTQtNC00eiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkFGMEU2IiBzdHJva2Utd2lkdGg9IjYuMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KCTxwYXRoIGQ9Ik0yMi41IDljLTIuMjEgMC00IDEuNzktNCA0IDAgLjg5LjI5IDEuNzEuNzggMi4zOEMxNy4zMyAxNi41IDE2IDE4LjU5IDE2IDIxYzAgMi4wMy45NCAzLjg0IDIuNDEgNS4wMy0zIDEuMDYtNy40MSA1LjU1LTcuNDEgMTMuNDdoMjNjMC03LjkyLTQuNDEtMTIuNDEtNy40MS0xMy40NyAxLjQ3LTEuMTkgMi40MS0zIDIuNDEtNS4wMyAwLTIuNDEtMS4zMy00LjUtMy4yOC01LjYyLjQ5LS42Ny43OC0xLjQ5Ljc4LTIuMzggMC0yLjIxLTEuNzktNC00LTR6IiBmaWxsPSIjMjgyQjI4IiBzdHJva2U9IiMyODJCMjgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+'); }
192 | .cg-paper.cg-wrap piece.rook.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy0zdi00aDIxdjRIMTJ6bS0xLTIyVjloNHYyaDVWOWg1djJoNVY5aDR2NSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz4gPHBhdGggZD0iTTM0IDE0bC0zIDNIMTRsLTMtMyIvPiA8cGF0aCBkPSJNMzEgMTd2MTIuNUgxNFYxNyIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4gPHBhdGggZD0iTTMxIDI5LjVsMS41IDIuNWgtMjBsMS41LTIuNSIvPiA8cGF0aCBkPSJNMTEgMTRoMjMiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCTwvZz4KCTxnIGZpbGw9IiNGQUYwRTYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMjgyQjI4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCQk8cGF0aCBkPSJNOSAzOWgyN3YtM0g5djN6bTMtM3YtNGgyMXY0SDEyem0tMS0yMlY5aDR2Mmg1VjloNXYyaDVWOWg0djUiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+IDxwYXRoIGQ9Ik0zNCAxNGwtMyAzSDE0bC0zLTMiLz4gPHBhdGggZD0iTTMxIDE3djEyLjVIMTRWMTciIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+IDxwYXRoIGQ9Ik0zMSAyOS41bDEuNSAyLjVoLTIwbDEuNS0yLjUiLz4gPHBhdGggZD0iTTExIDE0aDIzIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+Cgk8L2c+Cjwvc3ZnPg=='); }
193 | .cg-paper.cg-wrap piece.rook.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy41LTdsMS41LTIuNWgxN2wxLjUgMi41aC0yMHptLS41IDR2LTRoMjF2NEgxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTE0IDI5LjV2LTEzaDE3djEzSDE0eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMTQgMTYuNUwxMSAxNGgyM2wtMyAyLjVIMTR6TTExIDE0VjloNHYyaDVWOWg1djJoNVY5aDR2NUgxMXoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTEyIDM1LjVoMjFtLTIwLTRoMTltLTE4LTJoMTdtLTE3LTEzaDE3TTExIDE0aDIzIiBmaWxsPSJub25lIiBzdHJva2U9IiNGQUYwRTYiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJPC9nPgoJPGcgZmlsbD0iIzI4MkIyOCIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMyODJCMjgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy41LTdsMS41LTIuNWgxN2wxLjUgMi41aC0yMHptLS41IDR2LTRoMjF2NEgxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTE0IDI5LjV2LTEzaDE3djEzSDE0eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMTQgMTYuNUwxMSAxNGgyM2wtMyAyLjVIMTR6TTExIDE0VjloNHYyaDVWOWg1djJoNVY5aDR2NUgxMXoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTEyIDM1LjVoMjFtLTIwLTRoMTltLTE4LTJoMTdtLTE3LTEzaDE3TTExIDE0aDIzIiBmaWxsPSJub25lIiBzdHJva2U9IiNGQUYwRTYiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJPC9nPgo8L3N2Zz4='); }
194 | .cg-paper.cg-wrap piece.bishop.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNGQUYwRTYiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik05IDM2YzMuMzktLjk3IDEwLjExLjQzIDEzLjUtMiAzLjM5IDIuNDMgMTAuMTEgMS4wMyAxMy41IDIgMCAwIDEuNjUuNTQgMyAyLS42OC45Ny0xLjY1Ljk5LTMgLjUtMy4zOS0uOTctMTAuMTEuNDYtMTMuNS0xLTMuMzkgMS40Ni0xMC4xMS4wMy0xMy41IDEtMS4zNTQuNDktMi4zMjMuNDctMy0uNSAxLjM1NC0xLjk0IDMtMiAzLTJ6Ii8+IDxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPiA8cGF0aCBkPSJNMjUgOGEyLjUgMi41IDAgMSAxLTUgMCAyLjUgMi41IDAgMSAxIDUgMHoiLz4KCTwvZz4KCTxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMjgyQjI4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCQk8ZyBmaWxsPSIjRkFGMEU2IiBzdHJva2UtbGluZWNhcD0iYnV0dCI+CgkJCTxwYXRoIGQ9Ik05IDM2YzMuMzktLjk3IDEwLjExLjQzIDEzLjUtMiAzLjM5IDIuNDMgMTAuMTEgMS4wMyAxMy41IDIgMCAwIDEuNjUuNTQgMyAyLS42OC45Ny0xLjY1Ljk5LTMgLjUtMy4zOS0uOTctMTAuMTEuNDYtMTMuNS0xLTMuMzkgMS40Ni0xMC4xMS4wMy0xMy41IDEtMS4zNTQuNDktMi4zMjMuNDctMy0uNSAxLjM1NC0xLjk0IDMtMiAzLTJ6Ii8+IDxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPiA8cGF0aCBkPSJNMjUgOGEyLjUgMi41IDAgMSAxLTUgMCAyLjUgMi41IDAgMSAxIDUgMHoiLz4KCQk8L2c+CgkJPHBhdGggZD0iTTE3LjUgMjZoMTBNMTUgMzBoMTVtLTcuNS0xNC41djVNMjAgMThoNSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJPC9nPgo8L3N2Zz4='); }
195 | .cg-paper.cg-wrap piece.bishop.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNGQUYwRTYiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik05IDM2YzMuMzktLjk3IDEwLjExLjQzIDEzLjUtMiAzLjM5IDIuNDMgMTAuMTEgMS4wMyAxMy41IDIgMCAwIDEuNjUuNTQgMyAyLS42OC45Ny0xLjY1Ljk5LTMgLjUtMy4zOS0uOTctMTAuMTEuNDYtMTMuNS0xLTMuMzkgMS40Ni0xMC4xMS4wMy0xMy41IDEtMS4zNTQuNDktMi4zMjMuNDctMy0uNSAxLjM1NC0xLjk0IDMtMiAzLTJ6Ii8+PHBhdGggZD0iTTE1IDMyYzIuNSAyLjUgMTIuNSAyLjUgMTUgMCAuNS0xLjUgMC0yIDAtMiAwLTIuNS0yLjUtNC0yLjUtNCA1LjUtMS41IDYtMTEuNS01LTE1LjUtMTEgNC0xMC41IDE0LTUgMTUuNSAwIDAtMi41IDEuNS0yLjUgNCAwIDAtLjUuNSAwIDJ6Ii8+PHBhdGggZD0iTTI1IDhhMi41IDIuNSAwIDEgMS01IDAgMi41IDIuNSAwIDEgMSA1IDB6Ii8+Cgk8L2c+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzI4MkIyOCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CgkJPGcgZmlsbD0iIzI4MkIyOCIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiPgoJCQk8cGF0aCBkPSJNOSAzNmMzLjM5LS45NyAxMC4xMS40MyAxMy41LTIgMy4zOSAyLjQzIDEwLjExIDEuMDMgMTMuNSAyIDAgMCAxLjY1LjU0IDMgMi0uNjguOTctMS42NS45OS0zIC41LTMuMzktLjk3LTEwLjExLjQ2LTEzLjUtMS0zLjM5IDEuNDYtMTAuMTEuMDMtMTMuNSAxLTEuMzU0LjQ5LTIuMzIzLjQ3LTMtLjUgMS4zNTQtMS45NCAzLTIgMy0yeiIvPjxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPjxwYXRoIGQ9Ik0yNSA4YTIuNSAyLjUgMCAxIDEtNSAwIDIuNSAyLjUgMCAxIDEgNSAweiIvPgoJCTwvZz4KCQk8cGF0aCBkPSJNMTcuNSAyNmgxME0xNSAzMGgxNW0tNy41LTE0LjV2NU0yMCAxOGg1IiBzdHJva2U9IiNGQUYwRTYiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCTwvZz4KPC9zdmc+'); }
196 | .cg-paper.cg-wrap piece.knight.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIvPjxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiLz4KCTwvZz4KCTxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMjgyQjI4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCQk8cGF0aCBkPSJNMjIgMTBjMTAuNSAxIDE2LjUgOCAxNiAyOUgxNWMwLTkgMTAtNi41IDgtMjEiIGZpbGw9IiNGQUYwRTYiLz48cGF0aCBkPSJNMjQgMThjLjM4IDIuOTEtNS41NSA3LjM3LTggOS0zIDItMi44MiA0LjM0LTUgNC0xLjA0Mi0uOTQgMS40MS0zLjA0IDAtMy0xIDAgLjE5IDEuMjMtMSAyLTEgMC00LjAwMyAxLTQtNCAwLTIgNi0xMiA2LTEyczEuODktMS45IDItMy41Yy0uNzMtLjk5NC0uNS0yLS41LTMgMS0xIDMgMi41IDMgMi41aDJzLjc4LTEuOTkyIDIuNS0zYzEgMCAxIDMgMSAzIiBmaWxsPSIjRkFGMEU2Ii8+PHBhdGggZD0iTTkuNSAyNS41YS41LjUgMCAxIDEtMSAwIC41LjUgMCAxIDEgMSAwem01LjQzMy05Ljc1YS41IDEuNSAzMCAxIDEtLjg2Ni0uNS41IDEuNSAzMCAxIDEgLjg2Ni41eiIgZmlsbD0iIzI4MkIyOCIvPgoJPC9nPgo8L3N2Zz4='); }
197 | .cg-paper.cg-wrap piece.knight.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIvPgoJCTxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiLz4KCTwvZz4KCTxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMjgyQjI4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCQk8cGF0aCBkPSJNMjIgMTBjMTAuNSAxIDE2LjUgOCAxNiAyOUgxNWMwLTkgMTAtNi41IDgtMjEiIGZpbGw9IiMyODJCMjgiLz48cGF0aCBkPSJNMjQgMThjLjM4IDIuOTEtNS41NSA3LjM3LTggOS0zIDItMi44MiA0LjM0LTUgNC0xLjA0Mi0uOTQgMS40MS0zLjA0IDAtMy0xIDAgLjE5IDEuMjMtMSAyLTEgMC00LjAwMyAxLTQtNCAwLTIgNi0xMiA2LTEyczEuODktMS45IDItMy41Yy0uNzMtLjk5NC0uNS0yLS41LTMgMS0xIDMgMi41IDMgMi41aDJzLjc4LTEuOTkyIDIuNS0zYzEgMCAxIDMgMSAzIiBmaWxsPSIjMjgyQjI4Ii8+PHBhdGggZD0iTTkuNSAyNS41YS41LjUgMCAxIDEtMSAwIC41LjUgMCAxIDEgMSAwem01LjQzMy05Ljc1YS41IDEuNSAzMCAxIDEtLjg2Ni0uNS41IDEuNSAzMCAxIDEgLjg2Ni41eiIgZmlsbD0iI0ZBRjBFNiIgc3Ryb2tlPSIjRkFGMEU2Ii8+PHBhdGggZD0iTTI0LjU1IDEwLjRsLS40NSAxLjQ1LjUuMTVjMy4xNSAxIDUuNjUgMi40OSA3LjkgNi43NVMzNS43NSAyOS4wNiAzNS4yNSAzOWwtLjA1LjVoMi4yNWwuMDUtLjVjLjUtMTAuMDYtLjg4LTE2Ljg1LTMuMjUtMjEuMzQtMi4zNy00LjQ5LTUuNzktNi42NC05LjE5LTcuMTZsLS41MS0uMXoiIGZpbGw9IiNGQUYwRTYiIHN0cm9rZT0ibm9uZSIvPgoJPC9nPgo8L3N2Zz4='); }
198 | .cg-paper.cg-wrap piece.queen.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04IDEyYTIgMiAwIDEgMS00IDAgMiAyIDAgMSAxIDQgMHptMTYuNS00LjVhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAwek00MSAxMmEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTE2IDguNWEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTMzIDlhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAweiIvPjxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi0xMi03IDExVjExbC01LjUgMTMuNS0zLTE1LTMgMTUtNS41LTE0VjI1TDcgMTRsMiAxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTkgMjZjMCAyIDEuNSAyIDIuNSA0IDEgMS41IDEgMSAuNSAzLjUtMS41IDEtMS41IDIuNS0xLjUgMi41LTEuNSAxLjUuNSAyLjUuNSAyLjUgNi41IDEgMTYuNSAxIDIzIDAgMCAwIDEuNS0xIDAtMi41IDAgMCAuNS0xLjUtMS0yLjUtLjUtMi41LS41LTIgLjUtMy41IDEtMiAyLjUtMiAyLjUtNC04LjUtMS41LTE4LjUtMS41LTI3IDB6IiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPjxwYXRoIGQ9Ik0xMS41IDMwYzMuNS0xIDE4LjUtMSAyMiAwTTEyIDMzLjVjNi0xIDE1LTEgMjEgMCIgZmlsbD0ibm9uZSIvPjwvZz4KCTxnIGZpbGw9IiNGQUYwRTYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMjgyQjI4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNOCAxMmEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6bTE2LjUtNC41YTIgMiAwIDEgMS00IDAgMiAyIDAgMSAxIDQgMHpNNDEgMTJhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAwek0xNiA4LjVhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAwek0zMyA5YTIgMiAwIDEgMS00IDAgMiAyIDAgMSAxIDQgMHoiLz48cGF0aCBkPSJNOSAyNmM4LjUtMS41IDIxLTEuNSAyNyAwbDItMTItNyAxMVYxMWwtNS41IDEzLjUtMy0xNS0zIDE1LTUuNS0xNFYyNUw3IDE0bDIgMTJ6IiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPjxwYXRoIGQ9Ik05IDI2YzAgMiAxLjUgMiAyLjUgNCAxIDEuNSAxIDEgLjUgMy41LTEuNSAxLTEuNSAyLjUtMS41IDIuNS0xLjUgMS41LjUgMi41LjUgMi41IDYuNSAxIDE2LjUgMSAyMyAwIDAgMCAxLjUtMSAwLTIuNSAwIDAgLjUtMS41LTEtMi41LS41LTIuNS0uNS0yIC41LTMuNSAxLTIgMi41LTIgMi41LTQtOC41LTEuNS0xOC41LTEuNS0yNyAweiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMTEuNSAzMGMzLjUtMSAxOC41LTEgMjIgME0xMiAzMy41YzYtMSAxNS0xIDIxIDAiIGZpbGw9Im5vbmUiLz48L2c+Cjwvc3ZnPg=='); }
199 | .cg-paper.cg-wrap piece.queen.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxnIHN0cm9rZS13aWR0aD0iNC41Ij48Y2lyY2xlIGN4PSI2IiBjeT0iMTIiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIxNCIgY3k9IjkiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIyMi41IiBjeT0iOCIgcj0iMi43NSIvPjxjaXJjbGUgY3g9IjMxIiBjeT0iOSIgcj0iMi43NSIvPjxjaXJjbGUgY3g9IjM5IiBjeT0iMTIiIHI9IjIuNzUiLz48L2c+CgkJPHBhdGggZD0iTTkgMjZjOC41LTEuNSAyMS0xLjUgMjcgMGwyLjUtMTIuNUwzMSAyNWwtLjMtMTQuMS01LjIgMTMuNi0zLTE0LjUtMyAxNC41LTUuMi0xMy42TDE0IDI1IDYuNSAxMy41IDkgMjZ6IiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPgoJCTxwYXRoIGQ9Ik05IDI2YzAgMiAxLjUgMiAyLjUgNCAxIDEuNSAxIDEgLjUgMy41LTEuNSAxLTEuNSAyLjUtMS41IDIuNS0xLjUgMS41LjUgMi41LjUgMi41IDYuNSAxIDE2LjUgMSAyMyAwIDAgMCAxLjUtMSAwLTIuNSAwIDAgLjUtMS41LTEtMi41LS41LTIuNS0uNS0yIC41LTMuNSAxLTIgMi41LTIgMi41LTQtOC41LTEuNS0xOC41LTEuNS0yNyAweiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz4KCQk8cGF0aCBkPSJNMTEgMzguNWEzNSAzNSAxIDAgMCAyMyAwIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPgoJPC9nPgoJPGcgZmlsbD0iIzI4MkIyOCIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMyODJCMjgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxnIHN0cm9rZT0ibm9uZSI+PGNpcmNsZSBjeD0iNiIgY3k9IjEyIiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMTQiIGN5PSI5IiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMjIuNSIgY3k9IjgiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzMSIgY3k9IjkiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzOSIgY3k9IjEyIiByPSIyLjc1Ii8+PC9nPgoJCTxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi41LTEyLjVMMzEgMjVsLS4zLTE0LjEtNS4yIDEzLjYtMy0xNC41LTMgMTQuNS01LjItMTMuNkwxNCAyNSA2LjUgMTMuNSA5IDI2eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz4KCQk8cGF0aCBkPSJNOSAyNmMwIDIgMS41IDIgMi41IDQgMSAxLjUgMSAxIC41IDMuNS0xLjUgMS0xLjUgMi41LTEuNSAyLjUtMS41IDEuNS41IDIuNS41IDIuNSA2LjUgMSAxNi41IDEgMjMgMCAwIDAgMS41LTEgMC0yLjUgMCAwIC41LTEuNS0xLTIuNS0uNS0yLjUtLjUtMiAuNS0zLjUgMS0yIDIuNS0yIDIuNS00LTguNS0xLjUtMTguNS0xLjUtMjcgMHoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+CgkJPHBhdGggZD0iTTExIDM4LjVhMzUgMzUgMSAwIDAgMjMgMCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz4KCQk8cGF0aCBkPSJNMTEgMjlhMzUgMzUgMSAwIDEgMjMgMG0tMjEuNSAyLjVoMjBtLTIxIDNhMzUgMzUgMSAwIDAgMjIgMG0tMjMgM2EzNSAzNSAxIDAgMCAyNCAwIiBmaWxsPSJub25lIiBzdHJva2U9IiNGQUYwRTYiLz4KCTwvZz4KPC9zdmc+'); }
200 | .cg-paper.cg-wrap piece.king.white { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik0yMi41IDExLjYzVjZNMjAgOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+CgkJPHBhdGggZD0iTTIyLjUgMjVzNC41LTcuNSAzLTEwLjVjMCAwLTEtMi41LTMtMi41cy0zIDIuNS0zIDIuNWMtMS41IDMgMyAxMC41IDMgMTAuNSIgZmlsbD0iI0ZBRjBFNiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCQk8cGF0aCBkPSJNMTEuNSAzN2M1LjUgMy41IDE1LjUgMy41IDIxIDB2LTdzOS00LjUgNi0xMC41Yy00LTYuNS0xMy41LTMuNS0xNiA0VjI3di0zLjVjLTMuNS03LjUtMTMtMTAuNS0xNi00LTMgNiA1IDEwIDUgMTBWMzd6IiBmaWxsPSIjRkFGMEU2Ii8+Cgk8L2c+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzI4MkIyOCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CgkJPHBhdGggZD0iTTIyLjUgMTEuNjNWNk0yMCA4aDUiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCQk8cGF0aCBkPSJNMjIuNSAyNXM0LjUtNy41IDMtMTAuNWMwIDAtMS0yLjUtMy0yLjVzLTMgMi41LTMgMi41Yy0xLjUgMyAzIDEwLjUgMyAxMC41IiBmaWxsPSIjRkFGMEU2IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJCTxwYXRoIGQ9Ik0xMS41IDM3YzUuNSAzLjUgMTUuNSAzLjUgMjEgMHYtN3M5LTQuNSA2LTEwLjVjLTQtNi41LTEzLjUtMy41LTE2IDRWMjd2LTMuNWMtMy41LTcuNS0xMy0xMC41LTE2LTQtMyA2IDUgMTAgNSAxMFYzN3oiIGZpbGw9IiNGQUYwRTYiLz4KCQk8cGF0aCBkPSJNMTEuNSAzMGM1LjUtMyAxNS41LTMgMjEgMG0tMjEgMy41YzUuNS0zIDE1LjUtMyAyMSAwbS0yMSAzLjVjNS41LTMgMTUuNS0zIDIxIDAiLz4KCTwvZz4KPC9zdmc+'); }
201 | .cg-paper.cg-wrap piece.king.black { background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iI0ZBRjBFNiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgoJCTxwYXRoIGQ9Ik0yMi41IDExLjYzVjYiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCQk8cGF0aCBkPSJNMjIuNSAyNXM0LjUtNy41IDMtMTAuNWMwIDAtMS0yLjUtMy0yLjVzLTMgMi41LTMgMi41Yy0xLjUgMyAzIDEwLjUgMyAxMC41IiBmaWxsPSIjMjgyQjI4IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJCTxwYXRoIGQ9Ik0xMS41IDM3YzUuNSAzLjUgMTUuNSAzLjUgMjEgMHYtN3M5LTQuNSA2LTEwLjVjLTQtNi41LTEzLjUtMy41LTE2IDRWMjd2LTMuNWMtMy41LTcuNS0xMy0xMC41LTE2LTQtMyA2IDUgMTAgNSAxMFYzN3oiIGZpbGw9IiMyODJCMjgiLz4KCQk8cGF0aCBkPSJNMjAgOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+Cgk8L2c+Cgk8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzI4MkIyOCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CgkJPHBhdGggZD0iTTIyLjUgMTEuNjNWNiIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPgoJCTxwYXRoIGQ9Ik0yMi41IDI1czQuNS03LjUgMy0xMC41YzAgMC0xLTIuNS0zLTIuNXMtMyAyLjUtMyAyLjVjLTEuNSAzIDMgMTAuNSAzIDEwLjUiIGZpbGw9IiMyODJCMjgiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+CgkJPHBhdGggZD0iTTExLjUgMzdjNS41IDMuNSAxNS41IDMuNSAyMSAwdi03czktNC41IDYtMTAuNWMtNC02LjUtMTMuNS0zLjUtMTYgNFYyN3YtMy41Yy0zLjUtNy41LTEzLTEwLjUtMTYtNC0zIDYgNSAxMCA1IDEwVjM3eiIgZmlsbD0iIzI4MkIyOCIvPgoJCTxwYXRoIGQ9Ik0yMCA4aDUiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz4KCQk8cGF0aCBkPSJNMzIgMjkuNXM4LjUtNCA2LjAzLTkuNjVDMzQuMTUgMTQgMjUgMTggMjIuNSAyNC41bC4wMSAyLjEtLjAxLTIuMUMyMCAxOCA5LjkwNiAxNCA2Ljk5NyAxOS44NWMtMi40OTcgNS42NSA0Ljg1MyA5IDQuODUzIDkiIHN0cm9rZT0iI0ZBRjBFNiIvPgoJCTxwYXRoIGQ9Ik0xMS41IDMwYzUuNS0zIDE1LjUtMyAyMSAwbS0yMSAzLjVjNS41LTMgMTUuNS0zIDIxIDBtLTIxIDMuNWM1LjUtMyAxNS41LTMgMjEgMCIgc3Ryb2tlPSIjRkFGMEU2Ii8+Cgk8L2c+Cjwvc3ZnPg=='); }
202 |
--------------------------------------------------------------------------------