play.js 15 KB

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