├── .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 | 9 | 10 | 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 | 13 | 14 | 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 | 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 | 26 |
27 |
28 |
UCI messages from Stockfish
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 | ![Svelte-chess screenshots](https://github.com/gtim/svelte-chess/blob/main/static/screenshot.png?raw=true) 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 | 98 | 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 | --------------------------------------------------------------------------------