play.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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('token-invalid', function (data) {
  242. showModal('Game link is invalid or has expired.');
  243. });
  244. $socket.on('joined', function (data) {
  245. if (data.color === 'white') {
  246. $side = 'w';
  247. $('.chess_board.black').remove();
  248. $socket.emit('timer-white', {
  249. 'token': $token
  250. });
  251. } else {
  252. $side = 'b';
  253. $('.chess_board.white').remove();
  254. $('.chess_board.black').show();
  255. }
  256. $('#clock li.white').addClass('ticking');
  257. $('#sendMessage').find('input').addClass($side === 'b' ? 'black' : 'white');
  258. });
  259. $socket.on('move', function (data) {
  260. movePiece(from=data.move.from, to=data.move.to, promotion=data.move.promotion, rcvd=true);
  261. if (typeof document.hidden === undefined) return;
  262. if (document.hidden) {
  263. var title = $('title').text();
  264. $('title').text('* ' + title);
  265. $(window).on('focus', removeAsterisk);
  266. function removeAsterisk(e) {
  267. $('title').text(title);
  268. $(window).off('focus', removeAsterisk);
  269. }
  270. }
  271. });
  272. $socket.on('opponent-disconnected', function (data) {
  273. $('.resign').off().remove();
  274. $('#sendMessage').off();
  275. $('#sendMessage').submit(function (e) {
  276. e.preventDefault();
  277. showModal("Your opponent has disconnected. You can't send messages.");
  278. });
  279. $('.rematch').off();
  280. $('.rematch').click(function (e) {
  281. e.preventDefault();
  282. showModal('Your opponent has disconnected. You need to generate a new link.');
  283. })
  284. if (!$gameOver) {
  285. showModal("Your opponent has disconnected.");
  286. }
  287. });
  288. $socket.on('player-resigned', function (data) {
  289. $gameOver = true;
  290. $('.resign').hide();
  291. $('.rematch').show();
  292. unbindMoveHandlers();
  293. var winner = data.color === 'w' ? 'Black' : 'White';
  294. var loser = data.color === 'w' ? 'White' : 'Black';
  295. var message = loser + ' resigned. ' + winner + ' wins.';
  296. showModal(message);
  297. $('.feedback-move').text('');
  298. $('.feedback-status').text(message);
  299. });
  300. $socket.on('full', function (data) {
  301. alert("This game already has two players. You have to create a new one.");
  302. window.location = '/';
  303. });
  304. $socket.on('receive-message', function (data) {
  305. var chat = $('ul#chat');
  306. var chat_node = $('ul#chat')[0];
  307. var messageSnd = $("#messageSnd")[0];
  308. chat.append('<li class="' + data.color + ' left" >' + escapeHTML(data.message) + '</li>');
  309. if (chat.is(':visible') && chat_node.scrollHeight > 300) {
  310. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  311. } else if (!chat.is(':visible') && !$('.new-message').is(':visible')) {
  312. $('#bubble').before('<span class="new-message">You have a new message!</span>');
  313. }
  314. if ($('#sounds').is(':checked')) {
  315. messageSnd.play();
  316. }
  317. });
  318. $socket.on('countdown', function (data) {
  319. var color = data.color;
  320. var opp_color = color === 'black' ? 'white' : 'black';
  321. var min = Math.floor(data.time / 60);
  322. var sec = data.time % 60;
  323. if (sec.toString().length === 1) {
  324. sec = '0' + sec;
  325. }
  326. $('#clock li.' + color).text(min + ':' + sec);
  327. });
  328. $socket.on('countdown-gameover', function (data) {
  329. $gameOver = true;
  330. unbindMoveHandlers();
  331. var loser = data.color === 'black' ? 'Black' : 'White';
  332. var winner = data.color === 'black' ? 'White' : 'Black';
  333. var message = loser + "'s time is out. " + winner + " wins.";
  334. $('.resign').hide();
  335. $('.rematch').show();
  336. showModal(message);
  337. $('.feedback-move').text('');
  338. $('.feedback-status').text(message);
  339. });
  340. $socket.on('rematch-offered', function (data) {
  341. hideModal();
  342. showOffer('Your opponent sent you a rematch offer.', {
  343. accept: rematchAccepted,
  344. decline: rematchDeclined
  345. });
  346. });
  347. $socket.on('rematch-declined', function (data) {
  348. showModal('Rematch offer was declined.');
  349. });
  350. $socket.on('rematch-confirmed', function (data) {
  351. hideModal();
  352. $side = $side === 'w' ? 'b' : 'w'; //swap sides
  353. $piece = null;
  354. $chess = new Chess();
  355. $gameOver = false;
  356. $('#clock li').each(function () {
  357. $(this).text($time + ':00');
  358. });
  359. if ($('#clock li.black').hasClass('ticking')) {
  360. $('#clock li.black').removeClass('ticking');
  361. $('#clock li.white').addClass('ticking');
  362. }
  363. $('#moves tbody tr').empty();
  364. $('#captured-pieces ul').each(function () {
  365. $(this).empty();
  366. })
  367. $('.rematch').hide();
  368. $('.resign').show();
  369. if ($side === 'w') {
  370. $('.chess_board.black').remove();
  371. $('#board_wrapper').append($chessboardWhite.clone());
  372. $socket.emit('timer-white', {
  373. 'token': $token
  374. });
  375. } else {
  376. $('.chess_board.white').remove();
  377. $('#board_wrapper').append($chessboardBlack.clone());
  378. $('.chess_board.black').show();
  379. }
  380. bindMoveHandlers();
  381. $('#sendMessage').find('input').removeClass('white black').addClass($side === 'b' ? 'black' : 'white');
  382. });
  383. /* gameplay */
  384. $('#clock li').each(function() {
  385. $(this).text($time + ':00');
  386. });
  387. $('#game-type').text($time + ' | ' + $increment);
  388. function movePieceFromHandler(e) {
  389. var piece = $(this);
  390. if ((piece.hasClass('white') && $side !== 'w') ||
  391. (piece.hasClass('black') && $side !== 'b')) {
  392. if ($piece) {
  393. movePiece(
  394. from=$piece.parent().data('id').toLowerCase(),
  395. to=$(this).parent().data('id').toLowerCase(),
  396. promotion=$('#promotion option:selected').val()
  397. );
  398. }
  399. } else {
  400. if ($chess.turn() !== $side) {
  401. return false;
  402. }
  403. if (e && $piece && isSelected($(this).parent())) {
  404. unselectPiece($piece.parent());
  405. $piece = null;
  406. } else {
  407. if ($piece) {
  408. unselectPiece($piece.parent());
  409. $piece = null;
  410. }
  411. $piece = $(this);
  412. selectPiece($piece.parent());
  413. }
  414. }
  415. if (e) { // only on click event, not drag and drop
  416. e.stopImmediatePropagation();
  417. e.preventDefault();
  418. }
  419. }
  420. function movePieceToHandler(e) {
  421. if ($piece) {
  422. movePiece(
  423. from=$piece.parent().data('id').toLowerCase(),
  424. to=$(this).data('id').toLowerCase(),
  425. promotion=$('#promotion option:selected').val()
  426. )
  427. }
  428. }
  429. bindMoveHandlers();
  430. function dndSupported() {
  431. return 'draggable' in document.createElement('span');
  432. }
  433. function dragstartHandler(e) {
  434. var el = $(this);
  435. $drgSrcEl = el;
  436. $drgSrcEl.parent().addClass('moving');
  437. e.originalEvent.dataTransfer.effectAllowed = 'move';
  438. e.originalEvent.dataTransfer.setData('text/html', el.html());
  439. movePieceFromHandler.call(this, undefined);
  440. }
  441. function dropHandler(e) {
  442. e.stopPropagation();
  443. e.preventDefault();
  444. movePieceToHandler.call(this, undefined);
  445. }
  446. $('#modal-mask, #modal-ok').click(function (e) {
  447. e.preventDefault();
  448. hideModal();
  449. });
  450. $('#offer-accept').click(function (e) {
  451. e.preventDefault();
  452. hideOffer();
  453. rematchAccepted();
  454. });
  455. $('#offer-decline').click(function (e) {
  456. e.preventDefault();
  457. hideOffer();
  458. rematchDeclined();
  459. });
  460. $('#modal-window, #offer-window').click(function (e) {
  461. e.stopPropagation();
  462. });
  463. $('.resign').click(function (e) {
  464. e.preventDefault();
  465. $socket.emit('resign', {
  466. 'token': $token,
  467. 'color': $side
  468. });
  469. });
  470. $('.rematch').click(function (e) {
  471. e.preventDefault();
  472. showModal('Your offer has been sent.');
  473. $socket.emit('rematch-offer', {
  474. 'token': $token
  475. });
  476. })
  477. $('a.chat').click(function (e) {
  478. $('#chat-wrapper').toggle();
  479. $('.new-message').remove();
  480. var chat_node = $('ul#chat')[0];
  481. if (chat_node.scrollHeight > 300) {
  482. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  483. }
  484. });
  485. $('#chat-wrapper .close').click(function (e) {
  486. $('#chat-wrapper').hide();
  487. });
  488. $('#sendMessage').submit(function (e) {
  489. e.preventDefault();
  490. var input = $(this).find('input');
  491. var message = input.val();
  492. var color = $side === 'b' ? 'black' : 'white';
  493. if (!/^\W*$/.test(message)) {
  494. input.val('');
  495. $('ul#chat').append('<li class="' + color + ' right" >' + escapeHTML(message) + '</li>');
  496. var chat_node = $('ul#chat')[0];
  497. if (chat_node.scrollHeight > 300) {
  498. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  499. }
  500. $socket.emit('send-message', {
  501. 'message': message,
  502. 'color': color,
  503. 'token': $token
  504. });
  505. }
  506. });
  507. });