play.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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. }
  188. }
  189. }
  190. /* socket.io */
  191. function rematchAccepted() {
  192. $socket.emit('rematch-confirm', {
  193. 'token': $token,
  194. 'time': $time * 60,
  195. 'increment': $increment
  196. });
  197. }
  198. function rematchDeclined() {
  199. $socket.emit('rematch-decline', {
  200. 'token': $token
  201. });
  202. }
  203. $socket.emit('join', {
  204. 'token': $token,
  205. 'time': $time * 60,
  206. 'increment': $increment
  207. });
  208. $socket.on('joined', function (data) {
  209. if (data.color === 'white') {
  210. $side = 'w';
  211. $('.chess_board.black').remove();
  212. $socket.emit('timer-white', {
  213. 'token': $token
  214. });
  215. } else {
  216. $side = 'b';
  217. $('.chess_board.white').remove();
  218. $('.chess_board.black').show();
  219. }
  220. $('#sendMessage').find('input').addClass($side === 'b' ? 'black' : 'white');
  221. });
  222. $socket.on('move', function (data) {
  223. movePiece(from=data.move.from, to=data.move.to, promotion=data.move.promotion, rcvd=true);
  224. });
  225. $socket.on('opponent-disconnected', function (data) {
  226. $('.resign').off().remove();
  227. $('.chess_board a').off('click', movePieceFromHandler);
  228. $('.chess_board td').off('click', movePieceToHandler);
  229. $('#sendMessage').off();
  230. $('#sendMessage').submit(function (e) {
  231. e.preventDefault();
  232. showModal("Your opponent has disconnected. You can't send messages.");
  233. });
  234. $('.rematch').off();
  235. $('.rematch').click(function (e) {
  236. e.preventDefault();
  237. showModal('Your opponent has disconnected. You need to generate a new link.');
  238. })
  239. if (!$gameOver) {
  240. showModal("Your opponent has disconnected.");
  241. }
  242. });
  243. $socket.on('player-resigned', function (data) {
  244. $gameOver = true;
  245. $('.resign').hide();
  246. $('.rematch').show();
  247. $('.chess_board a').off('click', movePieceFromHandler);
  248. $('.chess_board td').off('click', movePieceToHandler);
  249. var winner = data.color === 'w' ? 'Black' : 'White';
  250. var loser = data.color === 'w' ? 'White' : 'Black';
  251. var message = loser + ' resigned. ' + winner + ' wins.';
  252. showModal(message);
  253. $('.feedback-move').text('');
  254. $('.feedback-status').text(message);
  255. });
  256. $socket.on('full', function (data) {
  257. alert("This game already has two players. You have to create a new one.");
  258. window.location = '/';
  259. });
  260. $socket.on('receive-message', function (data) {
  261. var chat = $('ul#chat');
  262. var chat_node = $('ul#chat')[0];
  263. var messageSnd = $("#messageSnd")[0];
  264. chat.append('<li class="' + data.color + ' left" >' + data.message + '</li>');
  265. if (chat.is(':visible') && chat_node.scrollHeight > 300) {
  266. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  267. } else if (!chat.is(':visible') && !$('.new-message').is(':visible')) {
  268. $('#bubble').before('<span class="new-message">You have a new message!</span>');
  269. }
  270. if ($('#sounds').is(':checked')) {
  271. messageSnd.play();
  272. }
  273. });
  274. $socket.on('countdown', function (data) {
  275. var color = data.color;
  276. var opp_color = color === 'black' ? 'white' : 'black';
  277. var min = Math.floor(data.time / 60);
  278. var sec = data.time % 60;
  279. if (sec.toString().length === 1) {
  280. sec = '0' + sec;
  281. }
  282. $('#clock li.' + opp_color).removeClass('ticking');
  283. $('#clock li.' + color).addClass('ticking').text(min + ':' + sec);
  284. });
  285. $socket.on('countdown-gameover', function (data) {
  286. $gameOver = true;
  287. $('.chess_board a').off('click', movePieceFromHandler);
  288. $('.chess_board td').off('click', movePieceToHandler);
  289. var loser = data.color === 'black' ? 'Black' : 'White';
  290. var winner = data.color === 'black' ? 'White' : 'Black';
  291. var message = loser + "'s time is out. " + winner + " wins.";
  292. $('.resign').hide();
  293. $('.rematch').show();
  294. showModal(message);
  295. $('.feedback-move').text('');
  296. $('.feedback-status').text(message);
  297. });
  298. $socket.on('rematch-offered', function (data) {
  299. hideModal();
  300. showOffer('Your opponent sent you a rematch offer.', {
  301. accept: rematchAccepted,
  302. decline: rematchDeclined
  303. });
  304. });
  305. $socket.on('rematch-declined', function (data) {
  306. showModal('Rematch offer was declined.');
  307. });
  308. $socket.on('rematch-confirmed', function (data) {
  309. hideModal();
  310. $side = $side === 'w' ? 'b' : 'w'; //swap sides
  311. $piece = null;
  312. $chess = new Chess();
  313. $gameOver = false;
  314. $('#clock li').each(function () {
  315. $(this).text($time + ':00');
  316. });
  317. $('#moves tbody tr').empty();
  318. $('#captured-pieces ul').each(function () {
  319. $(this).empty();
  320. })
  321. $('.rematch').hide();
  322. $('.resign').show();
  323. if ($side === 'w') {
  324. $('.chess_board.black').remove();
  325. $('#board_wrapper').append($chessboardWhite.clone());
  326. $socket.emit('timer-white', {
  327. 'token': $token
  328. });
  329. } else {
  330. $('.chess_board.white').remove();
  331. $('#board_wrapper').append($chessboardBlack.clone());
  332. $('.chess_board.black').show();
  333. }
  334. $('.chess_board a').on('click', movePieceFromHandler);
  335. $('.chess_board td').on('click', movePieceToHandler);
  336. $('#sendMessage').find('input').removeClass('white black').addClass($side === 'b' ? 'black' : 'white');
  337. });
  338. /* gameplay */
  339. $('#clock li').each(function() {
  340. $(this).text($time + ':00');
  341. });
  342. $('#game-type').text($time + '|' + $increment);
  343. function movePieceFromHandler(e) {
  344. var piece = $(this);
  345. if ((piece.hasClass('white') && $side !== 'w') ||
  346. (piece.hasClass('black') && $side !== 'b')) {
  347. if ($piece) {
  348. movePiece(
  349. from=$piece.parent().data('id').toLowerCase(),
  350. to=$(this).parent().data('id').toLowerCase(),
  351. promotion=$('#promotion option:selected').val()
  352. )
  353. }
  354. } else {
  355. if ($chess.turn() !== $side) {
  356. return false;
  357. }
  358. if ($piece && isSelected($(this).parent())) {
  359. unselectPiece($piece.parent());
  360. $piece = null;
  361. } else {
  362. if ($piece) {
  363. unselectPiece($piece.parent());
  364. $piece = null;
  365. }
  366. $piece = $(this);
  367. selectPiece($piece.parent());
  368. }
  369. }
  370. e.stopImmediatePropagation();
  371. e.preventDefault();
  372. }
  373. function movePieceToHandler(e) {
  374. if ($piece) {
  375. movePiece(
  376. from=$piece.parent().data('id').toLowerCase(),
  377. to=$(this).data('id').toLowerCase(),
  378. promotion=$('#promotion option:selected').val()
  379. )
  380. }
  381. }
  382. $('.chess_board a').on('click', movePieceFromHandler);
  383. $('.chess_board td').on('click', movePieceToHandler);
  384. $('#modal-mask, #modal-ok').click(function (e) {
  385. e.preventDefault();
  386. hideModal();
  387. });
  388. $('#offer-accept').click(function (e) {
  389. e.preventDefault();
  390. hideOffer();
  391. rematchAccepted();
  392. });
  393. $('#offer-decline').click(function (e) {
  394. e.preventDefault();
  395. hideOffer();
  396. rematchDeclined();
  397. });
  398. $('#modal-window, #offer-window').click(function (e) {
  399. e.stopPropagation();
  400. });
  401. $('.resign').click(function (e) {
  402. e.preventDefault();
  403. $socket.emit('resign', {
  404. 'token': $token,
  405. 'color': $side
  406. });
  407. });
  408. $('.rematch').click(function (e) {
  409. e.preventDefault();
  410. showModal('Your offer has been sent.');
  411. $socket.emit('rematch-offer', {
  412. 'token': $token
  413. });
  414. })
  415. $('a.chat').click(function (e) {
  416. $('#chat-wrapper').toggle();
  417. $('.new-message').remove();
  418. var chat_node = $('ul#chat')[0];
  419. if (chat_node.scrollHeight > 300) {
  420. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  421. }
  422. });
  423. $('#chat-wrapper .close').click(function (e) {
  424. $('#chat-wrapper').hide();
  425. });
  426. $('#sendMessage').submit(function (e) {
  427. e.preventDefault();
  428. var input = $(this).find('input');
  429. var message = input.val();
  430. var color = $side === 'b' ? 'black' : 'white';
  431. if (!/^\W*$/.test(message)) {
  432. input.val('');
  433. $('ul#chat').append('<li class="' + color + ' right" >' + message + '</li>');
  434. var chat_node = $('ul#chat')[0];
  435. if (chat_node.scrollHeight > 300) {
  436. setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
  437. }
  438. $socket.emit('send-message', {
  439. 'message': message,
  440. 'color': color,
  441. 'token': $token
  442. });
  443. }
  444. });
  445. });