├── .DS_Store ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── assets ├── dist │ ├── css │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap.rtl.min.css │ │ └── bootstrap.rtl.min.css.map │ └── js │ │ ├── bootstrap.bundle.min.js │ │ └── bootstrap.bundle.min.js.map ├── images │ ├── chessboard-sprite-staunty.svg │ └── chessboard-sprite.svg └── styles │ ├── _cm-chessboard-theme.scss │ ├── cm-chessboard.css │ ├── cm-chessboard.css.map │ └── cm-chessboard.scss ├── chessboard-1.0.0.min.css ├── chessboard-1.0.0.min.js ├── cm-chessboard ├── Chessboard.js ├── extensions │ ├── accessibility │ │ └── Accessibility.js │ └── arrows │ │ ├── Arrows.js │ │ └── assets │ │ ├── arrows.css │ │ ├── arrows.css.map │ │ ├── arrows.scss │ │ └── arrows.svg ├── model │ ├── ChessboardState.js │ ├── Extension.js │ └── Position.js └── view │ ├── ChessboardView.js │ ├── PositionAnimationsQueue.js │ └── VisualMoveInput.js ├── cover.png ├── img └── chesspieces │ └── wikipedia │ ├── bB.png │ ├── bK.png │ ├── bN.png │ ├── bP.png │ ├── bQ.png │ ├── bR.png │ ├── wB.png │ ├── wK.png │ ├── wN.png │ ├── wP.png │ ├── wQ.png │ └── wR.png ├── index.html ├── js ├── .DS_Store ├── betafish.js └── main.js └── styles.css /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ch63 2 | cm-chessboard-master -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | js/evaluate.js 2 | js/pst.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gavin Ong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Betafish 2 | 3 | Betafish 4 | 5 | Betafish - An amalgamation of AlphaZero and Stockfish. 6 | 7 | Play it [here](https://betafish.gavinong.com). 8 | 9 | [Read more about it on my blog.](https://gavinong.com/projects/betafish) 10 | 11 | ## Introduction 12 | 13 | Betafish is a chess engine and AI move finder written in Javascript, based on the Negamax algorithm. It beats Stockfish Level 6 on Lichess, and I estimate its around 1800-2000 Elo, depending on the thinking time afforded. 14 | 15 | ## Features 16 | 17 | - Negamax algorithm with alpha-beta pruning 18 | - Move ordering using the MVV-LVA heuristic 19 | - Principal variation search 20 | - Quiescence search 21 | - Iterative deepening 22 | - Piece-square tables 23 | - Tapered evaluation 24 | 25 | This enables Betafish to search to a depth of 7-8 plies and around ~1mil possible nodes, given a thinking time of around 1-2 seconds. 26 | 27 | ## Possible Improvements 28 | 29 | - Transposition tables 30 | - Zobrist hashing 31 | - Opening tables 32 | 33 | Of course, re-writing this in a compiled language like C++ would be a huge improvement. I wrote it in Javacript as I wanted to deploy it as a web app, as what fun would a game be without letting your friends challenge it? However, I quickly realised the limitations, as JS is definitely not suited for these computing-intensive tasks. 34 | 35 | ## Changelogs 36 | - 1.1 - Changed evaluation function from [Simplified Eval](https://www.chessprogramming.org/Simplified_Evaluation_Function) to [PeSTO's Eval Function](https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function). Major improvements ~ 200 elo. 37 | 38 | ## Credits 39 | 40 | - [Chess Engines: A Zero to One](https://www.chessengines.org/) - The article that jumpstarted my journey into chess programming. 41 | - [WukongJS](https://github.com/maksimKorzh/wukongJS) - A JS chess engine, written by Maksim Korzh, who was patient enough to answer my questions, give me pointers and point me in the right direction in this journey. 42 | - [Bluefever Software's YouTube series](https://www.youtube.com/watch?v=2eA0bD3wV3Q&list=PLZ1QII7yudbe4gz2gh9BCI6VDA-xafLog) - A 63-part series on chess programming, which was an absolute gold mine and served as inspiration for the bulk of the chess engine. 43 | - [Chess Programming Wiki](https://www.chessprogramming.org/Main_Page) - A great resource for chess programming. 44 | - [CM Chessboard](https://github.com/shaack/cm-chessboard) - The library I used for the GUI. 45 | -------------------------------------------------------------------------------- /assets/images/chessboard-sprite-staunty.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | cm-chessboard Staunty pieces and markers sprite 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /assets/images/chessboard-sprite.svg: -------------------------------------------------------------------------------- 1 | 23 | 24 | cm-chessbord pieces and markers sprite 25 | Chess pieces and markers for the cm-chessboard (https://shaack.com/projekte/cm-chessboard/). 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /assets/styles/_cm-chessboard-theme.scss: -------------------------------------------------------------------------------- 1 | @mixin cm-chessboard-theme($name, 2 | $square-white, $square-black, 3 | $coordinates-white: $square-black, $coordinates-black: $square-white, 4 | $coordinates-frame: $square-black, 5 | $border: $square-black, $frame-bg: $square-white,) { 6 | .cm-chessboard.#{$name} { 7 | .board { 8 | .square { 9 | &.white { 10 | fill: $square-white; 11 | } 12 | 13 | &.black { 14 | fill: $square-black; 15 | } 16 | } 17 | } 18 | // border, with borderType "thin" 19 | &.border-type-thin { 20 | .board { 21 | .border { 22 | stroke: $border; 23 | stroke-width: 0.7%; 24 | fill: $square-white; 25 | } 26 | } 27 | } 28 | &.border-type-none { 29 | .board { 30 | .border { 31 | stroke: $border; 32 | stroke-width: 0; 33 | fill: $square-white; 34 | } 35 | } 36 | } 37 | // border, with borderType "frame" 38 | &.border-type-frame { 39 | .board { 40 | .border { 41 | fill: $frame-bg; 42 | stroke: none; 43 | } 44 | .border-inner { 45 | fill: transparent; 46 | stroke: $border; 47 | stroke-width: 0.7%; 48 | } 49 | } 50 | } 51 | .coordinates { 52 | pointer-events: none; 53 | user-select: none; 54 | 55 | .coordinate { 56 | fill: $coordinates-frame; 57 | font-size: 7px; 58 | cursor: default; 59 | &.black { 60 | fill: $coordinates-black; 61 | } 62 | &.white { 63 | fill: $coordinates-white; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /assets/styles/cm-chessboard.css: -------------------------------------------------------------------------------- 1 | .cm-chessboard .board.input-enabled .square { 2 | cursor: pointer; } 3 | 4 | .cm-chessboard .markers .marker.markerFrame, .cm-chessboard .markers .marker.marker-frame { 5 | stroke: black; 6 | stroke-width: 1.8px; 7 | opacity: 0.5; } 8 | 9 | .cm-chessboard .markers .marker.markerFrameRed, .cm-chessboard .markers .marker.marker-frame-red { 10 | stroke: #aa0000; 11 | stroke-width: 1.8px; 12 | opacity: 0.4; } 13 | 14 | .cm-chessboard .markers .marker.markerSquare, .cm-chessboard .markers .marker.marker-square { 15 | fill: black; 16 | opacity: 0.11; } 17 | 18 | .cm-chessboard .markers .marker.markerDot, .cm-chessboard .markers .marker.marker-dot { 19 | fill: black; 20 | opacity: 0.3; } 21 | 22 | .cm-chessboard .markers .marker.markerCircle, .cm-chessboard .markers .marker.marker-circle { 23 | stroke: #000055; 24 | stroke-width: 3px; 25 | opacity: 0.4; } 26 | 27 | .cm-chessboard .markers .marker.markerCircleRed, .cm-chessboard .markers .marker.marker-circle-red { 28 | stroke: #aa0000; 29 | stroke-width: 3px; 30 | opacity: 0.4; } 31 | 32 | .cm-chessboard .pieces, .cm-chessboard .markers { 33 | pointer-events: none; } 34 | 35 | .cm-chessboard-content .list-inline { 36 | padding-left: 0; 37 | list-style: none; } 38 | 39 | .cm-chessboard-content .list-inline-item { 40 | display: inline-block; } 41 | .cm-chessboard-content .list-inline-item:not(:last-child) { 42 | margin-right: 1rem; } 43 | 44 | .cm-chessboard-content .list-inline { 45 | padding-left: 0; 46 | list-style: none; } 47 | 48 | .cm-chessboard-content .list-inline-item { 49 | display: inline-block; } 50 | .cm-chessboard-content .list-inline-item:not(:last-child) { 51 | margin-right: 1rem; } 52 | 53 | .cm-visually-hidden { 54 | position: absolute; 55 | left: -10000px; 56 | top: auto; 57 | width: 1px; 58 | height: 1px; 59 | overflow: hidden; } 60 | 61 | .cm-chessboard.default .board .square.white { 62 | fill: #ecdab9; } 63 | 64 | .cm-chessboard.default .board .square.black { 65 | fill: #c5a076; } 66 | 67 | .cm-chessboard.default.border-type-thin .board .border { 68 | stroke: #c5a076; 69 | stroke-width: 0.7%; 70 | fill: #ecdab9; } 71 | 72 | .cm-chessboard.default.border-type-none .board .border { 73 | stroke: #c5a076; 74 | stroke-width: 0; 75 | fill: #ecdab9; } 76 | 77 | .cm-chessboard.default.border-type-frame .board .border { 78 | fill: #ecdab9; 79 | stroke: none; } 80 | 81 | .cm-chessboard.default.border-type-frame .board .border-inner { 82 | fill: transparent; 83 | stroke: #c5a076; 84 | stroke-width: 0.7%; } 85 | 86 | .cm-chessboard.default .coordinates { 87 | pointer-events: none; 88 | user-select: none; } 89 | .cm-chessboard.default .coordinates .coordinate { 90 | fill: #c5a076; 91 | font-size: 7px; 92 | cursor: default; } 93 | .cm-chessboard.default .coordinates .coordinate.black { 94 | fill: #ecdab9; } 95 | .cm-chessboard.default .coordinates .coordinate.white { 96 | fill: #c5a076; } 97 | 98 | .cm-chessboard.default-contrast .board .square.white { 99 | fill: #ecdab9; } 100 | 101 | .cm-chessboard.default-contrast .board .square.black { 102 | fill: #c5a076; } 103 | 104 | .cm-chessboard.default-contrast.border-type-thin .board .border { 105 | stroke: #c5a076; 106 | stroke-width: 0.7%; 107 | fill: #ecdab9; } 108 | 109 | .cm-chessboard.default-contrast.border-type-none .board .border { 110 | stroke: #c5a076; 111 | stroke-width: 0; 112 | fill: #ecdab9; } 113 | 114 | .cm-chessboard.default-contrast.border-type-frame .board .border { 115 | fill: #ecdab9; 116 | stroke: none; } 117 | 118 | .cm-chessboard.default-contrast.border-type-frame .board .border-inner { 119 | fill: transparent; 120 | stroke: #c5a076; 121 | stroke-width: 0.7%; } 122 | 123 | .cm-chessboard.default-contrast .coordinates { 124 | pointer-events: none; 125 | user-select: none; } 126 | .cm-chessboard.default-contrast .coordinates .coordinate { 127 | fill: #c5a076; 128 | font-size: 7px; 129 | cursor: default; } 130 | .cm-chessboard.default-contrast .coordinates .coordinate.black { 131 | fill: #333; } 132 | .cm-chessboard.default-contrast .coordinates .coordinate.white { 133 | fill: #333; } 134 | 135 | .cm-chessboard.green .board .square.white { 136 | fill: #E0DDCC; } 137 | 138 | .cm-chessboard.green .board .square.black { 139 | fill: #4c946a; } 140 | 141 | .cm-chessboard.green.border-type-thin .board .border { 142 | stroke: #4c946a; 143 | stroke-width: 0.7%; 144 | fill: #E0DDCC; } 145 | 146 | .cm-chessboard.green.border-type-none .board .border { 147 | stroke: #4c946a; 148 | stroke-width: 0; 149 | fill: #E0DDCC; } 150 | 151 | .cm-chessboard.green.border-type-frame .board .border { 152 | fill: #E0DDCC; 153 | stroke: none; } 154 | 155 | .cm-chessboard.green.border-type-frame .board .border-inner { 156 | fill: transparent; 157 | stroke: #4c946a; 158 | stroke-width: 0.7%; } 159 | 160 | .cm-chessboard.green .coordinates { 161 | pointer-events: none; 162 | user-select: none; } 163 | .cm-chessboard.green .coordinates .coordinate { 164 | fill: #4c946a; 165 | font-size: 7px; 166 | cursor: default; } 167 | .cm-chessboard.green .coordinates .coordinate.black { 168 | fill: #E0DDCC; } 169 | .cm-chessboard.green .coordinates .coordinate.white { 170 | fill: #4c946a; } 171 | 172 | .cm-chessboard.blue .board .square.white { 173 | fill: #d8ecfb; } 174 | 175 | .cm-chessboard.blue .board .square.black { 176 | fill: #86afcf; } 177 | 178 | .cm-chessboard.blue.border-type-thin .board .border { 179 | stroke: #86afcf; 180 | stroke-width: 0.7%; 181 | fill: #d8ecfb; } 182 | 183 | .cm-chessboard.blue.border-type-none .board .border { 184 | stroke: #86afcf; 185 | stroke-width: 0; 186 | fill: #d8ecfb; } 187 | 188 | .cm-chessboard.blue.border-type-frame .board .border { 189 | fill: #d8ecfb; 190 | stroke: none; } 191 | 192 | .cm-chessboard.blue.border-type-frame .board .border-inner { 193 | fill: transparent; 194 | stroke: #86afcf; 195 | stroke-width: 0.7%; } 196 | 197 | .cm-chessboard.blue .coordinates { 198 | pointer-events: none; 199 | user-select: none; } 200 | .cm-chessboard.blue .coordinates .coordinate { 201 | fill: #86afcf; 202 | font-size: 7px; 203 | cursor: default; } 204 | .cm-chessboard.blue .coordinates .coordinate.black { 205 | fill: #d8ecfb; } 206 | .cm-chessboard.blue .coordinates .coordinate.white { 207 | fill: #86afcf; } 208 | 209 | .cm-chessboard.chess-club .board .square.white { 210 | fill: #E6D3B1; } 211 | 212 | .cm-chessboard.chess-club .board .square.black { 213 | fill: #AF6B3F; } 214 | 215 | .cm-chessboard.chess-club.border-type-thin .board .border { 216 | stroke: #692e2b; 217 | stroke-width: 0.7%; 218 | fill: #E6D3B1; } 219 | 220 | .cm-chessboard.chess-club.border-type-none .board .border { 221 | stroke: #692e2b; 222 | stroke-width: 0; 223 | fill: #E6D3B1; } 224 | 225 | .cm-chessboard.chess-club.border-type-frame .board .border { 226 | fill: #692e2b; 227 | stroke: none; } 228 | 229 | .cm-chessboard.chess-club.border-type-frame .board .border-inner { 230 | fill: transparent; 231 | stroke: #692e2b; 232 | stroke-width: 0.7%; } 233 | 234 | .cm-chessboard.chess-club .coordinates { 235 | pointer-events: none; 236 | user-select: none; } 237 | .cm-chessboard.chess-club .coordinates .coordinate { 238 | fill: #E6D3B1; 239 | font-size: 7px; 240 | cursor: default; } 241 | .cm-chessboard.chess-club .coordinates .coordinate.black { 242 | fill: #E6D3B1; } 243 | .cm-chessboard.chess-club .coordinates .coordinate.white { 244 | fill: #AF6B3F; } 245 | 246 | .cm-chessboard.chessboard-js .board .square.white { 247 | fill: #f0d9b5; } 248 | 249 | .cm-chessboard.chessboard-js .board .square.black { 250 | fill: #b58863; } 251 | 252 | .cm-chessboard.chessboard-js.border-type-thin .board .border { 253 | stroke: #404040; 254 | stroke-width: 0.7%; 255 | fill: #f0d9b5; } 256 | 257 | .cm-chessboard.chessboard-js.border-type-none .board .border { 258 | stroke: #404040; 259 | stroke-width: 0; 260 | fill: #f0d9b5; } 261 | 262 | .cm-chessboard.chessboard-js.border-type-frame .board .border { 263 | fill: #f0d9b5; 264 | stroke: none; } 265 | 266 | .cm-chessboard.chessboard-js.border-type-frame .board .border-inner { 267 | fill: transparent; 268 | stroke: #404040; 269 | stroke-width: 0.7%; } 270 | 271 | .cm-chessboard.chessboard-js .coordinates { 272 | pointer-events: none; 273 | user-select: none; } 274 | .cm-chessboard.chessboard-js .coordinates .coordinate { 275 | fill: #404040; 276 | font-size: 7px; 277 | cursor: default; } 278 | .cm-chessboard.chessboard-js .coordinates .coordinate.black { 279 | fill: #f0d9b5; } 280 | .cm-chessboard.chessboard-js .coordinates .coordinate.white { 281 | fill: #b58863; } 282 | 283 | .cm-chessboard.black-and-white .board .square.white { 284 | fill: #ffffff; } 285 | 286 | .cm-chessboard.black-and-white .board .square.black { 287 | fill: #9c9c9c; } 288 | 289 | .cm-chessboard.black-and-white.border-type-thin .board .border { 290 | stroke: #9c9c9c; 291 | stroke-width: 0.7%; 292 | fill: #ffffff; } 293 | 294 | .cm-chessboard.black-and-white.border-type-none .board .border { 295 | stroke: #9c9c9c; 296 | stroke-width: 0; 297 | fill: #ffffff; } 298 | 299 | .cm-chessboard.black-and-white.border-type-frame .board .border { 300 | fill: #ffffff; 301 | stroke: none; } 302 | 303 | .cm-chessboard.black-and-white.border-type-frame .board .border-inner { 304 | fill: transparent; 305 | stroke: #9c9c9c; 306 | stroke-width: 0.7%; } 307 | 308 | .cm-chessboard.black-and-white .coordinates { 309 | pointer-events: none; 310 | user-select: none; } 311 | .cm-chessboard.black-and-white .coordinates .coordinate { 312 | fill: #9c9c9c; 313 | font-size: 7px; 314 | cursor: default; } 315 | .cm-chessboard.black-and-white .coordinates .coordinate.black { 316 | fill: #ffffff; } 317 | .cm-chessboard.black-and-white .coordinates .coordinate.white { 318 | fill: #9c9c9c; } 319 | 320 | /*# sourceMappingURL=cm-chessboard.css.map */ -------------------------------------------------------------------------------- /assets/styles/cm-chessboard.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "cm-chessboard.css", 4 | "sources": [ 5 | "cm-chessboard.scss", 6 | "_cm-chessboard-theme.scss" 7 | ], 8 | "names": [], 9 | "mappings": "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" 10 | } -------------------------------------------------------------------------------- /assets/styles/cm-chessboard.scss: -------------------------------------------------------------------------------- 1 | @import "cm-chessboard-theme"; 2 | 3 | // define markers (for all themes) 4 | .cm-chessboard { 5 | .board { 6 | &.input-enabled { 7 | .square { 8 | cursor: pointer; 9 | } 10 | } 11 | } 12 | 13 | .markers { 14 | .marker { 15 | // use the second, the hyphens syntax 16 | &.markerFrame, &.marker-frame { 17 | stroke: black; 18 | stroke-width: 1.8px; 19 | opacity: 0.5; 20 | } 21 | 22 | &.markerFrameRed, &.marker-frame-red { 23 | stroke: #aa0000; 24 | stroke-width: 1.8px; 25 | opacity: 0.4; 26 | } 27 | 28 | &.markerSquare, &.marker-square { 29 | fill: black; 30 | opacity: 0.11; 31 | } 32 | 33 | &.markerDot, &.marker-dot { 34 | fill: black; 35 | opacity: 0.3; 36 | } 37 | 38 | &.markerCircle, &.marker-circle { 39 | stroke: #000055; 40 | stroke-width: 3px; 41 | opacity: 0.4; 42 | } 43 | 44 | &.markerCircleRed, &.marker-circle-red { 45 | stroke: #aa0000; 46 | stroke-width: 3px; 47 | opacity: 0.4; 48 | } 49 | } 50 | } 51 | 52 | .pieces, .markers { 53 | // important for the proper function 54 | pointer-events: none; 55 | } 56 | } 57 | .cm-chessboard-content { 58 | .list-inline { 59 | padding-left: 0; 60 | list-style: none; 61 | } 62 | .list-inline-item { 63 | display: inline-block; 64 | 65 | &:not(:last-child) { 66 | margin-right: 1rem; 67 | } 68 | } 69 | } 70 | 71 | .cm-chessboard-content { 72 | 73 | .list-inline { 74 | padding-left: 0; 75 | list-style: none; 76 | } 77 | 78 | .list-inline-item { 79 | display: inline-block; 80 | 81 | &:not(:last-child) { 82 | margin-right: 1rem; 83 | } 84 | } 85 | 86 | } 87 | 88 | .cm-visually-hidden { 89 | position: absolute; 90 | left: -10000px; 91 | top: auto; 92 | width: 1px; 93 | height: 1px; 94 | overflow: hidden; 95 | } 96 | 97 | // define themes 98 | @include cm-chessboard-theme( 99 | "default", // name 100 | #ecdab9, #c5a076 // squares 101 | ); 102 | @include cm-chessboard-theme( 103 | "default-contrast", // name 104 | #ecdab9, #c5a076, // squares 105 | #333, #333, // coordinates in squares (white, black) 106 | ); 107 | @include cm-chessboard-theme( 108 | "green", // name 109 | #E0DDCC, #4c946a // squares 110 | ); 111 | @include cm-chessboard-theme( 112 | "blue", // name 113 | #d8ecfb, #86afcf // squares 114 | ); 115 | @include cm-chessboard-theme( 116 | "chess-club", // name 117 | #E6D3B1, #AF6B3F, // squares 118 | #AF6B3F, #E6D3B1, // coordinates in squares (white, black) 119 | #E6D3B1, // coordinates in frame-mode 120 | #692e2b, #692e2b // border and frame background 121 | ); 122 | @include cm-chessboard-theme( 123 | "chessboard-js", // name 124 | #f0d9b5, #b58863, // squares 125 | #b58863, #f0d9b5, // coordinates in squares (white, black) 126 | #404040, // coordinates in frame-mode 127 | #404040, #f0d9b5 // border and frame background 128 | ); 129 | @include cm-chessboard-theme( 130 | "black-and-white", // name 131 | #ffffff, #9c9c9c // squares 132 | ); 133 | -------------------------------------------------------------------------------- /chessboard-1.0.0.min.css: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */ 2 | .clearfix-7da63{clear:both}.board-b72b1{border:2px solid #404040;box-sizing:content-box}.square-55d63{float:left;position:relative;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.white-1e1d7{background-color:#f0d9b5;color:#b58863}.black-3c85d{background-color:#b58863;color:#f0d9b5}.highlight1-32417,.highlight2-9c5d2{box-shadow:inset 0 0 3px 3px #ff0}.notation-322f9{cursor:default;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;position:absolute}.alpha-d2270{bottom:1px;right:3px}.numeric-fc462{top:2px;left:2px} -------------------------------------------------------------------------------- /chessboard-1.0.0.min.js: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */ 2 | !function(){"use strict";var z=window.jQuery,F="abcdefgh".split(""),r=20,A="…",W="1.8.3",e="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",G=pe(e),n=200,t=200,o=60,a=30,i=100,H={};function V(e,r,n){function t(){o=0,a&&(a=!1,s())}var o=0,a=!1,i=[],s=function(){o=window.setTimeout(t,r),e.apply(n,i)};return function(e){i=arguments,o?a=!0:s()}}function Z(){return"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx".replace(/x/g,function(e){return(16*Math.random()|0).toString(16)})}function _(e){return JSON.parse(JSON.stringify(e))}function s(e){var r=e.split(".");return{major:parseInt(r[0],10),minor:parseInt(r[1],10),patch:parseInt(r[2],10)}}function ee(e,r){for(var n in r)if(r.hasOwnProperty(n))for(var t="{"+n+"}",o=r[n];-1!==e.indexOf(t);)e=e.replace(t,o);return e}function re(e){return"string"==typeof e}function ne(e){return"function"==typeof e}function p(e){return"number"==typeof e&&isFinite(e)&&Math.floor(e)===e}function c(e){return"fast"===e||"slow"===e||!!p(e)&&0<=e}function te(e){if(!re(e))return!1;var r=e.split("-");return 2===r.length&&(oe(r[0])&&oe(r[1]))}function oe(e){return re(e)&&-1!==e.search(/^[a-h][1-8]$/)}function u(e){return re(e)&&-1!==e.search(/^[bw][KQRNBP]$/)}function ae(e){if(!re(e))return!1;var r=(e=function(e){return e.replace(/8/g,"11111111").replace(/7/g,"1111111").replace(/6/g,"111111").replace(/5/g,"11111").replace(/4/g,"1111").replace(/3/g,"111").replace(/2/g,"11")}(e=e.replace(/ .+$/,""))).split("/");if(8!==r.length)return!1;for(var n=0;n<8;n++)if(8!==r[n].length||-1!==r[n].search(/[^kqrnbpKQRNBP1]/))return!1;return!0}function ie(e){if(!z.isPlainObject(e))return!1;for(var r in e)if(e.hasOwnProperty(r)&&(!oe(r)||!u(e[r])))return!1;return!0}function se(){return typeof window.$&&z.fn&&z.fn.jquery&&function(e,r){e=s(e),r=s(r);var n=1e5*e.major*1e5+1e5*e.minor+e.patch;return 1e5*r.major*1e5+1e5*r.minor+r.patch<=n}(z.fn.jquery,W)}function pe(e){if(!ae(e))return!1;for(var r,n=(e=e.replace(/ .+$/,"")).split("/"),t={},o=8,a=0;a<8;a++){for(var i=n[a].split(""),s=0,p=0;p';for(var i=0;i<8;i++){var s=n[i]+t;r+='
',f.showNotation&&(("white"===e&&1===t||"black"===e&&8===t)&&(r+='
'+n[i]+"
"),0===i&&(r+='
'+t+"
")),r+="
",o="white"===o?"black":"white"}r+='
',o="white"===o?"black":"white","white"===e?t-=1:t+=1}return ee(r,H)}(p,f.showNotation)),T(),f.sparePieces&&("white"===p?(t.html(x("black")),o.html(x("white"))):(t.html(x("white")),o.html(x("black"))))}function k(e){var r=_(c),n=_(e);ce(r)!==ce(n)&&(ne(f.onChange)&&f.onChange(r,n),c=e)}function E(e,r){for(var n in w)if(w.hasOwnProperty(n)){var t=w[n];if(e>=t.left&&e=t.top&&r { 141 | this.boardTurning = false 142 | this.state.invokeExtensionPoints(EXTENSION_POINT.boardChanged) 143 | }) 144 | } 145 | 146 | getPiece(square) { 147 | return this.state.position.getPiece(square) 148 | } 149 | 150 | getPosition() { 151 | return this.state.position.getFen() 152 | } 153 | 154 | getOrientation() { 155 | return this.state.orientation 156 | } 157 | 158 | addMarker(type, square) { 159 | if (typeof type === "string" || typeof square === "object") { // todo remove 2022-12-01 160 | console.error("changed the signature of `addMarker` to `(type, square)` with v5.1.x") 161 | return 162 | } 163 | this.state.addMarker(square, type) 164 | this.view.drawMarkers() 165 | } 166 | 167 | getMarkers(type = undefined, square = undefined) { 168 | if (typeof type === "string" || typeof square === "object") { // todo remove 2022-12-01 169 | console.error("changed the signature of `getMarkers` to `(type, square)` with v5.1.x") 170 | return 171 | } 172 | const markersFound = [] 173 | this.state.markers.forEach((marker) => { 174 | const markerSquare = marker.square 175 | if (!square && (!type || type === marker.type) || 176 | !type && square === markerSquare || 177 | type === marker.type && square === markerSquare) { 178 | markersFound.push({square: marker.square, type: marker.type}) 179 | } 180 | }) 181 | return markersFound 182 | } 183 | 184 | removeMarkers(type = undefined, square = undefined) { 185 | if (typeof type === "string" || typeof square === "object") { // todo remove 2022-12-01 186 | console.error("changed the signature of `removeMarkers` to `(type, square)` with v5.1.x") 187 | return 188 | } 189 | this.state.removeMarkers(square, type) 190 | this.view.drawMarkers() 191 | } 192 | 193 | enableMoveInput(eventHandler, color = undefined) { 194 | this.view.enableMoveInput(eventHandler, color) 195 | } 196 | 197 | disableMoveInput() { 198 | this.view.disableMoveInput() 199 | } 200 | 201 | enableSquareSelect(eventHandler) { 202 | if (this.squareSelectListener) { 203 | console.warn("squareSelectListener already existing") 204 | return 205 | } 206 | this.squareSelectListener = function (e) { 207 | const square = e.target.getAttribute("data-square") 208 | if (e.type === "contextmenu") { 209 | // disable context menu 210 | e.preventDefault() 211 | return 212 | } 213 | eventHandler({ 214 | chessboard: this, 215 | type: e.button === 2 ? SQUARE_SELECT_TYPE.secondary : SQUARE_SELECT_TYPE.primary, 216 | square: square 217 | }) 218 | } 219 | this.context.addEventListener("contextmenu", this.squareSelectListener) 220 | this.context.addEventListener("mouseup", this.squareSelectListener) 221 | this.context.addEventListener("touchend", this.squareSelectListener) 222 | this.state.squareSelectEnabled = true 223 | this.view.visualizeInputState() 224 | } 225 | 226 | disableSquareSelect() { 227 | this.context.removeEventListener("contextmenu", this.squareSelectListener) 228 | this.context.removeEventListener("mouseup", this.squareSelectListener) 229 | this.context.removeEventListener("touchend", this.squareSelectListener) 230 | this.squareSelectListener = undefined 231 | this.state.squareSelectEnabled = false 232 | this.view.visualizeInputState() 233 | } 234 | 235 | destroy() { 236 | this.state.invokeExtensionPoints(EXTENSION_POINT.destroy) 237 | this.positionAnimationsQueue.destroy() 238 | this.view.destroy() 239 | this.view = undefined 240 | this.state = undefined 241 | if (this.squareSelectListener) { 242 | this.context.removeEventListener("contextmenu", this.squareSelectListener) 243 | this.context.removeEventListener("mouseup", this.squareSelectListener) 244 | this.context.removeEventListener("touchend", this.squareSelectListener) 245 | } 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /cm-chessboard/extensions/accessibility/Accessibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | import {Extension, EXTENSION_POINT} from "../../model/Extension.js" 7 | import {piecesTranslations, renderPieceTitle} from "../../view/ChessboardView.js" 8 | import {COLOR, INPUT_EVENT_TYPE} from "../../Chessboard.js" 9 | 10 | const hlTranslations = { 11 | de: { 12 | pieces_lists: "Figurenlisten", 13 | board_as_table: "Schachbrett als Tabelle", 14 | move_piece: "Figur bewegen", 15 | from: "von", 16 | to: "nach", 17 | move: "bewegen", 18 | input_white_enabled: "Eingabe Weiß aktiviert", 19 | input_black_enabled: "Eingabe Schwarz aktiviert", 20 | input_disabled: "Eingabe deaktiviert" 21 | }, 22 | en: { 23 | pieces_lists: "Pieces lists", 24 | board_as_table: "Chessboard as table", 25 | move_piece: "Move piece", 26 | from: "from", 27 | to: "to", 28 | move: "move", 29 | input_white_enabled: "Input white enabled", 30 | input_black_enabled: "Input black enabled", 31 | input_disabled: "Input disabled" 32 | } 33 | } 34 | 35 | export class Accessibility extends Extension { 36 | 37 | constructor(chessboard, props) { 38 | super(chessboard, props) 39 | this.props = { 40 | brailleNotationInAlt: true, // show the braille notation of the position in the alt attribute of the SVG image 41 | boardAsTable: false, // display the board additionally as HTML table 42 | movePieceForm: false, // display a form to move a piece (from, to, move) 43 | piecesAsList: false, // display the pieces additionally as List 44 | visuallyHidden: true // hide all those extra outputs visually but keep them accessible for screen readers and braille displays 45 | } 46 | Object.assign(this.props, props) 47 | this.lang = chessboard.props.language 48 | this.translations = piecesTranslations 49 | this.t = this.translations[this.lang] 50 | this.th = hlTranslations[this.lang] 51 | if (this.props.movePieceForm) { 52 | this.movePieceFormContainer = this.createElement(`

${this.th.move_piece}

53 | 54 | 55 | 56 |
`) 57 | this.form = this.movePieceFormContainer.querySelector("form") 58 | this.inputFrom = this.form.querySelector(".input-from") 59 | this.inputTo = this.form.querySelector(".input-to") 60 | this.moveButton = this.form.querySelector(".button-move") 61 | if (this.props.visuallyHidden) { 62 | this.movePieceFormContainer.classList.add("cm-visually-hidden") 63 | } 64 | this.form.addEventListener("submit", (evt) => { 65 | evt.preventDefault() 66 | if (this.chessboard.view.moveInputCallback({ 67 | chessboard: this.chessboard, 68 | type: INPUT_EVENT_TYPE.validateMoveInput, 69 | squareFrom: this.inputFrom.value, 70 | squareTo: this.inputTo.value 71 | })) { 72 | this.chessboard.movePiece(this.inputFrom.value, this.inputTo.value, 73 | true).then(() => { 74 | this.inputFrom.value = "" 75 | this.inputTo.value = "" 76 | }) 77 | } 78 | }) 79 | this.chessboard.context.appendChild(this.movePieceFormContainer) 80 | } 81 | if (this.props.boardAsTable) { 82 | this.boardAsTableContainer = this.createElement(`

${this.th.board_as_table}

`) 83 | this.boardAsTable = this.boardAsTableContainer.querySelector(".table") 84 | this.chessboard.context.appendChild(this.boardAsTableContainer) 85 | if (this.props.visuallyHidden) { 86 | this.boardAsTableContainer.classList.add("cm-visually-hidden") 87 | } 88 | } 89 | if (this.props.piecesAsList) { 90 | this.piecesListContainer = this.createElement(`

${this.th.pieces_lists}

`) 91 | this.piecesList = this.piecesListContainer.querySelector(".list") 92 | this.chessboard.context.appendChild(this.piecesListContainer) 93 | if (this.props.visuallyHidden) { 94 | this.piecesListContainer.classList.add("cm-visually-hidden") 95 | } 96 | } 97 | this.registerExtensionPoint(EXTENSION_POINT.moveInput, () => { 98 | this.updateFormInputs() 99 | }) 100 | this.registerExtensionPoint(EXTENSION_POINT.moveInputToggled, () => { 101 | this.updateFormInputs() 102 | }) 103 | this.registerExtensionPoint(EXTENSION_POINT.positionChanged, () => { 104 | if(this.chessboard.state) { // not destroyed 105 | this.redrawPositionInAltAttribute() 106 | if (this.props.boardAsTable) { 107 | this.redrawBoardAsTable() 108 | } 109 | if (this.props.piecesAsList) { 110 | this.redrawPiecesLists() 111 | } 112 | } 113 | }) 114 | this.registerExtensionPoint(EXTENSION_POINT.boardChanged, () => { 115 | console.log("EXTENSION_POINT.boardChanged") 116 | if (this.props.boardAsTable) { 117 | this.redrawBoardAsTable() 118 | } 119 | }) 120 | this.redrawPositionInAltAttribute() 121 | if (this.props.boardAsTable) { 122 | this.redrawBoardAsTable() 123 | } 124 | if (this.props.piecesAsList) { 125 | this.redrawPiecesLists() 126 | } 127 | } 128 | 129 | updateFormInputs() { 130 | if (this.inputFrom) { 131 | if (this.chessboard.state.inputWhiteEnabled || this.chessboard.state.inputBlackEnabled) { 132 | this.inputFrom.disabled = false 133 | this.inputTo.disabled = false 134 | this.moveButton.disabled = false 135 | } else { 136 | this.inputFrom.disabled = true 137 | this.inputTo.disabled = true 138 | this.moveButton.disabled = true 139 | } 140 | } 141 | } 142 | 143 | redrawPositionInAltAttribute() { 144 | const pieces = this.chessboard.state.position.getPieces() 145 | let listW = piecesTranslations[this.lang].colors.w.toUpperCase() + ":" 146 | let listB = piecesTranslations[this.lang].colors.b.toUpperCase() + ":" 147 | for (const piece of pieces) { 148 | const pieceName = piece.name === "p" ? "" : piecesTranslations[this.lang].pieces[piece.name].toUpperCase() 149 | if (piece.color === "w") { 150 | listW += " " + pieceName + piece.position 151 | } else { 152 | listB += " " + pieceName + piece.position 153 | } 154 | } 155 | const altText = `${listW} 156 | ${listB}` 157 | this.chessboard.view.svg.setAttribute("alt", altText) 158 | } 159 | 160 | redrawPiecesLists() { 161 | const pieces = this.chessboard.state.position.getPieces() 162 | let listW = "" 163 | let listB = "" 164 | for (const piece of pieces) { 165 | if (piece.color === "w") { 166 | listW += `
  • ${renderPieceTitle(this.lang, piece.name)} ${piece.position}
  • ` 167 | } else { 168 | listB += `
  • ${renderPieceTitle(this.lang, piece.name)} ${piece.position}
  • ` 169 | } 170 | } 171 | this.piecesList.innerHTML = ` 172 |

    ${this.t.colors_long.w}

    173 |
      ${listW}
    174 |

    ${this.t.colors_long.b}

    175 |
      ${listB}
    ` 176 | } 177 | 178 | redrawBoardAsTable() { 179 | const squares = this.chessboard.state.position.squares.slice() 180 | const ranks = ["a", "b", "c", "d", "e", "f", "g", "h"] 181 | const files = ["8", "7", "6", "5", "4", "3", "2", "1"] 182 | if (this.chessboard.state.orientation === COLOR.black) { 183 | ranks.reverse() 184 | files.reverse() 185 | squares.reverse() 186 | } 187 | let html = `` 188 | for (const rank of ranks) { 189 | html += `` 190 | } 191 | html += "" 192 | for (let x = 7; x >= 0; x--) { 193 | html += `` 194 | for (let y = 0; y < 8; y++) { 195 | const pieceCode = squares[y % 8 + x * 8] 196 | let color, name 197 | if (pieceCode) { 198 | color = pieceCode.charAt(0) 199 | name = pieceCode.charAt(1) 200 | html += `` 201 | } else { 202 | html += `` 203 | } 204 | } 205 | html += "" 206 | } 207 | html += "
    ${rank}
    ${files[7 - x]}${renderPieceTitle(this.lang, name, color)}
    " 208 | this.boardAsTable.innerHTML = html 209 | } 210 | 211 | createElement(html) { 212 | const template = document.createElement('template') 213 | template.innerHTML = html.trim() 214 | return template.content.firstChild 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /cm-chessboard/extensions/arrows/Arrows.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Barak Michener (@barakmich) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | 7 | import {Extension, EXTENSION_POINT} from "../../model/Extension.js" 8 | import {Svg} from "../../view/ChessboardView.js" 9 | 10 | export const ARROW_TYPE = { 11 | default: {class: "arrow-default", slice: "arrowDefault", headSize: 7}, 12 | danger: {class: "arrow-danger", slice: "arrowDefault", headSize: 7}, 13 | pointy: {class: "arrow-pointy", slice: "arrowPointy", headSize: 7}, 14 | } 15 | 16 | export class Arrows extends Extension { 17 | constructor(chessboard, props) { 18 | super(chessboard, props) 19 | this.registerExtensionPoint(EXTENSION_POINT.redrawBoard, () => { 20 | this.onRedrawBoard() 21 | }) 22 | let defaultProps = { 23 | sprite: { 24 | url: "./assets/images/chessboard-arrows.svg", 25 | size: 40, 26 | cache: true 27 | }, 28 | } 29 | this.props = {} 30 | Object.assign(this.props, defaultProps) 31 | Object.assign(this.props, props) 32 | this.props.sprite = defaultProps.sprite 33 | if (props.sprite) { 34 | Object.assign(this.props.sprite, props.sprite) 35 | } 36 | if (this.props.sprite.cache) { 37 | this.chessboard.view.cacheSpriteToDiv("chessboardArrowSpriteCache", this.props.sprite.url) 38 | } 39 | this.registerMethod("addArrow", this.addArrow) 40 | this.registerMethod("getArrows", this.getArrows) 41 | this.registerMethod("removeArrows", this.removeArrows) 42 | this.arrowGroup = Svg.addElement(chessboard.view.piecesLayer, "g", {class: "arrows"}) 43 | this.arrows = [] 44 | } 45 | 46 | onRedrawBoard() { 47 | while (this.arrowGroup.firstChild) { 48 | this.arrowGroup.removeChild(this.arrowGroup.firstChild) 49 | } 50 | this.arrows.forEach((arrow) => { 51 | this.drawArrow(arrow) 52 | }) 53 | } 54 | 55 | drawArrow(arrow) { 56 | const arrowsGroup = Svg.addElement(this.arrowGroup, "g") 57 | arrowsGroup.setAttribute("data-arrow", arrow.from + arrow.to) 58 | arrowsGroup.setAttribute("class", "arrow " + arrow.type.class) 59 | 60 | const view = this.chessboard.view 61 | const sqfrom = document.querySelectorAll('[data-square="' + arrow.from + '"]')[0] 62 | const sqto = document.querySelectorAll('[data-square="' + arrow.to + '"]')[0] 63 | const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url 64 | const defs = Svg.addElement(arrowsGroup, "defs") 65 | const id = "arrow-" + arrow.from + arrow.to 66 | const marker = Svg.addElement(defs, "marker", { 67 | id: id, 68 | markerWidth: arrow.type.headSize, 69 | markerHeight: arrow.type.headSize, 70 | //markerUnits: "userSpaceOnUse", 71 | refX: 20, 72 | refY: 20, 73 | viewBox: "0 0 40 40", 74 | orient: "auto", 75 | class: "arrow-head", 76 | }) 77 | 78 | const ignored = Svg.addElement(marker, "use", { 79 | href: `${spriteUrl}#${arrow.type.slice}`, 80 | }) 81 | 82 | 83 | const x1 = sqfrom.x.baseVal.value + (sqfrom.width.baseVal.value / 2) 84 | const x2 = sqto.x.baseVal.value + (sqto.width.baseVal.value / 2) 85 | const y1 = sqfrom.y.baseVal.value + (sqfrom.height.baseVal.value / 2) 86 | const y2 = sqto.y.baseVal.value + (sqto.height.baseVal.value / 2) 87 | 88 | const width = ((view.scalingX + view.scalingY) / 2) * 4 89 | let lineFill = Svg.addElement(arrowsGroup, "line") 90 | lineFill.setAttribute('x1', x1) 91 | lineFill.setAttribute('x2', x2) 92 | lineFill.setAttribute('y1', y1) 93 | lineFill.setAttribute('y2', y2) 94 | lineFill.setAttribute('class', 'arrow-line') 95 | lineFill.setAttribute("marker-end", "url(#" + id + ")") 96 | lineFill.setAttribute('stroke-width', width + "px") 97 | 98 | } 99 | 100 | addArrow(type, from, to) { 101 | if (typeof type === "string" || typeof from === "object" || typeof to === "object") { // todo remove 2022-12-01 102 | console.error("changed the signature of `addArrow` to `(type, from, to)` with v5.1.x") 103 | return 104 | } 105 | this.arrows.push(new Arrow(from, to, type)) 106 | this.chessboard.view.redrawBoard() 107 | } 108 | 109 | getArrows(type = undefined, from = undefined, to = undefined) { 110 | if (typeof type === "string" || typeof from === "object" || typeof to === "object") { // todo remove 2022-12-01 111 | console.error("changed the signature of `getArrows` to `(type, from, to)` with v5.1.x") 112 | return 113 | } 114 | let arrows = [] 115 | this.arrows.forEach((arrow) => { 116 | if (arrow.matches(from, to, type)) { 117 | arrows.push(arrow) 118 | } 119 | }) 120 | return arrows 121 | } 122 | 123 | removeArrows(type = undefined, from = undefined, to = undefined) { 124 | if (typeof type === "string" || typeof from === "object" || typeof to === "object") { // todo remove 2022-12-01 125 | console.error("changed the signature of `removeArrows` to `(type, from, to)` with v5.1.x") 126 | return 127 | } 128 | this.arrows = this.arrows.filter((arrow) => !arrow.matches(from, to, type)) 129 | this.chessboard.view.redrawBoard() 130 | } 131 | } 132 | 133 | class Arrow { 134 | constructor(from, to, type) { 135 | this.from = from 136 | this.to = to 137 | this.type = type 138 | } 139 | 140 | matches(from = undefined, to = undefined, type = undefined) { 141 | if (from && from !== this.from) { 142 | return false 143 | } 144 | if (to && to !== this.to) { 145 | return false 146 | } 147 | return !(type && type !== this.type) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /cm-chessboard/extensions/arrows/assets/arrows.css: -------------------------------------------------------------------------------- 1 | .cm-chessboard .arrow-default .arrow-head { 2 | fill: blue; 3 | fill-rule: nonzero; 4 | fill-opacity: 1; } 5 | 6 | .cm-chessboard .arrow-default .arrow-line { 7 | stroke: blue; 8 | stroke-linecap: round; 9 | opacity: 0.5; } 10 | 11 | .cm-chessboard .arrow-danger .arrow-head { 12 | fill: red; 13 | fill-rule: nonzero; 14 | fill-opacity: 1; } 15 | 16 | .cm-chessboard .arrow-danger .arrow-line { 17 | stroke: red; 18 | stroke-linecap: round; 19 | opacity: 0.5; } 20 | 21 | .cm-chessboard .arrow-pointy .arrow-head { 22 | fill: black; 23 | fill-rule: nonzero; 24 | fill-opacity: 1; } 25 | 26 | .cm-chessboard .arrow-pointy .arrow-line { 27 | stroke: black; 28 | stroke-linecap: round; 29 | opacity: 0.5; } 30 | 31 | /*# sourceMappingURL=arrows.css.map */ -------------------------------------------------------------------------------- /cm-chessboard/extensions/arrows/assets/arrows.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "arrows.css", 4 | "sources": [ 5 | "arrows.scss" 6 | ], 7 | "names": [], 8 | "mappings": "AAAA,AAEI,cAFU,CACZ,cAAc,CACZ,WAAW,CAAC;EACV,IAAI,EAAE,IAAI;EACV,SAAS,EAAE,OAAO;EAClB,YAAY,EAAE,CAAC,GAChB;;AANL,AAOI,cAPU,CACZ,cAAc,CAMZ,WAAW,CAAC;EACV,MAAM,EAAE,IAAI;EACZ,cAAc,EAAE,KAAK;EACrB,OAAO,EAAE,GAAG,GACb;;AAXL,AAcI,cAdU,CAaZ,aAAa,CACX,WAAW,CAAC;EACV,IAAI,EAAE,GAAG;EACT,SAAS,EAAE,OAAO;EAClB,YAAY,EAAE,CAAC,GAChB;;AAlBL,AAmBI,cAnBU,CAaZ,aAAa,CAMX,WAAW,CAAC;EACV,MAAM,EAAE,GAAG;EACX,cAAc,EAAE,KAAK;EACrB,OAAO,EAAE,GAAG,GACb;;AAvBL,AA0BI,cA1BU,CAyBZ,aAAa,CACX,WAAW,CAAC;EACV,IAAI,EAAE,KAAK;EACX,SAAS,EAAE,OAAO;EAClB,YAAY,EAAE,CAAC,GAChB;;AA9BL,AA+BI,cA/BU,CAyBZ,aAAa,CAMX,WAAW,CAAC;EACV,MAAM,EAAE,KAAK;EACb,cAAc,EAAE,KAAK;EACrB,OAAO,EAAE,GAAG,GACb" 9 | } -------------------------------------------------------------------------------- /cm-chessboard/extensions/arrows/assets/arrows.scss: -------------------------------------------------------------------------------- 1 | .cm-chessboard { 2 | .arrow-default { 3 | .arrow-head { 4 | fill: blue; 5 | fill-rule: nonzero; 6 | fill-opacity: 1; 7 | } 8 | .arrow-line { 9 | stroke: blue; 10 | stroke-linecap: round; 11 | opacity: 0.5; 12 | } 13 | } 14 | .arrow-danger { 15 | .arrow-head { 16 | fill: red; 17 | fill-rule: nonzero; 18 | fill-opacity: 1; 19 | } 20 | .arrow-line { 21 | stroke: red; 22 | stroke-linecap: round; 23 | opacity: 0.5; 24 | } 25 | } 26 | .arrow-pointy { 27 | .arrow-head { 28 | fill: black; 29 | fill-rule: nonzero; 30 | fill-opacity: 1; 31 | } 32 | .arrow-line { 33 | stroke: black; 34 | stroke-linecap: round; 35 | opacity: 0.5; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /cm-chessboard/extensions/arrows/assets/arrows.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 19 | cm-chessbord arrowheads 21 | Arrowheads for cm-chessboard (https://shaack.com/projekte/cm-chessboard/). 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /cm-chessboard/model/ChessboardState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | import {Position} from "./Position.js" 7 | 8 | export function createTask() { 9 | let resolve, reject 10 | const promise = new Promise(function (_resolve, _reject) { 11 | resolve = _resolve 12 | reject = _reject 13 | }) 14 | promise.resolve = resolve 15 | promise.reject = reject 16 | return promise 17 | } 18 | 19 | export class ChessboardState { 20 | 21 | constructor() { 22 | this.position = new Position() 23 | this.orientation = undefined 24 | this.markers = [] 25 | this.inputWhiteEnabled = false 26 | this.inputBlackEnabled = false 27 | this.inputEnabled = false 28 | this.squareSelectEnabled = false 29 | this.extensionPoints = {} 30 | this.moveInputProcess = createTask().resolve() 31 | } 32 | 33 | setPosition(fen, animated = false) { 34 | this.position = new Position(fen, animated) 35 | } 36 | 37 | movePiece(fromSquare, toSquare, animated = false) { 38 | const position = this._position.clone() 39 | position.animated = animated 40 | const piece = position.getPiece(fromSquare) 41 | if(!piece) { 42 | console.error("no piece on", fromSquare) 43 | } 44 | position.setPiece(fromSquare, undefined) 45 | position.setPiece(toSquare, piece) 46 | this._position = position 47 | } 48 | 49 | setPiece(square, piece, animated = false) { 50 | const position = this._position.clone() 51 | position.animated = animated 52 | position.setPiece(square, piece) 53 | this._position = position 54 | } 55 | 56 | addMarker(square, type) { 57 | this.markers.push({square: square, type: type}) 58 | } 59 | 60 | removeMarkers(square = undefined, type = undefined) { 61 | if (!square && !type) { 62 | this.markers = [] 63 | } else { 64 | this.markers = this.markers.filter((marker) => { 65 | if (!type) { 66 | if (square === marker.square) { 67 | return false 68 | } 69 | } else if (!square) { 70 | if (marker.type === type) { 71 | return false 72 | } 73 | } else if (marker.type === type && square === marker.square) { 74 | return false 75 | } 76 | return true 77 | }) 78 | } 79 | } 80 | 81 | invokeExtensionPoints(name, data = {}) { 82 | const extensionPoints = this.extensionPoints[name] 83 | const dataCloned = Object.assign({}, data); 84 | dataCloned.extensionPoint = name 85 | if(extensionPoints) { 86 | for (const extensionPoint of extensionPoints) { 87 | setTimeout(() => { 88 | extensionPoint(dataCloned) 89 | }) 90 | } 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /cm-chessboard/model/Extension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | 7 | export const EXTENSION_POINT = { 8 | positionChanged: "positionChanged", // the positions of the pieces was changed 9 | boardChanged: "boardChanged", // the board (orientation) was changed 10 | moveInputToggled: "moveInputToggled", // move input was enabled or disabled 11 | moveInput: "moveInput", // move started, to validate or canceled // TODO validation not possible for now, see https://github.com/shaack/cm-chessboard/issues/82 12 | moveInputStateChanged: "moveInput", // TODO deprecated, use `moveInput` 13 | redrawBoard: "redrawBoard", // called after redrawing the board 14 | destroy: "destroy" // called, before the board is destroyed 15 | } 16 | 17 | export class Extension { 18 | 19 | constructor(chessboard, props) { 20 | this.chessboard = chessboard 21 | this.props = props 22 | } 23 | 24 | registerExtensionPoint(name, callback) { 25 | if (!this.chessboard.state.extensionPoints[name]) { 26 | this.chessboard.state.extensionPoints[name] = [] 27 | } 28 | this.chessboard.state.extensionPoints[name].push(callback) 29 | } 30 | 31 | registerMethod(name, callback) { 32 | if (!this.chessboard[name]) { 33 | this.chessboard[name] = (...args) => { 34 | callback.apply(this, args) 35 | } 36 | } else { 37 | log.error("method", name, "already exists") 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /cm-chessboard/model/Position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | export const FEN_START_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" // TODO deprecated, use FEN 7 | export const FEN_EMPTY_POSITION = "8/8/8/8/8/8/8/8" // TODO deprecated, use FEN 8 | export const FEN = { 9 | start: FEN_START_POSITION, 10 | empty: FEN_EMPTY_POSITION 11 | } 12 | 13 | export class Position { 14 | 15 | constructor(fen = undefined) { 16 | this.squares = new Array(64).fill(null) 17 | this.setFen(fen) 18 | } 19 | 20 | setFen(fen = FEN.empty) { 21 | let fenNormalized 22 | if (fen === "start") { 23 | console.warn("setting the position with the strings 'start' or 'empty' is deprecated, use FEN.start or FEN.empty") 24 | fenNormalized = FEN.start 25 | } else if (fen === "empty" || fen === undefined) { 26 | console.warn("setting the position with the strings 'start' or 'empty' is deprecated, use FEN.start or FEN.empty") 27 | fenNormalized = FEN.empty 28 | } else { 29 | fenNormalized = fen 30 | } 31 | const parts = fenNormalized.replace(/^\s*/, "").replace(/\s*$/, "").split(/\/|\s/) 32 | for (let part = 0; part < 8; part++) { 33 | const row = parts[7 - part].replace(/\d/g, (str) => { 34 | const numSpaces = parseInt(str) 35 | let ret = '' 36 | for (let i = 0; i < numSpaces; i++) { 37 | ret += '-' 38 | } 39 | return ret 40 | }) 41 | for (let c = 0; c < 8; c++) { 42 | const char = row.substring(c, c + 1) 43 | let piece = null 44 | if (char !== '-') { 45 | if (char.toUpperCase() === char) { 46 | piece = `w${char.toLowerCase()}` 47 | } else { 48 | piece = `b${char}` 49 | } 50 | } 51 | this.squares[part * 8 + c] = piece 52 | } 53 | } 54 | } 55 | 56 | getFen() { 57 | let parts = new Array(8).fill("") 58 | for (let part = 0; part < 8; part++) { 59 | let spaceCounter = 0 60 | for (let i = 0; i < 8; i++) { 61 | const piece = this.squares[part * 8 + i] 62 | if (!piece) { 63 | spaceCounter++ 64 | } else { 65 | if (spaceCounter > 0) { 66 | parts[7 - part] += spaceCounter 67 | spaceCounter = 0 68 | } 69 | const color = piece.substring(0, 1) 70 | const name = piece.substring(1, 2) 71 | if (color === "w") { 72 | parts[7 - part] += name.toUpperCase() 73 | } else { 74 | parts[7 - part] += name 75 | } 76 | } 77 | } 78 | if (spaceCounter > 0) { 79 | parts[7 - part] += spaceCounter 80 | spaceCounter = 0 81 | } 82 | } 83 | return parts.join("/") 84 | } 85 | 86 | getPieces(sortBy = ['k', 'q', 'r', 'b', 'n', 'p']) { 87 | const pieces = [] 88 | const sort = (a, b) => { 89 | return sortBy.indexOf(a.name) - sortBy.indexOf(b.name) 90 | } 91 | for(let i=0; i<64; i++) { 92 | const piece = this.squares[i] 93 | if(piece) { 94 | pieces.push({ 95 | name: piece.charAt(1), 96 | color: piece.charAt(0), 97 | position: Position.indexToSquare(i) 98 | }) 99 | } 100 | } 101 | if(sortBy) { 102 | pieces.sort(sort) 103 | } 104 | return pieces 105 | } 106 | 107 | movePiece(squareFrom, squareTo) { 108 | if(!this.squares[Position.squareToIndex(squareFrom)]) { 109 | console.warn("no piece on", squareFrom) 110 | return 111 | } 112 | this.squares[Position.squareToIndex(squareTo)] = this.squares[Position.squareToIndex(squareFrom)] 113 | this.squares[Position.squareToIndex(squareFrom)] = undefined 114 | } 115 | 116 | setPiece(square, piece) { 117 | this.squares[Position.squareToIndex(square)] = piece 118 | } 119 | 120 | getPiece(square) { 121 | return this.squares[Position.squareToIndex(square)] 122 | } 123 | 124 | static squareToIndex(square) { 125 | const file = square.substring(0, 1).charCodeAt(0) - 97 126 | const rank = square.substring(1, 2) - 1 127 | return 8 * rank + file 128 | } 129 | 130 | static indexToSquare(index) { 131 | const file = String.fromCharCode(index % 8 + 97) 132 | const rank = Math.floor(index / 8) + 1 133 | return file + rank 134 | } 135 | 136 | clone() { 137 | const cloned = new Position() 138 | cloned.squares = this.squares.slice(0) 139 | return cloned 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /cm-chessboard/view/ChessboardView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | 7 | import {VisualMoveInput} from "./VisualMoveInput.js" 8 | import {COLOR, INPUT_EVENT_TYPE, BORDER_TYPE} from "../Chessboard.js" 9 | import {Position} from "../model/Position.js" 10 | import {EXTENSION_POINT} from "../model/Extension.js" 11 | 12 | export const piecesTranslations = { 13 | en: { 14 | colors: { 15 | w: "w", b: "b" 16 | }, 17 | colors_long: { 18 | w: "White", b: "Black" 19 | }, 20 | pieces: { 21 | p: "p", n: "n", b: "b", r: "r", q: "q", k: "k" 22 | }, 23 | pieces_long: { 24 | p: "Pawn", n: "Knight", b: "Bishop", r: "Rook", q: "Queen", k: "King" 25 | } 26 | }, 27 | de: { 28 | colors: { 29 | w: "w", b: "s" 30 | }, 31 | colors_long: { 32 | w: "Weiß", b: "Schwarz" 33 | }, 34 | pieces: { 35 | p: "b", n: "s", b: "l", r: "t", q: "d", k: "k" 36 | }, 37 | pieces_long: { 38 | p: "Bauer", n: "Springer", b: "Läufer", r: "Turm", q: "Dame", k: "König" 39 | } 40 | } 41 | } 42 | 43 | export function renderPieceTitle(lang, name, color = undefined) { 44 | let title = piecesTranslations[lang].pieces_long[name] 45 | if (color) { 46 | title += " " + piecesTranslations[lang].colors_long[color] 47 | } 48 | return title 49 | } 50 | 51 | export class ChessboardView { 52 | 53 | constructor(chessboard) { 54 | this.chessboard = chessboard 55 | this.moveInput = new VisualMoveInput(this, 56 | this.moveInputStartedCallback.bind(this), 57 | this.validateMoveInputCallback.bind(this), 58 | this.moveInputCanceledCallback.bind(this) 59 | ) 60 | if (chessboard.props.sprite.cache) { 61 | this.cacheSpriteToDiv("chessboardSpriteCache", this.chessboard.props.sprite.url) 62 | } 63 | this.context = document.createElement("div") 64 | this.chessboard.context.appendChild(this.context) 65 | if (chessboard.props.responsive) { 66 | if (typeof ResizeObserver !== "undefined") { 67 | this.resizeObserver = new ResizeObserver(() => { 68 | this.handleResize() 69 | }) 70 | this.resizeObserver.observe(this.chessboard.context) 71 | } else { 72 | this.resizeListener = this.handleResize.bind(this) 73 | window.addEventListener("resize", this.resizeListener) 74 | } 75 | } 76 | 77 | this.pointerDownListener = this.pointerDownHandler.bind(this) 78 | this.pointerDownListener = this.pointerDownHandler.bind(this) 79 | this.context.addEventListener("mousedown", this.pointerDownListener) 80 | this.context.addEventListener("touchstart", this.pointerDownListener) 81 | 82 | this.createSvgAndGroups() 83 | this.updateMetrics() 84 | this.handleResize() 85 | this.redrawBoard() 86 | } 87 | 88 | pointerDownHandler(e) { 89 | this.moveInput.onPointerDown(e) 90 | } 91 | 92 | destroy() { 93 | this.moveInput.destroy() 94 | if (this.resizeObserver) { 95 | this.resizeObserver.unobserve(this.chessboard.context) 96 | } 97 | if (this.resizeListener) { 98 | window.removeEventListener("resize", this.resizeListener) 99 | } 100 | this.chessboard.context.removeEventListener("mousedown", this.pointerDownListener) 101 | this.chessboard.context.removeEventListener("touchstart", this.pointerDownListener) 102 | this.animationQueue = [] 103 | if (this.currentAnimation) { 104 | cancelAnimationFrame(this.currentAnimation.frameHandle) 105 | } 106 | Svg.removeElement(this.svg) 107 | } 108 | 109 | // Sprite // 110 | 111 | cacheSpriteToDiv(wrapperId, url) { 112 | if (!document.getElementById(wrapperId)) { 113 | const wrapper = document.createElement("div") 114 | wrapper.style.display = "none" 115 | wrapper.id = wrapperId 116 | document.body.appendChild(wrapper) 117 | const xhr = new XMLHttpRequest() 118 | xhr.open("GET", url, true) 119 | xhr.onload = function () { 120 | wrapper.insertAdjacentHTML('afterbegin', xhr.response) 121 | } 122 | xhr.send() 123 | } 124 | } 125 | 126 | createSvgAndGroups() { 127 | this.svg = Svg.createSvg(this.context) 128 | // let description = document.createElement("description") 129 | // description.innerText = "Chessboard" 130 | // description.id = "svg-description" 131 | // this.svg.appendChild(description) 132 | let cssClass = this.chessboard.props.style.cssClass ? this.chessboard.props.style.cssClass : "default" 133 | this.svg.setAttribute("class", "cm-chessboard border-type-" + this.chessboard.props.style.borderType + " " + cssClass) 134 | // this.svg.setAttribute("aria-describedby", "svg-description") 135 | this.svg.setAttribute("role", "img") 136 | this.updateMetrics() 137 | this.boardGroup = Svg.addElement(this.svg, "g", {class: "board"}) 138 | this.coordinatesGroup = Svg.addElement(this.svg, "g", {class: "coordinates"}) 139 | this.markersLayer = Svg.addElement(this.svg, "g", {class: "markers-layer"}) 140 | this.markersGroup = Svg.addElement(this.markersLayer, "g", {class: "markers"}) 141 | this.piecesLayer = Svg.addElement(this.svg, "g", {class: "pieces-layer"}) 142 | this.piecesGroup = Svg.addElement(this.piecesLayer, "g", {class: "pieces"}) 143 | } 144 | 145 | updateMetrics() { 146 | this.width = this.context.clientWidth 147 | this.height = this.context.clientWidth * (this.chessboard.props.style.aspectRatio || 1) 148 | if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { 149 | this.borderSize = this.width / 25 150 | } else if (this.chessboard.props.style.borderType === BORDER_TYPE.thin) { 151 | this.borderSize = this.width / 320 152 | } else { 153 | this.borderSize = 0 154 | } 155 | this.innerWidth = this.width - 2 * this.borderSize 156 | this.innerHeight = this.height - 2 * this.borderSize 157 | this.squareWidth = this.innerWidth / 8 158 | this.squareHeight = this.innerHeight / 8 159 | this.scalingX = this.squareWidth / this.chessboard.props.sprite.size 160 | this.scalingY = this.squareHeight / this.chessboard.props.sprite.size 161 | this.pieceXTranslate = (this.squareWidth / 2 - this.chessboard.props.sprite.size * this.scalingY / 2) 162 | } 163 | 164 | handleResize() { 165 | this.context.style.width = this.chessboard.context.clientWidth + "px" 166 | this.context.style.height = (this.chessboard.context.clientWidth * this.chessboard.props.style.aspectRatio) + "px" 167 | if (this.context.clientWidth !== this.width || 168 | this.context.clientHeight !== this.height) { 169 | this.updateMetrics() 170 | this.redrawBoard() 171 | this.redrawPieces() 172 | } 173 | this.svg.setAttribute("width", "100%") // safari bugfix 174 | this.svg.setAttribute("height", "100%") 175 | } 176 | 177 | redrawBoard() { 178 | this.redrawSquares() 179 | this.drawCoordinates() 180 | this.drawMarkers() 181 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.redrawBoard) 182 | this.visualizeInputState() 183 | } 184 | 185 | // Board // 186 | 187 | redrawSquares() { 188 | while (this.boardGroup.firstChild) { 189 | this.boardGroup.removeChild(this.boardGroup.lastChild) 190 | } 191 | 192 | let boardBorder = Svg.addElement(this.boardGroup, "rect", {width: this.width, height: this.height}) 193 | boardBorder.setAttribute("class", "border") 194 | if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { 195 | const innerPos = this.borderSize 196 | let borderInner = Svg.addElement(this.boardGroup, "rect", { 197 | x: innerPos, 198 | y: innerPos, 199 | width: this.width - innerPos * 2, 200 | height: this.height - innerPos * 2 201 | }) 202 | borderInner.setAttribute("class", "border-inner") 203 | } 204 | 205 | for (let i = 0; i < 64; i++) { 206 | const index = this.chessboard.state.orientation === COLOR.white ? i : 63 - i 207 | const squareColor = ((9 * index) & 8) === 0 ? 'black' : 'white' 208 | const fieldClass = `square ${squareColor}` 209 | const point = this.squareToPoint(Position.indexToSquare(index)) 210 | const squareRect = Svg.addElement(this.boardGroup, "rect", { 211 | x: point.x, y: point.y, width: this.squareWidth, height: this.squareHeight 212 | }) 213 | squareRect.setAttribute("class", fieldClass) 214 | squareRect.setAttribute("data-square", Position.indexToSquare(index)) 215 | } 216 | } 217 | 218 | drawCoordinates() { 219 | if (!this.chessboard.props.style.showCoordinates) { 220 | return 221 | } 222 | while (this.coordinatesGroup.firstChild) { 223 | this.coordinatesGroup.removeChild(this.coordinatesGroup.lastChild) 224 | } 225 | const inline = this.chessboard.props.style.borderType !== BORDER_TYPE.frame 226 | for (let file = 0; file < 8; file++) { 227 | let x = this.borderSize + (17 + this.chessboard.props.sprite.size * file) * this.scalingX 228 | let y = this.height - this.scalingY * 3.5 229 | let cssClass = "coordinate file" 230 | if (inline) { 231 | x = x + this.scalingX * 15.5 232 | cssClass += file % 2 ? " white" : " black" 233 | } 234 | const textElement = Svg.addElement(this.coordinatesGroup, "text", { 235 | class: cssClass, 236 | x: x, 237 | y: y, 238 | style: `font-size: ${this.scalingY * 10}px` 239 | }) 240 | if (this.chessboard.state.orientation === COLOR.white) { 241 | textElement.textContent = String.fromCharCode(97 + file) 242 | } else { 243 | textElement.textContent = String.fromCharCode(104 - file) 244 | } 245 | } 246 | for (let rank = 0; rank < 8; rank++) { 247 | let x = (this.borderSize / 3.7) 248 | let y = this.borderSize + 25 * this.scalingY + rank * this.squareHeight 249 | let cssClass = "coordinate rank" 250 | if (inline) { 251 | cssClass += rank % 2 ? " black" : " white" 252 | if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { 253 | x = x + this.scalingX * 10 254 | y = y - this.scalingY * 15 255 | } else { 256 | x = x + this.scalingX * 2 257 | y = y - this.scalingY * 15 258 | } 259 | } 260 | const textElement = Svg.addElement(this.coordinatesGroup, "text", { 261 | class: cssClass, 262 | x: x, 263 | y: y, 264 | style: `font-size: ${this.scalingY * 10}px` 265 | }) 266 | if (this.chessboard.state.orientation === COLOR.white) { 267 | textElement.textContent = "" + (8 - rank) 268 | } else { 269 | textElement.textContent = "" + (1 + rank) 270 | } 271 | } 272 | } 273 | 274 | // Pieces // 275 | 276 | redrawPieces(squares = this.chessboard.state.position.squares) { 277 | const childNodes = Array.from(this.piecesGroup.childNodes) 278 | for (let i = 0; i < 64; i++) { 279 | const pieceName = squares[i] 280 | if (pieceName) { 281 | this.drawPiece(Position.indexToSquare(i), pieceName) 282 | } 283 | } 284 | for (const childNode of childNodes) { 285 | this.piecesGroup.removeChild(childNode) 286 | } 287 | } 288 | 289 | drawPiece(square, pieceName) { 290 | const pieceGroup = Svg.addElement(this.piecesGroup, "g") 291 | pieceGroup.setAttribute("data-piece", pieceName) 292 | pieceGroup.setAttribute("data-square", square) 293 | const point = this.squareToPoint(square) 294 | const transform = (this.svg.createSVGTransform()) 295 | transform.setTranslate(point.x, point.y) 296 | pieceGroup.transform.baseVal.appendItem(transform) 297 | const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url 298 | const pieceUse = Svg.addElement(pieceGroup, "use", { 299 | href: `${spriteUrl}#${pieceName}`, 300 | class: "piece" 301 | }) 302 | // center on square 303 | const transformTranslate = (this.svg.createSVGTransform()) 304 | transformTranslate.setTranslate(this.pieceXTranslate, 0) 305 | pieceUse.transform.baseVal.appendItem(transformTranslate) 306 | // scale 307 | const transformScale = (this.svg.createSVGTransform()) 308 | transformScale.setScale(this.scalingY, this.scalingY) 309 | pieceUse.transform.baseVal.appendItem(transformScale) 310 | return pieceGroup 311 | } 312 | 313 | setPieceVisibility(square, visible = true) { 314 | const piece = this.getPieceElement(square) 315 | if(piece) { 316 | if (visible) { 317 | piece.setAttribute("visibility", "visible") 318 | } else { 319 | piece.setAttribute("visibility", "hidden") 320 | } 321 | } else { 322 | console.warn("no piece on", square) 323 | } 324 | } 325 | 326 | getPieceElement(square) { 327 | if (square.length < 2) { 328 | throw new Error("980e03") 329 | } 330 | const piece = this.piecesGroup.querySelector(`g[data-square='${square}']`) 331 | if (!piece) { 332 | console.warn("no piece on", square) 333 | } 334 | return piece 335 | } 336 | 337 | // Markers // 338 | 339 | drawMarkers() { 340 | while (this.markersGroup.firstChild) { 341 | this.markersGroup.removeChild(this.markersGroup.firstChild) 342 | } 343 | this.chessboard.state.markers.forEach((marker) => { 344 | this.drawMarker(marker) 345 | } 346 | ) 347 | } 348 | 349 | drawMarker(marker) { 350 | // console.log("drawMarker", marker) 351 | const markerGroup = Svg.addElement(this.markersGroup, "g") 352 | markerGroup.setAttribute("data-square", marker.square) 353 | const point = this.squareToPoint(marker.square) 354 | const transform = (this.svg.createSVGTransform()) 355 | transform.setTranslate(point.x, point.y) 356 | markerGroup.transform.baseVal.appendItem(transform) 357 | const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url 358 | const markerUse = Svg.addElement(markerGroup, "use", 359 | {href: `${spriteUrl}#${marker.type.slice}`, class: "marker " + marker.type.class}) 360 | const transformScale = (this.svg.createSVGTransform()) 361 | transformScale.setScale(this.scalingX, this.scalingY) 362 | markerUse.transform.baseVal.appendItem(transformScale) 363 | return markerGroup 364 | } 365 | 366 | // enable and disable move input // 367 | 368 | enableMoveInput(eventHandler, color = undefined) { 369 | if (color === COLOR.white) { 370 | this.chessboard.state.inputWhiteEnabled = true 371 | } else if (color === COLOR.black) { 372 | this.chessboard.state.inputBlackEnabled = true 373 | } else { 374 | this.chessboard.state.inputWhiteEnabled = true 375 | this.chessboard.state.inputBlackEnabled = true 376 | } 377 | this.chessboard.state.inputEnabled = true 378 | this.moveInputCallback = eventHandler 379 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInputToggled, {enabled: true, color: color}) 380 | this.visualizeInputState() 381 | } 382 | 383 | disableMoveInput() { 384 | this.chessboard.state.inputWhiteEnabled = false 385 | this.chessboard.state.inputBlackEnabled = false 386 | this.chessboard.state.inputEnabled = false 387 | this.moveInputCallback = undefined 388 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInputToggled, {enabled: false}) 389 | this.visualizeInputState() 390 | } 391 | 392 | // callbacks // 393 | 394 | moveInputStartedCallback(square) { 395 | const data = { 396 | chessboard: this.chessboard, 397 | type: INPUT_EVENT_TYPE.moveInputStarted, 398 | square: square 399 | } 400 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInput, data) // TODO use the return value of this EP 401 | if (this.moveInputCallback) { 402 | return this.moveInputCallback(data) 403 | } else { 404 | return true 405 | } 406 | } 407 | 408 | validateMoveInputCallback(squareFrom, squareTo) { 409 | const data = { 410 | chessboard: this.chessboard, 411 | type: INPUT_EVENT_TYPE.validateMoveInput, 412 | squareFrom: squareFrom, 413 | squareTo: squareTo 414 | } 415 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInput, data) // TODO use the return value of this EP 416 | if (this.moveInputCallback) { 417 | return this.moveInputCallback(data) 418 | } else { 419 | return true 420 | } 421 | } 422 | 423 | moveInputCanceledCallback(reason, squareFrom, squareTo) { 424 | const data = { 425 | chessboard: this.chessboard, 426 | type: INPUT_EVENT_TYPE.moveInputCanceled, 427 | reason: reason, 428 | squareFrom: squareFrom, 429 | squareTo: squareTo 430 | } 431 | this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInput, data) 432 | if (this.moveInputCallback) { 433 | this.moveInputCallback(data) 434 | } 435 | } 436 | 437 | // Helpers // 438 | 439 | visualizeInputState() { 440 | if (this.chessboard.state) { // fix https://github.com/shaack/cm-chessboard/issues/47 441 | if (this.chessboard.state.inputWhiteEnabled || this.chessboard.state.inputBlackEnabled || this.chessboard.state.squareSelectEnabled) { 442 | this.boardGroup.setAttribute("class", "board input-enabled") 443 | } else { 444 | this.boardGroup.setAttribute("class", "board") 445 | } 446 | } 447 | } 448 | 449 | indexToPoint(index) { 450 | let x, y 451 | if (this.chessboard.state.orientation === COLOR.white) { 452 | x = this.borderSize + (index % 8) * this.squareWidth 453 | y = this.borderSize + (7 - Math.floor(index / 8)) * this.squareHeight 454 | } else { 455 | x = this.borderSize + (7 - index % 8) * this.squareWidth 456 | y = this.borderSize + (Math.floor(index / 8)) * this.squareHeight 457 | } 458 | return {x: x, y: y} 459 | } 460 | 461 | squareToPoint(square) { 462 | const index = Position.squareToIndex(square) 463 | return this.indexToPoint(index) 464 | } 465 | 466 | } 467 | 468 | const SVG_NAMESPACE = "http://www.w3.org/2000/svg" 469 | 470 | export class Svg { 471 | 472 | /** 473 | * create the Svg in the HTML DOM 474 | * @param containerElement 475 | * @returns {Element} 476 | */ 477 | static createSvg(containerElement = undefined) { 478 | let svg = document.createElementNS(SVG_NAMESPACE, "svg") 479 | if (containerElement) { 480 | svg.setAttribute("width", "100%") 481 | svg.setAttribute("height", "100%") 482 | containerElement.appendChild(svg) 483 | } 484 | return svg 485 | } 486 | 487 | /** 488 | * Add an Element to a SVG DOM 489 | * @param parent 490 | * @param name 491 | * @param attributes 492 | * @param sibling 493 | * @returns {Element} 494 | */ 495 | static addElement(parent, name, attributes, sibling = undefined) { 496 | let element = document.createElementNS(SVG_NAMESPACE, name) 497 | if (name === "use") { 498 | attributes["xlink:href"] = attributes["href"] // fix for safari 499 | } 500 | for (let attribute in attributes) { 501 | if (attributes.hasOwnProperty(attribute)) { 502 | if (attribute.indexOf(":") !== -1) { 503 | const value = attribute.split(":") 504 | element.setAttributeNS("http://www.w3.org/1999/" + value[0], value[1], attributes[attribute]) 505 | } else { 506 | element.setAttribute(attribute, attributes[attribute]) 507 | } 508 | } 509 | } 510 | if (sibling !== undefined) { 511 | parent.appendChild(element) 512 | } else { 513 | parent.insertBefore(element, sibling) 514 | } 515 | return element 516 | } 517 | 518 | /** 519 | * Remove an Element from a SVG DOM 520 | * @param element 521 | */ 522 | static removeElement(element) { 523 | if(element.parentNode) { 524 | element.parentNode.removeChild(element) 525 | } else { 526 | console.warn(element, "without parentNode") 527 | } 528 | } 529 | 530 | } 531 | -------------------------------------------------------------------------------- /cm-chessboard/view/PositionAnimationsQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | import {FEN, Position} from "../model/Position.js" 7 | import {Svg} from "./ChessboardView.js" 8 | 9 | /* 10 | * Thanks to markosyan for the idea of the PromiseQueue 11 | * https://medium.com/@karenmarkosyan/how-to-manage-promises-into-dynamic-queue-with-vanilla-javascript-9d0d1f8d4df5 12 | */ 13 | 14 | export class PromiseQueue { 15 | 16 | constructor() { 17 | this.queue = [] 18 | this.workingOnPromise = false 19 | this.stop = false 20 | } 21 | 22 | async enqueue(promise) { 23 | return new Promise((resolve, reject) => { 24 | this.queue.push({ 25 | promise, resolve, reject, 26 | }) 27 | this.dequeue() 28 | }) 29 | } 30 | 31 | dequeue() { 32 | if (this.workingOnPromise) { 33 | return 34 | } 35 | if (this.stop) { 36 | this.queue = [] 37 | this.stop = false 38 | return 39 | } 40 | const entry = this.queue.shift() 41 | if (!entry) { 42 | return 43 | } 44 | try { 45 | this.workingOnPromise = true 46 | entry.promise().then((value) => { 47 | this.workingOnPromise = false 48 | entry.resolve(value) 49 | this.dequeue() 50 | }).catch(err => { 51 | this.workingOnPromise = false 52 | entry.reject(err) 53 | this.dequeue() 54 | }) 55 | } catch (err) { 56 | this.workingOnPromise = false 57 | entry.reject(err) 58 | this.dequeue() 59 | } 60 | return true 61 | } 62 | 63 | destroy() { 64 | this.stop = true 65 | } 66 | 67 | } 68 | 69 | 70 | const CHANGE_TYPE = { 71 | move: 0, 72 | appear: 1, 73 | disappear: 2 74 | } 75 | 76 | export class PositionsAnimation { 77 | 78 | constructor(view, fromPosition, toPosition, duration, callback) { 79 | this.view = view 80 | if (fromPosition && toPosition) { 81 | this.animatedElements = this.createAnimation(fromPosition.squares, toPosition.squares) 82 | this.duration = duration 83 | this.callback = callback 84 | this.frameHandle = requestAnimationFrame(this.animationStep.bind(this)) 85 | } else { 86 | console.error("fromPosition", fromPosition, "toPosition", toPosition) 87 | } 88 | } 89 | 90 | static seekChanges(fromSquares, toSquares) { 91 | const appearedList = [], disappearedList = [], changes = [] 92 | for (let i = 0; i < 64; i++) { 93 | const previousSquare = fromSquares[i] 94 | const newSquare = toSquares[i] 95 | if (newSquare !== previousSquare) { 96 | if (newSquare) { 97 | appearedList.push({piece: newSquare, index: i}) 98 | } 99 | if (previousSquare) { 100 | disappearedList.push({piece: previousSquare, index: i}) 101 | } 102 | } 103 | } 104 | appearedList.forEach((appeared) => { 105 | let shortestDistance = 8 106 | let foundMoved = undefined 107 | disappearedList.forEach((disappeared) => { 108 | if (appeared.piece === disappeared.piece) { 109 | const moveDistance = PositionsAnimation.squareDistance(appeared.index, disappeared.index) 110 | if (moveDistance < shortestDistance) { 111 | foundMoved = disappeared 112 | shortestDistance = moveDistance 113 | } 114 | } 115 | }) 116 | if (foundMoved) { 117 | disappearedList.splice(disappearedList.indexOf(foundMoved), 1) // remove from disappearedList, because it is moved now 118 | changes.push({ 119 | type: CHANGE_TYPE.move, 120 | piece: appeared.piece, 121 | atIndex: foundMoved.index, 122 | toIndex: appeared.index 123 | }) 124 | } else { 125 | changes.push({type: CHANGE_TYPE.appear, piece: appeared.piece, atIndex: appeared.index}) 126 | } 127 | }) 128 | disappearedList.forEach((disappeared) => { 129 | changes.push({type: CHANGE_TYPE.disappear, piece: disappeared.piece, atIndex: disappeared.index}) 130 | }) 131 | return changes 132 | } 133 | 134 | createAnimation(fromSquares, toSquares) { 135 | const changes = PositionsAnimation.seekChanges(fromSquares, toSquares) 136 | // console.log("changes", changes) 137 | const animatedElements = [] 138 | changes.forEach((change) => { 139 | const animatedItem = { 140 | type: change.type 141 | } 142 | switch (change.type) { 143 | case CHANGE_TYPE.move: 144 | animatedItem.element = this.view.getPieceElement(Position.indexToSquare(change.atIndex)) 145 | animatedItem.element.parentNode.appendChild(animatedItem.element) // move element to top layer 146 | animatedItem.atPoint = this.view.indexToPoint(change.atIndex) 147 | animatedItem.toPoint = this.view.indexToPoint(change.toIndex) 148 | break 149 | case CHANGE_TYPE.appear: 150 | animatedItem.element = this.view.drawPiece(Position.indexToSquare(change.atIndex), change.piece) 151 | animatedItem.element.style.opacity = 0 152 | break 153 | case CHANGE_TYPE.disappear: 154 | animatedItem.element = this.view.getPieceElement(Position.indexToSquare(change.atIndex)) 155 | break 156 | } 157 | animatedElements.push(animatedItem) 158 | }) 159 | return animatedElements 160 | } 161 | 162 | animationStep(time) { 163 | // console.log("animationStep", time) 164 | if (!this.startTime) { 165 | this.startTime = time 166 | } 167 | const timeDiff = time - this.startTime 168 | if (timeDiff <= this.duration) { 169 | this.frameHandle = requestAnimationFrame(this.animationStep.bind(this)) 170 | } else { 171 | cancelAnimationFrame(this.frameHandle) 172 | // console.log("ANIMATION FINISHED") 173 | this.animatedElements.forEach((animatedItem) => { 174 | if (animatedItem.type === CHANGE_TYPE.disappear) { 175 | Svg.removeElement(animatedItem.element) 176 | } 177 | }) 178 | this.callback() 179 | return 180 | } 181 | const t = Math.min(1, timeDiff / this.duration) 182 | let progress = t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t // easeInOut 183 | if (isNaN(progress)) { 184 | progress = 1 185 | } 186 | this.animatedElements.forEach((animatedItem) => { 187 | // console.log("animatedItem", animatedItem) 188 | if (animatedItem.element) { 189 | switch (animatedItem.type) { 190 | case CHANGE_TYPE.move: 191 | animatedItem.element.transform.baseVal.removeItem(0) 192 | const transform = (this.view.svg.createSVGTransform()) 193 | transform.setTranslate( 194 | animatedItem.atPoint.x + (animatedItem.toPoint.x - animatedItem.atPoint.x) * progress, 195 | animatedItem.atPoint.y + (animatedItem.toPoint.y - animatedItem.atPoint.y) * progress) 196 | animatedItem.element.transform.baseVal.appendItem(transform) 197 | break 198 | case CHANGE_TYPE.appear: 199 | animatedItem.element.style.opacity = Math.round(progress * 100) / 100 200 | break 201 | case CHANGE_TYPE.disappear: 202 | animatedItem.element.style.opacity = Math.round((1 - progress) * 100) / 100 203 | break 204 | } 205 | } else { 206 | console.warn("animatedItem has no element", animatedItem) 207 | } 208 | }) 209 | } 210 | 211 | static squareDistance(index1, index2) { 212 | const file1 = index1 % 8 213 | const rank1 = Math.floor(index1 / 8) 214 | const file2 = index2 % 8 215 | const rank2 = Math.floor(index2 / 8) 216 | return Math.max(Math.abs(rank2 - rank1), Math.abs(file2 - file1)) 217 | } 218 | 219 | } 220 | 221 | export class PositionAnimationsQueue extends PromiseQueue { 222 | 223 | constructor(chessboard) { 224 | super() 225 | this.chessboard = chessboard 226 | } 227 | 228 | async enqueuePositionChange(positionFrom, positionTo, animated) { 229 | if(positionFrom.getFen() === positionTo.getFen()) { 230 | return Promise.resolve() 231 | } else { 232 | return super.enqueue(() => new Promise((resolve) => { 233 | let duration = animated ? this.chessboard.props.animationDuration : 0 234 | if (this.queue.length > 0) { 235 | duration = duration / (1 + Math.pow(this.queue.length / 5, 2)) 236 | } 237 | // console.log("duration", duration, animated, "this.chessboard.props.animationDuration", this.chessboard.props.animationDuration) 238 | new PositionsAnimation(this.chessboard.view, 239 | positionFrom, positionTo, animated ? duration : 0, 240 | () => { 241 | if (this.chessboard.view) { // if destroyed, no view anymore 242 | this.chessboard.view.redrawPieces(positionTo.squares) 243 | } 244 | resolve() 245 | } 246 | ) 247 | })) 248 | } 249 | } 250 | 251 | async enqueueTurnBoard(position, color, animated) { 252 | return super.enqueue(() => new Promise((resolve) => { 253 | const emptyPosition = new Position(FEN.empty) 254 | let duration = animated ? this.chessboard.props.animationDuration : 0 255 | if(this.queue.length > 0) { 256 | duration = duration / (1 + Math.pow(this.queue.length / 5, 2)) 257 | } 258 | new PositionsAnimation(this.chessboard.view, 259 | position, emptyPosition, animated ? duration : 0, 260 | () => { 261 | this.chessboard.state.orientation = color 262 | this.chessboard.view.redrawBoard() 263 | this.chessboard.view.redrawPieces(emptyPosition.squares) 264 | new PositionsAnimation(this.chessboard.view, 265 | emptyPosition, position, animated ? duration : 0, 266 | () => { 267 | this.chessboard.view.redrawPieces(position.squares) 268 | resolve() 269 | } 270 | ) 271 | } 272 | ) 273 | })) 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /cm-chessboard/view/VisualMoveInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cm-chessboard 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | 7 | import {Svg} from "./ChessboardView.js" 8 | import {createTask} from "../model/ChessboardState.js" 9 | 10 | const STATE = { 11 | waitForInputStart: 0, 12 | pieceClickedThreshold: 1, 13 | clickTo: 2, 14 | secondClickThreshold: 3, 15 | dragTo: 4, 16 | clickDragTo: 5, 17 | moveDone: 6, 18 | reset: 7 19 | } 20 | 21 | export const MOVE_CANCELED_REASON = { 22 | secondClick: "secondClick", 23 | movedOutOfBoard: "movedOutOfBoard", 24 | draggedBack: "draggedBack", 25 | clickedAnotherPiece: "clickedAnotherPiece" 26 | } 27 | 28 | const DRAG_THRESHOLD = 4 29 | 30 | export class VisualMoveInput { 31 | 32 | constructor(view, moveInputStartedCallback, validateMoveInputCallback, moveInputCanceledCallback) { 33 | this.view = view 34 | this.chessboard = view.chessboard 35 | this.moveInputStartedCallback = (square) => { 36 | const result = moveInputStartedCallback(square) 37 | if(result) { 38 | this.chessboard.state.moveInputProcess = createTask() 39 | } 40 | return result 41 | } 42 | this.validateMoveInputCallback = (fromSquare, toSquare) => { 43 | const result = validateMoveInputCallback(fromSquare, toSquare) 44 | this.chessboard.state.moveInputProcess.resolve(result) 45 | return result 46 | } 47 | this.moveInputCanceledCallback = (reason, fromSquare, toSquare) => { 48 | moveInputCanceledCallback(reason, fromSquare, toSquare) 49 | this.chessboard.state.moveInputProcess.resolve() 50 | } 51 | this.setMoveInputState(STATE.waitForInputStart) 52 | } 53 | 54 | setMoveInputState(newState, params = undefined) { 55 | 56 | // console.log("setMoveInputState", Object.keys(STATE)[this.moveInputState], "=>", Object.keys(STATE)[newState]); 57 | 58 | const prevState = this.moveInputState 59 | this.moveInputState = newState 60 | 61 | switch (newState) { 62 | 63 | case STATE.waitForInputStart: 64 | break 65 | 66 | case STATE.pieceClickedThreshold: 67 | if (STATE.waitForInputStart !== prevState && STATE.clickTo !== prevState) { 68 | throw new Error("moveInputState") 69 | } 70 | if (this.pointerMoveListener) { 71 | removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener) 72 | this.pointerMoveListener = undefined 73 | } 74 | if (this.pointerUpListener) { 75 | removeEventListener(this.pointerUpListener.type, this.pointerUpListener) 76 | this.pointerUpListener = undefined 77 | } 78 | this.fromSquare = params.square 79 | this.toSquare = undefined 80 | this.movedPiece = params.piece 81 | this.updateStartEndMarkers() 82 | this.startPoint = params.point 83 | if (!this.pointerMoveListener && !this.pointerUpListener) { 84 | if (params.type === "mousedown") { 85 | 86 | this.pointerMoveListener = this.onPointerMove.bind(this) 87 | this.pointerMoveListener.type = "mousemove" 88 | addEventListener("mousemove", this.pointerMoveListener) 89 | 90 | this.pointerUpListener = this.onPointerUp.bind(this) 91 | this.pointerUpListener.type = "mouseup" 92 | addEventListener("mouseup", this.pointerUpListener) 93 | 94 | } else if (params.type === "touchstart") { 95 | 96 | this.pointerMoveListener = this.onPointerMove.bind(this) 97 | this.pointerMoveListener.type = "touchmove" 98 | addEventListener("touchmove", this.pointerMoveListener) 99 | 100 | this.pointerUpListener = this.onPointerUp.bind(this) 101 | this.pointerUpListener.type = "touchend" 102 | addEventListener("touchend", this.pointerUpListener) 103 | 104 | } else { 105 | throw Error("event type") 106 | } 107 | } else { 108 | throw Error("_pointerMoveListener or _pointerUpListener") 109 | } 110 | break 111 | 112 | case STATE.clickTo: 113 | if (this.draggablePiece) { 114 | Svg.removeElement(this.draggablePiece) 115 | this.draggablePiece = undefined 116 | } 117 | if (prevState === STATE.dragTo) { 118 | this.view.setPieceVisibility(params.square, true) 119 | } 120 | break 121 | 122 | case STATE.secondClickThreshold: 123 | if (STATE.clickTo !== prevState) { 124 | throw new Error("moveInputState") 125 | } 126 | this.startPoint = params.point 127 | break 128 | 129 | case STATE.dragTo: 130 | if (STATE.pieceClickedThreshold !== prevState) { 131 | throw new Error("moveInputState") 132 | } 133 | if (this.view.chessboard.state.inputEnabled) { 134 | this.view.setPieceVisibility(params.square, false) 135 | this.createDraggablePiece(params.piece) 136 | } 137 | break 138 | 139 | case STATE.clickDragTo: 140 | if (STATE.secondClickThreshold !== prevState) { 141 | throw new Error("moveInputState") 142 | } 143 | if (this.view.chessboard.state.inputEnabled) { 144 | this.view.setPieceVisibility(params.square, false) 145 | this.createDraggablePiece(params.piece) 146 | } 147 | break 148 | 149 | case STATE.moveDone: 150 | if ([STATE.dragTo, STATE.clickTo, STATE.clickDragTo].indexOf(prevState) === -1) { 151 | throw new Error("moveInputState") 152 | } 153 | this.toSquare = params.square 154 | if (this.toSquare && this.validateMoveInputCallback(this.fromSquare, this.toSquare)) { 155 | if (prevState === STATE.clickTo) { 156 | this.chessboard.movePiece(this.fromSquare, this.toSquare, true).then(() => { 157 | this.setMoveInputState(STATE.reset) 158 | }) 159 | } else { 160 | this.chessboard.movePiece(this.fromSquare, this.toSquare, false).then(() => { 161 | this.view.setPieceVisibility(this.toSquare, true) 162 | this.setMoveInputState(STATE.reset) 163 | }) 164 | } 165 | } else { 166 | this.view.setPieceVisibility(this.fromSquare, true) 167 | this.setMoveInputState(STATE.reset) 168 | } 169 | break 170 | 171 | case STATE.reset: 172 | if (this.fromSquare && !this.toSquare && this.movedPiece) { 173 | this.chessboard.state.position.setPiece(this.fromSquare, this.movedPiece) 174 | } 175 | this.fromSquare = undefined 176 | this.toSquare = undefined 177 | this.movedPiece = undefined 178 | this.updateStartEndMarkers() 179 | if (this.draggablePiece) { 180 | Svg.removeElement(this.draggablePiece) 181 | this.draggablePiece = undefined 182 | } 183 | if (this.pointerMoveListener) { 184 | removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener) 185 | this.pointerMoveListener = undefined 186 | } 187 | if (this.pointerUpListener) { 188 | removeEventListener(this.pointerUpListener.type, this.pointerUpListener) 189 | this.pointerUpListener = undefined 190 | } 191 | this.setMoveInputState(STATE.waitForInputStart) 192 | break 193 | 194 | default: 195 | throw Error(`moveInputState ${newState}`) 196 | } 197 | } 198 | 199 | createDraggablePiece(pieceName) { 200 | // TODO use the existing piece from the board and don't create an new one 201 | if (this.draggablePiece) { 202 | throw Error("draggablePiece exists") 203 | } 204 | this.draggablePiece = Svg.createSvg(document.body) 205 | this.draggablePiece.classList.add("cm-chessboard-draggable-piece") 206 | this.draggablePiece.setAttribute("width", this.view.squareWidth) 207 | this.draggablePiece.setAttribute("height", this.view.squareHeight) 208 | this.draggablePiece.setAttribute("style", "pointer-events: none") 209 | this.draggablePiece.name = pieceName 210 | const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url 211 | const piece = Svg.addElement(this.draggablePiece, "use", { 212 | href: `${spriteUrl}#${pieceName}` 213 | }) 214 | const scaling = this.view.squareHeight / this.chessboard.props.sprite.size 215 | const transformScale = (this.draggablePiece.createSVGTransform()) 216 | transformScale.setScale(scaling, scaling) 217 | piece.transform.baseVal.appendItem(transformScale) 218 | } 219 | 220 | moveDraggablePiece(x, y) { 221 | this.draggablePiece.setAttribute("style", 222 | `pointer-events: none; position: absolute; left: ${x - (this.view.squareHeight / 2)}px; top: ${y - (this.view.squareHeight / 2)}px`) 223 | } 224 | 225 | onPointerDown(e) { 226 | if (e.type === "mousedown" && e.button === 0 || e.type === "touchstart") { 227 | const square = e.target.getAttribute("data-square") 228 | if (square) { // pointer on square 229 | const pieceName = this.chessboard.getPiece(square) 230 | // console.log("onPointerDown", square, pieceName) 231 | let color 232 | if (pieceName) { 233 | color = pieceName ? pieceName.substring(0, 1) : undefined 234 | // allow scrolling, if not pointed on draggable piece 235 | if (color === "w" && this.chessboard.state.inputWhiteEnabled || 236 | color === "b" && this.chessboard.state.inputBlackEnabled) { 237 | e.preventDefault() 238 | } 239 | } 240 | if (this.moveInputState !== STATE.waitForInputStart || 241 | this.chessboard.state.inputWhiteEnabled && color === "w" || 242 | this.chessboard.state.inputBlackEnabled && color === "b") { 243 | let point 244 | if (e.type === "mousedown") { 245 | point = {x: e.clientX, y: e.clientY} 246 | } else if (e.type === "touchstart") { 247 | point = {x: e.touches[0].clientX, y: e.touches[0].clientY} 248 | } 249 | if (this.moveInputState === STATE.waitForInputStart && pieceName && this.moveInputStartedCallback(square)) { 250 | this.setMoveInputState(STATE.pieceClickedThreshold, { 251 | square: square, 252 | piece: pieceName, 253 | point: point, 254 | type: e.type 255 | }) 256 | } else if (this.moveInputState === STATE.clickTo) { 257 | if (square === this.fromSquare) { 258 | this.setMoveInputState(STATE.secondClickThreshold, { 259 | square: square, 260 | piece: pieceName, 261 | point: point, 262 | type: e.type 263 | }) 264 | } else { 265 | const pieceName = this.chessboard.getPiece(square) 266 | const pieceColor = pieceName ? pieceName.substring(0, 1) : undefined 267 | const startPieceName = this.chessboard.getPiece(this.fromSquare) 268 | const startPieceColor = startPieceName ? startPieceName.substring(0, 1) : undefined 269 | if (color && startPieceColor === pieceColor) { 270 | this.moveInputCanceledCallback(MOVE_CANCELED_REASON.clickedAnotherPiece, this.fromSquare, square) 271 | if (this.moveInputStartedCallback(square)) { 272 | this.setMoveInputState(STATE.pieceClickedThreshold, { 273 | square: square, 274 | piece: pieceName, 275 | point: point, 276 | type: e.type 277 | }) 278 | } else { 279 | this.setMoveInputState(STATE.reset) 280 | } 281 | } else { 282 | this.setMoveInputState(STATE.moveDone, {square: square}) 283 | } 284 | } 285 | } 286 | } 287 | } 288 | } 289 | } 290 | 291 | onPointerMove(e) { 292 | let pageX, pageY, clientX, clientY, target 293 | if (e.type === "mousemove") { 294 | clientX = e.clientX 295 | clientY = e.clientY 296 | pageX = e.pageX 297 | pageY = e.pageY 298 | target = e.target 299 | } else if (e.type === "touchmove") { 300 | clientX = e.touches[0].clientX 301 | clientY = e.touches[0].clientY 302 | pageX = e.touches[0].pageX 303 | pageY = e.touches[0].pageY 304 | target = document.elementFromPoint(clientX, clientY) 305 | } 306 | if (this.moveInputState === STATE.pieceClickedThreshold || this.moveInputState === STATE.secondClickThreshold) { 307 | if (Math.abs(this.startPoint.x - clientX) > DRAG_THRESHOLD || Math.abs(this.startPoint.y - clientY) > DRAG_THRESHOLD) { 308 | if (this.moveInputState === STATE.secondClickThreshold) { 309 | this.setMoveInputState(STATE.clickDragTo, {square: this.fromSquare, piece: this.movedPiece}) 310 | } else { 311 | this.setMoveInputState(STATE.dragTo, {square: this.fromSquare, piece: this.movedPiece}) 312 | } 313 | if (this.view.chessboard.state.inputEnabled) { 314 | this.moveDraggablePiece(pageX, pageY) 315 | } 316 | } 317 | } else if (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo || this.moveInputState === STATE.clickTo) { 318 | if (target && target.getAttribute && target.parentElement === this.view.boardGroup) { 319 | const square = target.getAttribute("data-square") 320 | if (square !== this.fromSquare && square !== this.toSquare) { 321 | this.toSquare = square 322 | this.updateStartEndMarkers() 323 | } else if (square === this.fromSquare && this.toSquare !== undefined) { 324 | this.toSquare = undefined 325 | this.updateStartEndMarkers() 326 | } 327 | } else { 328 | if (this.toSquare !== undefined) { 329 | this.toSquare = undefined 330 | this.updateStartEndMarkers() 331 | } 332 | } 333 | if (this.view.chessboard.state.inputEnabled && (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo)) { 334 | this.moveDraggablePiece(pageX, pageY) 335 | } 336 | } 337 | } 338 | 339 | onPointerUp(e) { 340 | let target 341 | if (e.type === "mouseup") { 342 | target = e.target 343 | } else if (e.type === "touchend") { 344 | target = document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY) 345 | } 346 | if (target && target.getAttribute) { 347 | const square = target.getAttribute("data-square") 348 | 349 | if (square) { 350 | if (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo) { 351 | if (this.fromSquare === square) { 352 | if (this.moveInputState === STATE.clickDragTo) { 353 | this.chessboard.state.position.setPiece(this.fromSquare, this.movedPiece) 354 | this.view.setPieceVisibility(this.fromSquare) 355 | this.moveInputCanceledCallback(MOVE_CANCELED_REASON.draggedBack, square, square) 356 | this.setMoveInputState(STATE.reset) 357 | } else { 358 | this.setMoveInputState(STATE.clickTo, {square: square}) 359 | } 360 | } else { 361 | this.setMoveInputState(STATE.moveDone, {square: square}) 362 | } 363 | } else if (this.moveInputState === STATE.pieceClickedThreshold) { 364 | this.setMoveInputState(STATE.clickTo, {square: square}) 365 | } else if (this.moveInputState === STATE.secondClickThreshold) { 366 | this.setMoveInputState(STATE.reset) 367 | this.moveInputCanceledCallback(MOVE_CANCELED_REASON.secondClick, square, square) 368 | } 369 | } else { 370 | this.view.redrawPieces() 371 | const moveStartSquare = this.fromSquare 372 | this.setMoveInputState(STATE.reset) 373 | this.moveInputCanceledCallback(MOVE_CANCELED_REASON.movedOutOfBoard, moveStartSquare, undefined) 374 | } 375 | } else { 376 | this.view.redrawPieces() 377 | this.setMoveInputState(STATE.reset) 378 | } 379 | } 380 | 381 | updateStartEndMarkers() { 382 | if (this.chessboard.props.style.moveFromMarker) { 383 | this.chessboard.state.removeMarkers(undefined, this.chessboard.props.style.moveFromMarker) 384 | } 385 | if (this.chessboard.props.style.moveToMarker) { 386 | this.chessboard.state.removeMarkers(undefined, this.chessboard.props.style.moveToMarker) 387 | } 388 | if (this.chessboard.props.style.moveFromMarker) { 389 | if (this.fromSquare) { 390 | this.chessboard.state.addMarker(this.fromSquare, this.chessboard.props.style.moveFromMarker) 391 | } 392 | } 393 | if (this.chessboard.props.style.moveToMarker) { 394 | if (this.toSquare) { 395 | this.chessboard.state.addMarker(this.toSquare, this.chessboard.props.style.moveToMarker) 396 | } 397 | } 398 | this.view.drawMarkers() 399 | } 400 | 401 | reset() { 402 | this.setMoveInputState(STATE.reset) 403 | } 404 | 405 | destroy() { 406 | this.reset() 407 | } 408 | 409 | } 410 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/cover.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/bR.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/img/chesspieces/wikipedia/wR.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Betafish.js | Chess AI 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 |
    22 |

    Betafish.js

    23 | 24 |

    Status:

    25 |
    26 | 27 | 30 | 33 |
    34 |
    35 | 36 |
    37 |
    38 | 45 | 52 | 59 | 66 |
    67 |
    68 | 71 | 77 |
    78 |
    79 |
    80 |
    81 |
    82 | 85 | 91 |
    92 |
    93 |
    94 |
    95 |
    96 | 97 |
    98 |
    99 | Made with ❤️ by Gavin.
    101 | View source
    103 |
    104 |
    105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strryke/betafish/657b22b3cb6ad4b3fd64bf423ecf323920dba2c3/js/.DS_Store -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | INPUT_EVENT_TYPE, 3 | COLOR, 4 | Chessboard, 5 | MARKER_TYPE, 6 | } from "../cm-chessboard/Chessboard.js"; 7 | 8 | var game_over = false; 9 | 10 | // get elements 11 | const fen = document.getElementById("fen"); 12 | const setFEN = document.getElementById("setFEN"); 13 | const copyFEN = document.getElementById("copyFEN"); 14 | const reset = document.getElementById("reset"); 15 | const takeback = document.getElementById("takeback"); 16 | const makeMove = document.getElementById("makeMove"); 17 | const flipBoard = document.getElementById("flipBoard"); 18 | const aiMove = document.getElementById("aiMove"); 19 | const uiState = document.getElementById("uiState"); 20 | const thinkingTime = document.getElementById("thinkingTime"); 21 | 22 | // initialise engine 23 | var game = new engine(); 24 | 25 | // initialise chessboard 26 | const board = new Chessboard(document.getElementById("board"), { 27 | position: game.getFEN(), 28 | sprite: { url: "../assets/images/chessboard-sprite-staunty.svg" }, 29 | animationDuration: 200, 30 | }); 31 | 32 | updateStatus(); 33 | 34 | board.enableMoveInput(inputHandler); 35 | 36 | // IO functions 37 | 38 | function inputHandler(event) { 39 | event.chessboard.removeMarkers(MARKER_TYPE.dot); 40 | if (event.type === INPUT_EVENT_TYPE.moveInputStarted) { 41 | const moves = game.getMovesAtSquare(event.square); 42 | for (const move of moves) { 43 | // draw dots on possible squares 44 | event.chessboard.addMarker(MARKER_TYPE.dot, move); 45 | } 46 | return moves.length > 0; 47 | } else if (event.type === INPUT_EVENT_TYPE.validateMoveInput) { 48 | const result = game.move(event.squareFrom, event.squareTo); 49 | if (result) { 50 | event.chessboard.disableMoveInput(); 51 | this.chessboard.state.moveInputProcess.then(() => { 52 | // wait for the move input process has finished 53 | this.chessboard.setPosition(game.getFEN(), true).then(() => { 54 | event.chessboard.enableMoveInput(inputHandler); 55 | setTimeout(() => { 56 | game.makeAIMove(); 57 | this.chessboard.setPosition(game.getFEN(), true); 58 | setTimeout(() => updateStatus(), 300); 59 | }, 500); 60 | }); 61 | }); 62 | } else { 63 | console.warn("invalid move"); 64 | } 65 | return result; 66 | } 67 | } 68 | 69 | // Check board status 70 | function updateStatus() { 71 | if (game_over) return; 72 | 73 | // update FEN 74 | fen.value = game.getFEN(); 75 | 76 | const status = game.gameStatus(); 77 | 78 | if (status.over) { 79 | game_over = true; 80 | board.disableMoveInput(); 81 | alert(status.over); 82 | uiState.innerHTML = `${status.over}!`; 83 | return false; 84 | } else { 85 | // update status 86 | status.check 87 | ? (uiState.innerHTML = "Check!") 88 | : (uiState.innerHTML = 89 | `${status.sideToMove[0].toUpperCase()}${status.sideToMove.slice(1)}` + 90 | " to move"); 91 | } 92 | } 93 | 94 | // event listeners 95 | reset.addEventListener("click", () => { 96 | if (window.confirm("Are you sure you want to reset the board?")) { 97 | game.reset(); 98 | board.enableMoveInput(inputHandler); 99 | board.setPosition(game.getFEN(), true); 100 | } 101 | }); 102 | 103 | takeBack.addEventListener("click", () => { 104 | alert("Just like life, in chess, there are no takebacks."); 105 | }); 106 | 107 | makeMove.addEventListener("click", () => { 108 | setTimeout(() => { 109 | game.makeAIMove(); 110 | board.setPosition(game.getFEN(), true); 111 | board.enableMoveInput(inputHandler); 112 | }, 500); 113 | updateStatus(); 114 | }); 115 | 116 | flipBoard.addEventListener("click", () => { 117 | board.setOrientation(board.getOrientation() === "w" ? "b" : "w", true); 118 | }); 119 | 120 | copyFEN.addEventListener("click", () => { 121 | const fen = game.getFEN(); 122 | navigator.clipboard 123 | .writeText(fen) 124 | .then(() => { 125 | alert("FEN copied to clipboard"); 126 | }) 127 | .catch(() => { 128 | alert("Oops, something went wrong."); 129 | }); 130 | }); 131 | 132 | setFEN.addEventListener("click", () => { 133 | const fenField = fen.value; 134 | game.setFEN(fenField); 135 | board.setPosition(fenField, true); 136 | updateStatus(); 137 | }); 138 | 139 | thinkingTime.addEventListener("change", () => { 140 | game.setThinkingTime(thinkingTime.value); 141 | }); 142 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Custom page CSS 2 | -------------------------------------------------- */ 3 | /* Not required for template or sticky footer method. */ 4 | 5 | .container { 6 | width: auto; 7 | max-width: 680px; 8 | padding: 0 15px; 9 | } 10 | --------------------------------------------------------------------------------