play.js 15 KB

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