├── .gitignore ├── README.md ├── bg.png ├── chess.css ├── chess.html ├── chess.js ├── chess ├── ai.go ├── board.go └── board_test.go ├── favicon.ico └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | ChessBuddy 2 | *~ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ChessBuddy 2 | ========== 3 | 4 | Play chess with [Go][1], HTML5, [WebSockets][2] and random strangers! 5 | 6 | * Demo: 7 | 8 | Hint: Open the page in two different tabs, if there aren't any other 9 | visitors around. 10 | 11 | 12 | Quick Start 13 | ----------- 14 | 15 | ChessBuddy is compatible with Go 1. Use the [go tool][3] to install the latest 16 | version of ChessBuddy with the following command: 17 | 18 | go get github.com/tux21b/ChessBuddy 19 | 20 | This will install a new command `ChessBuddy` in your path (usually 21 | `$GOPATH/bin/ChessBuddy`). Start the HTTP server with the default arguments: 22 | 23 | ChessBuddy -http=:8000 -time=5m 24 | 25 | Visit , wait for a friend and start playing a game of 26 | chess. 27 | 28 | You can use `go get -u github.com/tux21b/ChessBuddy` to update ChessBuddy. 29 | 30 | 31 | Features 32 | -------- 33 | 34 | * web service connects all visitors in pairs and maintains the chess games 35 | * JavaScript client displays the chess board using the HTML 5 canvas API 36 | * Time control: 5 minutes (configurable) per side, sudden death 37 | * move history displays all moves using standard algebraic notation (SAN) 38 | 39 | 40 | Missing / Planned Features 41 | -------------------------- 42 | 43 | * support for underpomotion (currently pawns are always promoted to queens) 44 | * add some animations to the javascript interface 45 | * add a couple of AI players (maybe by connecting another engine such as 46 | gnuchess or crafty) which can be selected if no other visitors are available 47 | * starting new games without refreshing the page 48 | 49 | License 50 | ------- 51 | 52 | ChessBuddy is distributed under the Simplified BSD License: 53 | 54 | > Copyright © 2012 Christoph Hack. All rights reserved. 55 | > 56 | > Redistribution and use in source and binary forms, with or without 57 | > modification, are permitted provided that the following conditions are met: 58 | > 59 | > 1. Redistributions of source code must retain the above copyright notice, 60 | > this list of conditions and the following disclaimer. 61 | > 62 | > 2. Redistributions in binary form must reproduce the above copyright 63 | > notice, this list of conditions and the following disclaimer in the 64 | > documentation and/or other materials provided with the distribution. 65 | > 66 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 67 | > OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 68 | > OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 69 | > EVENT SHALL OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 70 | > INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 71 | > BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 72 | > DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 73 | > OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 74 | > NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 75 | > EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 76 | > 77 | > The views and conclusions contained in the software and documentation are 78 | > those of the authors and should not be interpreted as representing official 79 | > policies, either expressed or implied, of the copyright holder. 80 | 81 | 82 | [1]: http://golang.org/ 83 | [2]: http://dev.w3.org/html5/websockets/ 84 | [3]: http://golang.org/cmd/go/ 85 | -------------------------------------------------------------------------------- /bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tux21b/ChessBuddy/70a76b4d01596cb24b8bc7a1a400ba32d10d4299/bg.png -------------------------------------------------------------------------------- /chess.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ChessBuddy - Play chess with Go, HTML5, WebSockets and random strangers! 3 | * 4 | * Copyright (c) 2012 by Christoph Hack 5 | * All rights reserved. Distributed under the Simplified BSD License. 6 | */ 7 | 8 | body { 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | font-size: 13px; 11 | line-height: 18px; 12 | color: #222222; 13 | background: #ffffff url("/bg.png"); 14 | margin: 0; 15 | } 16 | 17 | a { 18 | color: #0088cc; 19 | text-decoration: none; 20 | } 21 | 22 | a:hover { 23 | color: #005580; 24 | text-decoration: underline; 25 | } 26 | 27 | div.wrap { 28 | margin: 0 auto; 29 | width: 700px; 30 | padding: 1.5em; 31 | } 32 | 33 | header { 34 | border-bottom: 2px solid #aaa; 35 | margin-bottom: 1em; 36 | } 37 | 38 | header h1 { 39 | margin: 0 0 .2em 0; 40 | padding: .5em 0 .4em; 41 | font-family: "Oleo Script", "Helvetica Neue", Helvetica, Arial, sans-serif; 42 | } 43 | 44 | header h1 a, header h1 a:hover { 45 | color: #333; 46 | text-decoration: none; 47 | } 48 | 49 | header p.intro { 50 | float: right; 51 | width: 60%; 52 | text-align: right; 53 | margin: 0; 54 | font-weight: bold; 55 | font-style: italic; 56 | color: #444; 57 | } 58 | 59 | footer { 60 | margin-top: .8em; 61 | } 62 | 63 | footer div.stats { 64 | float: right; 65 | text-align: right; 66 | } 67 | 68 | aside { 69 | float: right; 70 | width: 280px; 71 | } 72 | 73 | #board { 74 | -moz-user-select: none; 75 | -khtml-user-select: none; 76 | -webkit-user-select: none; 77 | user-select: none; 78 | -moz-box-shadow: 0 0 5px #666; 79 | -webkit-box-shadow: 0 0 5px#666; 80 | box-shadow: 0 0 5px #666; 81 | } 82 | 83 | #clocks { 84 | margin-bottom: 1em; 85 | } 86 | 87 | #history { 88 | margin-top: .5em; 89 | } 90 | 91 | label { 92 | font-weight: bold; 93 | color: #333; 94 | } 95 | 96 | #game { 97 | width: 400px; 98 | height: 400px; 99 | position: relative; 100 | -moz-box-shadow: 0 0 5px #666; 101 | -webkit-box-shadow: 0 0 5px#666; 102 | box-shadow: 0 0 5px #666; 103 | -moz-user-select: none; 104 | -khtml-user-select: none; 105 | -webkit-user-select: none; 106 | } 107 | 108 | canvas.layer { 109 | width: 400px; 110 | height: 400px; 111 | top: 0; 112 | left: 0; 113 | position: absolute; 114 | } 115 | 116 | div.dialog { 117 | position: absolute; 118 | width: 100%; 119 | left: 0; 120 | top: 50%; 121 | margin-top: -2.726em; 122 | height: 5.452em; 123 | background: rgba(220, 220, 220, 0.8); 124 | z-index: 40; 125 | color: #000; 126 | text-align: center; 127 | display: none; 128 | } 129 | 130 | div.dialog h3 { 131 | font-size: 2.1em; 132 | line-height: 100%; 133 | color: #000; 134 | margin: 0; 135 | padding: .4em 0 .2em; 136 | font-weight: normal; 137 | } 138 | 139 | div.dialog p { 140 | margin: 0; 141 | font-size: 1.4em; 142 | line-height: 100%; 143 | padding: 0 0 .38em; 144 | } 145 | 146 | div#dlg-waiting, 147 | div#dlg-result { 148 | height: 5.292em; 149 | margin-top: -2.646em; 150 | } 151 | 152 | div#dlg-connect { 153 | height: 3.78em; 154 | margin-top: -1.89em; 155 | } 156 | -------------------------------------------------------------------------------- /chess.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ChessBuddy 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

16 | Play chess with Go, HTML5,
17 | WebSockets and random strangers! 18 |

19 |

ChessBuddy

20 |
21 | 22 | 31 | 32 | 33 |
34 | 35 | Your browser does not support the canvas element. 36 | 37 | 38 | 39 | 40 | 41 |
42 |

Connecting…

43 |
44 |
45 |

Waiting for another player…

46 |

(or play against the computer)

47 |
48 |
49 |

Checkmate: White wins!

50 |

Do you want to start a new game?

51 |
52 |
53 | Promote to? 54 |
55 |
56 | 61 | 62 |
63 |
64 | N/A players online 65 |
66 | © Christoph Hack, 2012. Grab the source on 67 | GitHub.
68 | Proudly powered by Go 1.2! 69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /chess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ChessBuddy - Play chess with Go, HTML5, WebSockets and random strangers 3 | * 4 | * Copyright (c) 2012 by Christoph Hack 5 | * All rights reserved. Distributed under the Simplified BSD License. 6 | */ 7 | 8 | var P = 1; 9 | var N = 2; 10 | var B = 3; 11 | var R = 4; 12 | var Q = 5; 13 | var K = 6; 14 | 15 | var WHITE = 8; 16 | var BLACK = 16; 17 | 18 | var PIECE_MASK = 7; 19 | var COLOR_MASK = 24; 20 | 21 | var PIECES = [ 22 | " ", "♟", "♞", "♝", "♜", "♛", "♚", "?", 23 | " ", "♙", "♘", "♗", "♖", "♕", "♔", "?", 24 | ]; 25 | 26 | var requestAnim = window.requestAnimationFrame || 27 | window.webkitRequestAnimationFrame || 28 | window.mozRequestAnimationFrame || 29 | window.oRequestAnimationFrame || 30 | window.msRequestAnimationFrame || 31 | function (callback) { window.setTimeout(callback, 1000 / 60); }; 32 | 33 | 34 | function ChessGame(canvas, clocks, addr) { 35 | this.clocks = clocks; 36 | this.clocks_ctx = clocks.getContext("2d"); 37 | this.color = 0; 38 | this.turn = 0; 39 | this.board = []; 40 | this.sel = null; 41 | for (var i = 0; i < 64; i++) 42 | this.board[i] = 0; 43 | this.totalTime = 0; 44 | this.remainingA = 0; 45 | this.remainingB = 0; 46 | this.moves = []; 47 | this.anim = {}; 48 | this.size = 400 / 9.0; 49 | 50 | this.game = document.getElementById("game"); 51 | this.ctx_base = document.getElementById("base").getContext("2d"); 52 | this.ctx_mark = document.getElementById("mark").getContext("2d"); 53 | this.ctx_anim = document.getElementById("anim").getContext("2d"); 54 | 55 | this.renderBase(); 56 | this.renderClocks(); 57 | 58 | var _this = this; 59 | 60 | if ('WebSocket' in window) { 61 | this.ws = new WebSocket(addr); 62 | this.ws.onopen = function(e) { 63 | document.getElementById("dlg-connect").style.display = 'none'; 64 | document.getElementById("dlg-waiting").style.display = 'block'; 65 | } 66 | this.ws.onmessage = function(e) { 67 | _this.process(e); 68 | }; 69 | this.ws.onclose = function(e) { 70 | if (document.getElementById("dlg-result").style.display == 'none') { 71 | document.getElementById("dlg-waiting").style.display = 'none'; 72 | document.getElementById("dlg-connect").style.display = 'none'; 73 | document.getElementById("result").innerHTML = "Connection lost"; 74 | document.getElementById("dlg-result").style.display = 'block'; 75 | } 76 | _this.color = 0; 77 | }; 78 | this.ws.onerror = function(e) { 79 | document.getElementById("dlg-result").style.display = 'none'; 80 | document.getElementById("dlg-waiting").style.display = 'none'; 81 | document.getElementById("dlg-connect").style.display = 'none'; 82 | document.getElementById("result").innerHTML = "Connection error"; 83 | document.getElementById("dlg-result").style.display = 'block'; 84 | _this.color = 0; 85 | } 86 | } else { 87 | document.getElementById("connect").innerHTML = "Missing WebSocket Support"; 88 | } 89 | this.game.addEventListener('click', function(e) { 90 | _this.click(e) 91 | }); 92 | 93 | this.clocks_int = window.setInterval(function() { 94 | _this.tick(); 95 | }, 1000); 96 | 97 | window.onbeforeunload = function(e) { 98 | if (_this.color != 0) { 99 | return "Leaving the page will cancel the current game."; 100 | } 101 | }; 102 | }; 103 | 104 | 105 | ChessGame.prototype.renderBase = function() { 106 | var ctx = this.ctx_base; 107 | var size = this.size; 108 | 109 | ctx.fillStyle = "#6288b9"; 110 | ctx.fillRect(0, 0, 9*size, 9*size); 111 | 112 | ctx.font = 'bold 10pt "Helvetica Neue", Helvetica, Arial, sans-serif'; 113 | ctx.textAlign = "center"; 114 | ctx.textBaseline = "middle"; 115 | ctx.fillStyle = "#FFFFFF"; 116 | if (this.color != 0) { 117 | for (var i = 0; i < 8; i++) { 118 | var rank = this.color == WHITE ? 7-i : i; 119 | var file = this.color == WHITE ? i : 7-i; 120 | ctx.fillText(rank+1, 0.25*size, (i+1)*size); 121 | ctx.fillText(rank+1, 8.75*size, (i+1)*size); 122 | ctx.fillText(String.fromCharCode(file+97), (i+1)*size, 0.25*size); 123 | ctx.fillText(String.fromCharCode(file+97), (i+1)*size, 8.75*size); 124 | } 125 | } 126 | 127 | for (var sq = 0; sq < 64; sq++) { 128 | this.renderBaseSq(sq); 129 | } 130 | }; 131 | 132 | ChessGame.prototype.renderBaseSq = function(sq) { 133 | var ctx = this.ctx_base; 134 | var size = this.size; 135 | 136 | var x = (this.color != BLACK) ? sq&7 : 7-(sq&7); 137 | var y = (this.color != BLACK) ? 7-(sq>>3) : sq>>3; 138 | ctx.fillStyle = ((x&1) == (y&1)) ? "#FEFEFE" : "#83A5D2"; 139 | ctx.fillRect(0.5*size+x*size, 0.5*size+y*size, size, size); 140 | 141 | ctx.font = '26pt "Helvetica Neue", Helvetica, Arial, sans-serif'; 142 | ctx.textAlign = "center"; 143 | ctx.textBaseline = "middle"; 144 | ctx.fillStyle = (sq == this.sel) ? "#FF0000" : "#000000"; 145 | ctx.fillText(PIECES[this.board[sq]&15], (x+1)*size, (y+1)*size); 146 | }; 147 | 148 | ChessGame.prototype.renderMarkers = function(src, moves) { 149 | var ctx = this.ctx_mark; 150 | var size = this.size; 151 | 152 | ctx.clearRect(0, 0, size*9, size*9); 153 | if (moves) { 154 | ctx.font = '26pt "Helvetica Neue", Helvetica, Arial, sans-serif'; 155 | ctx.textAlign = "center"; 156 | ctx.textBaseline = "middle"; 157 | for (var i = 0; i < moves.length; i++) { 158 | var sq = moves[i]; 159 | var x = (this.color == WHITE) ? sq&7 : 7-(sq&7); 160 | var y = (this.color == WHITE) ? 7-(sq>>3) : sq>>3; 161 | 162 | ctx.fillStyle = "rgba(60, 60, 60, 0.2)"; 163 | var p = PIECES[this.board[src]&15]; 164 | if (this.board[sq] != 0) { 165 | ctx.fillStyle = "rgba(255, 0, 0, 0.6)"; 166 | p = "✘" 167 | } 168 | ctx.fillText(p, (x+1)*size, (y+1)*size); 169 | } 170 | } 171 | }; 172 | 173 | ChessGame.prototype.renderAnim = function(t) { 174 | var ctx = this.ctx_anim; 175 | var size = this.size; 176 | 177 | ctx.clearRect(0, 0, size*9, size*9); 178 | 179 | ctx.font = '26pt "Helvetica Neue", Helvetica, Arial, sans-serif'; 180 | ctx.textAlign = "center"; 181 | ctx.textBaseline = "middle"; 182 | ctx.fillStyle = "#000000"; 183 | 184 | var t = Date.now(); 185 | var update = false; 186 | for (var a in this.anim) { 187 | var an = this.anim[a]; 188 | var p = (t - an.t0) / (an.t1 - an.t0); 189 | if (p >= 1.0) { 190 | delete this.anim[a]; 191 | p = 1.0; 192 | this.renderBaseSq(a); 193 | } 194 | ctx.fillText(PIECES[this.board[a]&15], 195 | an.x0+(an.x1-an.x0)*p, 196 | an.y0+(an.y1-an.y0)*p); 197 | update = true; 198 | } 199 | 200 | if (update) { 201 | var _this = this; 202 | requestAnim(function(t) {_this.renderAnim(t)}); 203 | } 204 | }; 205 | 206 | ChessGame.prototype.movePiece = function(src, dst) { 207 | var first = true; 208 | for (a in this.anim) { 209 | first = false; 210 | break; 211 | } 212 | 213 | var size = this.size; 214 | var now = Date.now(); 215 | var dist = Math.sqrt(((src&7)-(dst&7))*((src&7)-(dst&7))+ 216 | ((src>>3)-(dst>>3))*((src>>3)-(dst>>3))); 217 | 218 | this.anim[dst] = { 219 | t0: now, 220 | t1: now+150*dist, 221 | x0: (this.color == WHITE ? (1+(src&7))*size : (8-(src&7))*size), 222 | y0: (this.color == WHITE ? (8-(src>>3))*size : (1+(src>>3))*size), 223 | x1: (this.color == WHITE ? (1+(dst&7))*size : (8-(dst&7))*size), 224 | y1: (this.color == WHITE ? (8-(dst>>3))*size : (1+(dst>>3))*size), 225 | }; 226 | 227 | this.board[dst] = this.board[src]; 228 | this.board[src] = 0; 229 | this.renderBaseSq(src); 230 | 231 | if (first) { 232 | var _this = this; 233 | requestAnim(function(t) {_this.renderAnim(t)}); 234 | } 235 | }; 236 | 237 | ChessGame.prototype.renderClocks = function() { 238 | this.renderClock(0, 0, 130, 239 | this.totalTime > 0 ? this.remainingA / this.totalTime : 0, WHITE); 240 | this.renderClock(150, 0, 130, 241 | this.totalTime > 0 ? this.remainingB / this.totalTime : 0, BLACK); 242 | }; 243 | 244 | 245 | ChessGame.prototype.renderClock = function(x, y, size, t, color) { 246 | var ctx = this.clocks_ctx; 247 | var active = false; 248 | if (this.color != 0) { 249 | active = (this.turn % 2 == 1) == (color == WHITE); 250 | } 251 | 252 | ctx.strokeStyle = "#cacad1"; 253 | ctx.fillStyle = "#fafafa"; 254 | ctx.lineWidth = 12; 255 | ctx.beginPath(); 256 | ctx.arc(x+0.5*size, y+0.5*size, 0.45*size, 0, Math.PI*2, true); 257 | ctx.closePath(); 258 | ctx.stroke(); 259 | ctx.fill(); 260 | 261 | ctx.globalCompositeOperation = "destination-out"; 262 | ctx.beginPath(); 263 | ctx.arc(x+0.5*size, y+0.5*size, 0.4*size, 0, Math.PI, true); 264 | ctx.arc(x+0.5*size, y+0.5*size, 0.1*size, Math.PI, 0, false); 265 | ctx.closePath(); 266 | ctx.fill(); 267 | ctx.globalCompositeOperation = "source-over"; 268 | 269 | /* draw label */ 270 | ctx.fillStyle = "#888"; 271 | ctx.font = 'bold 12pt "Helvetica Neue", Helvetica, Arial, sans-serif'; 272 | ctx.textAlign = "center"; 273 | ctx.textBaseline = "middle"; 274 | ctx.fillText(color == WHITE ? "white" : "black", x+0.5*size, y+0.7*size); 275 | 276 | /* draw pointer */ 277 | ctx.fillStyle = active ? "#ee0000" : "#222"; 278 | ctx.strokeStyle = active ? "#ee0000" : "#222"; 279 | ctx.lineWidth = 2; 280 | ctx.beginPath(); 281 | ctx.arc(x+0.5*size, y+0.5*size, 0.04*size, 282 | t*2*Math.PI-0.3*Math.PI, t*2*Math.PI-0.7*Math.PI, false); 283 | ctx.lineTo(x+0.5*size+0.475*size*Math.sin(t*2*Math.PI), 284 | y+0.5*size-0.475*size*Math.cos(t*2*Math.PI)); 285 | ctx.closePath(); 286 | ctx.fill(); 287 | ctx.stroke(); 288 | } 289 | 290 | 291 | ChessGame.prototype.click = function(e) { 292 | /* calculate the relative x and y position in pixels */ 293 | var x, y; 294 | if (e.pageX != undefined && e.pageY != undefined) { 295 | x = e.pageX; 296 | y = e.pageY; 297 | } else { 298 | x = e.clientX + document.body.scrollLeft + 299 | document.documentElement.scrollLeft; 300 | y = e.clientY + document.body.scrollTop + 301 | document.documentElement.scrollTop; 302 | } 303 | x -= this.game.offsetLeft; 304 | y -= this.game.offsetTop; 305 | 306 | /* convert to field coordinates */ 307 | var size = this.size; 308 | x = Math.floor((x - 0.5*size) / size); 309 | y = 7-Math.floor((y - 0.5*size) / size); 310 | if (this.color == BLACK) { 311 | x = 7 - x; 312 | y = 7 - y; 313 | } 314 | var pos = y*8+x; 315 | var prev_sel = this.sel; 316 | 317 | /* process the mouse click */ 318 | if (x < 0 || x > 7 || y < 0 || y > 7 || this.sel == pos) { 319 | this.sel = null; 320 | } else if ((this.board[pos]&COLOR_MASK) == this.color) { 321 | this.sel = pos; 322 | this.ws.send(JSON.stringify({cmd: "select", turn: this.turn, src: pos})); 323 | } else if (this.sel != null && (this.turn % 2 == 1) == (this.color == WHITE)) { 324 | this.ws.send(JSON.stringify({cmd: "move", turn: this.turn, src: this.sel, 325 | dst: pos})); 326 | this.sel = null; 327 | } 328 | 329 | if (this.sel != prev_sel) { 330 | this.renderMarkers(this.sel, []); 331 | if (prev_sel != null) this.renderBaseSq(prev_sel); 332 | if (this.sel != null) this.renderBaseSq(this.sel); 333 | } 334 | } 335 | 336 | 337 | ChessGame.prototype.process = function(e) { 338 | var msg = JSON.parse(e.data); 339 | 340 | if (msg.cmd == "move") { 341 | if (this.board[msg.dst] == 0 && (msg.src&7) != (msg.dst&7)) { 342 | if (this.board[msg.src] == (P|WHITE)) { 343 | this.board[msg.dst-8] = 0; 344 | this.renderBaseSq(msg.dst-8); 345 | } else if (this.board[msg.src] == (P|BLACK)) { 346 | this.board[msg.dst+8] = 0; 347 | this.renderBaseSq(msg.dst+8); 348 | } 349 | } 350 | if (this.board[msg.src]==(K|WHITE) && msg.src==4) { 351 | if (msg.dst == 6) { 352 | this.movePiece(7, 5); 353 | } else if (msg.dst == 2) { 354 | this.movePiece(0, 3); 355 | } 356 | } else if (this.board[msg.src]==(K|BLACK) && msg.src==60) { 357 | if (msg.dst == 62) { 358 | this.movePiece(63, 61); 359 | } else if (msg.dst == 58) { 360 | this.movePiece(56, 59); 361 | } 362 | } 363 | this.movePiece(msg.src, msg.dst); 364 | if ((this.board[msg.dst] == (P|WHITE)) && (msg.dst>>3) == 7) { 365 | this.board[msg.dst] = (Q|WHITE); 366 | } 367 | if ((this.board[msg.dst] == (P|BLACK)) && (msg.dst>>3) == 0) { 368 | this.board[msg.dst] = (Q|BLACK); 369 | } 370 | this.turn = msg.turn + 1; 371 | this.remainingA = msg.RemainingA; 372 | this.remainingB = msg.RemainingB; 373 | this.renderClocks(); 374 | if (msg.color == WHITE) { 375 | document.getElementById("history").innerHTML += 376 | Math.floor((msg.turn+1)/2) + ". " + msg.History + " "; 377 | } else { 378 | document.getElementById("history").innerHTML += 379 | msg.History + " "; 380 | } 381 | } 382 | else if (msg.cmd == "start") { 383 | document.getElementById("dlg-waiting").style.display = 'none'; 384 | this.board = [ 385 | R|WHITE, N|WHITE, B|WHITE, Q|WHITE, 386 | K|WHITE, B|WHITE, N|WHITE, R|WHITE, 387 | P|WHITE, P|WHITE, P|WHITE, P|WHITE, 388 | P|WHITE, P|WHITE, P|WHITE, P|WHITE, 389 | 0, 0, 0, 0, 0, 0, 0, 0, 390 | 0, 0, 0, 0, 0, 0, 0, 0, 391 | 0, 0, 0, 0, 0, 0, 0, 0, 392 | 0, 0, 0, 0, 0, 0, 0, 0, 393 | P|BLACK, P|BLACK, P|BLACK, P|BLACK, 394 | P|BLACK, P|BLACK, P|BLACK, P|BLACK, 395 | R|BLACK, N|BLACK, B|BLACK, Q|BLACK, 396 | K|BLACK, B|BLACK, N|BLACK, R|BLACK, 397 | ]; 398 | this.color = msg.color; 399 | this.turn = msg.turn; 400 | this.totalTime = msg.RemainingA; 401 | this.remainingA = msg.RemainingA; 402 | this.remainingB = msg.RemainingB; 403 | this.renderBase(); 404 | this.renderClocks(); 405 | } 406 | else if (msg.cmd == "msg") { 407 | document.getElementById("result").innerHTML = msg.Text; 408 | document.getElementById("dlg-result").style.display = "block"; 409 | this.color = 0; 410 | } 411 | else if (msg.cmd == "ping") { 412 | this.ws.send(JSON.stringify({cmd: "pong"})); 413 | } 414 | else if (msg.cmd == "stat") { 415 | document.getElementById("numPlayers").innerHTML = msg.NumPlayers; 416 | } 417 | else if (msg.cmd == "select" && msg.src == this.sel) { 418 | this.renderMarkers(msg.src, msg.moves); 419 | } 420 | } 421 | 422 | ChessGame.prototype.tick = function() { 423 | if (this.color != 0) { 424 | if (this.turn%2 == 1) { 425 | this.remainingA -= 1000000000; 426 | if (this.remainingA < 0) 427 | this.remainingA = 0; 428 | } else { 429 | this.remainingB -= 1000000000; 430 | if (this.remainingB < 0) 431 | this.remainingB = 0; 432 | } 433 | } 434 | this.renderClocks(); 435 | } 436 | -------------------------------------------------------------------------------- /chess/ai.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 by Christoph Hack 2 | // All rights reserved. Distributed under the Simplified BSD License. 3 | 4 | package chess 5 | 6 | import ( 7 | "math" 8 | "math/rand" 9 | ) 10 | 11 | func (b *Board) MoveAI() (src, dst Square) { 12 | src, dst, _ = b.negaMax(4) 13 | return 14 | } 15 | 16 | func (b *Board) negaMax(depth int) (bsrc, bdst Square, max float64) { 17 | if depth <= 0 { 18 | max = b.evaluate() 19 | return 20 | } 21 | 22 | max = math.Inf(-1) 23 | src := Square(rand.Intn(64)) 24 | for i := 0; i < 64; i++ { 25 | src = (src + 1) % 64 26 | if b.board[src]&ColorMask != b.color { 27 | continue 28 | } 29 | dst := Square(rand.Intn(64)) 30 | for j := 0; j < 64; j++ { 31 | dst = (dst + 1) % 64 32 | if b.mayMove(src, dst) { 33 | 34 | piece, victim := b.board[src], b.board[dst] 35 | b.board[dst], b.board[src] = piece, 0 36 | b.occupied &^= Bitboard(1) << uint(src) 37 | b.occupied |= Bitboard(1) << uint(dst) 38 | 39 | if !b.isCheck() { 40 | b.color ^= ColorMask 41 | _, _, score := b.negaMax(depth - 1) 42 | score = -score 43 | b.color ^= ColorMask 44 | 45 | if score > max { 46 | bsrc, bdst, max = src, dst, score 47 | } 48 | } 49 | 50 | b.board[src], b.board[dst] = piece, victim 51 | b.occupied |= Bitboard(1) << uint(src) 52 | if victim == 0 { 53 | b.occupied &^= Bitboard(1) << uint(dst) 54 | } 55 | } 56 | } 57 | } 58 | return 59 | } 60 | 61 | func (b *Board) evaluate() float64 { 62 | values := []float64{0, 1, 3, 3, 5, 9, 200} 63 | score := 0.0 64 | for p := Square(0); p < 64; p++ { 65 | s := values[b.board[p]&PieceMask] 66 | if (p>>3 == 0 || p>>3 == 7) && b.board[p]|PieceMask == P { 67 | s = 9 68 | } 69 | if b.board[p]&ColorMask != b.color { 70 | s = -s 71 | } 72 | score += s 73 | } 74 | return score 75 | } 76 | -------------------------------------------------------------------------------- /chess/board.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 by Christoph Hack 2 | // All rights reserved. Distributed under the Simplified BSD License. 3 | 4 | // Package chess implements a basic chess board which represents the state of 5 | // a chess game. It can be used for validating and applying moves, formatting 6 | // them using SAN (standard algebraic notation) and to generate a list of 7 | // possible moves. 8 | // 9 | // The package doesn't provide a way to rank and choose moves, but further 10 | // packages might be built on top of this one to add this functionality. 11 | // 12 | package chess 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "regexp" 18 | "strings" 19 | ) 20 | 21 | // The chess pieces are identified by a single letter from the standard 22 | // English names (i.e. pawn, knight, bishop, rook, queen, king). White pieces 23 | // have the 3rd bit set, black pieces the 4th. The bitmasks PieceMask and 24 | // ColorMask can be used to extract the piece or color information. 25 | const ( 26 | P uint8 = 0x1 27 | N uint8 = 0x2 28 | B uint8 = 0x3 29 | R uint8 = 0x4 30 | Q uint8 = 0x5 31 | K uint8 = 0x6 32 | 33 | White uint8 = 0x08 34 | Black uint8 = 0x10 35 | 36 | PieceMask uint8 = 0x07 37 | ColorMask uint8 = 0x18 38 | ) 39 | 40 | // A square represents a position on the chess board. 41 | type Square int 42 | 43 | // Sq parses a position on the chess board and returns that square. It will 44 | // panic if the input doesn't match the expression "[a-h][1-9]". 45 | func Sq(v string) Square { 46 | if len(v) != 2 || v[0] < 'a' || v[0] > 'h' || v[1] < '1' || v[1] > '9' { 47 | panic("invalid square") 48 | } 49 | return Square((v[1]-'1')*8 + v[0] - 'A') 50 | } 51 | 52 | // File returns the column number (ranging from 0 to 7) of the square. 53 | func (s Square) File() int { 54 | return int(s & 7) 55 | } 56 | 57 | // Rank returns the row number (ranging from 0 to 7) of the square. 58 | func (s Square) Rank() int { 59 | return int(s >> 3) 60 | } 61 | 62 | // String formats the square using the standard algebraic notation. 63 | func (s Square) String() string { 64 | return fmt.Sprintf("%c%c", 'a'+s&7, '1'+s>>3) 65 | } 66 | 67 | // A Bitboard is a good alternative to square centric representations of the 68 | // chessboard. The 8x8 bits are used to represent the existance of a certain 69 | // piece at that position. They are also usefull for lookup-tables storing 70 | // possible target locations because of they are quite compact. 71 | type Bitboard uint64 72 | 73 | // String will format the bitboard by using ASCII art to draw the chessboard. 74 | // Only useful for debugging purposes. 75 | func (b Bitboard) String() string { 76 | buf := &bytes.Buffer{} 77 | buf.WriteString(" A B C D E F G H\n") 78 | for rank := 7; rank >= 0; rank-- { 79 | buf.WriteByte('1' + byte(rank)) 80 | for file := 0; file <= 7; file++ { 81 | buf.WriteByte(' ') 82 | if b&(1<= 0; rank-- { 150 | empty := 0 151 | for file := 0; file <= 7; file++ { 152 | if piece := b.board[file+rank<<3]; piece != 0 { 153 | if empty > 0 { 154 | buf.WriteByte(byte('0' + empty)) 155 | empty = 0 156 | } 157 | switch piece & ColorMask { 158 | case White: 159 | buf.WriteByte(" PNBRQK"[piece&PieceMask]) 160 | case Black: 161 | buf.WriteByte(" pnbrqk"[piece&PieceMask]) 162 | } 163 | } else { 164 | empty++ 165 | } 166 | } 167 | if empty > 0 { 168 | buf.WriteByte(byte('0' + empty)) 169 | } 170 | if rank != 0 { 171 | buf.WriteByte('/') 172 | } 173 | } 174 | switch b.color { 175 | case White: 176 | buf.WriteString(" w ") 177 | case Black: 178 | buf.WriteString(" b ") 179 | } 180 | switch { 181 | case b.moved&0x90 == 0: 182 | buf.WriteByte('K') 183 | case b.moved&0x11 == 0: 184 | buf.WriteByte('Q') 185 | case b.moved&(0x90<<14) == 0: 186 | buf.WriteByte('k') 187 | case b.moved&(0x11<<14) == 0: 188 | buf.WriteByte('q') 189 | default: 190 | buf.WriteByte('-') 191 | } 192 | fmt.Fprintf(buf, " %d %d", len(b.hist), b.Turn()) 193 | return buf.String() 194 | } 195 | 196 | var reSAN = regexp.MustCompile(`^([PNBRQK]?)([a-h])?([1-8])?([\-x]?)([a-h])([1-8])$`) 197 | 198 | // MoveSAN applies a move given in the SAN (standard algebraic notation) format. 199 | func (b *Board) MoveSAN(text string) error { 200 | san := strings.Replace(strings.TrimRight(text, "?!+#"), "O", "0", -1) 201 | if san == "0-0" || san == "0-0-0" { 202 | switch { 203 | case san == "0-0" && b.color == White && b.doCastle(4, 7): 204 | case san == "0-0" && b.color == Black && b.doCastle(60, 63): 205 | case san == "0-0-0" && b.color == White && b.doCastle(4, 0): 206 | case san == "0-0-0" && b.color == Black && b.doCastle(60, 56): 207 | default: 208 | return fmt.Errorf("can not castle") 209 | } 210 | return nil 211 | } 212 | 213 | m := reSAN.FindStringSubmatch(san) 214 | if m == nil { 215 | return fmt.Errorf("invalid move text %q. Please use SAN.", text) 216 | } 217 | 218 | dst := Square(m[5][0] - 'a' + (m[6][0]-'1')<<3) 219 | if m[4] == "x" && b.board[dst]&ColorMask != b.color^ColorMask { 220 | return fmt.Errorf("can not capture the square %s", dst) 221 | } 222 | 223 | piece := P | b.color 224 | switch m[1] { 225 | case "N": 226 | piece = N | b.color 227 | case "B": 228 | piece = B | b.color 229 | case "R": 230 | piece = R | b.color 231 | case "Q": 232 | piece = Q | b.color 233 | case "K": 234 | piece = K | b.color 235 | } 236 | 237 | src := Square(-1) 238 | if m[2] != "" && m[3] != "" { 239 | src = Square(m[2][0] - 'a' + (m[3][0]-'1')<<3) 240 | } else { 241 | for p := Square(0); p < 64; p++ { 242 | if b.board[p] == piece && (m[2] == "" || m[2][0]-'a' == uint8(p&7)) && 243 | (m[3] == "" || m[3][0]-'1' == uint8(p>>3)) && b.mayMove(p, dst) { 244 | if src < 0 { 245 | src = p 246 | } else { 247 | return fmt.Errorf("The move %q is ambigous.", text) 248 | } 249 | } 250 | } 251 | } 252 | if src < 0 || !b.Move(src, dst) { 253 | return fmt.Errorf("The move %q is invalid.", text) 254 | } 255 | return nil 256 | } 257 | 258 | // Move moves a piece from square src to the square dst. The return value 259 | // indicates whetever the move was sucessful or not. 260 | func (b *Board) Move(src, dst Square) bool { 261 | if src < 0 || dst >= 64 || src < 0 || dst >= 64 { 262 | return false 263 | } 264 | 265 | if src == 4 && b.board[src] == K|White { 266 | switch dst { 267 | case 6: 268 | return b.doCastle(4, 7) 269 | case 2: 270 | return b.doCastle(4, 0) 271 | } 272 | } else if src == 60 && b.board[src] == K|Black { 273 | switch dst { 274 | case 62: 275 | return b.doCastle(60, 63) 276 | case 58: 277 | return b.doCastle(60, 56) 278 | } 279 | } 280 | 281 | if !b.canMove(src, dst) { 282 | return false 283 | } 284 | 285 | log := b.formatMove(src, dst) 286 | b.board[dst], b.board[src] = b.board[src], 0 287 | b.occupied &^= Bitboard(1) << uint(src) 288 | b.occupied |= Bitboard(1) << uint(dst) 289 | 290 | // additional rules for en-passant captures 291 | if b.board[dst] == P|White && dst == b.eps { 292 | b.board[dst-8] = 0 293 | b.occupied &^= Bitboard(1) << uint(dst-8) 294 | } else if b.board[dst] == P|Black && dst == b.eps { 295 | b.board[dst+8] = 0 296 | b.occupied &^= Bitboard(1) << uint(dst+8) 297 | } 298 | b.eps = -1 299 | if b.board[dst] == P|White && dst-src == 16 { 300 | b.eps = dst - 8 301 | } else if b.board[dst] == P|Black && dst-src == -16 { 302 | b.eps = dst + 8 303 | } 304 | 305 | // promotion 306 | if b.board[dst]&PieceMask == P && (dst>>3 == 0 || dst>>3 == 7) { 307 | b.board[dst] = Q | (b.board[dst] & ColorMask) 308 | } 309 | 310 | b.moved |= Bitboard(1) << uint(src) 311 | b.color ^= ColorMask 312 | b.check, b.stalemate = b.isCheck(), b.isStalemate() 313 | b.hist = append(b.hist, log+b.formatStatus()) 314 | 315 | return true 316 | } 317 | 318 | // Moves generates a list of all possible target squares for a specific piece 319 | // located at the square src. 320 | func (b *Board) Moves(src Square) (moves []Square) { 321 | for dst := Square(0); dst < 64; dst++ { 322 | if b.canMove(src, dst) { 323 | moves = append(moves, dst) 324 | } 325 | } 326 | if b.board[src] == K|White { 327 | if b.canCastle(4, 7) { 328 | moves = append(moves, 6) 329 | } 330 | if b.canCastle(4, 0) { 331 | moves = append(moves, 2) 332 | } 333 | } else if b.board[src] == K|Black { 334 | if b.canCastle(60, 63) { 335 | moves = append(moves, 62) 336 | } 337 | if b.canCastle(60, 56) { 338 | moves = append(moves, 58) 339 | } 340 | } 341 | return 342 | } 343 | 344 | // mayMove checks whetever it might be possible to move from src to dst. This 345 | // method ignores castling rules and might report pseud-legal moves. 346 | func (b *Board) mayMove(src, dst Square) bool { 347 | piece, victim := b.board[src], b.board[dst] 348 | 349 | // must not capture own pieces 350 | if piece&ColorMask == victim&ColorMask { 351 | return false 352 | } 353 | 354 | // check basic movement patterns 355 | x88diff := int(dst - src + (dst | 7) - (src | 7) + 120) 356 | occ := b.occupied>>Bitboard(src) | b.occupied< dst || (x88diff == 152 && src>>3 != 1))) || 365 | (piece == P|Black && (src < dst || (x88diff == 88 && src>>3 != 6)))) { 366 | return false 367 | } 368 | 369 | return true 370 | } 371 | 372 | // canMove checks if its possible to move from src to dst. This method ignores 373 | // castling rules. 374 | func (b *Board) canMove(src, dst Square) (valid bool) { 375 | if !b.mayMove(src, dst) { 376 | return false 377 | } 378 | 379 | piece, victim := b.board[src], b.board[dst] 380 | b.board[dst], b.board[src] = piece, 0 381 | b.occupied &^= Bitboard(1) << uint(src) 382 | b.occupied |= Bitboard(1) << uint(dst) 383 | 384 | valid = !b.isCheck() 385 | 386 | b.board[src], b.board[dst] = piece, victim 387 | b.occupied |= Bitboard(1) << uint(src) 388 | if victim == 0 { 389 | b.occupied &^= Bitboard(1) << uint(dst) 390 | } 391 | 392 | return 393 | } 394 | 395 | // canCastle checks if its possible to castle with the given king and 396 | // rook position. 397 | func (b *Board) canCastle(king, rook Square) (valid bool) { 398 | if b.moved&((Bitboard(1)<>3)) 505 | } 506 | 507 | if capture { 508 | buf.WriteByte('x') 509 | } 510 | 511 | buf.Write([]byte{byte('a' + dst&7), byte('1' + dst>>3)}) 512 | 513 | return buf.String() 514 | } 515 | 516 | // formatStatus returns the proper SAN annotations for moves which result 517 | // in a check or checkmate. 518 | func (b *Board) formatStatus() string { 519 | if b.check { 520 | if b.stalemate { 521 | return "#" 522 | } else { 523 | return "+" 524 | } 525 | } 526 | return "" 527 | } 528 | 529 | // Checkmate returns true if the current player is checkmate. 530 | func (b *Board) Checkmate() bool { 531 | return b.check && b.stalemate 532 | } 533 | 534 | // Stalemate returns true if the current player is stalemate. 535 | func (b *Board) Stalemate() bool { 536 | return !b.check && b.stalemate 537 | } 538 | 539 | // Check returns true if the current player is in check only. This method 540 | // returns false if the player is checkmate. 541 | func (b *Board) Check() bool { 542 | return b.check && !b.stalemate 543 | } 544 | 545 | // Color returns the color of the current side to play. 546 | func (b *Board) Color() uint8 { 547 | return b.color 548 | } 549 | 550 | // Turn returns the current halfturn number starting by one. 551 | func (b *Board) Turn() int { 552 | return len(b.hist) + 1 553 | } 554 | 555 | // LastMove returns the last half move formatted using the extended algebraic 556 | // notation. 557 | func (b *Board) LastMove() string { 558 | if len(b.hist) == 0 { 559 | return "" 560 | } 561 | return b.hist[len(b.hist)-1] 562 | } 563 | 564 | // blockers is a relatively small lookup table (just 14 KB) which stores for 565 | // each piece and 0x88 difference a set of possible blockers, i.e. squares 566 | // which can not be passed if they are non-empty. Impossible moves are blocked 567 | // by all other squares and non sliding moves are blocked by nothing. 568 | var blockers [7][240]Bitboard 569 | 570 | // init initializes the blockers lookup table. 571 | func init() { 572 | for i := 0; i < 240; i++ { 573 | blockers[0][i] = ^Bitboard(0) 574 | blockers[P][i] = ^Bitboard(0) 575 | blockers[N][i] = ^Bitboard(0) 576 | blockers[B][i] = ^Bitboard(0) 577 | blockers[R][i] = ^Bitboard(0) 578 | blockers[Q][i] = ^Bitboard(0) 579 | blockers[K][i] = ^Bitboard(0) 580 | } 581 | 582 | // pawns 583 | blockers[P][136] = 1 << 8 584 | blockers[P][152] = 1<<8 | 1<<16 585 | blockers[P][135] = 0 586 | blockers[P][137] = 0 587 | blockers[P][104] = 1 << 56 588 | blockers[P][88] = 1<<56 | 1<<48 589 | blockers[P][103] = 0 590 | blockers[P][105] = 0 591 | 592 | // knights 593 | blockers[N][153] = 0 594 | blockers[N][151] = 0 595 | blockers[N][138] = 0 596 | blockers[N][134] = 0 597 | blockers[N][106] = 0 598 | blockers[N][102] = 0 599 | blockers[N][89] = 0 600 | blockers[N][87] = 0 601 | 602 | // bishops 603 | blockers[B][137] = 0 604 | blockers[B][135] = 0 605 | blockers[B][105] = 0 606 | blockers[B][103] = 0 607 | 608 | // rooks 609 | blockers[R][121] = 0 610 | blockers[R][136] = 0 611 | blockers[R][119] = 0 612 | blockers[R][104] = 0 613 | 614 | // queens 615 | blockers[Q][121] = 0 616 | blockers[Q][136] = 0 617 | blockers[Q][119] = 0 618 | blockers[Q][104] = 0 619 | blockers[Q][137] = 0 620 | blockers[Q][135] = 0 621 | blockers[Q][105] = 0 622 | blockers[Q][103] = 0 623 | 624 | // kings 625 | blockers[K][137] = 0 626 | blockers[K][136] = 0 627 | blockers[K][135] = 0 628 | blockers[K][121] = 0 629 | blockers[K][119] = 0 630 | blockers[K][105] = 0 631 | blockers[K][104] = 0 632 | blockers[K][103] = 0 633 | 634 | // complete movement patterns of sliding pieces (bishops, rooks, queens) 635 | for _, p := range []uint8{B, R, Q} { 636 | for i := 1; i < 7; i++ { 637 | blockers[p][120+(i+1)*1] = blockers[p][120+i*1] | 1< 4 | // All rights reserved. Distributed under the Simplified BSD License. 5 | 6 | package main 7 | 8 | import ( 9 | "code.google.com/p/go.net/websocket" 10 | "expvar" 11 | "flag" 12 | "fmt" 13 | "github.com/tux21b/ChessBuddy/chess" 14 | "go/build" 15 | "html/template" 16 | "log" 17 | "math/rand" 18 | "net" 19 | "net/http" 20 | "path/filepath" 21 | "runtime" 22 | "sync/atomic" 23 | "time" 24 | ) 25 | 26 | // General message struct which is used for parsing client requests and sending 27 | // back responses. 28 | type Message struct { 29 | Cmd string `json:"cmd"` 30 | Turn int `json:"turn"` 31 | Src chess.Square `json:"src"` 32 | Dst chess.Square `json:"dst"` 33 | Color uint8 `json:"color"` 34 | NumPlayers int32 35 | History string 36 | RemainingA, RemainingB time.Duration 37 | Text string 38 | Moves []chess.Square `json:"moves"` 39 | } 40 | 41 | type Player struct { 42 | Conn *websocket.Conn 43 | Color uint8 44 | Remaining time.Duration 45 | Out chan<- Message 46 | ReqAI chan bool 47 | } 48 | 49 | // Check wethever the player is still connected by sending a ping command. 50 | func (p *Player) Alive() bool { 51 | if err := websocket.JSON.Send(p.Conn, Message{Cmd: "ping"}); err != nil { 52 | return false 53 | } 54 | var msg Message 55 | if err := websocket.JSON.Receive(p.Conn, &msg); err != nil { 56 | return false 57 | } 58 | return msg.Cmd == "pong" 59 | } 60 | 61 | func (p *Player) String() string { 62 | switch p.Color { 63 | case chess.White: 64 | return "White" 65 | case chess.Black: 66 | return "Black" 67 | } 68 | return "Unknown" 69 | } 70 | 71 | func (p *Player) Send(msg Message) { 72 | if p.Conn != nil { 73 | p.Out <- msg 74 | } 75 | } 76 | 77 | // Available Players which are currently looking for a taff opponent. 78 | var available = make(chan *Player, 100) 79 | 80 | // Total number of connected players 81 | var numPlayers int32 = 0 82 | 83 | // GoRoutine for hooking up pairs of available players. 84 | func hookUp() { 85 | a := <-available 86 | for { 87 | select { 88 | case b := <-available: 89 | if a.Alive() { 90 | go play(a, b) 91 | a = <-available 92 | } else { 93 | close(a.Out) 94 | a = b 95 | } 96 | case <-a.ReqAI: 97 | go play(a, &Player{}) 98 | a = <-available 99 | } 100 | } 101 | } 102 | 103 | func play(a, b *Player) { 104 | defer func() { 105 | if a.Conn != nil { 106 | close(a.Out) 107 | } 108 | if b.Conn != nil { 109 | close(b.Out) 110 | } 111 | }() 112 | 113 | log.Println("Starting new game") 114 | 115 | board := chess.NewBoard() 116 | if rand.Float32() > 0.5 { 117 | a, b = b, a 118 | } 119 | 120 | a.Color = chess.White 121 | a.Remaining = *timeLimit 122 | b.Color = chess.Black 123 | b.Remaining = *timeLimit 124 | 125 | a.Send(Message{Cmd: "start", Color: a.Color, Turn: board.Turn(), 126 | RemainingA: a.Remaining, RemainingB: b.Remaining}) 127 | b.Send(Message{Cmd: "start", Color: b.Color, Turn: board.Turn(), 128 | RemainingA: a.Remaining, RemainingB: b.Remaining}) 129 | 130 | start := time.Now() 131 | for { 132 | var msg Message 133 | if a.Conn == nil { 134 | msg.Cmd, msg.Turn = "move", board.Turn() 135 | msg.Src, msg.Dst = board.MoveAI() 136 | } else { 137 | a.Conn.SetReadDeadline(start.Add(a.Remaining)) 138 | if err := websocket.JSON.Receive(a.Conn, &msg); err != nil { 139 | if err, ok := err.(net.Error); ok && err.Timeout() { 140 | a.Remaining = 0 141 | msg = Message{ 142 | Cmd: "msg", 143 | Text: fmt.Sprintf("Out of time: %v wins!", b), 144 | } 145 | b.Send(msg) 146 | a.Send(msg) 147 | } else { 148 | msg = Message{ 149 | Cmd: "msg", 150 | Text: "Opponent quit... Reload?", 151 | } 152 | b.Send(msg) 153 | a.Send(msg) 154 | } 155 | break 156 | } 157 | } 158 | if msg.Cmd == "move" && msg.Turn == board.Turn() && 159 | a.Color == board.Color() && board.Move(msg.Src, msg.Dst) { 160 | msg.Color = a.Color 161 | msg.History = board.LastMove() 162 | now := time.Now() 163 | a.Remaining -= now.Sub(start) 164 | if a.Remaining <= 10*time.Millisecond { 165 | a.Remaining = 10 * time.Millisecond 166 | } 167 | start = now 168 | msg.RemainingA, msg.RemainingB = a.Remaining, b.Remaining 169 | if a.Color == chess.Black { 170 | msg.RemainingA, msg.RemainingB = b.Remaining, a.Remaining 171 | } 172 | a, b = b, a 173 | a.Send(msg) 174 | b.Send(msg) 175 | 176 | if board.Checkmate() { 177 | msg = Message{ 178 | Cmd: "msg", 179 | Text: fmt.Sprintf("Checkmate: %v wins!", b), 180 | } 181 | b.Send(msg) 182 | a.Send(msg) 183 | return 184 | } else if board.Stalemate() { 185 | msg = Message{ 186 | Cmd: "msg", 187 | Text: "Stalemate", 188 | } 189 | b.Send(msg) 190 | a.Send(msg) 191 | return 192 | } 193 | } else if msg.Cmd == "select" { 194 | msg.Moves = board.Moves(msg.Src) 195 | a.Send(msg) 196 | } 197 | } 198 | } 199 | 200 | // Serve the index page. 201 | func handleIndex(w http.ResponseWriter, r *http.Request) { 202 | wsURL := fmt.Sprintf("ws://%s/ws", r.Host) 203 | if r.URL.Path == "/ai" { 204 | wsURL += "?ai=true" 205 | } else if r.URL.Path != "/" { 206 | http.Error(w, "Not Found", http.StatusNotFound) 207 | return 208 | } 209 | if err := tmpl.Execute(w, wsURL); err != nil { 210 | log.Printf("tmpl.Execute: %v", err) 211 | } 212 | } 213 | 214 | // Serve a static file (e.g. style sheets, scripts or images). 215 | func handleFile(path string) http.HandlerFunc { 216 | path = filepath.Join(root, path) 217 | return func(w http.ResponseWriter, r *http.Request) { 218 | http.ServeFile(w, r, path) 219 | } 220 | } 221 | 222 | func handleWS(ws *websocket.Conn) { 223 | log.Println("Connected:", ws.Request().RemoteAddr) 224 | atomic.AddInt32(&numPlayers, 1) 225 | exitStat := make(chan bool, 1) 226 | 227 | defer func() { 228 | exitStat <- true 229 | atomic.AddInt32(&numPlayers, -1) 230 | log.Println("Disconnected", ws.Request().RemoteAddr) 231 | ws.Close() 232 | }() 233 | 234 | // Send statistics (i.e. player count) regularly. This will help to to 235 | // detect disconnected players earlier and will prevent stupid proxies 236 | // from closing inactive connections. 237 | go func() { 238 | ticker := time.NewTicker(20 * time.Second) 239 | defer ticker.Stop() 240 | msg := Message{Cmd: "stat"} 241 | for { 242 | msg.NumPlayers = atomic.LoadInt32(&numPlayers) 243 | if err := websocket.JSON.Send(ws, msg); err != nil { 244 | if nerr, ok := err.(net.Error); ok && !nerr.Temporary() { 245 | log.Printf("Network Error: %v", nerr) 246 | ws.Close() 247 | return 248 | } 249 | } 250 | select { 251 | case <-ticker.C: 252 | // continue 253 | case <-exitStat: 254 | return 255 | } 256 | } 257 | }() 258 | 259 | // Add the player to the pool of available players so that he can get 260 | // hooked up 261 | reqAI := make(chan bool, 1) 262 | if ws.Request().FormValue("ai") == "true" { 263 | reqAI <- true 264 | } 265 | out := make(chan Message, 1) 266 | available <- &Player{Conn: ws, Out: out, ReqAI: reqAI} 267 | 268 | // Send the move commands from the game asynchronously, so that a slow 269 | // internet connection can not be simulated to use up the opponents 270 | // time limit. 271 | for msg := range out { 272 | if err := websocket.JSON.Send(ws, msg); err != nil { 273 | log.Printf("websocket.Send: %v", err) 274 | return 275 | } 276 | } 277 | } 278 | 279 | const basePkg = "github.com/tux21b/ChessBuddy" 280 | 281 | var tmpl *template.Template 282 | var root string = "." 283 | 284 | var timeLimit *time.Duration = flag.Duration("time", 5*time.Minute, 285 | "time limit per side (sudden death, no add)") 286 | var listenAddr *string = flag.String("http", ":8000", 287 | "listen on this http address") 288 | 289 | func main() { 290 | runtime.GOMAXPROCS(runtime.NumCPU()) 291 | rand.Seed(time.Now().UnixNano()) 292 | flag.Parse() 293 | if flag.NArg() > 0 { 294 | flag.Usage() 295 | return 296 | } 297 | 298 | expvar.Publish("numplayers", expvar.Func(func() interface{} { 299 | return atomic.LoadInt32(&numPlayers) 300 | })) 301 | 302 | p, err := build.Default.Import(basePkg, "", build.FindOnly) 303 | if err != nil { 304 | log.Fatalf("Couldn't find ChessBuddy files: %v", err) 305 | } 306 | root = p.Dir 307 | 308 | tmpl, err = template.ParseFiles(filepath.Join(root, "chess.html")) 309 | if err != nil { 310 | log.Fatalf("Couldn't parse chess.html: %v", err) 311 | } 312 | 313 | go hookUp() 314 | 315 | http.HandleFunc("/", handleIndex) 316 | http.HandleFunc("/chess.js", handleFile("chess.js")) 317 | http.HandleFunc("/chess.css", handleFile("chess.css")) 318 | http.HandleFunc("/bg.png", handleFile("bg.png")) 319 | http.HandleFunc("/favicon.ico", handleFile("favicon.ico")) 320 | http.Handle("/ws", websocket.Handler(handleWS)) 321 | 322 | if err := http.ListenAndServe(*listenAddr, nil); err != nil { 323 | log.Fatalf("http.ListenAndServe: %v", err) 324 | } 325 | } 326 | --------------------------------------------------------------------------------