play.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. $(function() {
  2. var from, to, promotion, rcvd;
  3. var $side = 'w';
  4. var $piece = null;
  5. var $chess = new Chess();
  6. var $gameOver = false;
  7. var $chessboardWhite = $('.chess_board.white').clone();
  8. var $chessboardBlack = $('.chess_board.black').clone();
  9. function modalKeydownHandler(e) {
  10. e.preventDefault();
  11. if (e.which === 13 || e.which === 27) {
  12. hideModal();
  13. }
  14. }
  15. function offerKeydownHandler(e) {
  16. e.preventDefault();
  17. if (e.which === 13) {
  18. hideOffer();
  19. e.data.accept();
  20. } else if (e.which === 27) {
  21. hideOffer();
  22. e.data.decline();
  23. }
  24. }
  25. function showModal(message) {
  26. $('#modal-message').text(message);
  27. $('#modal-mask').fadeIn(200);
  28. $(document).on('keydown', modalKeydownHandler);
  29. }
  30. function hideModal() {
  31. $('#modal-mask').fadeOut(200);
  32. $(document).off('keydown', modalKeydownHandler);
  33. }
  34. function showOffer(offer, options) {
  35. $('#offer-message').text(offer);
  36. $('#offer-mask').fadeIn(200);
  37. $(document).on('keydown', options, offerKeydownHandler);
  38. }
  39. function hideOffer() {
  40. $('#offer-mask').fadeOut(200);
  41. $(document).off('keydown', offerKeydownHandler);
  42. }
  43. function selectPiece(el) {
  44. el.addClass('selected');
  45. }
  46. function unselectPiece(el) {
  47. el.removeClass('selected');
  48. }
  49. function isSelected(el) {
  50. return el ? el.hasClass('selected') : false;
  51. }
  52. function movePiece(from, to, promotion, rcvd) {
  53. var move = $chess.move({
  54. 'from': from,
  55. 'to': to,
  56. promotion: promotion
  57. });
  58. if (move && !$gameOver) {
  59. var tdFrom = $('td.' + from.toUpperCase());
  60. var tdTo = $('td.' + to.toUpperCase());
  61. //highlight moves
  62. if ($('td').hasClass('last-target')){
  63. $('td').removeClass('last-target last-origin');
  64. }
  65. tdFrom.addClass('last-origin');
  66. tdTo.addClass('last-target');
  67. var piece = tdFrom.find('a'); // piece being moved
  68. var moveSnd = $("#moveSnd")[0];
  69. unselectPiece(piece.parent());
  70. if (tdTo.html() !== '') { //place captured piece next to the chessboard
  71. $('#captured-pieces')
  72. .find($chess.turn() === 'b' ? '.b' : '.w')
  73. .append('<li>' + tdTo.find('a').html() + '</li>');
  74. }
  75. tdTo.html(piece);
  76. $piece = null;
  77. // en passant move
  78. if (move.flags === 'e'){
  79. var enpassant = move.to.charAt(0) + move.from.charAt(1);
  80. $('td.' + enpassant.toUpperCase()).html('');
  81. }
  82. //kingside castling
  83. var rook;
  84. if (move.flags === 'k'){
  85. if (move.to === 'g1'){
  86. rook = $('td.H1').find('a');
  87. $('td.F1').html(rook);
  88. }
  89. else if (move.to === 'g8'){
  90. rook = $('td.H8').find('a');
  91. $('td.F8').html(rook);
  92. }
  93. }
  94. //queenside castling
  95. if (move.flags === 'q'){
  96. if (move.to === 'c1'){
  97. rook = $('td.A1').find('a');
  98. $('td.D1').html(rook);
  99. }
  100. else if (move.to === 'c8'){
  101. rook = $('td.A8').find('a');
  102. $('td.D8').html(rook);
  103. }
  104. }
  105. //promotion
  106. if (move.flags === 'np' || move.flags === 'cp'){
  107. var square = $('td.' + move.to.toUpperCase()).find('a');
  108. var option = move.promotion;
  109. var promotion_w = {
  110. 'q': '&#9813;',
  111. 'r': '&#9814;',
  112. 'n': '&#9816;',
  113. 'b': '&#9815;'
  114. };
  115. var promotion_b = {
  116. 'q': '&#9819;',
  117. 'r': '&#9820;',
  118. 'n': '&#9822;',
  119. 'b': '&#9821;'
  120. };
  121. if (square.hasClass('white')){
  122. square.html(promotion_w[option]);
  123. } else {
  124. square.html(promotion_b[option]);
  125. }
  126. }
  127. if ($('#sounds').is(':checked')) {
  128. moveSnd.play();
  129. }
  130. //feedback
  131. var fm = $('.feedback-move');
  132. var fs = $('.feedback-status');
  133. $chess.turn() === 'b' ? fm.text('Black to move.') : fm.text('White to move.');
  134. fm.parent().toggleClass('blackfeedback whitefeedback');
  135. $chess.in_check() ? fs.text(' Check.') : fs.text('');
  136. //game over
  137. if ($chess.game_over()) {
  138. fm.text('');
  139. var result = "";
  140. if ($chess.in_checkmate())
  141. result = $chess.turn() === 'b' ? 'Checkmate. White wins!' : 'Checkmate. Black wins!'
  142. else if ($chess.in_draw())
  143. result = "Draw.";
  144. else if ($chess.in_stalemate())
  145. result = "Stalemate.";
  146. else if ($chess.in_threefold_repetition())
  147. result = "Draw. (Threefold Repetition)";
  148. else if ($chess.insufficient_material())
  149. result = "Draw. (Insufficient Material)";
  150. fs.text(result);
  151. }
  152. /* Add all moves to the table */
  153. var pgn = $chess.pgn({ max_width: 5, newline_char: ',' });
  154. var moves = pgn.split(',');
  155. var last_move = moves.pop().split('.');
  156. var move_number = last_move[0];
  157. var move_pgn = $.trim(last_move[1]);
  158. if (move_pgn.indexOf(' ') !== -1) {
  159. var moves = move_pgn.split(' ');
  160. move_pgn = moves[1];
  161. }
  162. $('#moves tbody tr').append('<td><strong>' + move_number + '</strong>. ' + move_pgn + '</td>');
  163. if (rcvd === undefined) {
  164. $socket.emit('new-move', {
  165. 'token': $token,
  166. 'move': move
  167. });
  168. }
  169. if ($chess.game_over()) {
  170. $gameOver = true;
  171. $socket.emit('timer-clear-interval', {
  172. 'token': $token
  173. });
  174. $('.resign').hide();
  175. $('.rematch').show();
  176. showModal(result);
  177. } else {
  178. if ($chess.turn() === 'b') {
  179. $socket.emit('timer-black', {
  180. 'token': $token
  181. });
  182. } else {
  183. $socket.emit('timer-white', {
  184. 'token': $token
  185. });
  186. }
  187. $('#clock li').each(function() {
  188. $(this).toggleClass('ticking');
  189. });
  190. }
  191. }
  192. }
  193. function unbindMoveHandlers() {
  194. var moveFrom = $('.chess_board a');
  195. var moveTo = $('.chess_board td');
  196. moveFrom.off('click', movePieceFromHandler);
  197. moveTo.off('click', movePieceToHandler);
  198. moveFrom.attr('draggable', false).off('dragstart', dragstartHandler);
  199. moveFrom.off('dragend');
  200. moveTo.attr('draggable', false).off('drop', dropHandler);
  201. moveTo.off('dragover');
  202. }
  203. function bindMoveHandlers() {
  204. var moveFrom = $('.chess_board a');
  205. var moveTo = $('.chess_board td');
  206. moveFrom.on('click', movePieceFromHandler);
  207. moveTo.on('click', movePieceToHandler);
  208. if (dndSupported()) {
  209. moveFrom.attr('draggable', true).on('dragstart', dragstartHandler);
  210. moveTo.on('draggable', true).on('drop', dropHandler);
  211. moveTo.on('dragover', function (e) {
  212. e.preventDefault();
  213. e.originalEvent.dataTransfer.dropEffect = 'move';
  214. });
  215. moveFrom.on('dragend', function (e) {
  216. moveTo.removeClass('moving');
  217. });
  218. }
  219. }
  220. function escapeHTML(html) {
  221. return $('<div/>').text(html).html();
  222. }
  223. /* socket.io */
  224. function rematchAccepted() {
  225. $socket.emit('rematch-confirm', {
  226. 'token': $token,
  227. 'time': $time * 60,
  228. 'increment': $increment
  229. });
  230. }
  231. function rematchDeclined() {
  232. $socket.emit('rematch-decline', {
  233. 'token': $token
  234. });
  235. }
  236. $socket.emit('join', {
  237. 'token': $token,
  238. 'time': $time * 60,
  239. 'increment': $increment
  240. });
  241. $socket.on('joined', function (data) {
  242. if (data.color === 'white') {
  243. $side = 'w';
  244. $('.chess_board.black').remove();
  245. $socket.emit('timer-white', {
  246. 'token': $token
  247. });
  248. } else {
  249. $side = 'b';
  250. $('.chess_board.white').remove();
  251. $('.chess_board.black').show();
  252. }
  253. $('#clock li.white').addClass('ticking');
  254. $('#sendMessage').find('input').addClass($side === 'b' ? 'black' : 'white');
  255. });
  256. $socket.on('move', function (data) {
  257. movePiece(from=data.move.from, to=data.move.to, promotion=data.move.promotion, rcvd=true);
  258. });
  259. $socket.on('opponent-disconnected', function (data) {
  260. $('.resign').off().remove();
  261. $('#sendMessage').off();
  262. $('#sendMessage').submit(function (e) {
  263. e.preventDefault();
  264. showModal("Your opponent has disconnected. You can't send messages.");
  265. });
  266. $('.rematch').off();
  267. $('.rematch').click(function (e) {
  268. e.preventDefault();
  269. showModal('Your opponent has disconnected. You need to generate a new link.');
  270. })
  271. if (!$gameOver) {
  272. showModal("Your opponent has disconnected.");
  273. }
  274. });
  275. $socket.on('player-resigned', function (data) {
  276. $gameOver = true;
  277. $('.resign').hide();
  278. $('.rematch').show();
  279. unbindMoveHandlers();
  280. var winner = data.color === 'w' ? 'Black' : 'White';
  281. var loser = data.color === 'w' ? 'White' : 'Black';
  282. var message = loser + ' resigned. ' + winner + ' wins.';
  283. showModal(message);
  284. $('.feedback-move').text('');
  285. $('.feedback-status').text(message);
  286. });
  287. $socket.on('full', function (data) {
  288. alert("This game already has two players. You have to create a new one.");
  289. window.location = '/';
  290. });
  291. $socket.on('receive-message', function (data) {
  292. var chat = $('ul#chat');
  293. var chat_node = $('ul#chat')[0];
  294. var messageSnd = $("#messageSnd")[0];
  295. chat.append('<li class="' + data.color + ' left" >' + escapeHTML(data.message) + '</li>');
  296. if (chat.is(':visible') && chat_node.scrollHeight > 300) {
  297. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  298. } else if (!chat.is(':visible') && !$('.new-message').is(':visible')) {
  299. $('#bubble').before('<span class="new-message">You have a new message!</span>');
  300. }
  301. if ($('#sounds').is(':checked')) {
  302. messageSnd.play();
  303. }
  304. });
  305. $socket.on('countdown', function (data) {
  306. var color = data.color;
  307. var opp_color = color === 'black' ? 'white' : 'black';
  308. var min = Math.floor(data.time / 60);
  309. var sec = data.time % 60;
  310. if (sec.toString().length === 1) {
  311. sec = '0' + sec;
  312. }
  313. $('#clock li.' + color).text(min + ':' + sec);
  314. });
  315. $socket.on('countdown-gameover', function (data) {
  316. $gameOver = true;
  317. unbindMoveHandlers();
  318. var loser = data.color === 'black' ? 'Black' : 'White';
  319. var winner = data.color === 'black' ? 'White' : 'Black';
  320. var message = loser + "'s time is out. " + winner + " wins.";
  321. $('.resign').hide();
  322. $('.rematch').show();
  323. showModal(message);
  324. $('.feedback-move').text('');
  325. $('.feedback-status').text(message);
  326. });
  327. $socket.on('rematch-offered', function (data) {
  328. hideModal();
  329. showOffer('Your opponent sent you a rematch offer.', {
  330. accept: rematchAccepted,
  331. decline: rematchDeclined
  332. });
  333. });
  334. $socket.on('rematch-declined', function (data) {
  335. showModal('Rematch offer was declined.');
  336. });
  337. $socket.on('rematch-confirmed', function (data) {
  338. hideModal();
  339. $side = $side === 'w' ? 'b' : 'w'; //swap sides
  340. $piece = null;
  341. $chess = new Chess();
  342. $gameOver = false;
  343. $('#clock li').each(function () {
  344. $(this).text($time + ':00');
  345. });
  346. if ($('#clock li.black').hasClass('ticking')) {
  347. $('#clock li.black').removeClass('ticking');
  348. $('#clock li.white').addClass('ticking');
  349. }
  350. $('#moves tbody tr').empty();
  351. $('#captured-pieces ul').each(function () {
  352. $(this).empty();
  353. })
  354. $('.rematch').hide();
  355. $('.resign').show();
  356. if ($side === 'w') {
  357. $('.chess_board.black').remove();
  358. $('#board_wrapper').append($chessboardWhite.clone());
  359. $socket.emit('timer-white', {
  360. 'token': $token
  361. });
  362. } else {
  363. $('.chess_board.white').remove();
  364. $('#board_wrapper').append($chessboardBlack.clone());
  365. $('.chess_board.black').show();
  366. }
  367. bindMoveHandlers();
  368. $('#sendMessage').find('input').removeClass('white black').addClass($side === 'b' ? 'black' : 'white');
  369. });
  370. /* gameplay */
  371. $('#clock li').each(function() {
  372. $(this).text($time + ':00');
  373. });
  374. $('#game-type').text($time + '|' + $increment);
  375. function movePieceFromHandler(e) {
  376. var piece = $(this);
  377. if ((piece.hasClass('white') && $side !== 'w') ||
  378. (piece.hasClass('black') && $side !== 'b')) {
  379. if ($piece) {
  380. movePiece(
  381. from=$piece.parent().data('id').toLowerCase(),
  382. to=$(this).parent().data('id').toLowerCase(),
  383. promotion=$('#promotion option:selected').val()
  384. );
  385. }
  386. } else {
  387. if ($chess.turn() !== $side) {
  388. return false;
  389. }
  390. if (e && $piece && isSelected($(this).parent())) {
  391. unselectPiece($piece.parent());
  392. $piece = null;
  393. } else {
  394. if ($piece) {
  395. unselectPiece($piece.parent());
  396. $piece = null;
  397. }
  398. $piece = $(this);
  399. selectPiece($piece.parent());
  400. }
  401. }
  402. if (e) { // only on click event, not drag and drop
  403. e.stopImmediatePropagation();
  404. e.preventDefault();
  405. }
  406. }
  407. function movePieceToHandler(e) {
  408. if ($piece) {
  409. movePiece(
  410. from=$piece.parent().data('id').toLowerCase(),
  411. to=$(this).data('id').toLowerCase(),
  412. promotion=$('#promotion option:selected').val()
  413. )
  414. }
  415. }
  416. bindMoveHandlers();
  417. function dndSupported() {
  418. return 'draggable' in document.createElement('span');
  419. }
  420. function dragstartHandler(e) {
  421. var el = $(this);
  422. $drgSrcEl = el;
  423. $drgSrcEl.parent().addClass('moving');
  424. e.originalEvent.dataTransfer.effectAllowed = 'move';
  425. e.originalEvent.dataTransfer.setData('text/html', el.html());
  426. movePieceFromHandler.call(this, undefined);
  427. }
  428. function dropHandler(e) {
  429. e.stopPropagation();
  430. e.preventDefault();
  431. movePieceToHandler.call(this, undefined);
  432. }
  433. $('#modal-mask, #modal-ok').click(function (e) {
  434. e.preventDefault();
  435. hideModal();
  436. });
  437. $('#offer-accept').click(function (e) {
  438. e.preventDefault();
  439. hideOffer();
  440. rematchAccepted();
  441. });
  442. $('#offer-decline').click(function (e) {
  443. e.preventDefault();
  444. hideOffer();
  445. rematchDeclined();
  446. });
  447. $('#modal-window, #offer-window').click(function (e) {
  448. e.stopPropagation();
  449. });
  450. $('.resign').click(function (e) {
  451. e.preventDefault();
  452. $socket.emit('resign', {
  453. 'token': $token,
  454. 'color': $side
  455. });
  456. });
  457. $('.rematch').click(function (e) {
  458. e.preventDefault();
  459. showModal('Your offer has been sent.');
  460. $socket.emit('rematch-offer', {
  461. 'token': $token
  462. });
  463. })
  464. $('a.chat').click(function (e) {
  465. $('#chat-wrapper').toggle();
  466. $('.new-message').remove();
  467. var chat_node = $('ul#chat')[0];
  468. if (chat_node.scrollHeight > 300) {
  469. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  470. }
  471. });
  472. $('#chat-wrapper .close').click(function (e) {
  473. $('#chat-wrapper').hide();
  474. });
  475. $('#sendMessage').submit(function (e) {
  476. e.preventDefault();
  477. var input = $(this).find('input');
  478. var message = input.val();
  479. var color = $side === 'b' ? 'black' : 'white';
  480. if (!/^\W*$/.test(message)) {
  481. input.val('');
  482. $('ul#chat').append('<li class="' + color + ' right" >' + escapeHTML(message) + '</li>');
  483. var chat_node = $('ul#chat')[0];
  484. if (chat_node.scrollHeight > 300) {
  485. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  486. }
  487. $socket.emit('send-message', {
  488. 'message': message,
  489. 'color': color,
  490. 'token': $token
  491. });
  492. }
  493. });
  494. });