├── .gitignore ├── images ├── icon.png ├── favicon.png └── generic_photo.png ├── audio ├── button-1.mp3 ├── button-2.mp3 ├── button-3.mp3 └── button-4.mp3 ├── css ├── fonts │ ├── roboto-light.ttf │ └── roboto-regular.ttf ├── chessboard-0.3.0.min.css ├── chessboard-0.3.0.css └── style.css ├── 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 ├── deploy.sh ├── manifest.json ├── README.md ├── bower.json ├── ns └── chess.ttl ├── doap.ttl ├── vendor ├── chess │ ├── LICENSE.txt │ └── js │ │ ├── chessboard-0.3.0.min.js │ │ ├── chessboard-0.3.0.js │ │ ├── chessboard.js │ │ └── chess.js └── common.js ├── index.html └── app └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | components 3 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/images/icon.png -------------------------------------------------------------------------------- /audio/button-1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/audio/button-1.mp3 -------------------------------------------------------------------------------- /audio/button-2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/audio/button-2.mp3 -------------------------------------------------------------------------------- /audio/button-3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/audio/button-3.mp3 -------------------------------------------------------------------------------- /audio/button-4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/audio/button-4.mp3 -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/images/favicon.png -------------------------------------------------------------------------------- /images/generic_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/images/generic_photo.png -------------------------------------------------------------------------------- /css/fonts/roboto-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/css/fonts/roboto-light.ttf -------------------------------------------------------------------------------- /css/fonts/roboto-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/css/fonts/roboto-regular.ttf -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/bR.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvincarvalho/chess/master/img/chesspieces/wikipedia/wR.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git add -u 4 | git commit -m "$1" 5 | git push origin master 6 | 7 | git checkout gh-pages 8 | git merge master 9 | git push origin gh-pages 10 | 11 | git checkout master 12 | 13 | bower install 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chess", 3 | "icons": [ 4 | { 5 | "src": "images/icon.png", 6 | "sizes": "192x192", 7 | "type": "image/png", 8 | "density": "4.0" 9 | } 10 | ], 11 | "start_url": "index.html", 12 | "display": "standalone", 13 | "orientation": "portrait" 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chess 2 | 3 | chess game based on the Solid platform 4 | 5 | Running live here: https://melvincarvalho.github.io/chess/ 6 | 7 | Quick Start for contributors 8 | ---------------------------- 9 | 10 | ``` 11 | $ git clone git://github.com/melvincarvalho/chess 12 | $ cd contacts 13 | $ sudo npm -g install bower 14 | $ bower install 15 | ``` 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contacts", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/linkeddata/contacts", 5 | "authors": [ 6 | "Melvin Carvalho " 7 | ], 8 | "description": "Clipboard", 9 | "license": "MIT", 10 | "keywords": "webid, linked data, clipboard, Solid", 11 | "private": false, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "components", 17 | "test", 18 | "tests" 19 | ], 20 | "dependencies": { 21 | "lumx": "~0.3.39", 22 | "angular-ui": "~0.4.0", 23 | "angular-audio": "~1.7.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /css/chessboard-0.3.0.min.css: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v0.3.0 | (c) 2013 Chris Oakman | MIT License chessboardjs.com/license */ 2 | .clearfix-7da63{clear:both}.board-b72b1{border:2px solid #404040;-moz-box-sizing:content-box;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{-webkit-box-shadow:inset 0 0 3px 3px yellow;-moz-box-shadow:inset 0 0 3px 3px yellow;box-shadow:inset 0 0 3px 3px yellow}.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} -------------------------------------------------------------------------------- /ns/chess.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | 3 | <> 4 | "Melvin Carvalho" ; 5 | <> ; 6 | ; 7 | "2015-11-18"^^ ; 8 | a ; 9 | "Solid Chess Vocab"@en ; 10 | . 11 | 12 | <#fen> 13 | a rdf:Property ; 14 | "Forsyth–Edwards Notation."@en ; 15 | ; 16 | <> ; 17 | "fen"@en . 18 | -------------------------------------------------------------------------------- /doap.ttl: -------------------------------------------------------------------------------- 1 | <#this> 2 | a , ; 3 | "A Chess App for the Solid platform" ; 4 | ; 5 | ; 6 | "mit" ; 7 | ; 8 | "Chess" ; 9 | "A Chess App for the Solid platform" ; 10 | "Chess" ; 11 | "A Chess App for the Solid platform" ; 12 | ; 13 | ; 14 | "Chess" . 15 | -------------------------------------------------------------------------------- /vendor/chess/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Chris Oakman 2 | http://chessboardjs.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /css/chessboard-0.3.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * chessboard.js v0.3.0 3 | * 4 | * Copyright 2013 Chris Oakman 5 | * Released under the MIT license 6 | * https://github.com/oakmac/chessboardjs/blob/master/LICENSE 7 | * 8 | * Date: 10 Aug 2013 9 | */ 10 | 11 | /* clearfix */ 12 | .clearfix-7da63 { 13 | clear: both; 14 | } 15 | 16 | /* board */ 17 | .board-b72b1 { 18 | border: 2px solid #404040; 19 | -moz-box-sizing: content-box; 20 | box-sizing: content-box; 21 | } 22 | 23 | /* square */ 24 | .square-55d63 { 25 | float: left; 26 | position: relative; 27 | 28 | /* disable any native browser highlighting */ 29 | -webkit-touch-callout: none; 30 | -webkit-user-select: none; 31 | -khtml-user-select: none; 32 | -moz-user-select: none; 33 | -ms-user-select: none; 34 | user-select: none; 35 | } 36 | 37 | /* white square */ 38 | .white-1e1d7 { 39 | background-color: #f0d9b5; 40 | color: #b58863; 41 | } 42 | 43 | /* black square */ 44 | .black-3c85d { 45 | background-color: #b58863; 46 | color: #f0d9b5; 47 | } 48 | 49 | /* highlighted square */ 50 | .highlight1-32417, .highlight2-9c5d2 { 51 | -webkit-box-shadow: inset 0 0 3px 3px yellow; 52 | -moz-box-shadow: inset 0 0 3px 3px yellow; 53 | box-shadow: inset 0 0 3px 3px yellow; 54 | } 55 | 56 | /* notation */ 57 | .notation-322f9 { 58 | cursor: default; 59 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 60 | font-size: 14px; 61 | position: absolute; 62 | } 63 | .alpha-d2270 { 64 | bottom: 1px; 65 | right: 3px; 66 | } 67 | .numeric-fc462 { 68 | top: 2px; 69 | left: 2px; 70 | } -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Roboto Light'), local('Roboto-Light'), url(fonts/roboto-light.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Roboto'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Roboto'), local('Roboto-Regular'), url(fonts/roboto-regular.ttf) format('truetype'); 12 | } 13 | 14 | html, body { 15 | font-family: 'Roboto', sans-serif; 16 | font-weight: 300; 17 | position: relative; 18 | height: 100%; 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | } 24 | 25 | li { 26 | line-height: 40px 27 | } 28 | 29 | .toolbar { 30 | padding: 12px 5px 12px 5px; 31 | } 32 | 33 | .toolbar img { 34 | min-width: 30px; 35 | min-height: 30px; 36 | max-width: 40px; 37 | max-height: 40px; 38 | margin-left: auto; 39 | margin-right: auto; 40 | vertical-align: middle; 41 | border-radius: 3px; 42 | } 43 | 44 | .absolute-center { 45 | position: fixed; 46 | width: 90%; 47 | height: 80%; 48 | left: 5%; 49 | top: 30%; 50 | text-align: center; 51 | } 52 | 53 | .align-center { 54 | text-align: center!important; 55 | margin-left: auto; 56 | margin-right: auto; 57 | } 58 | 59 | .main { 60 | position: relative; 61 | height: auto; min-height: 100%; 62 | } 63 | 64 | 65 | .welcome { 66 | min-height: 100%; 67 | width: 100%; 68 | justify-content: center; 69 | align-items: center; 70 | } 71 | 72 | .welcome-title { 73 | position: relative; 74 | display: inline-block; 75 | width: 350px; 76 | height: 100px; 77 | padding-top: 20px; 78 | top: -20px; 79 | border: 2px solid #fff; 80 | border-radius: 5px; 81 | } 82 | .app-name { 83 | position: relative; 84 | top: 5px; 85 | } 86 | 87 | 88 | .grad-bg { 89 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#cedbe9+0,aac5de+17,6199c7+50,3a84c3+51,419ad6+59,4bb8f0+71,3a8bc2+84,26558b+100;Blue+Gloss */ 90 | background: rgb(206,219,233); /* Old browsers */ 91 | background: -moz-linear-gradient(-45deg, rgba(206,219,233,1) 0%, rgba(170,197,222,1) 17%, rgba(97,153,199,1) 50%, rgba(58,132,195,1) 51%, rgba(65,154,214,1) 59%, rgba(75,184,240,1) 71%, rgba(58,139,194,1) 84%, rgba(38,85,139,1) 100%); /* FF3.6+ */ 92 | background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,rgba(206,219,233,1)), color-stop(17%,rgba(170,197,222,1)), color-stop(50%,rgba(97,153,199,1)), color-stop(51%,rgba(58,132,195,1)), color-stop(59%,rgba(65,154,214,1)), color-stop(71%,rgba(75,184,240,1)), color-stop(84%,rgba(58,139,194,1)), color-stop(100%,rgba(38,85,139,1))); /* Chrome,Safari4+ */ 93 | background: -webkit-linear-gradient(-45deg, rgba(206,219,233,1) 0%,rgba(170,197,222,1) 17%,rgba(97,153,199,1) 50%,rgba(58,132,195,1) 51%,rgba(65,154,214,1) 59%,rgba(75,184,240,1) 71%,rgba(58,139,194,1) 84%,rgba(38,85,139,1) 100%); /* Chrome10+,Safari5.1+ */ 94 | background: -o-linear-gradient(-45deg, rgba(206,219,233,1) 0%,rgba(170,197,222,1) 17%,rgba(97,153,199,1) 50%,rgba(58,132,195,1) 51%,rgba(65,154,214,1) 59%,rgba(75,184,240,1) 71%,rgba(58,139,194,1) 84%,rgba(38,85,139,1) 100%); /* Opera 11.10+ */ 95 | background: -ms-linear-gradient(-45deg, rgba(206,219,233,1) 0%,rgba(170,197,222,1) 17%,rgba(97,153,199,1) 50%,rgba(58,132,195,1) 51%,rgba(65,154,214,1) 59%,rgba(75,184,240,1) 71%,rgba(58,139,194,1) 84%,rgba(38,85,139,1) 100%); /* IE10+ */ 96 | background: linear-gradient(135deg, rgba(206,219,233,1) 0%,rgba(170,197,222,1) 17%,rgba(97,153,199,1) 50%,rgba(58,132,195,1) 51%,rgba(65,154,214,1) 59%,rgba(75,184,240,1) 71%,rgba(58,139,194,1) 84%,rgba(38,85,139,1) 100%); /* W3C */ 97 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#cedbe9', endColorstr='#26558b',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 98 | 99 | } 100 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chess 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | Chess 32 |
33 |
Your personal chess system
34 |
35 | 38 | 41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 |
52 |
53 |
54 | 57 |
58 | Solid Chess 59 |
60 | 61 | 64 | 65 | 69 | 70 | 71 |
72 |
73 |
74 | 75 | 76 | 77 | 78 |
79 | 80 | 81 |
82 | 89 |
90 | 91 | 92 | 93 |
94 |
95 |
96 | 97 |
{{fen}}
98 | 99 |
100 |
101 | 102 | 103 | 104 |
105 |
106 | 109 |
110 |
111 |
112 | 113 | 114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 |
125 | 126 | About Chess 127 | 128 |
129 | 132 |
133 |
134 |
135 |
136 |

Chess is a personal chess game manager, that uses Linked Data to store games on your personal online datastore.

137 |
138 |

This application follows the Solid specifications to ensure that your data stays private.

139 |
140 |

Source Code

141 |
142 |
143 | 144 |
145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /vendor/common.js: -------------------------------------------------------------------------------- 1 | var isInteger = function(a) { 2 | return ((typeof a !== 'number') || (a % 1 !== 0)) ? false : true; 3 | }; 4 | 5 | stripSchema = function (url) { 6 | url = url.split('://'); 7 | var schema = (url[0].substring(0, 4) == 'http')?url[0]:''; 8 | var path = (url[1].length > 0)?url[1]:url[0]; 9 | return url[0]+'/'+url[1]; 10 | }; 11 | 12 | dirname = function(path) { 13 | return path.replace(/\\/g, '/').replace(/\/[^\/]*\/?$/, ''); 14 | }; 15 | 16 | basename = function(path) { 17 | if (path.substring(path.length - 1) == '/') { 18 | path = path.substring(0, path.length - 1); 19 | } 20 | 21 | var a = path.split('/'); 22 | return a[a.length - 1]; 23 | }; 24 | 25 | function getParam(name) { 26 | name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); 27 | var regexS = "[\\?&]"+name+"=([^&#]*)"; 28 | var regex = new RegExp(regexS); 29 | var results = regex.exec(window.location.href); 30 | if( results == null ) { 31 | return ""; 32 | } else { 33 | return decodeURIComponent(results[1]); 34 | } 35 | } 36 | 37 | function objectPropInArray(list, prop, val) { 38 | if (list.length > 0 ) { 39 | for (i in list) { 40 | if (list[i][prop] === val) { 41 | return true 42 | } 43 | } 44 | } 45 | return false; 46 | } 47 | 48 | // unquote string (utility) 49 | function unquote(value) { 50 | if (value.charAt(0) == '"' && value.charAt(value.length - 1) == '"') { 51 | return value.substring(1, value.length - 1); 52 | } 53 | return value; 54 | } 55 | 56 | // parseLinkHeader(xhr..getResponseHeader('Link'))['meta']['href']; 57 | function parseLinkHeader(header) { 58 | var linkexp = /<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g; 59 | var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g; 60 | 61 | var matches = header.match(linkexp); 62 | var rels = {}; 63 | for (var i = 0; i < matches.length; i++) { 64 | var split = matches[i].split('>'); 65 | var href = split[0].substring(1); 66 | var ps = split[1]; 67 | var link = {}; 68 | link.href = href; 69 | var s = ps.match(paramexp); 70 | for (var j = 0; j < s.length; j++) { 71 | var p = s[j]; 72 | var paramsplit = p.split('='); 73 | var name = paramsplit[0]; 74 | link[name] = unquote(paramsplit[1]); 75 | } 76 | 77 | if (link.rel !== undefined) { 78 | if (!rels[link.rel] || rels[link.rel].length === 0) { 79 | rels[link.rel] = []; 80 | } 81 | rels[link.rel].push(link.href); 82 | } 83 | } 84 | 85 | return rels; 86 | } 87 | 88 | function PopupCenter(url, title, w, h) { 89 | // Fixes dual-screen position Most browsers Firefox 90 | var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left; 91 | var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top; 92 | 93 | width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; 94 | height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; 95 | 96 | var left = ((width / 2) - (w / 2)) + dualScreenLeft; 97 | var top = ((height / 2) - (h / 2)) + dualScreenTop; 98 | var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); 99 | 100 | // Puts focus on the newWindow 101 | if (window.focus) { 102 | newWindow.focus(); 103 | } 104 | } 105 | 106 | // notifications 107 | (function($){ 108 | var config = window.NotifierjsConfig = { 109 | defaultTimeOut: 5000, 110 | position: "top", 111 | notificationStyles: { 112 | padding: "12px", 113 | margin: "0 0 6px 0", 114 | backgroundColor: "#fff", 115 | opacity: 1, 116 | color: "#000", 117 | boxShadow: "#999 0 0 12px", 118 | width: "100%", 119 | height: "50px" 120 | }, 121 | notificationStylesHover: { 122 | opacity: 1, 123 | boxShadow: "#000 0 0 12px" 124 | }, 125 | container: $("
") 126 | }; 127 | 128 | $(function() { 129 | config.container.css("position", "fixed"); 130 | config.container.css("z-index", 9999); 131 | config.container.css(config.position, "0"); 132 | config.container.css("width", "100%"); 133 | config.container.appendTo(document.body); 134 | }); 135 | 136 | function getNotificationElement() { 137 | return $("
").css(config.notificationStyles).bind('hover', function() { 138 | $(this).css(config.notificationStylesHover); 139 | }, function() { 140 | $(this).css(config.notificationStyles); 141 | }); 142 | } 143 | 144 | var Notifier = window.Notifier = {}; 145 | 146 | Notifier.notify = function(message, title, iconUrl, txtcolor, bgcolor, timeOut) { 147 | var notificationElement = getNotificationElement(); 148 | notificationElement.addClass('valign-wrapper center'); 149 | 150 | if (bgcolor) { 151 | notificationElement.css("background-color", bgcolor); 152 | } 153 | 154 | timeOut = timeOut || config.defaultTimeOut; 155 | 156 | if (iconUrl) { 157 | var iconElement = $(""); 158 | iconElement.attr('class', iconUrl); 159 | if (txtcolor) { 160 | iconElement.addClass(txtcolor); 161 | } 162 | iconElement.addClass('small'); 163 | iconElement.addClass('valign'); 164 | iconElement.css("display", "inline-block!important"); 165 | notificationElement.append(iconElement); 166 | } 167 | 168 | var textElement = $("
").css({ 169 | padding: '0 12px' 170 | }).addClass('valign'); 171 | 172 | if (title) { 173 | var titleElement = $("
"); 174 | titleElement.append(document.createTextNode(title)); 175 | titleElement.css("font-weight", "bold"); 176 | titleElement.addClass('valign'); 177 | textElement.append(titleElement); 178 | } 179 | 180 | if (message) { 181 | var messageElement = $("
"); 182 | messageElement.addClass("truncate"); 183 | if (txtcolor) { 184 | messageElement.addClass(txtcolor); 185 | } 186 | messageElement.css("display", "inline-block!important"); 187 | messageElement.append(document.createTextNode(message)); 188 | textElement.append(messageElement); 189 | } 190 | 191 | setTimeout(function() { 192 | notificationElement.animate({ opacity: 0 }, 400, function(){ 193 | notificationElement.remove(); 194 | }); 195 | }, timeOut); 196 | 197 | notificationElement.bind("click", function() { 198 | notificationElement.hide(); 199 | }); 200 | 201 | notificationElement.append(textElement); 202 | config.container.prepend(notificationElement); 203 | }; 204 | 205 | Notifier.info = function(message, title) { 206 | Notifier.notify(message, title, 'mdi-action-info-outline blue-text'); 207 | }; 208 | Notifier.warning = function(message, title) { 209 | Notifier.notify(message, title, 'mdi-alert-warning', 'white-text', '#fb8c00'); 210 | }; 211 | Notifier.error = function(message, title) { 212 | Notifier.notify(message, title, 'mdi-action-highlight-remove', 'white-text', '#e53935', 10000); 213 | }; 214 | Notifier.success = function(message, title) { 215 | Notifier.notify(message, title, 'mdi-action-done', 'white-text', '#56BA57'); 216 | }; 217 | 218 | }(jQuery)); -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | var board, 2 | game = new Chess(), 3 | statusEl = $('#status'), 4 | fenEl = $('#fen'), 5 | pgnEl = $('#pgn'); 6 | 7 | 8 | 9 | // for debug only 10 | var __kb; 11 | var __scope; 12 | 13 | 14 | /** 15 | * The main app 16 | */ 17 | var App = angular.module('Chess', [ 18 | 'ngAudio', 19 | 'lumx' 20 | ]); 21 | 22 | App.config(function($locationProvider) { 23 | $locationProvider 24 | .html5Mode({ enabled: true, requireBase: false }); 25 | }); 26 | 27 | App.controller('Main', function($scope, $http, $location, $timeout, ngAudio, LxNotificationService, LxProgressService, LxDialogService) { 28 | // Namespaces 29 | var CHAT = $rdf.Namespace("https://ns.rww.io/chat#"); 30 | var CURR = $rdf.Namespace("https://w3id.org/cc#"); 31 | var CHESS = $rdf.Namespace("https://melvincarvalho.github.io/chess/ns/chess.ttl#"); 32 | var DCT = $rdf.Namespace("http://purl.org/dc/terms/"); 33 | var FACE = $rdf.Namespace("https://graph.facebook.com/schema/~/"); 34 | var FOAF = $rdf.Namespace("http://xmlns.com/foaf/0.1/"); 35 | var LIKE = $rdf.Namespace("http://ontologi.es/like#"); 36 | var LDP = $rdf.Namespace("http://www.w3.org/ns/ldp#"); 37 | var MBLOG = $rdf.Namespace("http://www.w3.org/ns/mblog#"); 38 | var OWL = $rdf.Namespace("http://www.w3.org/2002/07/owl#"); 39 | var PIM = $rdf.Namespace("http://www.w3.org/ns/pim/space#"); 40 | var RDF = $rdf.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#"); 41 | var RDFS = $rdf.Namespace("http://www.w3.org/2000/01/rdf-schema#"); 42 | var SIOC = $rdf.Namespace("http://rdfs.org/sioc/ns#"); 43 | var SOLID = $rdf.Namespace("http://www.w3.org/ns/solid/app#"); 44 | var TMP = $rdf.Namespace("urn:tmp:"); 45 | 46 | var f,g; 47 | 48 | var defaultStorageURI = 'https://chess.databox.me/Public/.chess/test'; 49 | 50 | // INIT 51 | /** 52 | * Init app 53 | */ 54 | $scope.initApp = function() { 55 | $scope.init(); 56 | }; 57 | 58 | /** 59 | * Set Initial variables 60 | */ 61 | $scope.init = function() { 62 | 63 | $scope.initRDF(); 64 | $scope.initBoard(); 65 | $scope.initUI(); 66 | 67 | /* 68 | $scope.initRDF(); 69 | $scope.initQueryString(); 70 | $scope.initLocalStorage(); 71 | */ 72 | 73 | __kb = g; 74 | __scope = $scope; 75 | }; 76 | 77 | 78 | /** 79 | * Init UI 80 | */ 81 | $scope.initUI = function() { 82 | $scope.initialized = true; 83 | $scope.loggedIn = false; 84 | $scope.loginTLSButtonText = "Login"; 85 | $scope.audio = ngAudio.load('audio/button-3.mp3'); 86 | }; 87 | 88 | /** 89 | * Init board 90 | */ 91 | $scope.initBoard = function() { 92 | 93 | // do not pick up pieces if the game is over 94 | // only pick up pieces for the side to move 95 | var onDragStart = function(source, piece, position, orientation) { 96 | if (game.game_over() === true || 97 | (game.turn() === 'w' && piece.search(/^b/) !== -1) || 98 | (game.turn() === 'b' && piece.search(/^w/) !== -1)) { 99 | return false; 100 | } 101 | }; 102 | 103 | var onDrop = function(source, target) { 104 | // see if the move is legal 105 | var move = game.move({ 106 | from: source, 107 | to: target, 108 | promotion: 'q' // NOTE: always promote to a queen for example simplicity 109 | }); 110 | 111 | // illegal move 112 | if (move === null) return 'snapback'; 113 | 114 | updateStatus(); 115 | }; 116 | 117 | // update the board position after the piece snap 118 | // for castling, en passant, pawn promotion 119 | var onSnapEnd = function() { 120 | board.position(game.fen()); 121 | $scope.position = game.fen(); 122 | $scope.save(); 123 | //$scope.audio.play(); 124 | }; 125 | 126 | var updateStatus = function() { 127 | var status = ''; 128 | 129 | var moveColor = 'White'; 130 | if (game.turn() === 'b') { 131 | moveColor = 'Black'; 132 | } 133 | 134 | // checkmate? 135 | if (game.in_checkmate() === true) { 136 | status = 'Game over, ' + moveColor + ' is in checkmate.'; 137 | } 138 | 139 | // draw? 140 | else if (game.in_draw() === true) { 141 | status = 'Game over, drawn position'; 142 | } 143 | 144 | // game still on 145 | else { 146 | status = moveColor + ' to move'; 147 | 148 | // check? 149 | if (game.in_check() === true) { 150 | status += ', ' + moveColor + ' is in check'; 151 | } 152 | } 153 | 154 | 155 | $scope.status = status; 156 | $scope.fen = game.fen(); 157 | $scope.pgn = game.pgn(); 158 | }; 159 | 160 | var cfg = { 161 | draggable: true, 162 | position: 'start', 163 | onDragStart: onDragStart, 164 | onDrop: onDrop, 165 | onSnapEnd: onSnapEnd 166 | }; 167 | board = new ChessBoard('board', cfg); 168 | 169 | updateStatus(); 170 | }; 171 | 172 | 173 | /** 174 | * Get values from localStorage 175 | */ 176 | $scope.initLocalStorage = function() { 177 | if (localStorage.getItem('user')) { 178 | var user = JSON.parse(localStorage.getItem('user')); 179 | $scope.loginSuccess(user); 180 | } 181 | }; 182 | 183 | /** 184 | * init RDF knowledge base 185 | */ 186 | $scope.initRDF = function() { 187 | var PROXY = "https://rww.io/proxy.php?uri={uri}"; 188 | var AUTH_PROXY = "https://rww.io/auth-proxy?uri="; 189 | var TIMEOUT = 90000; 190 | $rdf.Fetcher.crossSiteProxyTemplate=PROXY; 191 | 192 | g = $rdf.graph(); 193 | f = $rdf.fetcher(g, TIMEOUT); 194 | }; 195 | 196 | /** 197 | * init from query string 198 | */ 199 | $scope.initQueryString = function() { 200 | $scope.storageURI = defaultStorageURI; 201 | if ($location.search().storageURI) { 202 | $scope.storageURI = $location.search().storageURI; 203 | } 204 | $scope.setStorageURI($scope.storageURI); 205 | }; 206 | 207 | /** 208 | * setStorageURI set the storage URI for words 209 | * @param {String} the storage URI for words 210 | */ 211 | $scope.setStorageURI = function(storageURI) { 212 | $scope.storageURI = storageURI; 213 | $location.search('storageURI', $scope.storageURI); 214 | }; 215 | 216 | 217 | // AUTH 218 | /** 219 | * TLS Login with WebID 220 | */ 221 | $scope.TLSlogin = function() { 222 | var AUTHENDPOINT = "https://databox.me/"; 223 | $scope.loginTLSButtonText = 'Logging in...'; 224 | $http({ 225 | method: 'HEAD', 226 | url: AUTHENDPOINT, 227 | withCredentials: true 228 | }).success(function(data, status, headers) { 229 | var header = 'User'; 230 | var scheme = 'http'; 231 | var user = headers(header); 232 | if (user && user.length > 0 && user.slice(0,scheme.length) === scheme) { 233 | $scope.loginSuccess(user); 234 | } else { 235 | $scope.notify('WebID-TLS authentication failed.', 'error'); 236 | } 237 | $scope.loginTLSButtonText = 'Login'; 238 | }).error(function(data, status, headers) { 239 | $scope.notify('Could not connect to auth server: HTTP '+status); 240 | $scope.loginTLSButtonText = 'Login'; 241 | }); 242 | }; 243 | 244 | /** 245 | * loginSuccess called after successful login 246 | * @param {String} user the logged in user 247 | */ 248 | $scope.loginSuccess = function(user) { 249 | $scope.notify('Login Successful!'); 250 | $scope.loggedIn = true; 251 | $scope.user = user; 252 | $scope.fetchAll(); 253 | localStorage.setItem('user', JSON.stringify(user)); 254 | }; 255 | 256 | /** 257 | * Logout 258 | */ 259 | $scope.logout = function() { 260 | $scope.init(); 261 | $scope.notify('Logout Successful!'); 262 | localStorage.removeItem('user'); 263 | }; 264 | 265 | // FETCH functions 266 | // 267 | // 268 | $scope.fetchAll = function() { 269 | $scope.fetchBoard(); 270 | }; 271 | 272 | /** 273 | * Fetch the board 274 | * @param {String} position The URI for the position 275 | */ 276 | $scope.fetchBoard = function (position) { 277 | var storageURI = defaultStorageURI; 278 | if ($location.search().storageURI) { 279 | storageURI = $location.search().storageURI; 280 | } 281 | $scope.storageURI = storageURI; 282 | connectToSocket($scope.storageURI); 283 | 284 | f.requestURI(storageURI, undefined, true, function(ok, body) { 285 | var p = g.statementsMatching(undefined, CHESS('fen')); 286 | if (p.length) { 287 | $scope.position = p[0].object.value; 288 | $scope.render(); 289 | } 290 | }); 291 | }; 292 | 293 | /** 294 | * Invalidate a cached URI 295 | * @param {String} uri The URI to invalidate 296 | */ 297 | $scope.invalidate = function(uri) { 298 | console.log('invalidate : ' + uri); 299 | f.unload(uri); 300 | f.refresh($rdf.sym(uri)); 301 | }; 302 | 303 | /** 304 | * fetchSeeAlso fetches the see also 305 | */ 306 | $scope.fetchSeeAlso = function() { 307 | var seeAlso = 'https://melvincarvalho.github.io/chess/data/seeAlso.ttl'; 308 | if ($location.search().seeAlso) { 309 | seeAlso = $location.search().seeAlso; 310 | } 311 | f.nowOrWhenFetched(seeAlso, undefined, function(ok, body) { 312 | console.log('seeAlso fetched from : ' + seeAlso); 313 | }); 314 | 315 | }; 316 | 317 | /** 318 | * Save the position 319 | */ 320 | $scope.save = function() { 321 | var position = $scope.position; 322 | if (!position) { 323 | LxNotificationService.error('position is empty'); 324 | return; 325 | } 326 | console.log(position); 327 | 328 | $http({ 329 | method: 'PUT', 330 | url: $scope.storageURI, 331 | withCredentials: true, 332 | headers: { 333 | "Content-Type": "text/turtle" 334 | }, 335 | data: '<#this> '+ CHESS('fen') +' """' + position + '""" .', 336 | }). 337 | success(function(data, status, headers) { 338 | LxNotificationService.success('Position saved'); 339 | $location.search('storageURI', $scope.storageURI); 340 | $scope.renderBoard(position); 341 | }). 342 | error(function(data, status, headers) { 343 | LxNotificationService.error('could not save position'); 344 | }); 345 | 346 | }; 347 | 348 | 349 | // HELPER 350 | /** 351 | * Notify 352 | * @param {String} message the message to display 353 | * @param {String} type the type of notification, error or success 354 | */ 355 | $scope.notify = function(message, type) { 356 | console.log(message); 357 | if (type === 'error') { 358 | LxNotificationService.error(message); 359 | } else { 360 | LxNotificationService.success(message); 361 | } 362 | }; 363 | 364 | 365 | 366 | // RENDER 367 | /** 368 | * render screen 369 | */ 370 | $scope.render = function() { 371 | $scope.renderBoard(); 372 | }; 373 | 374 | /** 375 | * Render the board 376 | */ 377 | $scope.renderBoard = function () { 378 | if (!$scope.position) return; 379 | 380 | board.position($scope.position, false); 381 | game.load($scope.position); 382 | $scope.fen = game.fen(); 383 | }; 384 | 385 | /** 386 | * Refresh the board 387 | */ 388 | $scope.refresh = function() { 389 | $scope.fetchBoard(); 390 | $scope.render(); 391 | }; 392 | 393 | /** 394 | * openDialog opens a dialog box 395 | * @param {String} elem The element to display 396 | */ 397 | $scope.openDialog = function(elem) { 398 | LxDialogService.open(elem); 399 | $(document).keyup(function(e) { 400 | if (e.keyCode===27) { 401 | LxDialogService.close(elem); 402 | } 403 | }); 404 | }; 405 | 406 | 407 | // SOCKETS 408 | // 409 | // 410 | /** 411 | * Get wss from URI 412 | * @param {String} uri The URI to use 413 | */ 414 | function getWss(uri) { 415 | return 'wss://' + uri.split('/')[2]; 416 | } 417 | 418 | /** 419 | * Connect to a web socket 420 | * @param {String} sub Where to subscribe to 421 | * @param {boolean} quiet dont ping server 422 | */ 423 | function connectToSocket(sub, quiet) { 424 | // Some servers time out after 5 minutes inactive 425 | var INTERVAL = 240 * 1000; 426 | var RECONNECT = 60 * 1000; 427 | 428 | if ($scope.socket) return; 429 | 430 | var socket; 431 | 432 | var wss = getWss(sub); 433 | console.log('connecting to : ' + wss); 434 | 435 | socket = new WebSocket(wss); 436 | 437 | socket.onopen = function(){ 438 | console.log(sub); 439 | $scope.socket = socket; 440 | socket.send('sub ' + sub, socket); 441 | 442 | if (!quiet) { 443 | setInterval(function() { socket.send('ping'); }, INTERVAL); 444 | } 445 | 446 | }; 447 | 448 | socket.onerror = function(){ 449 | console.log('socket error'); 450 | setTimeout(connectToSocket, RECONNECT); 451 | }; 452 | 453 | socket.onclose = function(){ 454 | console.log('socket closed'); 455 | setTimeout(connectToSocket, RECONNECT); 456 | }; 457 | 458 | socket.onmessage = function(msg) { 459 | var a = msg.data.split(' '); 460 | if (a[0] !== 'pub') return; 461 | processSocket(a[1]); 462 | }; 463 | 464 | } 465 | 466 | /** 467 | * Process message from socket 468 | * @param {String} uri uri that has changed 469 | */ 470 | function processSocket(uri) { 471 | console.log(uri); 472 | 473 | $scope.invalidate(uri); 474 | $scope.fetchBoard(); 475 | $scope.audio.play(); 476 | } 477 | 478 | 479 | 480 | $scope.initApp(); 481 | 482 | }); 483 | 484 | /** 485 | * Escape URIs filter 486 | */ 487 | App.filter('escape', function() { 488 | return window.encodeURIComponent; 489 | }); 490 | -------------------------------------------------------------------------------- /vendor/chess/js/chessboard-0.3.0.min.js: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v0.3.0 | (c) 2013 Chris Oakman | MIT License chessboardjs.com/license */ 2 | (function(){function l(f){return"string"!==typeof f?!1:-1!==f.search(/^[a-h][1-8]$/)}function Q(f){if("string"!==typeof f)return!1;f=f.replace(/ .+$/,"");f=f.split("/");if(8!==f.length)return!1;for(var b=0;8>b;b++)if(""===f[b]||8m;m++){for(var l=f[m].split(""),r=0,w=0;wm;m++){for(var l=0;8>l;l++){var r=B[l]+n;!0===f.hasOwnProperty(r)?(r=f[r].split(""),r="w"===r[0]?r[1].toUpperCase(): 4 | r[1].toLowerCase(),b+=r):b+="1"}7!==m&&(b+="/");n--}b=b.replace(/11111111/g,"8");b=b.replace(/1111111/g,"7");b=b.replace(/111111/g,"6");b=b.replace(/11111/g,"5");b=b.replace(/1111/g,"4");b=b.replace(/111/g,"3");return b=b.replace(/11/g,"2")}var B="abcdefgh".split("");window.ChessBoard=window.ChessBoard||function(f,b){function n(){return"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx".replace(/x/g,function(a){return(16*Math.random()|0).toString(16)})}function m(a){return JSON.parse(JSON.stringify(a))}function X(a){a= 5 | a.split(".");return{major:parseInt(a[0],10),minor:parseInt(a[1],10),patch:parseInt(a[2],10)}}function r(a,e,c){if(!0===b.hasOwnProperty("showErrors")&&!1!==b.showErrors){var d="ChessBoard Error "+a+": "+e;"console"===b.showErrors&&"object"===typeof console&&"function"===typeof console.log?(console.log(d),2<=arguments.length&&console.log(c)):"alert"===b.showErrors?(c&&(d+="\n\n"+JSON.stringify(c)),window.alert(d)):"function"===typeof b.showErrors&&b.showErrors(a,e,c)}}function w(a){return"fast"=== 6 | a||"slow"===a?!0:parseInt(a,10)+""!==a+""?!1:0<=a}function I(){for(var a=0;a=b;b++){var c=B[a]+b;s[c]=c+"-"+n()}b="KQRBNP".split("");for(a=0;a';!0===b.sparePieces&&(a+='
');a+='
';!0===b.sparePieces&&(a+='
'); 7 | return a+"
"}function A(a){"black"!==a&&(a="white");var e="",c=m(B),d=8;"black"===a&&(c.reverse(),d=1);for(var C="white",f=0;8>f;f++){for(var e=e+('
'),k=0;8>k;k++){var g=c[k]+d,e=e+('
');if(!0===b.showNotation){if("white"===a&&1===d||"black"===a&&8===d)e+='
'+c[k]+"
";0===k&&(e+='
'+d+"
")}e+="
";C="white"===C?"black":"white"}e+='
';C="white"===C?"black":"white";"white"===a?d--:d++}return e}function Y(a){if("function"===typeof b.pieceTheme)return b.pieceTheme(a);if("string"===typeof b.pieceTheme)return b.pieceTheme.replace(/{piece}/g,a);r(8272,"Unable to build image source for cfg.pieceTheme.");return""}function D(a,b,c){var d=''}function N(a){var b="wK wQ wR wB wN wP".split(" ");"black"===a&&(b="bK bQ bR bB bN bP".split(" "));a="";for(var c=0;c=d?c:d}function la(a){for(var b=[],c=0;8>c;c++)for(var d=0;8>d;d++){var g=B[c]+(d+1);a!==g&&b.push({square:g,distance:ka(a,g)})}b.sort(function(a,b){return a.distance-b.distance});a=[];for(c=0;c=d.left&&a=d.top&&b=a)p=0;else{for(a-=1;0!==a%8&&0=1E8*b.major+1E4*b.minor+b.patch;return a?!0:(window.alert("ChessBoard Error 1005: Unable to find a valid version of jQuery. Please include jQuery 1.7.0 or higher on the page.\n\nExiting..."), 29 | !1)}()){if("string"===typeof b||!0===F(b))b={position:b};"black"!==b.orientation&&(b.orientation="white");u=b.orientation;!1!==b.showNotation&&(b.showNotation=!0);!0!==b.draggable&&(b.draggable=!1);"trash"!==b.dropOffBoard&&(b.dropOffBoard="snapback");!0!==b.sparePieces&&(b.sparePieces=!1);!0===b.sparePieces&&(b.draggable=!0);if(!0!==b.hasOwnProperty("pieceTheme")||"string"!==typeof b.pieceTheme&&"function"!==typeof b.pieceTheme)b.pieceTheme="img/chesspieces/wikipedia/{piece}.png";if(!0!==b.hasOwnProperty("appearSpeed")|| 30 | !0!==w(b.appearSpeed))b.appearSpeed=200;if(!0!==b.hasOwnProperty("moveSpeed")||!0!==w(b.moveSpeed))b.moveSpeed=200;if(!0!==b.hasOwnProperty("snapbackSpeed")||!0!==w(b.snapbackSpeed))b.snapbackSpeed=50;if(!0!==b.hasOwnProperty("snapSpeed")||!0!==w(b.snapSpeed))b.snapSpeed=25;if(!0!==b.hasOwnProperty("trashSpeed")||!0!==w(b.trashSpeed))b.trashSpeed=100;!0===b.hasOwnProperty("position")&&("start"===b.position?g=m(fa):!0===Q(b.position)?g=K(b.position):!0===F(b.position)?g=m(b.position):r(7263,"Invalid value passed to config.position.", 31 | b.position));W=!0}W&&(I(),ya(),xa());return q};window.ChessBoard.fenToObj=K;window.ChessBoard.objToFen=L})(); -------------------------------------------------------------------------------- /vendor/chess/js/chessboard-0.3.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chessboard.js v0.3.0 3 | * 4 | * Copyright 2013 Chris Oakman 5 | * Released under the MIT license 6 | * http://chessboardjs.com/license 7 | * 8 | * Date: 10 Aug 2013 9 | */ 10 | 11 | // start anonymous scope 12 | ;(function() { 13 | 'use strict'; 14 | 15 | //------------------------------------------------------------------------------ 16 | // Chess Util Functions 17 | //------------------------------------------------------------------------------ 18 | var COLUMNS = 'abcdefgh'.split(''); 19 | 20 | function validMove(move) { 21 | // move should be a string 22 | if (typeof move !== 'string') return false; 23 | 24 | // move should be in the form of "e2-e4", "f6-d5" 25 | var tmp = move.split('-'); 26 | if (tmp.length !== 2) return false; 27 | 28 | return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true); 29 | } 30 | 31 | function validSquare(square) { 32 | if (typeof square !== 'string') return false; 33 | return (square.search(/^[a-h][1-8]$/) !== -1); 34 | } 35 | 36 | function validPieceCode(code) { 37 | if (typeof code !== 'string') return false; 38 | return (code.search(/^[bw][KQRNBP]$/) !== -1); 39 | } 40 | 41 | // TODO: this whole function could probably be replaced with a single regex 42 | function validFen(fen) { 43 | if (typeof fen !== 'string') return false; 44 | 45 | // cut off any move, castling, etc info from the end 46 | // we're only interested in position information 47 | fen = fen.replace(/ .+$/, ''); 48 | 49 | // FEN should be 8 sections separated by slashes 50 | var chunks = fen.split('/'); 51 | if (chunks.length !== 8) return false; 52 | 53 | // check the piece sections 54 | for (var i = 0; i < 8; i++) { 55 | if (chunks[i] === '' || 56 | chunks[i].length > 8 || 57 | chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | function validPositionObject(pos) { 66 | if (typeof pos !== 'object') return false; 67 | 68 | for (var i in pos) { 69 | if (pos.hasOwnProperty(i) !== true) continue; 70 | 71 | if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) { 72 | return false; 73 | } 74 | } 75 | 76 | return true; 77 | } 78 | 79 | // convert FEN piece code to bP, wK, etc 80 | function fenToPieceCode(piece) { 81 | // black piece 82 | if (piece.toLowerCase() === piece) { 83 | return 'b' + piece.toUpperCase(); 84 | } 85 | 86 | // white piece 87 | return 'w' + piece.toUpperCase(); 88 | } 89 | 90 | // convert bP, wK, etc code to FEN structure 91 | function pieceCodeToFen(piece) { 92 | var tmp = piece.split(''); 93 | 94 | // white piece 95 | if (tmp[0] === 'w') { 96 | return tmp[1].toUpperCase(); 97 | } 98 | 99 | // black piece 100 | return tmp[1].toLowerCase(); 101 | } 102 | 103 | // convert FEN string to position object 104 | // returns false if the FEN string is invalid 105 | function fenToObj(fen) { 106 | if (validFen(fen) !== true) { 107 | return false; 108 | } 109 | 110 | // cut off any move, castling, etc info from the end 111 | // we're only interested in position information 112 | fen = fen.replace(/ .+$/, ''); 113 | 114 | var rows = fen.split('/'); 115 | var position = {}; 116 | 117 | var currentRow = 8; 118 | for (var i = 0; i < 8; i++) { 119 | var row = rows[i].split(''); 120 | var colIndex = 0; 121 | 122 | // loop through each character in the FEN section 123 | for (var j = 0; j < row.length; j++) { 124 | // number / empty squares 125 | if (row[j].search(/[1-8]/) !== -1) { 126 | var emptySquares = parseInt(row[j], 10); 127 | colIndex += emptySquares; 128 | } 129 | // piece 130 | else { 131 | var square = COLUMNS[colIndex] + currentRow; 132 | position[square] = fenToPieceCode(row[j]); 133 | colIndex++; 134 | } 135 | } 136 | 137 | currentRow--; 138 | } 139 | 140 | return position; 141 | } 142 | 143 | // position object to FEN string 144 | // returns false if the obj is not a valid position object 145 | function objToFen(obj) { 146 | if (validPositionObject(obj) !== true) { 147 | return false; 148 | } 149 | 150 | var fen = ''; 151 | 152 | var currentRow = 8; 153 | for (var i = 0; i < 8; i++) { 154 | for (var j = 0; j < 8; j++) { 155 | var square = COLUMNS[j] + currentRow; 156 | 157 | // piece exists 158 | if (obj.hasOwnProperty(square) === true) { 159 | fen += pieceCodeToFen(obj[square]); 160 | } 161 | 162 | // empty space 163 | else { 164 | fen += '1'; 165 | } 166 | } 167 | 168 | if (i !== 7) { 169 | fen += '/'; 170 | } 171 | 172 | currentRow--; 173 | } 174 | 175 | // squeeze the numbers together 176 | // haha, I love this solution... 177 | fen = fen.replace(/11111111/g, '8'); 178 | fen = fen.replace(/1111111/g, '7'); 179 | fen = fen.replace(/111111/g, '6'); 180 | fen = fen.replace(/11111/g, '5'); 181 | fen = fen.replace(/1111/g, '4'); 182 | fen = fen.replace(/111/g, '3'); 183 | fen = fen.replace(/11/g, '2'); 184 | 185 | return fen; 186 | } 187 | 188 | window['ChessBoard'] = window['ChessBoard'] || function(containerElOrId, cfg) { 189 | 'use strict'; 190 | 191 | cfg = cfg || {}; 192 | 193 | //------------------------------------------------------------------------------ 194 | // Constants 195 | //------------------------------------------------------------------------------ 196 | 197 | var MINIMUM_JQUERY_VERSION = '1.7.0', 198 | START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 199 | START_POSITION = fenToObj(START_FEN); 200 | 201 | // use unique class names to prevent clashing with anything else on the page 202 | // and simplify selectors 203 | var CSS = { 204 | alpha: 'alpha-d2270', 205 | black: 'black-3c85d', 206 | board: 'board-b72b1', 207 | chessboard: 'chessboard-63f37', 208 | clearfix: 'clearfix-7da63', 209 | highlight1: 'highlight1-32417', 210 | highlight2: 'highlight2-9c5d2', 211 | notation: 'notation-322f9', 212 | numeric: 'numeric-fc462', 213 | piece: 'piece-417db', 214 | row: 'row-5277c', 215 | sparePieces: 'spare-pieces-7492f', 216 | sparePiecesBottom: 'spare-pieces-bottom-ae20f', 217 | sparePiecesTop: 'spare-pieces-top-4028b', 218 | square: 'square-55d63', 219 | white: 'white-1e1d7' 220 | }; 221 | 222 | //------------------------------------------------------------------------------ 223 | // Module Scope Variables 224 | //------------------------------------------------------------------------------ 225 | 226 | // DOM elements 227 | var containerEl, 228 | boardEl, 229 | draggedPieceEl, 230 | sparePiecesTopEl, 231 | sparePiecesBottomEl; 232 | 233 | // constructor return object 234 | var widget = {}; 235 | 236 | //------------------------------------------------------------------------------ 237 | // Stateful 238 | //------------------------------------------------------------------------------ 239 | 240 | var ANIMATION_HAPPENING = false, 241 | BOARD_BORDER_SIZE = 2, 242 | CURRENT_ORIENTATION = 'white', 243 | CURRENT_POSITION = {}, 244 | SQUARE_SIZE, 245 | DRAGGED_PIECE, 246 | DRAGGED_PIECE_LOCATION, 247 | DRAGGED_PIECE_SOURCE, 248 | DRAGGING_A_PIECE = false, 249 | SPARE_PIECE_ELS_IDS = {}, 250 | SQUARE_ELS_IDS = {}, 251 | SQUARE_ELS_OFFSETS; 252 | 253 | //------------------------------------------------------------------------------ 254 | // JS Util Functions 255 | //------------------------------------------------------------------------------ 256 | 257 | // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript 258 | function createId() { 259 | return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) { 260 | var r = Math.random() * 16 | 0; 261 | return r.toString(16); 262 | }); 263 | } 264 | 265 | function deepCopy(thing) { 266 | return JSON.parse(JSON.stringify(thing)); 267 | } 268 | 269 | function parseSemVer(version) { 270 | var tmp = version.split('.'); 271 | return { 272 | major: parseInt(tmp[0], 10), 273 | minor: parseInt(tmp[1], 10), 274 | patch: parseInt(tmp[2], 10) 275 | }; 276 | } 277 | 278 | // returns true if version is >= minimum 279 | function compareSemVer(version, minimum) { 280 | version = parseSemVer(version); 281 | minimum = parseSemVer(minimum); 282 | 283 | var versionNum = (version.major * 10000 * 10000) + 284 | (version.minor * 10000) + version.patch; 285 | var minimumNum = (minimum.major * 10000 * 10000) + 286 | (minimum.minor * 10000) + minimum.patch; 287 | 288 | return (versionNum >= minimumNum); 289 | } 290 | 291 | //------------------------------------------------------------------------------ 292 | // Validation / Errors 293 | //------------------------------------------------------------------------------ 294 | 295 | function error(code, msg, obj) { 296 | // do nothing if showErrors is not set 297 | if (cfg.hasOwnProperty('showErrors') !== true || 298 | cfg.showErrors === false) { 299 | return; 300 | } 301 | 302 | var errorText = 'ChessBoard Error ' + code + ': ' + msg; 303 | 304 | // print to console 305 | if (cfg.showErrors === 'console' && 306 | typeof console === 'object' && 307 | typeof console.log === 'function') { 308 | console.log(errorText); 309 | if (arguments.length >= 2) { 310 | console.log(obj); 311 | } 312 | return; 313 | } 314 | 315 | // alert errors 316 | if (cfg.showErrors === 'alert') { 317 | if (obj) { 318 | errorText += '\n\n' + JSON.stringify(obj); 319 | } 320 | window.alert(errorText); 321 | return; 322 | } 323 | 324 | // custom function 325 | if (typeof cfg.showErrors === 'function') { 326 | cfg.showErrors(code, msg, obj); 327 | } 328 | } 329 | 330 | // check dependencies 331 | function checkDeps() { 332 | // if containerId is a string, it must be the ID of a DOM node 333 | if (typeof containerElOrId === 'string') { 334 | // cannot be empty 335 | if (containerElOrId === '') { 336 | window.alert('ChessBoard Error 1001: ' + 337 | 'The first argument to ChessBoard() cannot be an empty string.' + 338 | '\n\nExiting...'); 339 | return false; 340 | } 341 | 342 | // make sure the container element exists in the DOM 343 | var el = document.getElementById(containerElOrId); 344 | if (! el) { 345 | window.alert('ChessBoard Error 1002: Element with id "' + 346 | containerElOrId + '" does not exist in the DOM.' + 347 | '\n\nExiting...'); 348 | return false; 349 | } 350 | 351 | // set the containerEl 352 | containerEl = $(el); 353 | } 354 | 355 | // else it must be something that becomes a jQuery collection 356 | // with size 1 357 | // ie: a single DOM node or jQuery object 358 | else { 359 | containerEl = $(containerElOrId); 360 | 361 | if (containerEl.length !== 1) { 362 | window.alert('ChessBoard Error 1003: The first argument to ' + 363 | 'ChessBoard() must be an ID or a single DOM node.' + 364 | '\n\nExiting...'); 365 | return false; 366 | } 367 | } 368 | 369 | // JSON must exist 370 | if (! window.JSON || 371 | typeof JSON.stringify !== 'function' || 372 | typeof JSON.parse !== 'function') { 373 | window.alert('ChessBoard Error 1004: JSON does not exist. ' + 374 | 'Please include a JSON polyfill.\n\nExiting...'); 375 | return false; 376 | } 377 | 378 | // check for a compatible version of jQuery 379 | if (! (typeof window.$ && $.fn && $.fn.jquery && 380 | compareSemVer($.fn.jquery, MINIMUM_JQUERY_VERSION) === true)) { 381 | window.alert('ChessBoard Error 1005: Unable to find a valid version ' + 382 | 'of jQuery. Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or ' + 383 | 'higher on the page.\n\nExiting...'); 384 | return false; 385 | } 386 | 387 | return true; 388 | } 389 | 390 | function validAnimationSpeed(speed) { 391 | if (speed === 'fast' || speed === 'slow') { 392 | return true; 393 | } 394 | 395 | if ((parseInt(speed, 10) + '') !== (speed + '')) { 396 | return false; 397 | } 398 | 399 | return (speed >= 0); 400 | } 401 | 402 | // validate config / set default options 403 | function expandConfig() { 404 | if (typeof cfg === 'string' || validPositionObject(cfg) === true) { 405 | cfg = { 406 | position: cfg 407 | }; 408 | } 409 | 410 | // default for orientation is white 411 | if (cfg.orientation !== 'black') { 412 | cfg.orientation = 'white'; 413 | } 414 | CURRENT_ORIENTATION = cfg.orientation; 415 | 416 | // default for showNotation is true 417 | if (cfg.showNotation !== false) { 418 | cfg.showNotation = true; 419 | } 420 | 421 | // default for draggable is false 422 | if (cfg.draggable !== true) { 423 | cfg.draggable = false; 424 | } 425 | 426 | // default for dropOffBoard is 'snapback' 427 | if (cfg.dropOffBoard !== 'trash') { 428 | cfg.dropOffBoard = 'snapback'; 429 | } 430 | 431 | // default for sparePieces is false 432 | if (cfg.sparePieces !== true) { 433 | cfg.sparePieces = false; 434 | } 435 | 436 | // draggable must be true if sparePieces is enabled 437 | if (cfg.sparePieces === true) { 438 | cfg.draggable = true; 439 | } 440 | 441 | // default piece theme is wikipedia 442 | if (cfg.hasOwnProperty('pieceTheme') !== true || 443 | (typeof cfg.pieceTheme !== 'string' && 444 | typeof cfg.pieceTheme !== 'function')) { 445 | cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'; 446 | } 447 | 448 | // animation speeds 449 | if (cfg.hasOwnProperty('appearSpeed') !== true || 450 | validAnimationSpeed(cfg.appearSpeed) !== true) { 451 | cfg.appearSpeed = 200; 452 | } 453 | if (cfg.hasOwnProperty('moveSpeed') !== true || 454 | validAnimationSpeed(cfg.moveSpeed) !== true) { 455 | cfg.moveSpeed = 200; 456 | } 457 | if (cfg.hasOwnProperty('snapbackSpeed') !== true || 458 | validAnimationSpeed(cfg.snapbackSpeed) !== true) { 459 | cfg.snapbackSpeed = 50; 460 | } 461 | if (cfg.hasOwnProperty('snapSpeed') !== true || 462 | validAnimationSpeed(cfg.snapSpeed) !== true) { 463 | cfg.snapSpeed = 25; 464 | } 465 | if (cfg.hasOwnProperty('trashSpeed') !== true || 466 | validAnimationSpeed(cfg.trashSpeed) !== true) { 467 | cfg.trashSpeed = 100; 468 | } 469 | 470 | // make sure position is valid 471 | if (cfg.hasOwnProperty('position') === true) { 472 | if (cfg.position === 'start') { 473 | CURRENT_POSITION = deepCopy(START_POSITION); 474 | } 475 | 476 | else if (validFen(cfg.position) === true) { 477 | CURRENT_POSITION = fenToObj(cfg.position); 478 | } 479 | 480 | else if (validPositionObject(cfg.position) === true) { 481 | CURRENT_POSITION = deepCopy(cfg.position); 482 | } 483 | 484 | else { 485 | error(7263, 'Invalid value passed to config.position.', cfg.position); 486 | } 487 | } 488 | 489 | return true; 490 | } 491 | 492 | //------------------------------------------------------------------------------ 493 | // DOM Misc 494 | //------------------------------------------------------------------------------ 495 | 496 | // calculates square size based on the width of the container 497 | // got a little CSS black magic here, so let me explain: 498 | // get the width of the container element (could be anything), reduce by 1 for 499 | // fudge factor, and then keep reducing until we find an exact mod 8 for 500 | // our square size 501 | function calculateSquareSize() { 502 | var containerWidth = parseInt(containerEl.css('width'), 10); 503 | 504 | // defensive, prevent infinite loop 505 | if (! containerWidth || containerWidth <= 0) { 506 | return 0; 507 | } 508 | 509 | // pad one pixel 510 | var boardWidth = containerWidth - 1; 511 | 512 | while (boardWidth % 8 !== 0 && boardWidth > 0) { 513 | boardWidth--; 514 | } 515 | 516 | return (boardWidth / 8); 517 | } 518 | 519 | // create random IDs for elements 520 | function createElIds() { 521 | // squares on the board 522 | for (var i = 0; i < COLUMNS.length; i++) { 523 | for (var j = 1; j <= 8; j++) { 524 | var square = COLUMNS[i] + j; 525 | SQUARE_ELS_IDS[square] = square + '-' + createId(); 526 | } 527 | } 528 | 529 | // spare pieces 530 | var pieces = 'KQRBNP'.split(''); 531 | for (var i = 0; i < pieces.length; i++) { 532 | var whitePiece = 'w' + pieces[i]; 533 | var blackPiece = 'b' + pieces[i]; 534 | SPARE_PIECE_ELS_IDS[whitePiece] = whitePiece + '-' + createId(); 535 | SPARE_PIECE_ELS_IDS[blackPiece] = blackPiece + '-' + createId(); 536 | } 537 | } 538 | 539 | //------------------------------------------------------------------------------ 540 | // Markup Building 541 | //------------------------------------------------------------------------------ 542 | 543 | function buildBoardContainer() { 544 | var html = '
'; 545 | 546 | if (cfg.sparePieces === true) { 547 | html += '
'; 549 | } 550 | 551 | html += '
'; 552 | 553 | if (cfg.sparePieces === true) { 554 | html += '
'; 556 | } 557 | 558 | html += '
'; 559 | 560 | return html; 561 | } 562 | 563 | /* 564 | var buildSquare = function(color, size, id) { 565 | var html = '
'; 568 | 569 | if (cfg.showNotation === true) { 570 | 571 | } 572 | 573 | html += '
'; 574 | 575 | return html; 576 | }; 577 | */ 578 | 579 | function buildBoard(orientation) { 580 | if (orientation !== 'black') { 581 | orientation = 'white'; 582 | } 583 | 584 | var html = ''; 585 | 586 | // algebraic notation / orientation 587 | var alpha = deepCopy(COLUMNS); 588 | var row = 8; 589 | if (orientation === 'black') { 590 | alpha.reverse(); 591 | row = 1; 592 | } 593 | 594 | var squareColor = 'white'; 595 | for (var i = 0; i < 8; i++) { 596 | html += '
'; 597 | for (var j = 0; j < 8; j++) { 598 | var square = alpha[j] + row; 599 | 600 | html += '
'; 605 | 606 | if (cfg.showNotation === true) { 607 | // alpha notation 608 | if ((orientation === 'white' && row === 1) || 609 | (orientation === 'black' && row === 8)) { 610 | html += '
' + 611 | alpha[j] + '
'; 612 | } 613 | 614 | // numeric notation 615 | if (j === 0) { 616 | html += '
' + 617 | row + '
'; 618 | } 619 | } 620 | 621 | html += '
'; // end .square 622 | 623 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 624 | } 625 | html += '
'; 626 | 627 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 628 | 629 | if (orientation === 'white') { 630 | row--; 631 | } 632 | else { 633 | row++; 634 | } 635 | } 636 | 637 | return html; 638 | } 639 | 640 | function buildPieceImgSrc(piece) { 641 | if (typeof cfg.pieceTheme === 'function') { 642 | return cfg.pieceTheme(piece); 643 | } 644 | 645 | if (typeof cfg.pieceTheme === 'string') { 646 | return cfg.pieceTheme.replace(/{piece}/g, piece); 647 | } 648 | 649 | // NOTE: this should never happen 650 | error(8272, 'Unable to build image source for cfg.pieceTheme.'); 651 | return ''; 652 | } 653 | 654 | function buildPiece(piece, hidden, id) { 655 | var html = ''; 668 | 669 | return html; 670 | } 671 | 672 | function buildSparePieces(color) { 673 | var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']; 674 | if (color === 'black') { 675 | pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']; 676 | } 677 | 678 | var html = ''; 679 | for (var i = 0; i < pieces.length; i++) { 680 | html += buildPiece(pieces[i], false, SPARE_PIECE_ELS_IDS[pieces[i]]); 681 | } 682 | 683 | return html; 684 | } 685 | 686 | //------------------------------------------------------------------------------ 687 | // Animations 688 | //------------------------------------------------------------------------------ 689 | 690 | function animateSquareToSquare(src, dest, piece, completeFn) { 691 | // get information about the source and destination squares 692 | var srcSquareEl = $('#' + SQUARE_ELS_IDS[src]); 693 | var srcSquarePosition = srcSquareEl.offset(); 694 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 695 | var destSquarePosition = destSquareEl.offset(); 696 | 697 | // create the animated piece and absolutely position it 698 | // over the source square 699 | var animatedPieceId = createId(); 700 | $('body').append(buildPiece(piece, true, animatedPieceId)); 701 | var animatedPieceEl = $('#' + animatedPieceId); 702 | animatedPieceEl.css({ 703 | display: '', 704 | position: 'absolute', 705 | top: srcSquarePosition.top, 706 | left: srcSquarePosition.left 707 | }); 708 | 709 | // remove original piece from source square 710 | srcSquareEl.find('.' + CSS.piece).remove(); 711 | 712 | // on complete 713 | var complete = function() { 714 | // add the "real" piece to the destination square 715 | destSquareEl.append(buildPiece(piece)); 716 | 717 | // remove the animated piece 718 | animatedPieceEl.remove(); 719 | 720 | // run complete function 721 | if (typeof completeFn === 'function') { 722 | completeFn(); 723 | } 724 | }; 725 | 726 | // animate the piece to the destination square 727 | var opts = { 728 | duration: cfg.moveSpeed, 729 | complete: complete 730 | }; 731 | animatedPieceEl.animate(destSquarePosition, opts); 732 | } 733 | 734 | function animateSparePieceToSquare(piece, dest, completeFn) { 735 | var srcOffset = $('#' + SPARE_PIECE_ELS_IDS[piece]).offset(); 736 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 737 | var destOffset = destSquareEl.offset(); 738 | 739 | // create the animate piece 740 | var pieceId = createId(); 741 | $('body').append(buildPiece(piece, true, pieceId)); 742 | var animatedPieceEl = $('#' + pieceId); 743 | animatedPieceEl.css({ 744 | display: '', 745 | position: 'absolute', 746 | left: srcOffset.left, 747 | top: srcOffset.top 748 | }); 749 | 750 | // on complete 751 | var complete = function() { 752 | // add the "real" piece to the destination square 753 | destSquareEl.find('.' + CSS.piece).remove(); 754 | destSquareEl.append(buildPiece(piece)); 755 | 756 | // remove the animated piece 757 | animatedPieceEl.remove(); 758 | 759 | // run complete function 760 | if (typeof completeFn === 'function') { 761 | completeFn(); 762 | } 763 | }; 764 | 765 | // animate the piece to the destination square 766 | var opts = { 767 | duration: cfg.moveSpeed, 768 | complete: complete 769 | }; 770 | animatedPieceEl.animate(destOffset, opts); 771 | } 772 | 773 | // execute an array of animations 774 | function doAnimations(a, oldPos, newPos) { 775 | ANIMATION_HAPPENING = true; 776 | 777 | var numFinished = 0; 778 | function onFinish() { 779 | numFinished++; 780 | 781 | // exit if all the animations aren't finished 782 | if (numFinished !== a.length) return; 783 | 784 | drawPositionInstant(); 785 | ANIMATION_HAPPENING = false; 786 | 787 | // run their onMoveEnd function 788 | if (cfg.hasOwnProperty('onMoveEnd') === true && 789 | typeof cfg.onMoveEnd === 'function') { 790 | cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos)); 791 | } 792 | } 793 | 794 | for (var i = 0; i < a.length; i++) { 795 | // clear a piece 796 | if (a[i].type === 'clear') { 797 | $('#' + SQUARE_ELS_IDS[a[i].square] + ' .' + CSS.piece) 798 | .fadeOut(cfg.trashSpeed, onFinish); 799 | } 800 | 801 | // add a piece (no spare pieces) 802 | if (a[i].type === 'add' && cfg.sparePieces !== true) { 803 | $('#' + SQUARE_ELS_IDS[a[i].square]) 804 | .append(buildPiece(a[i].piece, true)) 805 | .find('.' + CSS.piece) 806 | .fadeIn(cfg.appearSpeed, onFinish); 807 | } 808 | 809 | // add a piece from a spare piece 810 | if (a[i].type === 'add' && cfg.sparePieces === true) { 811 | animateSparePieceToSquare(a[i].piece, a[i].square, onFinish); 812 | } 813 | 814 | // move a piece 815 | if (a[i].type === 'move') { 816 | animateSquareToSquare(a[i].source, a[i].destination, a[i].piece, 817 | onFinish); 818 | } 819 | } 820 | } 821 | 822 | // returns the distance between two squares 823 | function squareDistance(s1, s2) { 824 | s1 = s1.split(''); 825 | var s1x = COLUMNS.indexOf(s1[0]) + 1; 826 | var s1y = parseInt(s1[1], 10); 827 | 828 | s2 = s2.split(''); 829 | var s2x = COLUMNS.indexOf(s2[0]) + 1; 830 | var s2y = parseInt(s2[1], 10); 831 | 832 | var xDelta = Math.abs(s1x - s2x); 833 | var yDelta = Math.abs(s1y - s2y); 834 | 835 | if (xDelta >= yDelta) return xDelta; 836 | return yDelta; 837 | } 838 | 839 | // returns an array of closest squares from square 840 | function createRadius(square) { 841 | var squares = []; 842 | 843 | // calculate distance of all squares 844 | for (var i = 0; i < 8; i++) { 845 | for (var j = 0; j < 8; j++) { 846 | var s = COLUMNS[i] + (j + 1); 847 | 848 | // skip the square we're starting from 849 | if (square === s) continue; 850 | 851 | squares.push({ 852 | square: s, 853 | distance: squareDistance(square, s) 854 | }); 855 | } 856 | } 857 | 858 | // sort by distance 859 | squares.sort(function(a, b) { 860 | return a.distance - b.distance; 861 | }); 862 | 863 | // just return the square code 864 | var squares2 = []; 865 | for (var i = 0; i < squares.length; i++) { 866 | squares2.push(squares[i].square); 867 | } 868 | 869 | return squares2; 870 | } 871 | 872 | // returns the square of the closest instance of piece 873 | // returns false if no instance of piece is found in position 874 | function findClosestPiece(position, piece, square) { 875 | // create array of closest squares from square 876 | var closestSquares = createRadius(square); 877 | 878 | // search through the position in order of distance for the piece 879 | for (var i = 0; i < closestSquares.length; i++) { 880 | var s = closestSquares[i]; 881 | 882 | if (position.hasOwnProperty(s) === true && position[s] === piece) { 883 | return s; 884 | } 885 | } 886 | 887 | return false; 888 | } 889 | 890 | // calculate an array of animations that need to happen in order to get 891 | // from pos1 to pos2 892 | function calculateAnimations(pos1, pos2) { 893 | // make copies of both 894 | pos1 = deepCopy(pos1); 895 | pos2 = deepCopy(pos2); 896 | 897 | var animations = []; 898 | var squaresMovedTo = {}; 899 | 900 | // remove pieces that are the same in both positions 901 | for (var i in pos2) { 902 | if (pos2.hasOwnProperty(i) !== true) continue; 903 | 904 | if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) { 905 | delete pos1[i]; 906 | delete pos2[i]; 907 | } 908 | } 909 | 910 | // find all the "move" animations 911 | for (var i in pos2) { 912 | if (pos2.hasOwnProperty(i) !== true) continue; 913 | 914 | var closestPiece = findClosestPiece(pos1, pos2[i], i); 915 | if (closestPiece !== false) { 916 | animations.push({ 917 | type: 'move', 918 | source: closestPiece, 919 | destination: i, 920 | piece: pos2[i] 921 | }); 922 | 923 | delete pos1[closestPiece]; 924 | delete pos2[i]; 925 | squaresMovedTo[i] = true; 926 | } 927 | } 928 | 929 | // add pieces to pos2 930 | for (var i in pos2) { 931 | if (pos2.hasOwnProperty(i) !== true) continue; 932 | 933 | animations.push({ 934 | type: 'add', 935 | square: i, 936 | piece: pos2[i] 937 | }) 938 | 939 | delete pos2[i]; 940 | } 941 | 942 | // clear pieces from pos1 943 | for (var i in pos1) { 944 | if (pos1.hasOwnProperty(i) !== true) continue; 945 | 946 | // do not clear a piece if it is on a square that is the result 947 | // of a "move", ie: a piece capture 948 | if (squaresMovedTo.hasOwnProperty(i) === true) continue; 949 | 950 | animations.push({ 951 | type: 'clear', 952 | square: i, 953 | piece: pos1[i] 954 | }); 955 | 956 | delete pos1[i]; 957 | } 958 | 959 | return animations; 960 | } 961 | 962 | //------------------------------------------------------------------------------ 963 | // Control Flow 964 | //------------------------------------------------------------------------------ 965 | 966 | function drawPositionInstant() { 967 | // clear the board 968 | boardEl.find('.' + CSS.piece).remove(); 969 | 970 | // add the pieces 971 | for (var i in CURRENT_POSITION) { 972 | if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue; 973 | 974 | $('#' + SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i])); 975 | } 976 | } 977 | 978 | function drawBoard() { 979 | boardEl.html(buildBoard(CURRENT_ORIENTATION)); 980 | drawPositionInstant(); 981 | 982 | if (cfg.sparePieces === true) { 983 | if (CURRENT_ORIENTATION === 'white') { 984 | sparePiecesTopEl.html(buildSparePieces('black')); 985 | sparePiecesBottomEl.html(buildSparePieces('white')); 986 | } 987 | else { 988 | sparePiecesTopEl.html(buildSparePieces('white')); 989 | sparePiecesBottomEl.html(buildSparePieces('black')); 990 | } 991 | } 992 | } 993 | 994 | // given a position and a set of moves, return a new position 995 | // with the moves executed 996 | function calculatePositionFromMoves(position, moves) { 997 | position = deepCopy(position); 998 | 999 | for (var i in moves) { 1000 | if (moves.hasOwnProperty(i) !== true) continue; 1001 | 1002 | // skip the move if the position doesn't have a piece on the source square 1003 | if (position.hasOwnProperty(i) !== true) continue; 1004 | 1005 | var piece = position[i]; 1006 | delete position[i]; 1007 | position[moves[i]] = piece; 1008 | } 1009 | 1010 | return position; 1011 | } 1012 | 1013 | function setCurrentPosition(position) { 1014 | var oldPos = deepCopy(CURRENT_POSITION); 1015 | var newPos = deepCopy(position); 1016 | var oldFen = objToFen(oldPos); 1017 | var newFen = objToFen(newPos); 1018 | 1019 | // do nothing if no change in position 1020 | if (oldFen === newFen) return; 1021 | 1022 | // run their onChange function 1023 | if (cfg.hasOwnProperty('onChange') === true && 1024 | typeof cfg.onChange === 'function') { 1025 | cfg.onChange(oldPos, newPos); 1026 | } 1027 | 1028 | // update state 1029 | CURRENT_POSITION = position; 1030 | } 1031 | 1032 | function isXYOnSquare(x, y) { 1033 | for (var i in SQUARE_ELS_OFFSETS) { 1034 | if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue; 1035 | 1036 | var s = SQUARE_ELS_OFFSETS[i]; 1037 | if (x >= s.left && x < s.left + SQUARE_SIZE && 1038 | y >= s.top && y < s.top + SQUARE_SIZE) { 1039 | return i; 1040 | } 1041 | } 1042 | 1043 | return 'offboard'; 1044 | } 1045 | 1046 | // records the XY coords of every square into memory 1047 | function captureSquareOffsets() { 1048 | SQUARE_ELS_OFFSETS = {}; 1049 | 1050 | for (var i in SQUARE_ELS_IDS) { 1051 | if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue; 1052 | 1053 | SQUARE_ELS_OFFSETS[i] = $('#' + SQUARE_ELS_IDS[i]).offset(); 1054 | } 1055 | } 1056 | 1057 | function removeSquareHighlights() { 1058 | boardEl.find('.' + CSS.square) 1059 | .removeClass(CSS.highlight1 + ' ' + CSS.highlight2); 1060 | } 1061 | 1062 | function snapbackDraggedPiece() { 1063 | // there is no "snapback" for spare pieces 1064 | if (DRAGGED_PIECE_SOURCE === 'spare') { 1065 | trashDraggedPiece(); 1066 | return; 1067 | } 1068 | 1069 | removeSquareHighlights(); 1070 | 1071 | // animation complete 1072 | function complete() { 1073 | drawPositionInstant(); 1074 | draggedPieceEl.css('display', 'none'); 1075 | 1076 | // run their onSnapbackEnd function 1077 | if (cfg.hasOwnProperty('onSnapbackEnd') === true && 1078 | typeof cfg.onSnapbackEnd === 'function') { 1079 | cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE, 1080 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1081 | } 1082 | } 1083 | 1084 | // get source square position 1085 | var sourceSquarePosition = 1086 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]).offset(); 1087 | 1088 | // animate the piece to the target square 1089 | var opts = { 1090 | duration: cfg.snapbackSpeed, 1091 | complete: complete 1092 | }; 1093 | draggedPieceEl.animate(sourceSquarePosition, opts); 1094 | 1095 | // set state 1096 | DRAGGING_A_PIECE = false; 1097 | } 1098 | 1099 | function trashDraggedPiece() { 1100 | removeSquareHighlights(); 1101 | 1102 | // remove the source piece 1103 | var newPosition = deepCopy(CURRENT_POSITION); 1104 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1105 | setCurrentPosition(newPosition); 1106 | 1107 | // redraw the position 1108 | drawPositionInstant(); 1109 | 1110 | // hide the dragged piece 1111 | draggedPieceEl.fadeOut(cfg.trashSpeed); 1112 | 1113 | // set state 1114 | DRAGGING_A_PIECE = false; 1115 | } 1116 | 1117 | function dropDraggedPieceOnSquare(square) { 1118 | removeSquareHighlights(); 1119 | 1120 | // update position 1121 | var newPosition = deepCopy(CURRENT_POSITION); 1122 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1123 | newPosition[square] = DRAGGED_PIECE; 1124 | setCurrentPosition(newPosition); 1125 | 1126 | // get target square information 1127 | var targetSquarePosition = $('#' + SQUARE_ELS_IDS[square]).offset(); 1128 | 1129 | // animation complete 1130 | var complete = function() { 1131 | drawPositionInstant(); 1132 | draggedPieceEl.css('display', 'none'); 1133 | 1134 | // execute their onSnapEnd function 1135 | if (cfg.hasOwnProperty('onSnapEnd') === true && 1136 | typeof cfg.onSnapEnd === 'function') { 1137 | cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE); 1138 | } 1139 | }; 1140 | 1141 | // snap the piece to the target square 1142 | var opts = { 1143 | duration: cfg.snapSpeed, 1144 | complete: complete 1145 | }; 1146 | draggedPieceEl.animate(targetSquarePosition, opts); 1147 | 1148 | // set state 1149 | DRAGGING_A_PIECE = false; 1150 | } 1151 | 1152 | function beginDraggingPiece(source, piece, x, y) { 1153 | // run their custom onDragStart function 1154 | // their custom onDragStart function can cancel drag start 1155 | if (typeof cfg.onDragStart === 'function' && 1156 | cfg.onDragStart(source, piece, 1157 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) { 1158 | return; 1159 | } 1160 | 1161 | // set state 1162 | DRAGGING_A_PIECE = true; 1163 | DRAGGED_PIECE = piece; 1164 | DRAGGED_PIECE_SOURCE = source; 1165 | 1166 | // if the piece came from spare pieces, location is offboard 1167 | if (source === 'spare') { 1168 | DRAGGED_PIECE_LOCATION = 'offboard'; 1169 | } 1170 | else { 1171 | DRAGGED_PIECE_LOCATION = source; 1172 | } 1173 | 1174 | // capture the x, y coords of all squares in memory 1175 | captureSquareOffsets(); 1176 | 1177 | // create the dragged piece 1178 | draggedPieceEl.attr('src', buildPieceImgSrc(piece)) 1179 | .css({ 1180 | display: '', 1181 | position: 'absolute', 1182 | left: x - (SQUARE_SIZE / 2), 1183 | top: y - (SQUARE_SIZE / 2) 1184 | }); 1185 | 1186 | if (source !== 'spare') { 1187 | // highlight the source square and hide the piece 1188 | $('#' + SQUARE_ELS_IDS[source]).addClass(CSS.highlight1) 1189 | .find('.' + CSS.piece).css('display', 'none'); 1190 | } 1191 | } 1192 | 1193 | function updateDraggedPiece(x, y) { 1194 | // put the dragged piece over the mouse cursor 1195 | draggedPieceEl.css({ 1196 | left: x - (SQUARE_SIZE / 2), 1197 | top: y - (SQUARE_SIZE / 2) 1198 | }); 1199 | 1200 | // get location 1201 | var location = isXYOnSquare(x, y); 1202 | 1203 | // do nothing if the location has not changed 1204 | if (location === DRAGGED_PIECE_LOCATION) return; 1205 | 1206 | // remove highlight from previous square 1207 | if (validSquare(DRAGGED_PIECE_LOCATION) === true) { 1208 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION]) 1209 | .removeClass(CSS.highlight2); 1210 | } 1211 | 1212 | // add highlight to new square 1213 | if (validSquare(location) === true) { 1214 | $('#' + SQUARE_ELS_IDS[location]).addClass(CSS.highlight2); 1215 | } 1216 | 1217 | // run onDragMove 1218 | if (typeof cfg.onDragMove === 'function') { 1219 | cfg.onDragMove(location, DRAGGED_PIECE_LOCATION, 1220 | DRAGGED_PIECE_SOURCE, DRAGGED_PIECE, 1221 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1222 | } 1223 | 1224 | // update state 1225 | DRAGGED_PIECE_LOCATION = location; 1226 | } 1227 | 1228 | function stopDraggedPiece(location) { 1229 | // determine what the action should be 1230 | var action = 'drop'; 1231 | if (location === 'offboard' && cfg.dropOffBoard === 'snapback') { 1232 | action = 'snapback'; 1233 | } 1234 | if (location === 'offboard' && cfg.dropOffBoard === 'trash') { 1235 | action = 'trash'; 1236 | } 1237 | 1238 | // run their onDrop function, which can potentially change the drop action 1239 | if (cfg.hasOwnProperty('onDrop') === true && 1240 | typeof cfg.onDrop === 'function') { 1241 | var newPosition = deepCopy(CURRENT_POSITION); 1242 | 1243 | // source piece is a spare piece and position is off the board 1244 | //if (DRAGGED_PIECE_SOURCE === 'spare' && location === 'offboard') {...} 1245 | // position has not changed; do nothing 1246 | 1247 | // source piece is a spare piece and position is on the board 1248 | if (DRAGGED_PIECE_SOURCE === 'spare' && validSquare(location) === true) { 1249 | // add the piece to the board 1250 | newPosition[location] = DRAGGED_PIECE; 1251 | } 1252 | 1253 | // source piece was on the board and position is off the board 1254 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') { 1255 | // remove the piece from the board 1256 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1257 | } 1258 | 1259 | // source piece was on the board and position is on the board 1260 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && 1261 | validSquare(location) === true) { 1262 | // move the piece 1263 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1264 | newPosition[location] = DRAGGED_PIECE; 1265 | } 1266 | 1267 | var oldPosition = deepCopy(CURRENT_POSITION); 1268 | 1269 | var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE, 1270 | newPosition, oldPosition, CURRENT_ORIENTATION); 1271 | if (result === 'snapback' || result === 'trash') { 1272 | action = result; 1273 | } 1274 | } 1275 | 1276 | // do it! 1277 | if (action === 'snapback') { 1278 | snapbackDraggedPiece(); 1279 | } 1280 | else if (action === 'trash') { 1281 | trashDraggedPiece(); 1282 | } 1283 | else if (action === 'drop') { 1284 | dropDraggedPieceOnSquare(location); 1285 | } 1286 | } 1287 | 1288 | //------------------------------------------------------------------------------ 1289 | // Public Methods 1290 | //------------------------------------------------------------------------------ 1291 | 1292 | // clear the board 1293 | widget.clear = function(useAnimation) { 1294 | widget.position({}, useAnimation); 1295 | }; 1296 | 1297 | /* 1298 | // get or set config properties 1299 | // TODO: write this, GitHub Issue #1 1300 | widget.config = function(arg1, arg2) { 1301 | // get the current config 1302 | if (arguments.length === 0) { 1303 | return deepCopy(cfg); 1304 | } 1305 | }; 1306 | */ 1307 | 1308 | // remove the widget from the page 1309 | widget.destroy = function() { 1310 | // remove markup 1311 | containerEl.html(''); 1312 | draggedPieceEl.remove(); 1313 | 1314 | // remove event handlers 1315 | containerEl.unbind(); 1316 | }; 1317 | 1318 | // shorthand method to get the current FEN 1319 | widget.fen = function() { 1320 | return widget.position('fen'); 1321 | }; 1322 | 1323 | // flip orientation 1324 | widget.flip = function() { 1325 | widget.orientation('flip'); 1326 | }; 1327 | 1328 | /* 1329 | // TODO: write this, GitHub Issue #5 1330 | widget.highlight = function() { 1331 | 1332 | }; 1333 | */ 1334 | 1335 | // move pieces 1336 | widget.move = function() { 1337 | // no need to throw an error here; just do nothing 1338 | if (arguments.length === 0) return; 1339 | 1340 | var useAnimation = true; 1341 | 1342 | // collect the moves into an object 1343 | var moves = {}; 1344 | for (var i = 0; i < arguments.length; i++) { 1345 | // any "false" to this function means no animations 1346 | if (arguments[i] === false) { 1347 | useAnimation = false; 1348 | continue; 1349 | } 1350 | 1351 | // skip invalid arguments 1352 | if (validMove(arguments[i]) !== true) { 1353 | error(2826, 'Invalid move passed to the move method.', arguments[i]); 1354 | continue; 1355 | } 1356 | 1357 | var tmp = arguments[i].split('-'); 1358 | moves[tmp[0]] = tmp[1]; 1359 | } 1360 | 1361 | // calculate position from moves 1362 | var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves); 1363 | 1364 | // update the board 1365 | widget.position(newPos, useAnimation); 1366 | 1367 | // return the new position object 1368 | return newPos; 1369 | }; 1370 | 1371 | widget.orientation = function(arg) { 1372 | // no arguments, return the current orientation 1373 | if (arguments.length === 0) { 1374 | return CURRENT_ORIENTATION; 1375 | } 1376 | 1377 | // set to white or black 1378 | if (arg === 'white' || arg === 'black') { 1379 | CURRENT_ORIENTATION = arg; 1380 | drawBoard(); 1381 | return; 1382 | } 1383 | 1384 | // flip orientation 1385 | if (arg === 'flip') { 1386 | CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white'; 1387 | drawBoard(); 1388 | return; 1389 | } 1390 | 1391 | error(5482, 'Invalid value passed to the orientation method.', arg); 1392 | }; 1393 | 1394 | widget.position = function(position, useAnimation) { 1395 | // no arguments, return the current position 1396 | if (arguments.length === 0) { 1397 | return deepCopy(CURRENT_POSITION); 1398 | } 1399 | 1400 | // get position as FEN 1401 | if (typeof position === 'string' && position.toLowerCase() === 'fen') { 1402 | return objToFen(CURRENT_POSITION); 1403 | } 1404 | 1405 | // default for useAnimations is true 1406 | if (useAnimation !== false) { 1407 | useAnimation = true; 1408 | } 1409 | 1410 | // start position 1411 | if (typeof position === 'string' && position.toLowerCase() === 'start') { 1412 | position = deepCopy(START_POSITION); 1413 | } 1414 | 1415 | // convert FEN to position object 1416 | if (validFen(position) === true) { 1417 | position = fenToObj(position); 1418 | } 1419 | 1420 | // validate position object 1421 | if (validPositionObject(position) !== true) { 1422 | error(6482, 'Invalid value passed to the position method.', position); 1423 | return; 1424 | } 1425 | 1426 | if (useAnimation === true) { 1427 | // start the animations 1428 | doAnimations(calculateAnimations(CURRENT_POSITION, position), 1429 | CURRENT_POSITION, position); 1430 | 1431 | // set the new position 1432 | setCurrentPosition(position); 1433 | } 1434 | // instant update 1435 | else { 1436 | setCurrentPosition(position); 1437 | drawPositionInstant(); 1438 | } 1439 | }; 1440 | 1441 | widget.resize = function() { 1442 | // calulate the new square size 1443 | SQUARE_SIZE = calculateSquareSize(); 1444 | 1445 | // set board width 1446 | boardEl.css('width', (SQUARE_SIZE * 8) + 'px'); 1447 | 1448 | // set drag piece size 1449 | draggedPieceEl.css({ 1450 | height: SQUARE_SIZE, 1451 | width: SQUARE_SIZE 1452 | }); 1453 | 1454 | // spare pieces 1455 | if (cfg.sparePieces === true) { 1456 | containerEl.find('.' + CSS.sparePieces) 1457 | .css('paddingLeft', (SQUARE_SIZE + BOARD_BORDER_SIZE) + 'px'); 1458 | } 1459 | 1460 | // redraw the board 1461 | drawBoard(); 1462 | }; 1463 | 1464 | // set the starting position 1465 | widget.start = function(useAnimation) { 1466 | widget.position('start', useAnimation); 1467 | }; 1468 | 1469 | //------------------------------------------------------------------------------ 1470 | // Browser Events 1471 | //------------------------------------------------------------------------------ 1472 | 1473 | function isTouchDevice() { 1474 | return ('ontouchstart' in document.documentElement); 1475 | } 1476 | 1477 | // reference: http://www.quirksmode.org/js/detect.html 1478 | function isMSIE() { 1479 | return (navigator && navigator.userAgent && 1480 | navigator.userAgent.search(/MSIE/) !== -1); 1481 | } 1482 | 1483 | function stopDefault(e) { 1484 | e.preventDefault(); 1485 | } 1486 | 1487 | function mousedownSquare(e) { 1488 | // do nothing if we're not draggable 1489 | if (cfg.draggable !== true) return; 1490 | 1491 | var square = $(this).attr('data-square'); 1492 | 1493 | // no piece on this square 1494 | if (validSquare(square) !== true || 1495 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1496 | return; 1497 | } 1498 | 1499 | beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY); 1500 | } 1501 | 1502 | function touchstartSquare(e) { 1503 | // do nothing if we're not draggable 1504 | if (cfg.draggable !== true) return; 1505 | 1506 | var square = $(this).attr('data-square'); 1507 | 1508 | // no piece on this square 1509 | if (validSquare(square) !== true || 1510 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1511 | return; 1512 | } 1513 | 1514 | e = e.originalEvent; 1515 | beginDraggingPiece(square, CURRENT_POSITION[square], 1516 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1517 | } 1518 | 1519 | function mousedownSparePiece(e) { 1520 | // do nothing if sparePieces is not enabled 1521 | if (cfg.sparePieces !== true) return; 1522 | 1523 | var piece = $(this).attr('data-piece'); 1524 | 1525 | beginDraggingPiece('spare', piece, e.pageX, e.pageY); 1526 | } 1527 | 1528 | function touchstartSparePiece(e) { 1529 | // do nothing if sparePieces is not enabled 1530 | if (cfg.sparePieces !== true) return; 1531 | 1532 | var piece = $(this).attr('data-piece'); 1533 | 1534 | e = e.originalEvent; 1535 | beginDraggingPiece('spare', piece, 1536 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1537 | } 1538 | 1539 | function mousemoveWindow(e) { 1540 | // do nothing if we are not dragging a piece 1541 | if (DRAGGING_A_PIECE !== true) return; 1542 | 1543 | updateDraggedPiece(e.pageX, e.pageY); 1544 | } 1545 | 1546 | function touchmoveWindow(e) { 1547 | // do nothing if we are not dragging a piece 1548 | if (DRAGGING_A_PIECE !== true) return; 1549 | 1550 | // prevent screen from scrolling 1551 | e.preventDefault(); 1552 | 1553 | updateDraggedPiece(e.originalEvent.changedTouches[0].pageX, 1554 | e.originalEvent.changedTouches[0].pageY); 1555 | } 1556 | 1557 | function mouseupWindow(e) { 1558 | // do nothing if we are not dragging a piece 1559 | if (DRAGGING_A_PIECE !== true) return; 1560 | 1561 | // get the location 1562 | var location = isXYOnSquare(e.pageX, e.pageY); 1563 | 1564 | stopDraggedPiece(location); 1565 | } 1566 | 1567 | function touchendWindow(e) { 1568 | // do nothing if we are not dragging a piece 1569 | if (DRAGGING_A_PIECE !== true) return; 1570 | 1571 | // get the location 1572 | var location = isXYOnSquare(e.originalEvent.changedTouches[0].pageX, 1573 | e.originalEvent.changedTouches[0].pageY); 1574 | 1575 | stopDraggedPiece(location); 1576 | } 1577 | 1578 | function mouseenterSquare(e) { 1579 | // do not fire this event if we are dragging a piece 1580 | // NOTE: this should never happen, but it's a safeguard 1581 | if (DRAGGING_A_PIECE !== false) return; 1582 | 1583 | if (cfg.hasOwnProperty('onMouseoverSquare') !== true || 1584 | typeof cfg.onMouseoverSquare !== 'function') return; 1585 | 1586 | // get the square 1587 | var square = $(e.currentTarget).attr('data-square'); 1588 | 1589 | // NOTE: this should never happen; defensive 1590 | if (validSquare(square) !== true) return; 1591 | 1592 | // get the piece on this square 1593 | var piece = false; 1594 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1595 | piece = CURRENT_POSITION[square]; 1596 | } 1597 | 1598 | // execute their function 1599 | cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION), 1600 | CURRENT_ORIENTATION); 1601 | } 1602 | 1603 | function mouseleaveSquare(e) { 1604 | // do not fire this event if we are dragging a piece 1605 | // NOTE: this should never happen, but it's a safeguard 1606 | if (DRAGGING_A_PIECE !== false) return; 1607 | 1608 | if (cfg.hasOwnProperty('onMouseoutSquare') !== true || 1609 | typeof cfg.onMouseoutSquare !== 'function') return; 1610 | 1611 | // get the square 1612 | var square = $(e.currentTarget).attr('data-square'); 1613 | 1614 | // NOTE: this should never happen; defensive 1615 | if (validSquare(square) !== true) return; 1616 | 1617 | // get the piece on this square 1618 | var piece = false; 1619 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1620 | piece = CURRENT_POSITION[square]; 1621 | } 1622 | 1623 | // execute their function 1624 | cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION), 1625 | CURRENT_ORIENTATION); 1626 | } 1627 | 1628 | //------------------------------------------------------------------------------ 1629 | // Initialization 1630 | //------------------------------------------------------------------------------ 1631 | 1632 | function addEvents() { 1633 | // prevent browser "image drag" 1634 | $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault); 1635 | 1636 | // mouse drag pieces 1637 | boardEl.on('mousedown', '.' + CSS.square, mousedownSquare); 1638 | containerEl.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, 1639 | mousedownSparePiece); 1640 | 1641 | // mouse enter / leave square 1642 | boardEl.on('mouseenter', '.' + CSS.square, mouseenterSquare); 1643 | boardEl.on('mouseleave', '.' + CSS.square, mouseleaveSquare); 1644 | 1645 | // IE doesn't like the events on the window object, but other browsers 1646 | // perform better that way 1647 | if (isMSIE() === true) { 1648 | // IE-specific prevent browser "image drag" 1649 | document.ondragstart = function() { return false; }; 1650 | 1651 | $('body').on('mousemove', mousemoveWindow); 1652 | $('body').on('mouseup', mouseupWindow); 1653 | } 1654 | else { 1655 | $(window).on('mousemove', mousemoveWindow); 1656 | $(window).on('mouseup', mouseupWindow); 1657 | } 1658 | 1659 | // touch drag pieces 1660 | if (isTouchDevice() === true) { 1661 | boardEl.on('touchstart', '.' + CSS.square, touchstartSquare); 1662 | containerEl.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, 1663 | touchstartSparePiece); 1664 | $(window).on('touchmove', touchmoveWindow); 1665 | $(window).on('touchend', touchendWindow); 1666 | } 1667 | } 1668 | 1669 | function initDom() { 1670 | // build board and save it in memory 1671 | containerEl.html(buildBoardContainer()); 1672 | boardEl = containerEl.find('.' + CSS.board); 1673 | 1674 | if (cfg.sparePieces === true) { 1675 | sparePiecesTopEl = containerEl.find('.' + CSS.sparePiecesTop); 1676 | sparePiecesBottomEl = containerEl.find('.' + CSS.sparePiecesBottom); 1677 | } 1678 | 1679 | // create the drag piece 1680 | var draggedPieceId = createId(); 1681 | $('body').append(buildPiece('wP', true, draggedPieceId)); 1682 | draggedPieceEl = $('#' + draggedPieceId); 1683 | 1684 | // get the border size 1685 | BOARD_BORDER_SIZE = parseInt(boardEl.css('borderLeftWidth'), 10); 1686 | 1687 | // set the size and draw the board 1688 | widget.resize(); 1689 | } 1690 | 1691 | function init() { 1692 | if (checkDeps() !== true || 1693 | expandConfig() !== true) return; 1694 | 1695 | // create unique IDs for all the elements we will create 1696 | createElIds(); 1697 | 1698 | initDom(); 1699 | addEvents(); 1700 | } 1701 | 1702 | // go time 1703 | init(); 1704 | 1705 | // return the widget object 1706 | return widget; 1707 | 1708 | }; // end window.ChessBoard 1709 | 1710 | // expose util functions 1711 | window.ChessBoard.fenToObj = fenToObj; 1712 | window.ChessBoard.objToFen = objToFen; 1713 | 1714 | })(); // end anonymous wrapper 1715 | -------------------------------------------------------------------------------- /vendor/chess/js/chessboard.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chessboard.js $version$ 3 | * 4 | * Copyright 2013 Chris Oakman 5 | * Released under the MIT license 6 | * https://github.com/oakmac/chessboardjs/blob/master/LICENSE 7 | * 8 | * Date: $date$ 9 | */ 10 | 11 | // start anonymous scope 12 | ;(function() { 13 | 'use strict'; 14 | 15 | //------------------------------------------------------------------------------ 16 | // Chess Util Functions 17 | //------------------------------------------------------------------------------ 18 | var COLUMNS = 'abcdefgh'.split(''); 19 | 20 | function validMove(move) { 21 | // move should be a string 22 | if (typeof move !== 'string') return false; 23 | 24 | // move should be in the form of "e2-e4", "f6-d5" 25 | var tmp = move.split('-'); 26 | if (tmp.length !== 2) return false; 27 | 28 | return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true); 29 | } 30 | 31 | function validSquare(square) { 32 | if (typeof square !== 'string') return false; 33 | return (square.search(/^[a-h][1-8]$/) !== -1); 34 | } 35 | 36 | function validPieceCode(code) { 37 | if (typeof code !== 'string') return false; 38 | return (code.search(/^[bw][KQRNBP]$/) !== -1); 39 | } 40 | 41 | // TODO: this whole function could probably be replaced with a single regex 42 | function validFen(fen) { 43 | if (typeof fen !== 'string') return false; 44 | 45 | // cut off any move, castling, etc info from the end 46 | // we're only interested in position information 47 | fen = fen.replace(/ .+$/, ''); 48 | 49 | // FEN should be 8 sections separated by slashes 50 | var chunks = fen.split('/'); 51 | if (chunks.length !== 8) return false; 52 | 53 | // check the piece sections 54 | for (var i = 0; i < 8; i++) { 55 | if (chunks[i] === '' || 56 | chunks[i].length > 8 || 57 | chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | function validPositionObject(pos) { 66 | if (typeof pos !== 'object') return false; 67 | 68 | for (var i in pos) { 69 | if (pos.hasOwnProperty(i) !== true) continue; 70 | 71 | if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) { 72 | return false; 73 | } 74 | } 75 | 76 | return true; 77 | } 78 | 79 | // convert FEN piece code to bP, wK, etc 80 | function fenToPieceCode(piece) { 81 | // black piece 82 | if (piece.toLowerCase() === piece) { 83 | return 'b' + piece.toUpperCase(); 84 | } 85 | 86 | // white piece 87 | return 'w' + piece.toUpperCase(); 88 | } 89 | 90 | // convert bP, wK, etc code to FEN structure 91 | function pieceCodeToFen(piece) { 92 | var tmp = piece.split(''); 93 | 94 | // white piece 95 | if (tmp[0] === 'w') { 96 | return tmp[1].toUpperCase(); 97 | } 98 | 99 | // black piece 100 | return tmp[1].toLowerCase(); 101 | } 102 | 103 | // convert FEN string to position object 104 | // returns false if the FEN string is invalid 105 | function fenToObj(fen) { 106 | if (validFen(fen) !== true) { 107 | return false; 108 | } 109 | 110 | // cut off any move, castling, etc info from the end 111 | // we're only interested in position information 112 | fen = fen.replace(/ .+$/, ''); 113 | 114 | var rows = fen.split('/'); 115 | var position = {}; 116 | 117 | var currentRow = 8; 118 | for (var i = 0; i < 8; i++) { 119 | var row = rows[i].split(''); 120 | var colIndex = 0; 121 | 122 | // loop through each character in the FEN section 123 | for (var j = 0; j < row.length; j++) { 124 | // number / empty squares 125 | if (row[j].search(/[1-8]/) !== -1) { 126 | var emptySquares = parseInt(row[j], 10); 127 | colIndex += emptySquares; 128 | } 129 | // piece 130 | else { 131 | var square = COLUMNS[colIndex] + currentRow; 132 | position[square] = fenToPieceCode(row[j]); 133 | colIndex++; 134 | } 135 | } 136 | 137 | currentRow--; 138 | } 139 | 140 | return position; 141 | } 142 | 143 | // position object to FEN string 144 | // returns false if the obj is not a valid position object 145 | function objToFen(obj) { 146 | if (validPositionObject(obj) !== true) { 147 | return false; 148 | } 149 | 150 | var fen = ''; 151 | 152 | var currentRow = 8; 153 | for (var i = 0; i < 8; i++) { 154 | for (var j = 0; j < 8; j++) { 155 | var square = COLUMNS[j] + currentRow; 156 | 157 | // piece exists 158 | if (obj.hasOwnProperty(square) === true) { 159 | fen += pieceCodeToFen(obj[square]); 160 | } 161 | 162 | // empty space 163 | else { 164 | fen += '1'; 165 | } 166 | } 167 | 168 | if (i !== 7) { 169 | fen += '/'; 170 | } 171 | 172 | currentRow--; 173 | } 174 | 175 | // squeeze the numbers together 176 | // haha, I love this solution... 177 | fen = fen.replace(/11111111/g, '8'); 178 | fen = fen.replace(/1111111/g, '7'); 179 | fen = fen.replace(/111111/g, '6'); 180 | fen = fen.replace(/11111/g, '5'); 181 | fen = fen.replace(/1111/g, '4'); 182 | fen = fen.replace(/111/g, '3'); 183 | fen = fen.replace(/11/g, '2'); 184 | 185 | return fen; 186 | } 187 | 188 | window['ChessBoard'] = window['ChessBoard'] || function(containerElOrId, cfg) { 189 | 190 | cfg = cfg || {}; 191 | 192 | //------------------------------------------------------------------------------ 193 | // Constants 194 | //------------------------------------------------------------------------------ 195 | 196 | var MINIMUM_JQUERY_VERSION = '1.7.0', 197 | START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 198 | START_POSITION = fenToObj(START_FEN); 199 | 200 | // use unique class names to prevent clashing with anything else on the page 201 | // and simplify selectors 202 | // NOTE: these should never change 203 | var CSS = { 204 | alpha: 'alpha-d2270', 205 | black: 'black-3c85d', 206 | board: 'board-b72b1', 207 | chessboard: 'chessboard-63f37', 208 | clearfix: 'clearfix-7da63', 209 | highlight1: 'highlight1-32417', 210 | highlight2: 'highlight2-9c5d2', 211 | notation: 'notation-322f9', 212 | numeric: 'numeric-fc462', 213 | piece: 'piece-417db', 214 | row: 'row-5277c', 215 | sparePieces: 'spare-pieces-7492f', 216 | sparePiecesBottom: 'spare-pieces-bottom-ae20f', 217 | sparePiecesTop: 'spare-pieces-top-4028b', 218 | square: 'square-55d63', 219 | white: 'white-1e1d7' 220 | }; 221 | 222 | //------------------------------------------------------------------------------ 223 | // Module Scope Variables 224 | //------------------------------------------------------------------------------ 225 | 226 | // DOM elements 227 | var containerEl, 228 | boardEl, 229 | draggedPieceEl, 230 | sparePiecesTopEl, 231 | sparePiecesBottomEl; 232 | 233 | // constructor return object 234 | var widget = {}; 235 | 236 | //------------------------------------------------------------------------------ 237 | // Stateful 238 | //------------------------------------------------------------------------------ 239 | 240 | var ANIMATION_HAPPENING = false, 241 | BOARD_BORDER_SIZE = 2, 242 | CURRENT_ORIENTATION = 'white', 243 | CURRENT_POSITION = {}, 244 | SQUARE_SIZE, 245 | DRAGGED_PIECE, 246 | DRAGGED_PIECE_LOCATION, 247 | DRAGGED_PIECE_SOURCE, 248 | DRAGGING_A_PIECE = false, 249 | SPARE_PIECE_ELS_IDS = {}, 250 | SQUARE_ELS_IDS = {}, 251 | SQUARE_ELS_OFFSETS; 252 | 253 | //------------------------------------------------------------------------------ 254 | // JS Util Functions 255 | //------------------------------------------------------------------------------ 256 | 257 | // http://tinyurl.com/3ttloxj 258 | function uuid() { 259 | return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) { 260 | var r = Math.random() * 16 | 0; 261 | return r.toString(16); 262 | }); 263 | } 264 | 265 | function deepCopy(thing) { 266 | return JSON.parse(JSON.stringify(thing)); 267 | } 268 | 269 | function parseSemVer(version) { 270 | var tmp = version.split('.'); 271 | return { 272 | major: parseInt(tmp[0], 10), 273 | minor: parseInt(tmp[1], 10), 274 | patch: parseInt(tmp[2], 10) 275 | }; 276 | } 277 | 278 | // returns true if version is >= minimum 279 | function compareSemVer(version, minimum) { 280 | version = parseSemVer(version); 281 | minimum = parseSemVer(minimum); 282 | 283 | var versionNum = (version.major * 10000 * 10000) + 284 | (version.minor * 10000) + version.patch; 285 | var minimumNum = (minimum.major * 10000 * 10000) + 286 | (minimum.minor * 10000) + minimum.patch; 287 | 288 | return (versionNum >= minimumNum); 289 | } 290 | 291 | //------------------------------------------------------------------------------ 292 | // Validation / Errors 293 | //------------------------------------------------------------------------------ 294 | 295 | function error(code, msg, obj) { 296 | // do nothing if showErrors is not set 297 | if (cfg.hasOwnProperty('showErrors') !== true || 298 | cfg.showErrors === false) { 299 | return; 300 | } 301 | 302 | var errorText = 'ChessBoard Error ' + code + ': ' + msg; 303 | 304 | // print to console 305 | if (cfg.showErrors === 'console' && 306 | typeof console === 'object' && 307 | typeof console.log === 'function') { 308 | console.log(errorText); 309 | if (arguments.length >= 2) { 310 | console.log(obj); 311 | } 312 | return; 313 | } 314 | 315 | // alert errors 316 | if (cfg.showErrors === 'alert') { 317 | if (obj) { 318 | errorText += '\n\n' + JSON.stringify(obj); 319 | } 320 | window.alert(errorText); 321 | return; 322 | } 323 | 324 | // custom function 325 | if (typeof cfg.showErrors === 'function') { 326 | cfg.showErrors(code, msg, obj); 327 | } 328 | } 329 | 330 | // check dependencies 331 | function checkDeps() { 332 | // if containerId is a string, it must be the ID of a DOM node 333 | if (typeof containerElOrId === 'string') { 334 | // cannot be empty 335 | if (containerElOrId === '') { 336 | window.alert('ChessBoard Error 1001: ' + 337 | 'The first argument to ChessBoard() cannot be an empty string.' + 338 | '\n\nExiting...'); 339 | return false; 340 | } 341 | 342 | // make sure the container element exists in the DOM 343 | var el = document.getElementById(containerElOrId); 344 | if (! el) { 345 | window.alert('ChessBoard Error 1002: Element with id "' + 346 | containerElOrId + '" does not exist in the DOM.' + 347 | '\n\nExiting...'); 348 | return false; 349 | } 350 | 351 | // set the containerEl 352 | containerEl = $(el); 353 | } 354 | 355 | // else it must be something that becomes a jQuery collection 356 | // with size 1 357 | // ie: a single DOM node or jQuery object 358 | else { 359 | containerEl = $(containerElOrId); 360 | 361 | if (containerEl.length !== 1) { 362 | window.alert('ChessBoard Error 1003: The first argument to ' + 363 | 'ChessBoard() must be an ID or a single DOM node.' + 364 | '\n\nExiting...'); 365 | return false; 366 | } 367 | } 368 | 369 | // JSON must exist 370 | if (! window.JSON || 371 | typeof JSON.stringify !== 'function' || 372 | typeof JSON.parse !== 'function') { 373 | window.alert('ChessBoard Error 1004: JSON does not exist. ' + 374 | 'Please include a JSON polyfill.\n\nExiting...'); 375 | return false; 376 | } 377 | 378 | // check for a compatible version of jQuery 379 | if (! (typeof window.$ && $.fn && $.fn.jquery && 380 | compareSemVer($.fn.jquery, MINIMUM_JQUERY_VERSION) === true)) { 381 | window.alert('ChessBoard Error 1005: Unable to find a valid version ' + 382 | 'of jQuery. Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or ' + 383 | 'higher on the page.\n\nExiting...'); 384 | return false; 385 | } 386 | 387 | return true; 388 | } 389 | 390 | function validAnimationSpeed(speed) { 391 | if (speed === 'fast' || speed === 'slow') { 392 | return true; 393 | } 394 | 395 | if ((parseInt(speed, 10) + '') !== (speed + '')) { 396 | return false; 397 | } 398 | 399 | return (speed >= 0); 400 | } 401 | 402 | // validate config / set default options 403 | function expandConfig() { 404 | if (typeof cfg === 'string' || validPositionObject(cfg) === true) { 405 | cfg = { 406 | position: cfg 407 | }; 408 | } 409 | 410 | // default for orientation is white 411 | if (cfg.orientation !== 'black') { 412 | cfg.orientation = 'white'; 413 | } 414 | CURRENT_ORIENTATION = cfg.orientation; 415 | 416 | // default for showNotation is true 417 | if (cfg.showNotation !== false) { 418 | cfg.showNotation = true; 419 | } 420 | 421 | // default for draggable is false 422 | if (cfg.draggable !== true) { 423 | cfg.draggable = false; 424 | } 425 | 426 | // default for dropOffBoard is 'snapback' 427 | if (cfg.dropOffBoard !== 'trash') { 428 | cfg.dropOffBoard = 'snapback'; 429 | } 430 | 431 | // default for sparePieces is false 432 | if (cfg.sparePieces !== true) { 433 | cfg.sparePieces = false; 434 | } 435 | 436 | // draggable must be true if sparePieces is enabled 437 | if (cfg.sparePieces === true) { 438 | cfg.draggable = true; 439 | } 440 | 441 | // default piece theme is wikipedia 442 | if (cfg.hasOwnProperty('pieceTheme') !== true || 443 | (typeof cfg.pieceTheme !== 'string' && 444 | typeof cfg.pieceTheme !== 'function')) { 445 | cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'; 446 | } 447 | 448 | // animation speeds 449 | if (cfg.hasOwnProperty('appearSpeed') !== true || 450 | validAnimationSpeed(cfg.appearSpeed) !== true) { 451 | cfg.appearSpeed = 200; 452 | } 453 | if (cfg.hasOwnProperty('moveSpeed') !== true || 454 | validAnimationSpeed(cfg.moveSpeed) !== true) { 455 | cfg.moveSpeed = 200; 456 | } 457 | if (cfg.hasOwnProperty('snapbackSpeed') !== true || 458 | validAnimationSpeed(cfg.snapbackSpeed) !== true) { 459 | cfg.snapbackSpeed = 50; 460 | } 461 | if (cfg.hasOwnProperty('snapSpeed') !== true || 462 | validAnimationSpeed(cfg.snapSpeed) !== true) { 463 | cfg.snapSpeed = 25; 464 | } 465 | if (cfg.hasOwnProperty('trashSpeed') !== true || 466 | validAnimationSpeed(cfg.trashSpeed) !== true) { 467 | cfg.trashSpeed = 100; 468 | } 469 | 470 | // make sure position is valid 471 | if (cfg.hasOwnProperty('position') === true) { 472 | if (cfg.position === 'start') { 473 | CURRENT_POSITION = deepCopy(START_POSITION); 474 | } 475 | 476 | else if (validFen(cfg.position) === true) { 477 | CURRENT_POSITION = fenToObj(cfg.position); 478 | } 479 | 480 | else if (validPositionObject(cfg.position) === true) { 481 | CURRENT_POSITION = deepCopy(cfg.position); 482 | } 483 | 484 | else { 485 | error(7263, 'Invalid value passed to config.position.', cfg.position); 486 | } 487 | } 488 | 489 | return true; 490 | } 491 | 492 | //------------------------------------------------------------------------------ 493 | // DOM Misc 494 | //------------------------------------------------------------------------------ 495 | 496 | // calculates square size based on the width of the container 497 | // got a little CSS black magic here, so let me explain: 498 | // get the width of the container element (could be anything), reduce by 1 for 499 | // fudge factor, and then keep reducing until we find an exact mod 8 for 500 | // our square size 501 | function calculateSquareSize() { 502 | var containerWidth = parseInt(containerEl.width(), 10); 503 | 504 | // defensive, prevent infinite loop 505 | if (! containerWidth || containerWidth <= 0) { 506 | return 0; 507 | } 508 | 509 | // pad one pixel 510 | var boardWidth = containerWidth - 1; 511 | 512 | while (boardWidth % 8 !== 0 && boardWidth > 0) { 513 | boardWidth--; 514 | } 515 | 516 | return (boardWidth / 8); 517 | } 518 | 519 | // create random IDs for elements 520 | function createElIds() { 521 | // squares on the board 522 | for (var i = 0; i < COLUMNS.length; i++) { 523 | for (var j = 1; j <= 8; j++) { 524 | var square = COLUMNS[i] + j; 525 | SQUARE_ELS_IDS[square] = square + '-' + uuid(); 526 | } 527 | } 528 | 529 | // spare pieces 530 | var pieces = 'KQRBNP'.split(''); 531 | for (var i = 0; i < pieces.length; i++) { 532 | var whitePiece = 'w' + pieces[i]; 533 | var blackPiece = 'b' + pieces[i]; 534 | SPARE_PIECE_ELS_IDS[whitePiece] = whitePiece + '-' + uuid(); 535 | SPARE_PIECE_ELS_IDS[blackPiece] = blackPiece + '-' + uuid(); 536 | } 537 | } 538 | 539 | //------------------------------------------------------------------------------ 540 | // Markup Building 541 | //------------------------------------------------------------------------------ 542 | 543 | function buildBoardContainer() { 544 | var html = '
'; 545 | 546 | if (cfg.sparePieces === true) { 547 | html += '
'; 549 | } 550 | 551 | html += '
'; 552 | 553 | if (cfg.sparePieces === true) { 554 | html += '
'; 556 | } 557 | 558 | html += '
'; 559 | 560 | return html; 561 | } 562 | 563 | /* 564 | var buildSquare = function(color, size, id) { 565 | var html = '
'; 568 | 569 | if (cfg.showNotation === true) { 570 | 571 | } 572 | 573 | html += '
'; 574 | 575 | return html; 576 | }; 577 | */ 578 | 579 | function buildBoard(orientation) { 580 | if (orientation !== 'black') { 581 | orientation = 'white'; 582 | } 583 | 584 | var html = ''; 585 | 586 | // algebraic notation / orientation 587 | var alpha = deepCopy(COLUMNS); 588 | var row = 8; 589 | if (orientation === 'black') { 590 | alpha.reverse(); 591 | row = 1; 592 | } 593 | 594 | var squareColor = 'white'; 595 | for (var i = 0; i < 8; i++) { 596 | html += '
'; 597 | for (var j = 0; j < 8; j++) { 598 | var square = alpha[j] + row; 599 | 600 | html += '
'; 605 | 606 | if (cfg.showNotation === true) { 607 | // alpha notation 608 | if ((orientation === 'white' && row === 1) || 609 | (orientation === 'black' && row === 8)) { 610 | html += '
' + 611 | alpha[j] + '
'; 612 | } 613 | 614 | // numeric notation 615 | if (j === 0) { 616 | html += '
' + 617 | row + '
'; 618 | } 619 | } 620 | 621 | html += '
'; // end .square 622 | 623 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 624 | } 625 | html += '
'; 626 | 627 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 628 | 629 | if (orientation === 'white') { 630 | row--; 631 | } 632 | else { 633 | row++; 634 | } 635 | } 636 | 637 | return html; 638 | } 639 | 640 | function buildPieceImgSrc(piece) { 641 | if (typeof cfg.pieceTheme === 'function') { 642 | return cfg.pieceTheme(piece); 643 | } 644 | 645 | if (typeof cfg.pieceTheme === 'string') { 646 | return cfg.pieceTheme.replace(/{piece}/g, piece); 647 | } 648 | 649 | // NOTE: this should never happen 650 | error(8272, 'Unable to build image source for cfg.pieceTheme.'); 651 | return ''; 652 | } 653 | 654 | function buildPiece(piece, hidden, id) { 655 | var html = ''; 668 | 669 | return html; 670 | } 671 | 672 | function buildSparePieces(color) { 673 | var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']; 674 | if (color === 'black') { 675 | pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']; 676 | } 677 | 678 | var html = ''; 679 | for (var i = 0; i < pieces.length; i++) { 680 | html += buildPiece(pieces[i], false, SPARE_PIECE_ELS_IDS[pieces[i]]); 681 | } 682 | 683 | return html; 684 | } 685 | 686 | //------------------------------------------------------------------------------ 687 | // Animations 688 | //------------------------------------------------------------------------------ 689 | 690 | function animateSquareToSquare(src, dest, piece, completeFn) { 691 | // get information about the source and destination squares 692 | var srcSquareEl = $('#' + SQUARE_ELS_IDS[src]); 693 | var srcSquarePosition = srcSquareEl.offset(); 694 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 695 | var destSquarePosition = destSquareEl.offset(); 696 | 697 | // create the animated piece and absolutely position it 698 | // over the source square 699 | var animatedPieceId = uuid(); 700 | $('body').append(buildPiece(piece, true, animatedPieceId)); 701 | var animatedPieceEl = $('#' + animatedPieceId); 702 | animatedPieceEl.css({ 703 | display: '', 704 | position: 'absolute', 705 | top: srcSquarePosition.top, 706 | left: srcSquarePosition.left 707 | }); 708 | 709 | // remove original piece from source square 710 | srcSquareEl.find('.' + CSS.piece).remove(); 711 | 712 | // on complete 713 | var complete = function() { 714 | // add the "real" piece to the destination square 715 | destSquareEl.append(buildPiece(piece)); 716 | 717 | // remove the animated piece 718 | animatedPieceEl.remove(); 719 | 720 | // run complete function 721 | if (typeof completeFn === 'function') { 722 | completeFn(); 723 | } 724 | }; 725 | 726 | // animate the piece to the destination square 727 | var opts = { 728 | duration: cfg.moveSpeed, 729 | complete: complete 730 | }; 731 | animatedPieceEl.animate(destSquarePosition, opts); 732 | } 733 | 734 | function animateSparePieceToSquare(piece, dest, completeFn) { 735 | var srcOffset = $('#' + SPARE_PIECE_ELS_IDS[piece]).offset(); 736 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 737 | var destOffset = destSquareEl.offset(); 738 | 739 | // create the animate piece 740 | var pieceId = uuid(); 741 | $('body').append(buildPiece(piece, true, pieceId)); 742 | var animatedPieceEl = $('#' + pieceId); 743 | animatedPieceEl.css({ 744 | display: '', 745 | position: 'absolute', 746 | left: srcOffset.left, 747 | top: srcOffset.top 748 | }); 749 | 750 | // on complete 751 | var complete = function() { 752 | // add the "real" piece to the destination square 753 | destSquareEl.find('.' + CSS.piece).remove(); 754 | destSquareEl.append(buildPiece(piece)); 755 | 756 | // remove the animated piece 757 | animatedPieceEl.remove(); 758 | 759 | // run complete function 760 | if (typeof completeFn === 'function') { 761 | completeFn(); 762 | } 763 | }; 764 | 765 | // animate the piece to the destination square 766 | var opts = { 767 | duration: cfg.moveSpeed, 768 | complete: complete 769 | }; 770 | animatedPieceEl.animate(destOffset, opts); 771 | } 772 | 773 | // execute an array of animations 774 | function doAnimations(a, oldPos, newPos) { 775 | if (a.length === 0) { 776 | return; 777 | } 778 | 779 | ANIMATION_HAPPENING = true; 780 | 781 | var numFinished = 0; 782 | function onFinish() { 783 | numFinished++; 784 | 785 | // exit if all the animations aren't finished 786 | if (numFinished !== a.length) return; 787 | 788 | drawPositionInstant(); 789 | ANIMATION_HAPPENING = false; 790 | 791 | // run their onMoveEnd function 792 | if (cfg.hasOwnProperty('onMoveEnd') === true && 793 | typeof cfg.onMoveEnd === 'function') { 794 | cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos)); 795 | } 796 | } 797 | 798 | for (var i = 0; i < a.length; i++) { 799 | // clear a piece 800 | if (a[i].type === 'clear') { 801 | $('#' + SQUARE_ELS_IDS[a[i].square] + ' .' + CSS.piece) 802 | .fadeOut(cfg.trashSpeed, onFinish); 803 | } 804 | 805 | // add a piece (no spare pieces) 806 | if (a[i].type === 'add' && cfg.sparePieces !== true) { 807 | $('#' + SQUARE_ELS_IDS[a[i].square]) 808 | .append(buildPiece(a[i].piece, true)) 809 | .find('.' + CSS.piece) 810 | .fadeIn(cfg.appearSpeed, onFinish); 811 | } 812 | 813 | // add a piece from a spare piece 814 | if (a[i].type === 'add' && cfg.sparePieces === true) { 815 | animateSparePieceToSquare(a[i].piece, a[i].square, onFinish); 816 | } 817 | 818 | // move a piece 819 | if (a[i].type === 'move') { 820 | animateSquareToSquare(a[i].source, a[i].destination, a[i].piece, 821 | onFinish); 822 | } 823 | } 824 | } 825 | 826 | // returns the distance between two squares 827 | function squareDistance(s1, s2) { 828 | s1 = s1.split(''); 829 | var s1x = COLUMNS.indexOf(s1[0]) + 1; 830 | var s1y = parseInt(s1[1], 10); 831 | 832 | s2 = s2.split(''); 833 | var s2x = COLUMNS.indexOf(s2[0]) + 1; 834 | var s2y = parseInt(s2[1], 10); 835 | 836 | var xDelta = Math.abs(s1x - s2x); 837 | var yDelta = Math.abs(s1y - s2y); 838 | 839 | if (xDelta >= yDelta) return xDelta; 840 | return yDelta; 841 | } 842 | 843 | // returns an array of closest squares from square 844 | function createRadius(square) { 845 | var squares = []; 846 | 847 | // calculate distance of all squares 848 | for (var i = 0; i < 8; i++) { 849 | for (var j = 0; j < 8; j++) { 850 | var s = COLUMNS[i] + (j + 1); 851 | 852 | // skip the square we're starting from 853 | if (square === s) continue; 854 | 855 | squares.push({ 856 | square: s, 857 | distance: squareDistance(square, s) 858 | }); 859 | } 860 | } 861 | 862 | // sort by distance 863 | squares.sort(function(a, b) { 864 | return a.distance - b.distance; 865 | }); 866 | 867 | // just return the square code 868 | var squares2 = []; 869 | for (var i = 0; i < squares.length; i++) { 870 | squares2.push(squares[i].square); 871 | } 872 | 873 | return squares2; 874 | } 875 | 876 | // returns the square of the closest instance of piece 877 | // returns false if no instance of piece is found in position 878 | function findClosestPiece(position, piece, square) { 879 | // create array of closest squares from square 880 | var closestSquares = createRadius(square); 881 | 882 | // search through the position in order of distance for the piece 883 | for (var i = 0; i < closestSquares.length; i++) { 884 | var s = closestSquares[i]; 885 | 886 | if (position.hasOwnProperty(s) === true && position[s] === piece) { 887 | return s; 888 | } 889 | } 890 | 891 | return false; 892 | } 893 | 894 | // calculate an array of animations that need to happen in order to get 895 | // from pos1 to pos2 896 | function calculateAnimations(pos1, pos2) { 897 | // make copies of both 898 | pos1 = deepCopy(pos1); 899 | pos2 = deepCopy(pos2); 900 | 901 | var animations = []; 902 | var squaresMovedTo = {}; 903 | 904 | // remove pieces that are the same in both positions 905 | for (var i in pos2) { 906 | if (pos2.hasOwnProperty(i) !== true) continue; 907 | 908 | if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) { 909 | delete pos1[i]; 910 | delete pos2[i]; 911 | } 912 | } 913 | 914 | // find all the "move" animations 915 | for (var i in pos2) { 916 | if (pos2.hasOwnProperty(i) !== true) continue; 917 | 918 | var closestPiece = findClosestPiece(pos1, pos2[i], i); 919 | if (closestPiece !== false) { 920 | animations.push({ 921 | type: 'move', 922 | source: closestPiece, 923 | destination: i, 924 | piece: pos2[i] 925 | }); 926 | 927 | delete pos1[closestPiece]; 928 | delete pos2[i]; 929 | squaresMovedTo[i] = true; 930 | } 931 | } 932 | 933 | // add pieces to pos2 934 | for (var i in pos2) { 935 | if (pos2.hasOwnProperty(i) !== true) continue; 936 | 937 | animations.push({ 938 | type: 'add', 939 | square: i, 940 | piece: pos2[i] 941 | }) 942 | 943 | delete pos2[i]; 944 | } 945 | 946 | // clear pieces from pos1 947 | for (var i in pos1) { 948 | if (pos1.hasOwnProperty(i) !== true) continue; 949 | 950 | // do not clear a piece if it is on a square that is the result 951 | // of a "move", ie: a piece capture 952 | if (squaresMovedTo.hasOwnProperty(i) === true) continue; 953 | 954 | animations.push({ 955 | type: 'clear', 956 | square: i, 957 | piece: pos1[i] 958 | }); 959 | 960 | delete pos1[i]; 961 | } 962 | 963 | return animations; 964 | } 965 | 966 | //------------------------------------------------------------------------------ 967 | // Control Flow 968 | //------------------------------------------------------------------------------ 969 | 970 | function drawPositionInstant() { 971 | // clear the board 972 | boardEl.find('.' + CSS.piece).remove(); 973 | 974 | // add the pieces 975 | for (var i in CURRENT_POSITION) { 976 | if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue; 977 | 978 | $('#' + SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i])); 979 | } 980 | } 981 | 982 | function drawBoard() { 983 | boardEl.html(buildBoard(CURRENT_ORIENTATION)); 984 | drawPositionInstant(); 985 | 986 | if (cfg.sparePieces === true) { 987 | if (CURRENT_ORIENTATION === 'white') { 988 | sparePiecesTopEl.html(buildSparePieces('black')); 989 | sparePiecesBottomEl.html(buildSparePieces('white')); 990 | } 991 | else { 992 | sparePiecesTopEl.html(buildSparePieces('white')); 993 | sparePiecesBottomEl.html(buildSparePieces('black')); 994 | } 995 | } 996 | } 997 | 998 | // given a position and a set of moves, return a new position 999 | // with the moves executed 1000 | function calculatePositionFromMoves(position, moves) { 1001 | position = deepCopy(position); 1002 | 1003 | for (var i in moves) { 1004 | if (moves.hasOwnProperty(i) !== true) continue; 1005 | 1006 | // skip the move if the position doesn't have a piece on the source square 1007 | if (position.hasOwnProperty(i) !== true) continue; 1008 | 1009 | var piece = position[i]; 1010 | delete position[i]; 1011 | position[moves[i]] = piece; 1012 | } 1013 | 1014 | return position; 1015 | } 1016 | 1017 | function setCurrentPosition(position) { 1018 | var oldPos = deepCopy(CURRENT_POSITION); 1019 | var newPos = deepCopy(position); 1020 | var oldFen = objToFen(oldPos); 1021 | var newFen = objToFen(newPos); 1022 | 1023 | // do nothing if no change in position 1024 | if (oldFen === newFen) return; 1025 | 1026 | // run their onChange function 1027 | if (cfg.hasOwnProperty('onChange') === true && 1028 | typeof cfg.onChange === 'function') { 1029 | cfg.onChange(oldPos, newPos); 1030 | } 1031 | 1032 | // update state 1033 | CURRENT_POSITION = position; 1034 | } 1035 | 1036 | function isXYOnSquare(x, y) { 1037 | for (var i in SQUARE_ELS_OFFSETS) { 1038 | if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue; 1039 | 1040 | var s = SQUARE_ELS_OFFSETS[i]; 1041 | if (x >= s.left && x < s.left + SQUARE_SIZE && 1042 | y >= s.top && y < s.top + SQUARE_SIZE) { 1043 | return i; 1044 | } 1045 | } 1046 | 1047 | return 'offboard'; 1048 | } 1049 | 1050 | // records the XY coords of every square into memory 1051 | function captureSquareOffsets() { 1052 | SQUARE_ELS_OFFSETS = {}; 1053 | 1054 | for (var i in SQUARE_ELS_IDS) { 1055 | if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue; 1056 | 1057 | SQUARE_ELS_OFFSETS[i] = $('#' + SQUARE_ELS_IDS[i]).offset(); 1058 | } 1059 | } 1060 | 1061 | function removeSquareHighlights() { 1062 | boardEl.find('.' + CSS.square) 1063 | .removeClass(CSS.highlight1 + ' ' + CSS.highlight2); 1064 | } 1065 | 1066 | function snapbackDraggedPiece() { 1067 | // there is no "snapback" for spare pieces 1068 | if (DRAGGED_PIECE_SOURCE === 'spare') { 1069 | trashDraggedPiece(); 1070 | return; 1071 | } 1072 | 1073 | removeSquareHighlights(); 1074 | 1075 | // animation complete 1076 | function complete() { 1077 | drawPositionInstant(); 1078 | draggedPieceEl.css('display', 'none'); 1079 | 1080 | // run their onSnapbackEnd function 1081 | if (cfg.hasOwnProperty('onSnapbackEnd') === true && 1082 | typeof cfg.onSnapbackEnd === 'function') { 1083 | cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE, 1084 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1085 | } 1086 | } 1087 | 1088 | // get source square position 1089 | var sourceSquarePosition = 1090 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]).offset(); 1091 | 1092 | // animate the piece to the target square 1093 | var opts = { 1094 | duration: cfg.snapbackSpeed, 1095 | complete: complete 1096 | }; 1097 | draggedPieceEl.animate(sourceSquarePosition, opts); 1098 | 1099 | // set state 1100 | DRAGGING_A_PIECE = false; 1101 | } 1102 | 1103 | function trashDraggedPiece() { 1104 | removeSquareHighlights(); 1105 | 1106 | // remove the source piece 1107 | var newPosition = deepCopy(CURRENT_POSITION); 1108 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1109 | setCurrentPosition(newPosition); 1110 | 1111 | // redraw the position 1112 | drawPositionInstant(); 1113 | 1114 | // hide the dragged piece 1115 | draggedPieceEl.fadeOut(cfg.trashSpeed); 1116 | 1117 | // set state 1118 | DRAGGING_A_PIECE = false; 1119 | } 1120 | 1121 | function dropDraggedPieceOnSquare(square) { 1122 | removeSquareHighlights(); 1123 | 1124 | // update position 1125 | var newPosition = deepCopy(CURRENT_POSITION); 1126 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1127 | newPosition[square] = DRAGGED_PIECE; 1128 | setCurrentPosition(newPosition); 1129 | 1130 | // get target square information 1131 | var targetSquarePosition = $('#' + SQUARE_ELS_IDS[square]).offset(); 1132 | 1133 | // animation complete 1134 | var complete = function() { 1135 | drawPositionInstant(); 1136 | draggedPieceEl.css('display', 'none'); 1137 | 1138 | // execute their onSnapEnd function 1139 | if (cfg.hasOwnProperty('onSnapEnd') === true && 1140 | typeof cfg.onSnapEnd === 'function') { 1141 | cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE); 1142 | } 1143 | }; 1144 | 1145 | // snap the piece to the target square 1146 | var opts = { 1147 | duration: cfg.snapSpeed, 1148 | complete: complete 1149 | }; 1150 | draggedPieceEl.animate(targetSquarePosition, opts); 1151 | 1152 | // set state 1153 | DRAGGING_A_PIECE = false; 1154 | } 1155 | 1156 | function beginDraggingPiece(source, piece, x, y) { 1157 | // run their custom onDragStart function 1158 | // their custom onDragStart function can cancel drag start 1159 | if (typeof cfg.onDragStart === 'function' && 1160 | cfg.onDragStart(source, piece, 1161 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) { 1162 | return; 1163 | } 1164 | 1165 | // set state 1166 | DRAGGING_A_PIECE = true; 1167 | DRAGGED_PIECE = piece; 1168 | DRAGGED_PIECE_SOURCE = source; 1169 | 1170 | // if the piece came from spare pieces, location is offboard 1171 | if (source === 'spare') { 1172 | DRAGGED_PIECE_LOCATION = 'offboard'; 1173 | } 1174 | else { 1175 | DRAGGED_PIECE_LOCATION = source; 1176 | } 1177 | 1178 | // capture the x, y coords of all squares in memory 1179 | captureSquareOffsets(); 1180 | 1181 | // create the dragged piece 1182 | draggedPieceEl.attr('src', buildPieceImgSrc(piece)) 1183 | .css({ 1184 | display: '', 1185 | position: 'absolute', 1186 | left: x - (SQUARE_SIZE / 2), 1187 | top: y - (SQUARE_SIZE / 2) 1188 | }); 1189 | 1190 | if (source !== 'spare') { 1191 | // highlight the source square and hide the piece 1192 | $('#' + SQUARE_ELS_IDS[source]).addClass(CSS.highlight1) 1193 | .find('.' + CSS.piece).css('display', 'none'); 1194 | } 1195 | } 1196 | 1197 | function updateDraggedPiece(x, y) { 1198 | // put the dragged piece over the mouse cursor 1199 | draggedPieceEl.css({ 1200 | left: x - (SQUARE_SIZE / 2), 1201 | top: y - (SQUARE_SIZE / 2) 1202 | }); 1203 | 1204 | // get location 1205 | var location = isXYOnSquare(x, y); 1206 | 1207 | // do nothing if the location has not changed 1208 | if (location === DRAGGED_PIECE_LOCATION) return; 1209 | 1210 | // remove highlight from previous square 1211 | if (validSquare(DRAGGED_PIECE_LOCATION) === true) { 1212 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION]) 1213 | .removeClass(CSS.highlight2); 1214 | } 1215 | 1216 | // add highlight to new square 1217 | if (validSquare(location) === true) { 1218 | $('#' + SQUARE_ELS_IDS[location]).addClass(CSS.highlight2); 1219 | } 1220 | 1221 | // run onDragMove 1222 | if (typeof cfg.onDragMove === 'function') { 1223 | cfg.onDragMove(location, DRAGGED_PIECE_LOCATION, 1224 | DRAGGED_PIECE_SOURCE, DRAGGED_PIECE, 1225 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1226 | } 1227 | 1228 | // update state 1229 | DRAGGED_PIECE_LOCATION = location; 1230 | } 1231 | 1232 | function stopDraggedPiece(location) { 1233 | // determine what the action should be 1234 | var action = 'drop'; 1235 | if (location === 'offboard' && cfg.dropOffBoard === 'snapback') { 1236 | action = 'snapback'; 1237 | } 1238 | if (location === 'offboard' && cfg.dropOffBoard === 'trash') { 1239 | action = 'trash'; 1240 | } 1241 | 1242 | // run their onDrop function, which can potentially change the drop action 1243 | if (cfg.hasOwnProperty('onDrop') === true && 1244 | typeof cfg.onDrop === 'function') { 1245 | var newPosition = deepCopy(CURRENT_POSITION); 1246 | 1247 | // source piece is a spare piece and position is off the board 1248 | //if (DRAGGED_PIECE_SOURCE === 'spare' && location === 'offboard') {...} 1249 | // position has not changed; do nothing 1250 | 1251 | // source piece is a spare piece and position is on the board 1252 | if (DRAGGED_PIECE_SOURCE === 'spare' && validSquare(location) === true) { 1253 | // add the piece to the board 1254 | newPosition[location] = DRAGGED_PIECE; 1255 | } 1256 | 1257 | // source piece was on the board and position is off the board 1258 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') { 1259 | // remove the piece from the board 1260 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1261 | } 1262 | 1263 | // source piece was on the board and position is on the board 1264 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && 1265 | validSquare(location) === true) { 1266 | // move the piece 1267 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1268 | newPosition[location] = DRAGGED_PIECE; 1269 | } 1270 | 1271 | var oldPosition = deepCopy(CURRENT_POSITION); 1272 | 1273 | var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE, 1274 | newPosition, oldPosition, CURRENT_ORIENTATION); 1275 | if (result === 'snapback' || result === 'trash') { 1276 | action = result; 1277 | } 1278 | } 1279 | 1280 | // do it! 1281 | if (action === 'snapback') { 1282 | snapbackDraggedPiece(); 1283 | } 1284 | else if (action === 'trash') { 1285 | trashDraggedPiece(); 1286 | } 1287 | else if (action === 'drop') { 1288 | dropDraggedPieceOnSquare(location); 1289 | } 1290 | } 1291 | 1292 | //------------------------------------------------------------------------------ 1293 | // Public Methods 1294 | //------------------------------------------------------------------------------ 1295 | 1296 | // clear the board 1297 | widget.clear = function(useAnimation) { 1298 | widget.position({}, useAnimation); 1299 | }; 1300 | 1301 | // remove the widget from the page 1302 | widget.destroy = function() { 1303 | // remove markup 1304 | containerEl.html(''); 1305 | draggedPieceEl.remove(); 1306 | 1307 | // remove event handlers 1308 | containerEl.unbind(); 1309 | }; 1310 | 1311 | // shorthand method to get the current FEN 1312 | widget.fen = function() { 1313 | return widget.position('fen'); 1314 | }; 1315 | 1316 | // flip orientation 1317 | widget.flip = function() { 1318 | return widget.orientation('flip'); 1319 | }; 1320 | 1321 | /* 1322 | // TODO: write this, GitHub Issue #5 1323 | widget.highlight = function() { 1324 | 1325 | }; 1326 | */ 1327 | 1328 | // move pieces 1329 | widget.move = function() { 1330 | // no need to throw an error here; just do nothing 1331 | if (arguments.length === 0) return; 1332 | 1333 | var useAnimation = true; 1334 | 1335 | // collect the moves into an object 1336 | var moves = {}; 1337 | for (var i = 0; i < arguments.length; i++) { 1338 | // any "false" to this function means no animations 1339 | if (arguments[i] === false) { 1340 | useAnimation = false; 1341 | continue; 1342 | } 1343 | 1344 | // skip invalid arguments 1345 | if (validMove(arguments[i]) !== true) { 1346 | error(2826, 'Invalid move passed to the move method.', arguments[i]); 1347 | continue; 1348 | } 1349 | 1350 | var tmp = arguments[i].split('-'); 1351 | moves[tmp[0]] = tmp[1]; 1352 | } 1353 | 1354 | // calculate position from moves 1355 | var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves); 1356 | 1357 | // update the board 1358 | widget.position(newPos, useAnimation); 1359 | 1360 | // return the new position object 1361 | return newPos; 1362 | }; 1363 | 1364 | widget.orientation = function(arg) { 1365 | // no arguments, return the current orientation 1366 | if (arguments.length === 0) { 1367 | return CURRENT_ORIENTATION; 1368 | } 1369 | 1370 | // set to white or black 1371 | if (arg === 'white' || arg === 'black') { 1372 | CURRENT_ORIENTATION = arg; 1373 | drawBoard(); 1374 | return CURRENT_ORIENTATION; 1375 | } 1376 | 1377 | // flip orientation 1378 | if (arg === 'flip') { 1379 | CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white'; 1380 | drawBoard(); 1381 | return CURRENT_ORIENTATION; 1382 | } 1383 | 1384 | error(5482, 'Invalid value passed to the orientation method.', arg); 1385 | }; 1386 | 1387 | widget.position = function(position, useAnimation) { 1388 | // no arguments, return the current position 1389 | if (arguments.length === 0) { 1390 | return deepCopy(CURRENT_POSITION); 1391 | } 1392 | 1393 | // get position as FEN 1394 | if (typeof position === 'string' && position.toLowerCase() === 'fen') { 1395 | return objToFen(CURRENT_POSITION); 1396 | } 1397 | 1398 | // default for useAnimations is true 1399 | if (useAnimation !== false) { 1400 | useAnimation = true; 1401 | } 1402 | 1403 | // start position 1404 | if (typeof position === 'string' && position.toLowerCase() === 'start') { 1405 | position = deepCopy(START_POSITION); 1406 | } 1407 | 1408 | // convert FEN to position object 1409 | if (validFen(position) === true) { 1410 | position = fenToObj(position); 1411 | } 1412 | 1413 | // validate position object 1414 | if (validPositionObject(position) !== true) { 1415 | error(6482, 'Invalid value passed to the position method.', position); 1416 | return; 1417 | } 1418 | 1419 | if (useAnimation === true) { 1420 | // start the animations 1421 | doAnimations(calculateAnimations(CURRENT_POSITION, position), 1422 | CURRENT_POSITION, position); 1423 | 1424 | // set the new position 1425 | setCurrentPosition(position); 1426 | } 1427 | // instant update 1428 | else { 1429 | setCurrentPosition(position); 1430 | drawPositionInstant(); 1431 | } 1432 | }; 1433 | 1434 | widget.resize = function() { 1435 | // calulate the new square size 1436 | SQUARE_SIZE = calculateSquareSize(); 1437 | 1438 | // set board width 1439 | boardEl.css('width', (SQUARE_SIZE * 8) + 'px'); 1440 | 1441 | // set drag piece size 1442 | draggedPieceEl.css({ 1443 | height: SQUARE_SIZE, 1444 | width: SQUARE_SIZE 1445 | }); 1446 | 1447 | // spare pieces 1448 | if (cfg.sparePieces === true) { 1449 | containerEl.find('.' + CSS.sparePieces) 1450 | .css('paddingLeft', (SQUARE_SIZE + BOARD_BORDER_SIZE) + 'px'); 1451 | } 1452 | 1453 | // redraw the board 1454 | drawBoard(); 1455 | }; 1456 | 1457 | // set the starting position 1458 | widget.start = function(useAnimation) { 1459 | widget.position('start', useAnimation); 1460 | }; 1461 | 1462 | //------------------------------------------------------------------------------ 1463 | // Browser Events 1464 | //------------------------------------------------------------------------------ 1465 | 1466 | function isTouchDevice() { 1467 | return ('ontouchstart' in document.documentElement); 1468 | } 1469 | 1470 | // reference: http://www.quirksmode.org/js/detect.html 1471 | function isMSIE() { 1472 | return (navigator && navigator.userAgent && 1473 | navigator.userAgent.search(/MSIE/) !== -1); 1474 | } 1475 | 1476 | function stopDefault(e) { 1477 | e.preventDefault(); 1478 | } 1479 | 1480 | function mousedownSquare(e) { 1481 | // do nothing if we're not draggable 1482 | if (cfg.draggable !== true) return; 1483 | 1484 | var square = $(this).attr('data-square'); 1485 | 1486 | // no piece on this square 1487 | if (validSquare(square) !== true || 1488 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1489 | return; 1490 | } 1491 | 1492 | beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY); 1493 | } 1494 | 1495 | function touchstartSquare(e) { 1496 | // do nothing if we're not draggable 1497 | if (cfg.draggable !== true) return; 1498 | 1499 | var square = $(this).attr('data-square'); 1500 | 1501 | // no piece on this square 1502 | if (validSquare(square) !== true || 1503 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1504 | return; 1505 | } 1506 | 1507 | e = e.originalEvent; 1508 | beginDraggingPiece(square, CURRENT_POSITION[square], 1509 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1510 | } 1511 | 1512 | function mousedownSparePiece(e) { 1513 | // do nothing if sparePieces is not enabled 1514 | if (cfg.sparePieces !== true) return; 1515 | 1516 | var piece = $(this).attr('data-piece'); 1517 | 1518 | beginDraggingPiece('spare', piece, e.pageX, e.pageY); 1519 | } 1520 | 1521 | function touchstartSparePiece(e) { 1522 | // do nothing if sparePieces is not enabled 1523 | if (cfg.sparePieces !== true) return; 1524 | 1525 | var piece = $(this).attr('data-piece'); 1526 | 1527 | e = e.originalEvent; 1528 | beginDraggingPiece('spare', piece, 1529 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1530 | } 1531 | 1532 | function mousemoveWindow(e) { 1533 | // do nothing if we are not dragging a piece 1534 | if (DRAGGING_A_PIECE !== true) return; 1535 | 1536 | updateDraggedPiece(e.pageX, e.pageY); 1537 | } 1538 | 1539 | function touchmoveWindow(e) { 1540 | // do nothing if we are not dragging a piece 1541 | if (DRAGGING_A_PIECE !== true) return; 1542 | 1543 | // prevent screen from scrolling 1544 | e.preventDefault(); 1545 | 1546 | updateDraggedPiece(e.originalEvent.changedTouches[0].pageX, 1547 | e.originalEvent.changedTouches[0].pageY); 1548 | } 1549 | 1550 | function mouseupWindow(e) { 1551 | // do nothing if we are not dragging a piece 1552 | if (DRAGGING_A_PIECE !== true) return; 1553 | 1554 | // get the location 1555 | var location = isXYOnSquare(e.pageX, e.pageY); 1556 | 1557 | stopDraggedPiece(location); 1558 | } 1559 | 1560 | function touchendWindow(e) { 1561 | // do nothing if we are not dragging a piece 1562 | if (DRAGGING_A_PIECE !== true) return; 1563 | 1564 | // get the location 1565 | var location = isXYOnSquare(e.originalEvent.changedTouches[0].pageX, 1566 | e.originalEvent.changedTouches[0].pageY); 1567 | 1568 | stopDraggedPiece(location); 1569 | } 1570 | 1571 | function mouseenterSquare(e) { 1572 | // do not fire this event if we are dragging a piece 1573 | // NOTE: this should never happen, but it's a safeguard 1574 | if (DRAGGING_A_PIECE !== false) return; 1575 | 1576 | if (cfg.hasOwnProperty('onMouseoverSquare') !== true || 1577 | typeof cfg.onMouseoverSquare !== 'function') return; 1578 | 1579 | // get the square 1580 | var square = $(e.currentTarget).attr('data-square'); 1581 | 1582 | // NOTE: this should never happen; defensive 1583 | if (validSquare(square) !== true) return; 1584 | 1585 | // get the piece on this square 1586 | var piece = false; 1587 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1588 | piece = CURRENT_POSITION[square]; 1589 | } 1590 | 1591 | // execute their function 1592 | cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION), 1593 | CURRENT_ORIENTATION); 1594 | } 1595 | 1596 | function mouseleaveSquare(e) { 1597 | // do not fire this event if we are dragging a piece 1598 | // NOTE: this should never happen, but it's a safeguard 1599 | if (DRAGGING_A_PIECE !== false) return; 1600 | 1601 | if (cfg.hasOwnProperty('onMouseoutSquare') !== true || 1602 | typeof cfg.onMouseoutSquare !== 'function') return; 1603 | 1604 | // get the square 1605 | var square = $(e.currentTarget).attr('data-square'); 1606 | 1607 | // NOTE: this should never happen; defensive 1608 | if (validSquare(square) !== true) return; 1609 | 1610 | // get the piece on this square 1611 | var piece = false; 1612 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1613 | piece = CURRENT_POSITION[square]; 1614 | } 1615 | 1616 | // execute their function 1617 | cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION), 1618 | CURRENT_ORIENTATION); 1619 | } 1620 | 1621 | //------------------------------------------------------------------------------ 1622 | // Initialization 1623 | //------------------------------------------------------------------------------ 1624 | 1625 | function addEvents() { 1626 | // prevent browser "image drag" 1627 | $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault); 1628 | 1629 | // mouse drag pieces 1630 | boardEl.on('mousedown', '.' + CSS.square, mousedownSquare); 1631 | containerEl.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, 1632 | mousedownSparePiece); 1633 | 1634 | // mouse enter / leave square 1635 | boardEl.on('mouseenter', '.' + CSS.square, mouseenterSquare) 1636 | .on('mouseleave', '.' + CSS.square, mouseleaveSquare); 1637 | 1638 | // IE doesn't like the events on the window object, but other browsers 1639 | // perform better that way 1640 | if (isMSIE() === true) { 1641 | // IE-specific prevent browser "image drag" 1642 | document.ondragstart = function() { return false; }; 1643 | 1644 | $('body').on('mousemove', mousemoveWindow) 1645 | .on('mouseup', mouseupWindow); 1646 | } 1647 | else { 1648 | $(window).on('mousemove', mousemoveWindow) 1649 | .on('mouseup', mouseupWindow); 1650 | } 1651 | 1652 | // touch drag pieces 1653 | if (isTouchDevice() === true) { 1654 | boardEl.on('touchstart', '.' + CSS.square, touchstartSquare); 1655 | containerEl.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, 1656 | touchstartSparePiece); 1657 | $(window).on('touchmove', touchmoveWindow) 1658 | .on('touchend', touchendWindow); 1659 | } 1660 | } 1661 | 1662 | function initDom() { 1663 | // create unique IDs for all the elements we will create 1664 | createElIds(); 1665 | 1666 | // build board and save it in memory 1667 | containerEl.html(buildBoardContainer()); 1668 | boardEl = containerEl.find('.' + CSS.board); 1669 | 1670 | if (cfg.sparePieces === true) { 1671 | sparePiecesTopEl = containerEl.find('.' + CSS.sparePiecesTop); 1672 | sparePiecesBottomEl = containerEl.find('.' + CSS.sparePiecesBottom); 1673 | } 1674 | 1675 | // create the drag piece 1676 | var draggedPieceId = uuid(); 1677 | $('body').append(buildPiece('wP', true, draggedPieceId)); 1678 | draggedPieceEl = $('#' + draggedPieceId); 1679 | 1680 | // get the border size 1681 | BOARD_BORDER_SIZE = parseInt(boardEl.css('borderLeftWidth'), 10); 1682 | 1683 | // set the size and draw the board 1684 | widget.resize(); 1685 | } 1686 | 1687 | function init() { 1688 | if (checkDeps() !== true || 1689 | expandConfig() !== true) return; 1690 | 1691 | initDom(); 1692 | addEvents(); 1693 | } 1694 | 1695 | // go time 1696 | init(); 1697 | 1698 | // return the widget object 1699 | return widget; 1700 | 1701 | }; // end window.ChessBoard 1702 | 1703 | // expose util functions 1704 | window.ChessBoard.fenToObj = fenToObj; 1705 | window.ChessBoard.objToFen = objToFen; 1706 | 1707 | })(); // end anonymous wrapper 1708 | -------------------------------------------------------------------------------- /vendor/chess/js/chess.js: -------------------------------------------------------------------------------- 1 | // downloaded from https://github.com/jhlywa/chess.js/blob/master/chess.js 2 | // on 27 July 2013 3 | // commit: eed8f8a3b96d99fcd570b7ced105e8415409a800 4 | 5 | 'use strict'; 6 | 7 | /* 8 | * Copyright (c) 2011, Jeff Hlywa (jhlywa@gmail.com) 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without 12 | * modification, are permitted provided that the following conditions are met: 13 | * 14 | * 1. Redistributions of source code must retain the above copyright notice, 15 | * this list of conditions and the following disclaimer. 16 | * 2. Redistributions in binary form must reproduce the above copyright notice, 17 | * this list of conditions and the following disclaimer in the documentation 18 | * and/or other materials provided with the distribution. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | * 32 | *----------------------------------------------------------------------------*/ 33 | 34 | window['Chess'] = window['Chess'] || function(fen) { 35 | //var Chess = function(fen) { 36 | 37 | /* jshint indent: false */ 38 | 39 | var BLACK = 'b'; 40 | var WHITE = 'w'; 41 | 42 | var EMPTY = -1; 43 | 44 | var PAWN = 'p'; 45 | var KNIGHT = 'n'; 46 | var BISHOP = 'b'; 47 | var ROOK = 'r'; 48 | var QUEEN = 'q'; 49 | var KING = 'k'; 50 | 51 | var SYMBOLS = 'pnbrqkPNBRQK'; 52 | 53 | var DEFAULT_POSITION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; 54 | 55 | var POSSIBLE_RESULTS = ['1-0', '0-1', '1/2-1/2', '*']; 56 | 57 | var PAWN_OFFSETS = { 58 | b: [16, 32, 17, 15], 59 | w: [-16, -32, -17, -15] 60 | }; 61 | 62 | var PIECE_OFFSETS = { 63 | n: [-18, -33, -31, -14, 18, 33, 31, 14], 64 | b: [-17, -15, 17, 15], 65 | r: [-16, 1, 16, -1], 66 | q: [-17, -16, -15, 1, 17, 16, 15, -1], 67 | k: [-17, -16, -15, 1, 17, 16, 15, -1] 68 | }; 69 | 70 | var ATTACKS = [ 71 | 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0, 72 | 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, 73 | 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, 74 | 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, 75 | 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, 76 | 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, 77 | 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, 78 | 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0, 79 | 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, 80 | 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, 81 | 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, 82 | 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, 83 | 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, 84 | 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, 85 | 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20 86 | ]; 87 | 88 | var RAYS = [ 89 | 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, 90 | 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, 91 | 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, 92 | 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, 93 | 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, 95 | 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, 96 | 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0, 97 | 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0, 98 | 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0, 99 | 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0, 100 | 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0, 101 | 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0, 102 | 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0, 103 | -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17 104 | ]; 105 | 106 | var SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 }; 107 | 108 | var FLAGS = { 109 | NORMAL: 'n', 110 | CAPTURE: 'c', 111 | BIG_PAWN: 'b', 112 | EP_CAPTURE: 'e', 113 | PROMOTION: 'p', 114 | KSIDE_CASTLE: 'k', 115 | QSIDE_CASTLE: 'q' 116 | }; 117 | 118 | var BITS = { 119 | NORMAL: 1, 120 | CAPTURE: 2, 121 | BIG_PAWN: 4, 122 | EP_CAPTURE: 8, 123 | PROMOTION: 16, 124 | KSIDE_CASTLE: 32, 125 | QSIDE_CASTLE: 64 126 | }; 127 | 128 | var RANK_1 = 7; 129 | var RANK_2 = 6; 130 | var RANK_3 = 5; 131 | var RANK_4 = 4; 132 | var RANK_5 = 3; 133 | var RANK_6 = 2; 134 | var RANK_7 = 1; 135 | var RANK_8 = 0; 136 | 137 | var SQUARES = { 138 | a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7, 139 | a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23, 140 | a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39, 141 | a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55, 142 | a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71, 143 | a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87, 144 | a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103, 145 | a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119 146 | }; 147 | 148 | var ROOKS = { 149 | w: [{square: SQUARES.a1, flag: BITS.QSIDE_CASTLE}, 150 | {square: SQUARES.h1, flag: BITS.KSIDE_CASTLE}], 151 | b: [{square: SQUARES.a8, flag: BITS.QSIDE_CASTLE}, 152 | {square: SQUARES.h8, flag: BITS.KSIDE_CASTLE}] 153 | }; 154 | 155 | var board = new Array(128); 156 | var kings = {w: EMPTY, b: EMPTY}; 157 | var turn = WHITE; 158 | var castling = {w: 0, b: 0}; 159 | var ep_square = EMPTY; 160 | var half_moves = 0; 161 | var move_number = 1; 162 | var history = []; 163 | var header = {}; 164 | 165 | /* if the user passes in a fen string, load it, else default to 166 | * starting position 167 | */ 168 | if (typeof fen === 'undefined') { 169 | load(DEFAULT_POSITION); 170 | } else { 171 | load(fen); 172 | } 173 | 174 | function clear() { 175 | board = new Array(128); 176 | kings = {w: EMPTY, b: EMPTY}; 177 | turn = WHITE; 178 | castling = {w: 0, b: 0}; 179 | ep_square = EMPTY; 180 | half_moves = 0; 181 | move_number = 1; 182 | history = []; 183 | header = {}; 184 | update_setup(generate_fen()); 185 | } 186 | 187 | function reset() { 188 | load(DEFAULT_POSITION); 189 | } 190 | 191 | function load(fen) { 192 | var tokens = fen.split(/\s+/); 193 | var position = tokens[0]; 194 | var square = 0; 195 | var valid = SYMBOLS + '12345678/'; 196 | 197 | if (!validate_fen(fen).valid) { 198 | return false; 199 | } 200 | 201 | clear(); 202 | 203 | for (var i = 0; i < position.length; i++) { 204 | var piece = position.charAt(i); 205 | 206 | if (piece === '/') { 207 | square += 8; 208 | } else if (is_digit(piece)) { 209 | square += parseInt(piece, 10); 210 | } else { 211 | var color = (piece < 'a') ? WHITE : BLACK; 212 | put({type: piece.toLowerCase(), color: color}, algebraic(square)); 213 | square++; 214 | } 215 | } 216 | 217 | turn = tokens[1]; 218 | 219 | if (tokens[2].indexOf('K') > -1) { 220 | castling.w |= BITS.KSIDE_CASTLE; 221 | } 222 | if (tokens[2].indexOf('Q') > -1) { 223 | castling.w |= BITS.QSIDE_CASTLE; 224 | } 225 | if (tokens[2].indexOf('k') > -1) { 226 | castling.b |= BITS.KSIDE_CASTLE; 227 | } 228 | if (tokens[2].indexOf('q') > -1) { 229 | castling.b |= BITS.QSIDE_CASTLE; 230 | } 231 | 232 | ep_square = (tokens[3] === '-') ? EMPTY : SQUARES[tokens[3]]; 233 | half_moves = parseInt(tokens[4], 10); 234 | move_number = parseInt(tokens[5], 10); 235 | 236 | update_setup(generate_fen()); 237 | 238 | return true; 239 | } 240 | 241 | function validate_fen(fen) { 242 | var errors = { 243 | 0: 'No errors.', 244 | 1: 'FEN string must contain six space-delimited fields.', 245 | 2: '6th field (move number) must be a positive integer.', 246 | 3: '5th field (half move counter) must be a non-negative integer.', 247 | 4: '4th field (en-passant square) is invalid.', 248 | 5: '3rd field (castling availability) is invalid.', 249 | 6: '2nd field (side to move) is invalid.', 250 | 7: '1st field (piece positions) does not contain 8 \'/\'-delimited rows.', 251 | 8: '1st field (piece positions) is invalid [consecutive numbers].', 252 | 9: '1st field (piece positions) is invalid [invalid piece].', 253 | 10: '1st field (piece positions) is invalid [row too large].', 254 | }; 255 | 256 | /* 1st criterion: 6 space-seperated fields? */ 257 | var tokens = fen.split(/\s+/); 258 | if (tokens.length !== 6) { 259 | return {valid: false, error_number: 1, error: errors[1]}; 260 | } 261 | 262 | /* 2nd criterion: move number field is a integer value > 0? */ 263 | if (isNaN(tokens[5]) || (parseInt(tokens[5], 10) <= 0)) { 264 | return {valid: false, error_number: 2, error: errors[2]}; 265 | } 266 | 267 | /* 3rd criterion: half move counter is an integer >= 0? */ 268 | if (isNaN(tokens[4]) || (parseInt(tokens[4], 10) < 0)) { 269 | return {valid: false, error_number: 3, error: errors[3]}; 270 | } 271 | 272 | /* 4th criterion: 4th field is a valid e.p.-string? */ 273 | if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) { 274 | return {valid: false, error_number: 4, error: errors[4]}; 275 | } 276 | 277 | /* 5th criterion: 3th field is a valid castle-string? */ 278 | if( !/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) { 279 | return {valid: false, error_number: 5, error: errors[5]}; 280 | } 281 | 282 | /* 6th criterion: 2nd field is "w" (white) or "b" (black)? */ 283 | if (!/^(w|b)$/.test(tokens[1])) { 284 | return {valid: false, error_number: 6, error: errors[6]}; 285 | } 286 | 287 | /* 7th criterion: 1st field contains 8 rows? */ 288 | var rows = tokens[0].split('/'); 289 | if (rows.length !== 8) { 290 | return {valid: false, error_number: 7, error: errors[7]}; 291 | } 292 | 293 | /* 8th criterion: every row is valid? */ 294 | for (var i = 0; i < rows.length; i++) { 295 | /* check for right sum of fields AND not two numbers in succession */ 296 | var sum_fields = 0; 297 | var previous_was_number = false; 298 | 299 | for (var k = 0; k < rows[i].length; k++) { 300 | if (!isNaN(rows[i][k])) { 301 | if (previous_was_number) { 302 | return {valid: false, error_number: 8, error: errors[8]}; 303 | } 304 | sum_fields += parseInt(rows[i][k], 10); 305 | previous_was_number = true; 306 | } else { 307 | if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) { 308 | return {valid: false, error_number: 9, error: errors[9]}; 309 | } 310 | sum_fields += 1; 311 | previous_was_number = false; 312 | } 313 | } 314 | if (sum_fields !== 8) { 315 | return {valid: false, error_number: 10, error: errors[10]}; 316 | } 317 | } 318 | 319 | /* everything's okay! */ 320 | return {valid: true, error_number: 0, error: errors[0]}; 321 | } 322 | 323 | function generate_fen() { 324 | var empty = 0; 325 | var fen = ''; 326 | 327 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 328 | if (board[i] == null) { 329 | empty++; 330 | } else { 331 | if (empty > 0) { 332 | fen += empty; 333 | empty = 0; 334 | } 335 | var color = board[i].color; 336 | var piece = board[i].type; 337 | 338 | fen += (color === WHITE) ? 339 | piece.toUpperCase() : piece.toLowerCase(); 340 | } 341 | 342 | if ((i + 1) & 0x88) { 343 | if (empty > 0) { 344 | fen += empty; 345 | } 346 | 347 | if (i !== SQUARES.h1) { 348 | fen += '/'; 349 | } 350 | 351 | empty = 0; 352 | i += 8; 353 | } 354 | } 355 | 356 | var cflags = ''; 357 | if (castling[WHITE] & BITS.KSIDE_CASTLE) { cflags += 'K'; } 358 | if (castling[WHITE] & BITS.QSIDE_CASTLE) { cflags += 'Q'; } 359 | if (castling[BLACK] & BITS.KSIDE_CASTLE) { cflags += 'k'; } 360 | if (castling[BLACK] & BITS.QSIDE_CASTLE) { cflags += 'q'; } 361 | 362 | /* do we have an empty castling flag? */ 363 | cflags = cflags || '-'; 364 | var epflags = (ep_square === EMPTY) ? '-' : algebraic(ep_square); 365 | 366 | return [fen, turn, cflags, epflags, half_moves, move_number].join(' '); 367 | } 368 | 369 | function set_header(args) { 370 | for (var i = 0; i < args.length; i += 2) { 371 | if (typeof args[i] === 'string' && 372 | typeof args[i + 1] === 'string') { 373 | header[args[i]] = args[i + 1]; 374 | } 375 | } 376 | return header; 377 | } 378 | 379 | /* called when the initial board setup is changed with put() or remove(). 380 | * modifies the SetUp and FEN properties of the header object. if the FEN is 381 | * equal to the default position, the SetUp and FEN are deleted 382 | * the setup is only updated if history.length is zero, ie moves haven't been 383 | * made. 384 | */ 385 | function update_setup(fen) { 386 | if (history.length > 0) return; 387 | 388 | if (fen !== DEFAULT_POSITION) { 389 | header['SetUp'] = fen; 390 | header['FEN'] = '1'; 391 | } else { 392 | delete header['SetUp']; 393 | delete header['FEN']; 394 | } 395 | } 396 | 397 | function get(square) { 398 | var piece = board[SQUARES[square]]; 399 | return (piece) ? {type: piece.type, color: piece.color} : null; 400 | } 401 | 402 | function put(piece, square) { 403 | /* check for valid piece object */ 404 | if (!('type' in piece && 'color' in piece)) { 405 | return false; 406 | } 407 | 408 | /* check for piece */ 409 | if (SYMBOLS.indexOf(piece.type.toLowerCase()) === -1) { 410 | return false; 411 | } 412 | 413 | /* check for valid square */ 414 | if (!(square in SQUARES)) { 415 | return false; 416 | } 417 | 418 | var sq = SQUARES[square]; 419 | board[sq] = {type: piece.type, color: piece.color}; 420 | if (piece.type === KING) { 421 | kings[piece.color] = sq; 422 | } 423 | 424 | update_setup(generate_fen()); 425 | 426 | return true; 427 | } 428 | 429 | function remove(square) { 430 | var piece = get(square); 431 | board[SQUARES[square]] = null; 432 | if (piece && piece.type === KING) { 433 | kings[piece.color] = EMPTY; 434 | } 435 | 436 | update_setup(generate_fen()); 437 | 438 | return piece; 439 | } 440 | 441 | function build_move(board, from, to, flags, promotion) { 442 | var move = { 443 | color: turn, 444 | from: from, 445 | to: to, 446 | flags: flags, 447 | piece: board[from].type 448 | }; 449 | 450 | if (promotion) { 451 | move.flags |= BITS.PROMOTION; 452 | move.promotion = promotion; 453 | } 454 | 455 | if (board[to]) { 456 | move.captured = board[to].type; 457 | } else if (flags & BITS.EP_CAPTURE) { 458 | move.captured = PAWN; 459 | } 460 | return move; 461 | } 462 | 463 | function generate_moves(options) { 464 | function add_move(board, moves, from, to, flags) { 465 | /* if pawn promotion */ 466 | if (board[from].type === PAWN && 467 | (rank(to) === RANK_8 || rank(to) === RANK_1)) { 468 | var pieces = [QUEEN, ROOK, BISHOP, KNIGHT]; 469 | for (var i = 0, len = pieces.length; i < len; i++) { 470 | moves.push(build_move(board, from, to, flags, pieces[i])); 471 | } 472 | } else { 473 | moves.push(build_move(board, from, to, flags)); 474 | } 475 | } 476 | 477 | var moves = []; 478 | var us = turn; 479 | var them = swap_color(us); 480 | var second_rank = {b: RANK_7, w: RANK_2}; 481 | 482 | var first_sq = SQUARES.a8; 483 | var last_sq = SQUARES.h1; 484 | var single_square = false; 485 | 486 | /* do we want legal moves? */ 487 | var legal = (typeof options !== 'undefined' && 'legal' in options) ? 488 | options.legal : true; 489 | 490 | /* are we generating moves for a single square? */ 491 | if (typeof options !== 'undefined' && 'square' in options) { 492 | if (options.square in SQUARES) { 493 | first_sq = last_sq = SQUARES[options.square]; 494 | single_square = true; 495 | } else { 496 | /* invalid square */ 497 | return []; 498 | } 499 | } 500 | 501 | for (var i = first_sq; i <= last_sq; i++) { 502 | /* did we run off the end of the board */ 503 | if (i & 0x88) { i += 7; continue; } 504 | 505 | var piece = board[i]; 506 | if (piece == null || piece.color !== us) { 507 | continue; 508 | } 509 | 510 | if (piece.type === PAWN) { 511 | /* single square, non-capturing */ 512 | var square = i + PAWN_OFFSETS[us][0]; 513 | if (board[square] == null) { 514 | add_move(board, moves, i, square, BITS.NORMAL); 515 | 516 | /* double square */ 517 | var square = i + PAWN_OFFSETS[us][1]; 518 | if (second_rank[us] === rank(i) && board[square] == null) { 519 | add_move(board, moves, i, square, BITS.BIG_PAWN); 520 | } 521 | } 522 | 523 | /* pawn captures */ 524 | for (j = 2; j < 4; j++) { 525 | var square = i + PAWN_OFFSETS[us][j]; 526 | if (square & 0x88) continue; 527 | 528 | if (board[square] != null && 529 | board[square].color === them) { 530 | add_move(board, moves, i, square, BITS.CAPTURE); 531 | } else if (square === ep_square) { 532 | add_move(board, moves, i, ep_square, BITS.EP_CAPTURE); 533 | } 534 | } 535 | } else { 536 | for (var j = 0, len = PIECE_OFFSETS[piece.type].length; j < len; j++) { 537 | var offset = PIECE_OFFSETS[piece.type][j]; 538 | var square = i; 539 | 540 | while (true) { 541 | square += offset; 542 | if (square & 0x88) break; 543 | 544 | if (board[square] == null) { 545 | add_move(board, moves, i, square, BITS.NORMAL); 546 | } else { 547 | if (board[square].color === us) break; 548 | add_move(board, moves, i, square, BITS.CAPTURE); 549 | break; 550 | } 551 | 552 | /* break, if knight or king */ 553 | if (piece.type === 'n' || piece.type === 'k') break; 554 | } 555 | } 556 | } 557 | } 558 | 559 | /* check for castling if: a) we're generating all moves, or b) we're doing 560 | * single square move generation on the king's square 561 | */ 562 | if ((!single_square) || last_sq === kings[us]) { 563 | /* king-side castling */ 564 | if (castling[us] & BITS.KSIDE_CASTLE) { 565 | var castling_from = kings[us]; 566 | var castling_to = castling_from + 2; 567 | 568 | if (board[castling_from + 1] == null && 569 | board[castling_to] == null && 570 | !attacked(them, kings[us]) && 571 | !attacked(them, castling_from + 1) && 572 | !attacked(them, castling_to)) { 573 | add_move(board, moves, kings[us] , castling_to, 574 | BITS.KSIDE_CASTLE); 575 | } 576 | } 577 | 578 | /* queen-side castling */ 579 | if (castling[us] & BITS.QSIDE_CASTLE) { 580 | var castling_from = kings[us]; 581 | var castling_to = castling_from - 2; 582 | 583 | if (board[castling_from - 1] == null && 584 | board[castling_from - 2] == null && 585 | board[castling_from - 3] == null && 586 | !attacked(them, kings[us]) && 587 | !attacked(them, castling_from - 1) && 588 | !attacked(them, castling_to)) { 589 | add_move(board, moves, kings[us], castling_to, 590 | BITS.QSIDE_CASTLE); 591 | } 592 | } 593 | } 594 | 595 | /* return all pseudo-legal moves (this includes moves that allow the king 596 | * to be captured) 597 | */ 598 | if (!legal) { 599 | return moves; 600 | } 601 | 602 | /* filter out illegal moves */ 603 | var legal_moves = []; 604 | for (var i = 0, len = moves.length; i < len; i++) { 605 | make_move(moves[i]); 606 | if (!king_attacked(us)) { 607 | legal_moves.push(moves[i]); 608 | } 609 | undo_move(); 610 | } 611 | 612 | return legal_moves; 613 | } 614 | 615 | /* convert a move from 0x88 coordinates to Standard Algebraic Notation 616 | * (SAN) 617 | */ 618 | function move_to_san(move) { 619 | var output = ''; 620 | 621 | if (move.flags & BITS.KSIDE_CASTLE) { 622 | output = 'O-O'; 623 | } else if (move.flags & BITS.QSIDE_CASTLE) { 624 | output = 'O-O-O'; 625 | } else { 626 | var disambiguator = get_disambiguator(move); 627 | 628 | if (move.piece !== PAWN) { 629 | output += move.piece.toUpperCase() + disambiguator; 630 | } 631 | 632 | if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { 633 | if (move.piece === PAWN) { 634 | output += algebraic(move.from)[0]; 635 | } 636 | output += 'x'; 637 | } 638 | 639 | output += algebraic(move.to); 640 | 641 | if (move.flags & BITS.PROMOTION) { 642 | output += '=' + move.promotion.toUpperCase(); 643 | } 644 | } 645 | 646 | make_move(move); 647 | if (in_check()) { 648 | if (in_checkmate()) { 649 | output += '#'; 650 | } else { 651 | output += '+'; 652 | } 653 | } 654 | undo_move(); 655 | 656 | return output; 657 | } 658 | 659 | function attacked(color, square) { 660 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 661 | /* did we run off the end of the board */ 662 | if (i & 0x88) { i += 7; continue; } 663 | 664 | /* if empty square or wrong color */ 665 | if (board[i] == null || board[i].color !== color) continue; 666 | 667 | var piece = board[i]; 668 | var difference = i - square; 669 | var index = difference + 119; 670 | 671 | if (ATTACKS[index] & (1 << SHIFTS[piece.type])) { 672 | if (piece.type === PAWN) { 673 | if (difference > 0) { 674 | if (piece.color === WHITE) return true; 675 | } else { 676 | if (piece.color === BLACK) return true; 677 | } 678 | continue; 679 | } 680 | 681 | /* if the piece is a knight or a king */ 682 | if (piece.type === 'n' || piece.type === 'k') return true; 683 | 684 | var offset = RAYS[index]; 685 | var j = i + offset; 686 | 687 | var blocked = false; 688 | while (j !== square) { 689 | if (board[j] != null) { blocked = true; break; } 690 | j += offset; 691 | } 692 | 693 | if (!blocked) return true; 694 | } 695 | } 696 | 697 | return false; 698 | } 699 | 700 | function king_attacked(color) { 701 | return attacked(swap_color(color), kings[color]); 702 | } 703 | 704 | function in_check() { 705 | return king_attacked(turn); 706 | } 707 | 708 | function in_checkmate() { 709 | return in_check() && generate_moves().length === 0; 710 | } 711 | 712 | function in_stalemate() { 713 | return !in_check() && generate_moves().length === 0; 714 | } 715 | 716 | function insufficient_material() { 717 | var pieces = {}; 718 | var bishops = []; 719 | var num_pieces = 0; 720 | var sq_color = 0; 721 | 722 | for (var i = SQUARES.a8; i<= SQUARES.h1; i++) { 723 | sq_color = (sq_color + 1) % 2; 724 | if (i & 0x88) { i += 7; continue; } 725 | 726 | var piece = board[i]; 727 | if (piece) { 728 | pieces[piece.type] = (piece.type in pieces) ? 729 | pieces[piece.type] + 1 : 1; 730 | if (piece.type === BISHOP) { 731 | bishops.push(sq_color); 732 | } 733 | num_pieces++; 734 | } 735 | } 736 | 737 | /* k vs. k */ 738 | if (num_pieces === 2) { return true; } 739 | 740 | /* k vs. kn .... or .... k vs. kb */ 741 | else if (num_pieces === 3 && (pieces[BISHOP] === 1 || 742 | pieces[KNIGHT] === 1)) { return true; } 743 | 744 | /* kb vs. kb where any number of bishops are all on the same color */ 745 | else if (num_pieces === pieces[BISHOP] + 2) { 746 | var sum = 0; 747 | var len = bishops.length; 748 | for (var i = 0; i < len; i++) { 749 | sum += bishops[i]; 750 | } 751 | if (sum === 0 || sum === len) { return true; } 752 | } 753 | 754 | return false; 755 | } 756 | 757 | function in_threefold_repetition() { 758 | /* TODO: while this function is fine for casual use, a better 759 | * implementation would use a Zobrist key (instead of FEN). the 760 | * Zobrist key would be maintained in the make_move/undo_move functions, 761 | * avoiding the costly that we do below. 762 | */ 763 | var moves = []; 764 | var positions = {}; 765 | var repetition = false; 766 | 767 | while (true) { 768 | var move = undo_move(); 769 | if (!move) break; 770 | moves.push(move); 771 | } 772 | 773 | while (true) { 774 | /* remove the last two fields in the FEN string, they're not needed 775 | * when checking for draw by rep */ 776 | var fen = generate_fen().split(' ').slice(0,4).join(' '); 777 | 778 | /* has the position occurred three or move times */ 779 | positions[fen] = (fen in positions) ? positions[fen] + 1 : 1; 780 | if (positions[fen] >= 3) { 781 | repetition = true; 782 | } 783 | 784 | if (!moves.length) { 785 | break; 786 | } 787 | make_move(moves.pop()); 788 | } 789 | 790 | return repetition; 791 | } 792 | 793 | function push(move) { 794 | history.push({ 795 | move: move, 796 | kings: {b: kings.b, w: kings.w}, 797 | turn: turn, 798 | castling: {b: castling.b, w: castling.w}, 799 | ep_square: ep_square, 800 | half_moves: half_moves, 801 | move_number: move_number 802 | }); 803 | } 804 | 805 | function make_move(move) { 806 | var us = turn; 807 | var them = swap_color(us); 808 | push(move); 809 | 810 | board[move.to] = board[move.from]; 811 | board[move.from] = null; 812 | 813 | /* if ep capture, remove the captured pawn */ 814 | if (move.flags & BITS.EP_CAPTURE) { 815 | if (turn === BLACK) { 816 | board[move.to - 16] = null; 817 | } else { 818 | board[move.to + 16] = null; 819 | } 820 | } 821 | 822 | /* if pawn promotion, replace with new piece */ 823 | if (move.flags & BITS.PROMOTION) { 824 | board[move.to] = {type: move.promotion, color: us}; 825 | } 826 | 827 | /* if we moved the king */ 828 | if (board[move.to].type === KING) { 829 | kings[board[move.to].color] = move.to; 830 | 831 | /* if we castled, move the rook next to the king */ 832 | if (move.flags & BITS.KSIDE_CASTLE) { 833 | var castling_to = move.to - 1; 834 | var castling_from = move.to + 1; 835 | board[castling_to] = board[castling_from]; 836 | board[castling_from] = null; 837 | } else if (move.flags & BITS.QSIDE_CASTLE) { 838 | var castling_to = move.to + 1; 839 | var castling_from = move.to - 2; 840 | board[castling_to] = board[castling_from]; 841 | board[castling_from] = null; 842 | } 843 | 844 | /* turn off castling */ 845 | castling[us] = ''; 846 | } 847 | 848 | /* turn off castling if we move a rook */ 849 | if (castling[us]) { 850 | for (var i = 0, len = ROOKS[us].length; i < len; i++) { 851 | if (move.from === ROOKS[us][i].square && 852 | castling[us] & ROOKS[us][i].flag) { 853 | castling[us] ^= ROOKS[us][i].flag; 854 | break; 855 | } 856 | } 857 | } 858 | 859 | /* turn off castling if we capture a rook */ 860 | if (castling[them]) { 861 | for (var i = 0, len = ROOKS[them].length; i < len; i++) { 862 | if (move.to === ROOKS[them][i].square && 863 | castling[them] & ROOKS[them][i].flag) { 864 | castling[them] ^= ROOKS[them][i].flag; 865 | break; 866 | } 867 | } 868 | } 869 | 870 | /* if big pawn move, update the en passant square */ 871 | if (move.flags & BITS.BIG_PAWN) { 872 | if (turn === 'b') { 873 | ep_square = move.to - 16; 874 | } else { 875 | ep_square = move.to + 16; 876 | } 877 | } else { 878 | ep_square = EMPTY; 879 | } 880 | 881 | /* reset the 50 move counter if a pawn is moved or a piece is captured */ 882 | if (move.piece === PAWN) { 883 | half_moves = 0; 884 | } else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { 885 | half_moves = 0; 886 | } else { 887 | half_moves++; 888 | } 889 | 890 | if (turn === BLACK) { 891 | move_number++; 892 | } 893 | turn = swap_color(turn); 894 | } 895 | 896 | function undo_move() { 897 | var old = history.pop(); 898 | if (old == null) { return null; } 899 | 900 | var move = old.move; 901 | kings = old.kings; 902 | turn = old.turn; 903 | castling = old.castling; 904 | ep_square = old.ep_square; 905 | half_moves = old.half_moves; 906 | move_number = old.move_number; 907 | 908 | var us = turn; 909 | var them = swap_color(turn); 910 | 911 | board[move.from] = board[move.to]; 912 | board[move.from].type = move.piece; // to undo any promotions 913 | board[move.to] = null; 914 | 915 | if (move.flags & BITS.CAPTURE) { 916 | board[move.to] = {type: move.captured, color: them}; 917 | } else if (move.flags & BITS.EP_CAPTURE) { 918 | var index; 919 | if (us === BLACK) { 920 | index = move.to - 16; 921 | } else { 922 | index = move.to + 16; 923 | } 924 | board[index] = {type: PAWN, color: them}; 925 | } 926 | 927 | 928 | if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { 929 | var castling_to, castling_from; 930 | if (move.flags & BITS.KSIDE_CASTLE) { 931 | castling_to = move.to + 1; 932 | castling_from = move.to - 1; 933 | } else if (move.flags & BITS.QSIDE_CASTLE) { 934 | castling_to = move.to - 2; 935 | castling_from = move.to + 1; 936 | } 937 | 938 | board[castling_to] = board[castling_from]; 939 | board[castling_from] = null; 940 | } 941 | 942 | return move; 943 | } 944 | 945 | /* this function is used to uniquely identify ambiguous moves */ 946 | function get_disambiguator(move) { 947 | var moves = generate_moves(); 948 | 949 | var from = move.from; 950 | var to = move.to; 951 | var piece = move.piece; 952 | 953 | var ambiguities = 0; 954 | var same_rank = 0; 955 | var same_file = 0; 956 | 957 | for (var i = 0, len = moves.length; i < len; i++) { 958 | var ambig_from = moves[i].from; 959 | var ambig_to = moves[i].to; 960 | var ambig_piece = moves[i].piece; 961 | 962 | /* if a move of the same piece type ends on the same to square, we'll 963 | * need to add a disambiguator to the algebraic notation 964 | */ 965 | if (piece === ambig_piece && from !== ambig_from && to === ambig_to) { 966 | ambiguities++; 967 | 968 | if (rank(from) === rank(ambig_from)) { 969 | same_rank++; 970 | } 971 | 972 | if (file(from) === file(ambig_from)) { 973 | same_file++; 974 | } 975 | } 976 | } 977 | 978 | if (ambiguities > 0) { 979 | /* if there exists a similar moving piece on the same rank and file as 980 | * the move in question, use the square as the disambiguator 981 | */ 982 | if (same_rank > 0 && same_file > 0) { 983 | return algebraic(from); 984 | } 985 | /* if the moving piece rests on the same file, use the rank symbol as the 986 | * disambiguator 987 | */ 988 | else if (same_file > 0) { 989 | return algebraic(from).charAt(1); 990 | } 991 | /* else use the file symbol */ 992 | else { 993 | return algebraic(from).charAt(0); 994 | } 995 | } 996 | 997 | return ''; 998 | } 999 | 1000 | function ascii() { 1001 | var s = ' +------------------------+\n'; 1002 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 1003 | /* display the rank */ 1004 | if (file(i) === 0) { 1005 | s += ' ' + '87654321'[rank(i)] + ' |'; 1006 | } 1007 | 1008 | /* empty piece */ 1009 | if (board[i] == null) { 1010 | s += ' . '; 1011 | } else { 1012 | var piece = board[i].type; 1013 | var color = board[i].color; 1014 | var symbol = (color === WHITE) ? 1015 | piece.toUpperCase() : piece.toLowerCase(); 1016 | s += ' ' + symbol + ' '; 1017 | } 1018 | 1019 | if ((i + 1) & 0x88) { 1020 | s += '|\n'; 1021 | i += 8; 1022 | } 1023 | } 1024 | s += ' +------------------------+\n'; 1025 | s += ' a b c d e f g h\n'; 1026 | 1027 | return s; 1028 | } 1029 | 1030 | /***************************************************************************** 1031 | * UTILITY FUNCTIONS 1032 | ****************************************************************************/ 1033 | function rank(i) { 1034 | return i >> 4; 1035 | } 1036 | 1037 | function file(i) { 1038 | return i & 15; 1039 | } 1040 | 1041 | function algebraic(i){ 1042 | var f = file(i), r = rank(i); 1043 | return 'abcdefgh'.substring(f,f+1) + '87654321'.substring(r,r+1); 1044 | } 1045 | 1046 | function swap_color(c) { 1047 | return c === WHITE ? BLACK : WHITE; 1048 | } 1049 | 1050 | function is_digit(c) { 1051 | return '0123456789'.indexOf(c) !== -1; 1052 | } 1053 | 1054 | /* pretty = external move object */ 1055 | function make_pretty(ugly_move) { 1056 | var move = clone(ugly_move); 1057 | move.san = move_to_san(move); 1058 | move.to = algebraic(move.to); 1059 | move.from = algebraic(move.from); 1060 | 1061 | var flags = ''; 1062 | 1063 | for (var flag in BITS) { 1064 | if (BITS[flag] & move.flags) { 1065 | flags += FLAGS[flag]; 1066 | } 1067 | } 1068 | move.flags = flags; 1069 | 1070 | return move; 1071 | } 1072 | 1073 | function clone(obj) { 1074 | var dupe = (obj instanceof Array) ? [] : {}; 1075 | 1076 | for (var property in obj) { 1077 | if (typeof property === 'object') { 1078 | dupe[property] = clone(obj[property]); 1079 | } else { 1080 | dupe[property] = obj[property]; 1081 | } 1082 | } 1083 | 1084 | return dupe; 1085 | } 1086 | 1087 | function trim(str) { 1088 | return str.replace(/^\s+|\s+$/g, ''); 1089 | } 1090 | 1091 | /***************************************************************************** 1092 | * DEBUGGING UTILITIES 1093 | ****************************************************************************/ 1094 | function perft(depth) { 1095 | var moves = generate_moves({legal: false}); 1096 | var nodes = 0; 1097 | var color = turn; 1098 | 1099 | for (var i = 0, len = moves.length; i < len; i++) { 1100 | make_move(moves[i]); 1101 | if (!king_attacked(color)) { 1102 | if (depth - 1 > 0) { 1103 | var child_nodes = perft(depth - 1); 1104 | nodes += child_nodes; 1105 | } else { 1106 | nodes++; 1107 | } 1108 | } 1109 | undo_move(); 1110 | } 1111 | 1112 | return nodes; 1113 | } 1114 | 1115 | return { 1116 | /*************************************************************************** 1117 | * PUBLIC CONSTANTS (is there a better way to do this?) 1118 | **************************************************************************/ 1119 | WHITE: WHITE, 1120 | BLACK: BLACK, 1121 | PAWN: PAWN, 1122 | KNIGHT: KNIGHT, 1123 | BISHOP: BISHOP, 1124 | ROOK: ROOK, 1125 | QUEEN: QUEEN, 1126 | KING: KING, 1127 | SQUARES: (function() { 1128 | /* from the ECMA-262 spec (section 12.6.4): 1129 | * "The mechanics of enumerating the properties ... is 1130 | * implementation dependent" 1131 | * so: for (var sq in SQUARES) { keys.push(sq); } might not be 1132 | * ordered correctly 1133 | */ 1134 | var keys = []; 1135 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 1136 | if (i & 0x88) { i += 7; continue; } 1137 | keys.push(algebraic(i)); 1138 | } 1139 | return keys; 1140 | })(), 1141 | FLAGS: FLAGS, 1142 | 1143 | /*************************************************************************** 1144 | * PUBLIC API 1145 | **************************************************************************/ 1146 | load: function(fen) { 1147 | return load(fen); 1148 | }, 1149 | 1150 | reset: function() { 1151 | return reset(); 1152 | }, 1153 | 1154 | moves: function(options) { 1155 | /* The internal representation of a chess move is in 0x88 format, and 1156 | * not meant to be human-readable. The code below converts the 0x88 1157 | * square coordinates to algebraic coordinates. It also prunes an 1158 | * unnecessary move keys resulting from a verbose call. 1159 | */ 1160 | 1161 | var ugly_moves = generate_moves(options); 1162 | var moves = []; 1163 | 1164 | for (var i = 0, len = ugly_moves.length; i < len; i++) { 1165 | 1166 | /* does the user want a full move object (most likely not), or just 1167 | * SAN 1168 | */ 1169 | if (typeof options !== 'undefined' && 'verbose' in options && 1170 | options.verbose) { 1171 | moves.push(make_pretty(ugly_moves[i])); 1172 | } else { 1173 | moves.push(move_to_san(ugly_moves[i])); 1174 | } 1175 | } 1176 | 1177 | return moves; 1178 | }, 1179 | 1180 | in_check: function() { 1181 | return in_check(); 1182 | }, 1183 | 1184 | in_checkmate: function() { 1185 | return in_checkmate(); 1186 | }, 1187 | 1188 | in_stalemate: function() { 1189 | return in_stalemate(); 1190 | }, 1191 | 1192 | in_draw: function() { 1193 | return half_moves >= 100 || 1194 | in_stalemate() || 1195 | insufficient_material() || 1196 | in_threefold_repetition(); 1197 | }, 1198 | 1199 | insufficient_material: function() { 1200 | return insufficient_material(); 1201 | }, 1202 | 1203 | in_threefold_repetition: function() { 1204 | return in_threefold_repetition(); 1205 | }, 1206 | 1207 | game_over: function() { 1208 | return half_moves >= 100 || 1209 | in_checkmate() || 1210 | in_stalemate() || 1211 | insufficient_material() || 1212 | in_threefold_repetition(); 1213 | }, 1214 | 1215 | validate_fen: function(fen) { 1216 | return validate_fen(fen); 1217 | }, 1218 | 1219 | fen: function() { 1220 | return generate_fen(); 1221 | }, 1222 | 1223 | pgn: function(options) { 1224 | /* using the specification from http://www.chessclub.com/help/PGN-spec 1225 | * example for html usage: .pgn({ max_width: 72, newline_char: "
" }) 1226 | */ 1227 | var newline = (typeof options === 'object' && 1228 | typeof options.newline_char === 'string') ? 1229 | options.newline_char : '\n'; 1230 | var max_width = (typeof options === 'object' && 1231 | typeof options.max_width === 'number') ? 1232 | options.max_width : 0; 1233 | var result = []; 1234 | var header_exists = false; 1235 | 1236 | /* add the PGN header headerrmation */ 1237 | for (var i in header) { 1238 | /* TODO: order of enumerated properties in header object is not 1239 | * guaranteed, see ECMA-262 spec (section 12.6.4) 1240 | */ 1241 | result.push('[' + i + ' \"' + header[i] + '\"]' + newline); 1242 | header_exists = true; 1243 | } 1244 | 1245 | if (header_exists && history.length) { 1246 | result.push(newline); 1247 | } 1248 | 1249 | /* pop all of history onto reversed_history */ 1250 | var reversed_history = []; 1251 | while (history.length > 0) { 1252 | reversed_history.push(undo_move()); 1253 | } 1254 | 1255 | var moves = []; 1256 | var move_string = ''; 1257 | var pgn_move_number = 1; 1258 | 1259 | /* build the list of moves. a move_string looks like: "3. e3 e6" */ 1260 | while (reversed_history.length > 0) { 1261 | var move = reversed_history.pop(); 1262 | 1263 | /* if the position started with black to move, start PGN with 1. ... */ 1264 | if (pgn_move_number === 1 && move.color === 'b') { 1265 | move_string = '1. ...'; 1266 | pgn_move_number++; 1267 | } else if (move.color === 'w') { 1268 | /* store the previous generated move_string if we have one */ 1269 | if (move_string.length) { 1270 | moves.push(move_string); 1271 | } 1272 | move_string = pgn_move_number + '.'; 1273 | pgn_move_number++; 1274 | } 1275 | 1276 | move_string = move_string + ' ' + move_to_san(move); 1277 | make_move(move); 1278 | } 1279 | 1280 | /* are there any other leftover moves? */ 1281 | if (move_string.length) { 1282 | moves.push(move_string); 1283 | } 1284 | 1285 | /* is there a result? */ 1286 | if (typeof header.Result !== 'undefined') { 1287 | moves.push(header.Result); 1288 | } 1289 | 1290 | /* history should be back to what is was before we started generating PGN, 1291 | * so join together moves 1292 | */ 1293 | if (max_width === 0) { 1294 | return result.join('') + moves.join(' '); 1295 | } 1296 | 1297 | /* wrap the PGN output at max_width */ 1298 | var current_width = 0; 1299 | for (var i = 0; i < moves.length; i++) { 1300 | /* if the current move will push past max_width */ 1301 | if (current_width + moves[i].length > max_width && i !== 0) { 1302 | 1303 | /* don't end the line with whitespace */ 1304 | if (result[result.length - 1] === ' ') { 1305 | result.pop(); 1306 | } 1307 | 1308 | result.push(newline); 1309 | current_width = 0; 1310 | } else if (i !== 0) { 1311 | result.push(' '); 1312 | current_width++; 1313 | } 1314 | result.push(moves[i]); 1315 | current_width += moves[i].length; 1316 | } 1317 | 1318 | return result.join(''); 1319 | }, 1320 | 1321 | load_pgn: function(pgn, options) { 1322 | function mask(str) { 1323 | return str.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); 1324 | } 1325 | 1326 | /* convert a move from Standard Algebraic Notation (SAN) to 0x88 1327 | * coordinates 1328 | */ 1329 | function move_from_san(move) { 1330 | var to, from, flags = BITS.NORMAL, promotion; 1331 | var parse = move.match(/^([NBKRQ])?([abcdefgh12345678][12345678]?)?(x)?([abcdefgh][12345678])(=?[NBRQ])?/); 1332 | if (move.slice(0, 5) === 'O-O-O') { 1333 | from = kings[turn]; 1334 | to = from - 2; 1335 | flags = BITS.QSIDE_CASTLE; 1336 | } else if (move.slice(0, 3) === 'O-O') { 1337 | from = kings[turn]; 1338 | to = from + 2; 1339 | flags = BITS.KSIDE_CASTLE; 1340 | } else if (parse && parse[1]) { 1341 | // regular moves 1342 | var piece = parse[1].toLowerCase(); 1343 | if (parse[3]) { 1344 | // capture 1345 | flags = BITS.CAPTURE; 1346 | } 1347 | to = SQUARES[parse[4]]; 1348 | for (var j = 0, len = PIECE_OFFSETS[piece].length; j < len; j++) { 1349 | var offset = PIECE_OFFSETS[piece][j]; 1350 | var square = to; 1351 | 1352 | while (true) { 1353 | square += offset; 1354 | if (square & 0x88) break; 1355 | 1356 | var b = board[square]; 1357 | if (b) { 1358 | if (b.color === turn && b.type === piece && (!parse[2] || algebraic(square).indexOf(parse[2]) >= 0)) { 1359 | from = square; 1360 | } 1361 | break; 1362 | } 1363 | 1364 | /* break, if knight or king */ 1365 | if (piece === 'n' || piece === 'k') break; 1366 | } 1367 | } 1368 | } else if (parse) { 1369 | // pawn move 1370 | if (parse[3]) { 1371 | // capture 1372 | to = SQUARES[parse[4]]; 1373 | for (var j = 2; j < 4; j++) { 1374 | var square = to - PAWN_OFFSETS[turn][j]; 1375 | if (square & 0x88) continue; 1376 | 1377 | if (board[square] != null && 1378 | board[square].color === turn && 1379 | algebraic(square)[0] === parse[2]) { 1380 | from = square; 1381 | } 1382 | } 1383 | if (board[to]) { 1384 | flags = BITS.CAPTURE; 1385 | } else { 1386 | flags = BITS.EP_CAPTURE; 1387 | } 1388 | } else { 1389 | // normal move 1390 | to = SQUARES[move.slice(0,2)]; 1391 | var c = to - PAWN_OFFSETS[turn][0], 1392 | b = board[c]; 1393 | if (b && b.type === PAWN && b.color === turn) { 1394 | from = c; 1395 | } else { 1396 | c = to - PAWN_OFFSETS[turn][1]; 1397 | b = board[c]; 1398 | if (b && b.type === PAWN && b.color === turn) { 1399 | from = c; 1400 | flags = BITS.BIG_PAWN; 1401 | } 1402 | } 1403 | } 1404 | // promotion? 1405 | if (parse[5]) { 1406 | if(typeof parse[5][1] == 'undefined') { 1407 | promotion = parse[5][0].toLowerCase(); 1408 | } else { 1409 | promotion = parse[5][1].toLowerCase(); 1410 | } 1411 | } 1412 | } 1413 | if (from >=0 && to >=0 && flags) { 1414 | return build_move(board, from, to, flags, promotion); 1415 | } else if (move.length > 0) { 1416 | /* alert(move); // error in PGN, or in parsing. */ 1417 | } 1418 | } 1419 | 1420 | function get_move_obj(move) { 1421 | return move_from_san(trim(move)); 1422 | } 1423 | 1424 | function has_keys(object) { 1425 | var has_keys = false; 1426 | for (var key in object) { 1427 | has_keys = true; 1428 | } 1429 | return has_keys; 1430 | } 1431 | 1432 | function parse_pgn_header(header, options) { 1433 | var newline_char = (typeof options === 'object' && 1434 | typeof options.newline_char === 'string') ? 1435 | options.newline_char : '\r?\n'; 1436 | var header_obj = {}; 1437 | var headers = header.split(newline_char); 1438 | var key = ''; 1439 | var value = ''; 1440 | 1441 | for (var i = 0; i < headers.length; i++) { 1442 | key = headers[i].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/, '$1'); 1443 | value = headers[i].replace(/^\[[A-Za-z]+\s"(.*)"\]$/, '$1'); 1444 | if (trim(key).length > 0) { 1445 | header_obj[key] = value; 1446 | } 1447 | } 1448 | 1449 | return header_obj; 1450 | } 1451 | 1452 | var newline_char = (typeof options === 'object' && 1453 | typeof options.newline_char === 'string') ? 1454 | options.newline_char : '\r?\n'; 1455 | var regex = new RegExp('^(\\[(.|' + mask(newline_char) + ')*\\])' + 1456 | '(' + mask(newline_char) + ')*' + 1457 | '1.(' + mask(newline_char) + '|.)*$', 'g'); 1458 | 1459 | /* get header part of the PGN file */ 1460 | var header_string = pgn.replace(regex, '$1'); 1461 | 1462 | /* no info part given, begins with moves */ 1463 | if (header_string[0] !== '[') { 1464 | header_string = ''; 1465 | } 1466 | 1467 | reset(); 1468 | 1469 | /* parse PGN header */ 1470 | var headers = parse_pgn_header(header_string, options); 1471 | for (var key in headers) { 1472 | set_header([key, headers[key]]); 1473 | } 1474 | 1475 | /* delete header to get the moves */ 1476 | var ms = pgn.replace(header_string, '').replace(new RegExp(mask(newline_char), 'g'), ' '); 1477 | 1478 | /* delete comments */ 1479 | ms = ms.replace(/(\{[^}]+\})+?/g, ''); 1480 | 1481 | /* delete move numbers */ 1482 | ms = ms.replace(/\d+\./g, ''); 1483 | 1484 | 1485 | /* trim and get array of moves */ 1486 | var moves = trim(ms).split(new RegExp(/\s+/)); 1487 | 1488 | /* delete empty entries */ 1489 | moves = moves.join(',').replace(/,,+/g, ',').split(','); 1490 | var move = ''; 1491 | 1492 | for (var half_move = 0; half_move < moves.length - 1; half_move++) { 1493 | move = get_move_obj(moves[half_move]); 1494 | 1495 | /* move not possible! (don't clear the board to examine to show the 1496 | * latest valid position) 1497 | */ 1498 | if (move == null) { 1499 | return false; 1500 | } else { 1501 | make_move(move); 1502 | } 1503 | } 1504 | 1505 | /* examine last move */ 1506 | move = moves[moves.length - 1]; 1507 | if (POSSIBLE_RESULTS.indexOf(move) > -1) { 1508 | if (has_keys(header) && typeof header.Result === 'undefined') { 1509 | set_header(['Result', move]); 1510 | } 1511 | } 1512 | else { 1513 | move = get_move_obj(move); 1514 | if (move == null) { 1515 | return false; 1516 | } else { 1517 | make_move(move); 1518 | } 1519 | } 1520 | return true; 1521 | }, 1522 | 1523 | header: function() { 1524 | return set_header(arguments); 1525 | }, 1526 | 1527 | ascii: function() { 1528 | return ascii(); 1529 | }, 1530 | 1531 | turn: function() { 1532 | return turn; 1533 | }, 1534 | 1535 | move: function(move) { 1536 | /* The move function can be called with in the following parameters: 1537 | * 1538 | * .move('Nxb7') <- where 'move' is a case-sensitive SAN string 1539 | * 1540 | * .move({ from: 'h7', <- where the 'move' is a move object (additional 1541 | * to :'h8', fields are ignored) 1542 | * promotion: 'q', 1543 | * }) 1544 | */ 1545 | var move_obj = null; 1546 | var moves = generate_moves(); 1547 | 1548 | if (typeof move === 'string') { 1549 | /* convert the move string to a move object */ 1550 | for (var i = 0, len = moves.length; i < len; i++) { 1551 | if (move === move_to_san(moves[i])) { 1552 | move_obj = moves[i]; 1553 | break; 1554 | } 1555 | } 1556 | } else if (typeof move === 'object') { 1557 | /* convert the pretty move object to an ugly move object */ 1558 | for (var i = 0, len = moves.length; i < len; i++) { 1559 | if (move.from === algebraic(moves[i].from) && 1560 | move.to === algebraic(moves[i].to) && 1561 | (!('promotion' in moves[i]) || 1562 | move.promotion === moves[i].promotion)) { 1563 | move_obj = moves[i]; 1564 | break; 1565 | } 1566 | } 1567 | } 1568 | 1569 | /* failed to find move */ 1570 | if (!move_obj) { 1571 | return null; 1572 | } 1573 | 1574 | /* need to make a copy of move because we can't generate SAN after the 1575 | * move is made 1576 | */ 1577 | var pretty_move = make_pretty(move_obj); 1578 | 1579 | make_move(move_obj); 1580 | 1581 | return pretty_move; 1582 | }, 1583 | 1584 | undo: function() { 1585 | var move = undo_move(); 1586 | return (move) ? make_pretty(move) : null; 1587 | }, 1588 | 1589 | clear: function() { 1590 | return clear(); 1591 | }, 1592 | 1593 | put: function(piece, square) { 1594 | return put(piece, square); 1595 | }, 1596 | 1597 | get: function(square) { 1598 | return get(square); 1599 | }, 1600 | 1601 | remove: function(square) { 1602 | return remove(square); 1603 | }, 1604 | 1605 | perft: function(depth) { 1606 | return perft(depth); 1607 | }, 1608 | 1609 | square_color: function(square) { 1610 | if (square in SQUARES) { 1611 | var sq_0x88 = SQUARES[square]; 1612 | return ((rank(sq_0x88) + file(sq_0x88)) % 2 === 0) ? 'light' : 'dark'; 1613 | } 1614 | 1615 | return null; 1616 | }, 1617 | 1618 | history: function(options) { 1619 | var reversed_history = []; 1620 | var move_history = []; 1621 | var verbose = (typeof options !== 'undefined' && 'verbose' in options && 1622 | options.verbose); 1623 | 1624 | while (history.length > 0) { 1625 | reversed_history.push(undo_move()); 1626 | } 1627 | 1628 | while (reversed_history.length > 0) { 1629 | var move = reversed_history.pop(); 1630 | if (verbose) { 1631 | move_history.push(make_pretty(move)); 1632 | } else { 1633 | move_history.push(move_to_san(move)); 1634 | } 1635 | make_move(move); 1636 | } 1637 | 1638 | return move_history; 1639 | } 1640 | 1641 | }; 1642 | }; 1643 | 1644 | /* export Chess object if using node or any other CommonJS compatible 1645 | * environment */ 1646 | if (typeof exports !== 'undefined') exports.Chess = Chess; --------------------------------------------------------------------------------