Browse Source

debugging; more io events handling in components

romanmatiasko 9 years ago
parent
commit
7fa6a8b38a

+ 26 - 25
io.js

@@ -4,7 +4,7 @@ const io = require('socket.io').listen();
 const winston = require('./winston');
 const Immutable = require('immutable');
 const {Map, List} = Immutable;
-var games = Map();
+var _games = Map();
 
 io.sockets.on('connection', socket => {
   
@@ -15,13 +15,13 @@ io.sockets.on('connection', socket => {
 
     // token is valid for 5 minutes
     const timeout = setTimeout(() => {
-      if (games.getIn([token, 'players']).isEmpty()) {
-        games = games.delete(token);
+      if (_games.getIn([token, 'players']).isEmpty()) {
+        _games = _games.delete(token);
         socket.emit('token-expired');
       }
     }, 5 * 60 * 1000);
 
-    games = games.set(token, Map({
+    _games = _games.set(token, Map({
       creator: socket,
       players: List(),
       interval: null,
@@ -32,7 +32,8 @@ io.sockets.on('connection', socket => {
   });
 
   socket.on('join', data => {
-    const game = games.get(data.token);
+    const game = _games.get(data.token);
+    const nOfPlayers = game.get('players').size;
     const colors = ['black', 'white'];
     let color;
 
@@ -43,17 +44,17 @@ io.sockets.on('connection', socket => {
 
     clearTimeout(game.get('timeout'));
 
-    if (game.get('players').size >= 2) {
+    if (nOfPlayers >= 2) {
       socket.emit('full');
       return;
-    } else if (game.get('players').size === 1) {
+    } else if (nOfPlayers === 1) {
       if (game.getIn(['players', 0, 'color']) === 'black')
         color = 'white';
       else
         color = 'black';
 
       winston.log('info', 'Number of currently running games', {
-        '#': games.size
+        '#': _games.size
       });
     } else {
       color = colors[Math.floor(Math.random() * 2)];
@@ -62,7 +63,7 @@ io.sockets.on('connection', socket => {
     // join room
     socket.join(data.token);
 
-    games = games.updateIn([data.token, 'players'], players =>
+    _games = _games.updateIn([data.token, 'players'], players =>
       players.push(Map({
         id: socket.id,
         socket: socket,
@@ -80,13 +81,13 @@ io.sockets.on('connection', socket => {
   socket.on('new-move', data => {
     maybeEmit('move', data.move, data.token, socket);
     if (data.move.gameOver) {
-      clearInterval(games.getIn([data.token, 'interval']));
+      clearInterval(_games.getIn([data.token, 'interval']));
     }
   });
 
   socket.on('resign', data => {
-    if (!games.has(data.token)) return;
-    clearInterval(games.getIn([data.token, 'interval']));
+    if (!_games.has(data.token)) return;
+    clearInterval(_games.getIn([data.token, 'interval']));
 
     io.sockets.in(data.token).emit('player-resigned', {
       color: data.color
@@ -100,9 +101,9 @@ io.sockets.on('connection', socket => {
     maybeEmit('rematch-declined', {}, data.token, socket));
 
   socket.on('rematch-confirm', data => {
-    if (!games.has(data.token)) return;
+    if (!_games.has(data.token)) return;
 
-    games = games.updateIn([data.token, 'players'], players =>
+    _games = _games.updateIn([data.token, 'players'], players =>
       players.map(player => player
         .set('time', data.time - data.inc + 1)
         .set('inc', data.inc)
@@ -114,7 +115,7 @@ io.sockets.on('connection', socket => {
   socket.on('disconnect', data => {
     let tokenToDelete;
 
-    games.forEach((game, token) => {
+    _games.forEach((game, token) => {
       const opponent = getOpponent(token, socket);
 
       if (opponent) {
@@ -127,7 +128,7 @@ io.sockets.on('connection', socket => {
     });
 
     if (tokenToDelete) {
-      games = games.delete(tokenToDelete);
+      _games = _games.delete(tokenToDelete);
     }
   });
 
@@ -136,7 +137,7 @@ io.sockets.on('connection', socket => {
 });
 
 function maybeEmit(event, data, token, socket) {
-  if (!games.has(token)) return;
+  if (!_games.has(token)) return;
 
   const opponent = getOpponent(token, socket);
   if (opponent) {
@@ -145,18 +146,18 @@ function maybeEmit(event, data, token, socket) {
 }
 
 function runClock(color, token, socket) {
-  if (!games.has(token)) return;
+  if (!_games.has(token)) return;
 
-  games.getIn([token, 'players']).forEach((player, idx) => {
+  _games.getIn([token, 'players']).forEach((player, idx) => {
     if (player.get('socket') === socket && player.get('color') === color) {
-      clearInterval(games.getIn([token, 'interval']));
+      clearInterval(_games.getIn([token, 'interval']));
       
-      games = games
+      _games = _games
         .updateIn([token, 'players', idx, 'time'], time =>
           time += player.get('inc'))
         .setIn([token, 'interval'], setInterval(() => {
           let timeLeft = 0;
-          games = games.updateIn([token, 'players', idx, 'time'], time => {
+          _games = _games.updateIn([token, 'players', idx, 'time'], time => {
             timeLeft = time - 1;
             return time - 1;
           });
@@ -170,7 +171,7 @@ function runClock(color, token, socket) {
             io.sockets.in(token).emit('countdown-gameover', {
               color: color
             });
-            clearInterval(games.getIn([token, 'interval']));
+            clearInterval(_games.getIn([token, 'interval']));
           }
         }, 1000));
 
@@ -182,7 +183,7 @@ function runClock(color, token, socket) {
 function getOpponent(token, socket) {
   let index = null;
 
-  games.getIn([token, 'players']).forEach((player, idx) => {
+  _games.getIn([token, 'players']).forEach((player, idx) => {
     if (player.get('socket') === socket) {
       index = Math.abs(idx - 1);
 
@@ -191,7 +192,7 @@ function getOpponent(token, socket) {
   });
 
   if (index !== null) {
-    return games.getIn([token, 'players', index]);
+    return _games.getIn([token, 'players', index]);
   }
 }
 

+ 0 - 529
public/javascripts/play.js

@@ -1,309 +1,7 @@
 $(function() {
 
-  var from, to, promotion, rcvd;
-  var $side  = 'w';
-  var $piece = null;
-  var $chess = new Chess();
-  var $gameOver = false;
-  var $chessboardWhite = $('.chessboard.white').clone();
-  var $chessboardBlack = $('.chessboard.black').clone();
-
-  function modalKeydownHandler(e) {
-    e.preventDefault();
-    if (e.which === 13 || e.which === 27) {
-      hideModal();
-    }
-  }
-
-  function offerKeydownHandler(e) {
-    e.preventDefault();
-    if (e.which === 13) {
-      hideOffer();
-      e.data.accept();
-    } else if (e.which === 27) {
-      hideOffer();
-      e.data.decline(); 
-    }
-  }
-
-  function showModal(message) {
-    $('#modal-message').text(message);
-    $('#modal-mask').fadeIn(200);
-    $(document).on('keydown', modalKeydownHandler);
-  }
-
-  function hideModal() {
-    $('#modal-mask').fadeOut(200);
-    $(document).off('keydown', modalKeydownHandler);
-  }
-
-  function showOffer(offer, options) {
-    $('#offer-message').text(offer);
-    $('#offer-mask').fadeIn(200);
-    $(document).on('keydown', options, offerKeydownHandler);
-  }
-
-  function hideOffer() {
-    $('#offer-mask').fadeOut(200);
-    $(document).off('keydown', offerKeydownHandler);
-  }
-
-  function selectPiece(el) {
-    el.addClass('selected');
-  }
-
-  function unselectPiece(el) {
-    el.removeClass('selected');
-  }
-
-  function isSelected(el) {
-    return el ? el.hasClass('selected') : false;
-  }
-
-  function movePiece(from, to, promotion, rcvd) {
-    var move = $chess.move({
-      'from': from,
-      'to': to,
-      promotion: promotion
-    });
-
-    if (move && !$gameOver) {
-      var tdFrom = $('td.' + from.toUpperCase());
-      var tdTo = $('td.' + to.toUpperCase());
-
-      //highlight moves
-      if ($('td').hasClass('last-target')){
-        $('td').removeClass('last-target last-origin');
-      }
-      tdFrom.addClass('last-origin');
-      tdTo.addClass('last-target');
-      
-      var piece = tdFrom.find('a'); // piece being moved
-      var moveSnd = $("#moveSnd")[0];
-      unselectPiece(piece.parent());
-      
-      if (tdTo.html() !== '') { //place captured piece next to the chessboard
-        $('#captured-pieces')
-          .find($chess.turn() === 'b' ? '.b' : '.w')
-          .append('<li>' + tdTo.find('a').html() + '</li>');
-      }
-      
-      tdTo.html(piece);
-
-      $piece = null;
-
-      // en passant move
-      if (move.flags === 'e'){
-        var enpassant = move.to.charAt(0) + move.from.charAt(1);
-        $('td.' + enpassant.toUpperCase()).html('');
-      }
-      
-      //kingside castling
-      var rook;
-      if (move.flags === 'k'){
-        if (move.to === 'g1'){
-          rook = $('td.H1').find('a');
-          $('td.F1').html(rook);
-        }
-        else if (move.to === 'g8'){
-          rook = $('td.H8').find('a');
-          $('td.F8').html(rook);
-        }
-      }
-
-      //queenside castling
-      if (move.flags === 'q'){
-        if (move.to === 'c1'){
-          rook = $('td.A1').find('a');
-          $('td.D1').html(rook);
-        }
-        else if (move.to === 'c8'){
-          rook = $('td.A8').find('a');
-          $('td.D8').html(rook);
-        }
-      }
-
-      //promotion
-      if (move.flags === 'np' || move.flags === 'cp'){
-        var square = $('td.' + move.to.toUpperCase()).find('a');
-        var option = move.promotion;
-        var promotion_w = {
-          'q': '&#9813;',
-          'r': '&#9814;',
-          'n': '&#9816;',
-          'b': '&#9815;'
-        };
-        var promotion_b = {
-          'q': '&#9819;',
-          'r': '&#9820;',
-          'n': '&#9822;',
-          'b': '&#9821;'
-        };
-        if (square.hasClass('white')){
-          square.html(promotion_w[option]);
-        } else {
-          square.html(promotion_b[option]);
-        }
-      }
-      
-      if ($('#sounds').is(':checked')) {
-        moveSnd.play();
-      }
-      
-      //feedback
-      var fm = $('.feedback-move');
-      var fs = $('.feedback-status');
-
-      $chess.turn() === 'b' ? fm.text('Black to move.') : fm.text('White to move.');
-      fm.parent().toggleClass('blackfeedback whitefeedback');
-
-      $chess.in_check() ? fs.text(' Check.') : fs.text('');
-
-      //game over
-      if ($chess.game_over()) {
-        fm.text('');
-        var result = "";
-
-        if ($chess.in_checkmate())
-          result = $chess.turn() === 'b' ? 'Checkmate. White wins!' : 'Checkmate. Black wins!'
-        else if ($chess.in_draw())
-          result = "Draw.";
-        else if ($chess.in_stalemate())
-          result = "Stalemate.";
-        else if ($chess.in_threefold_repetition())
-          result = "Draw. (Threefold Repetition)";
-        else if ($chess.insufficient_material())
-          result = "Draw. (Insufficient Material)";
-        fs.text(result);
-      }
-
-      /* Add all moves to the table */
-      var pgn = $chess.pgn({ max_width: 5, newline_char: ',' });
-      var moves = pgn.split(',');
-      var last_move = moves.pop().split('.');
-      var move_number = last_move[0];
-      var move_pgn = $.trim(last_move[1]);
-
-      if (move_pgn.indexOf(' ') !== -1) {
-        var moves = move_pgn.split(' ');
-        move_pgn = moves[1];
-      }
-
-      $('#moves tbody tr').append('<td><strong>' + move_number + '</strong>. ' + move_pgn + '</td>');
-
-      if (rcvd === undefined) {
-        $socket.emit('new-move', {
-          'token': $token,
-          'move': move
-        });
-      }
-
-      if ($chess.game_over()) {
-        $gameOver = true;
-        $socket.emit('timer-clear-interval', {
-          'token': $token
-        });
-
-        $('.resign').hide();
-        $('.rematch').show();
-        showModal(result);
-      } else {
-        if ($chess.turn() === 'b') {
-          $socket.emit('timer-black', {
-            'token': $token
-          });
-        } else {
-          $socket.emit('timer-white', {
-            'token': $token
-          });
-        }
-        $('#clock li').each(function() {
-          $(this).toggleClass('ticking');
-        });
-      }
-    }
-  }
-
-  function unbindMoveHandlers() {
-    var moveFrom = $('.chessboard a');
-    var moveTo = $('.chessboard td');
-
-    moveFrom.off('click', movePieceFromHandler);
-    moveTo.off('click', movePieceToHandler);
-
-    moveFrom.attr('draggable', false).off('dragstart', dragstartHandler);
-    moveFrom.off('dragend');
-    moveTo.attr('draggable', false).off('drop', dropHandler);
-    moveTo.off('dragover');
-  }
-
-  function bindMoveHandlers() {
-    var moveFrom = $('.chessboard a');
-    var moveTo = $('.chessboard td');
-
-    moveFrom.on('click', movePieceFromHandler);
-    moveTo.on('click', movePieceToHandler);
-
-    if (dndSupported()) {
-      moveFrom.attr('draggable', true).on('dragstart', dragstartHandler);
-      moveTo.on('draggable', true).on('drop', dropHandler);
-      moveTo.on('dragover', function (e) {
-        e.preventDefault();
-        e.originalEvent.dataTransfer.dropEffect = 'move';
-      });
-      moveFrom.on('dragend', function (e) {
-        moveTo.removeClass('moving');
-      });
-    }
-  }
-
-  function escapeHTML(html) {
-    return $('<div/>').text(html).html();
-  }
-
   /* socket.io */
 
-  function rematchAccepted() {
-    $socket.emit('rematch-confirm', {
-      'token': $token,
-      'time': $time * 60,
-      'increment': $increment
-    });
-  }
-
-  function rematchDeclined() {
-    $socket.emit('rematch-decline', {
-      'token': $token
-    });
-  }
-
-  $socket.emit('join', {
-    'token': $token,
-    'time': $time * 60,
-    'increment': $increment
-  });
-
-  $socket.on('token-invalid', function (data) {
-    showModal('Game link is invalid or has expired.');
-  });
-
-  $socket.on('joined', function (data) {
-    if (data.color === 'white') {
-      $side = 'w';
-      $('.chessboard.black').remove();
-      $socket.emit('timer-white', {
-        'token': $token
-      });
-    } else {
-      $side = 'b';
-      $('.chessboard.white').remove();
-      $('.chessboard.black').show();
-    }
-
-    $('#clock li.white').addClass('ticking');
-    $('#send-message').find('input').addClass($side === 'b' ? 'black' : 'white');
-  });
-
   $socket.on('move', function (data) {
     movePiece(from=data.move.from, to=data.move.to, promotion=data.move.promotion, rcvd=true);
 
@@ -321,147 +19,6 @@ $(function() {
     }
   });
 
-  $socket.on('opponent-disconnected', function (data) {
-    $('.resign').off().remove();
-    
-
-    $('#send-message').off();
-    $('#send-message').submit(function (e) {
-      e.preventDefault();
-      showModal("Your opponent has disconnected. You can't send messages.");
-    });
-    $('.rematch').off();
-    $('.rematch').click(function (e) {
-      e.preventDefault();
-      showModal('Your opponent has disconnected. You need to generate a new link.');
-    })
-
-    if (!$gameOver) {
-      showModal("Your opponent has disconnected.");
-    }
-  });
-
-  $socket.on('player-resigned', function (data) {
-    $gameOver = true;
-    $('.resign').hide();
-    $('.rematch').show();
-    unbindMoveHandlers();
-    var winner = data.color === 'w' ? 'Black' : 'White';
-    var loser = data.color === 'w' ? 'White' : 'Black';
-    var message = loser + ' resigned. ' + winner + ' wins.';
-    showModal(message);
-    $('.feedback-move').text('');
-    $('.feedback-status').text(message);
-  });
-
-  $socket.on('full', function (data) {
-    alert("This game already has two players. You have to create a new one.");
-    window.location = '/';
-  });
-
-  // $socket.on('receive-message', function (data) {
-  //   var chat = $('ul#chat');
-  //   var chat_node = $('ul#chat')[0];
-  //   var messageSnd = $("#messageSnd")[0];
-
-  //   chat.append('<li class="' + data.color + ' left" >' + escapeHTML(data.message) + '</li>');
-
-  //   if (chat.is(':visible') && chat_node.scrollHeight > 300) {
-  //     setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
-  //   } else if (!chat.is(':visible') && !$('.new-message').is(':visible')) {
-  //     $('#bubble').before('<span class="new-message">You have a new message!</span>');
-  //   }
-
-  //   if ($('#sounds').is(':checked')) {
-  //     messageSnd.play();
-  //   }
-  // });
-
-  // $socket.on('countdown', function (data) {
-  //   var color = data.color;
-  //   var opp_color = color === 'black' ? 'white' : 'black';
-  //   var min = Math.floor(data.time / 60);
-  //   var sec = data.time % 60;
-  //   if (sec.toString().length === 1) {
-  //     sec = '0' + sec;
-  //   }
-    
-  //   $('#clock li.' + color).text(min + ':' + sec);
-  // });
-
-  // $socket.on('countdown-gameover', function (data) {
-  //   $gameOver = true;
-  //   unbindMoveHandlers();
-  //   var loser = data.color === 'black' ? 'Black' : 'White';
-  //   var winner = data.color === 'black' ? 'White' : 'Black';
-  //   var message = loser + "'s time is out. " + winner + " wins.";
-  //   $('.resign').hide();
-  //   $('.rematch').show();
-  //   showModal(message);
-  //   $('.feedback-move').text('');
-  //   $('.feedback-status').text(message);
-  // });
-
-  $socket.on('rematch-offered', function (data) {
-    hideModal();
-    showOffer('Your opponent sent you a rematch offer.', {
-      accept: rematchAccepted,
-      decline: rematchDeclined
-    });
-  });
-
-  $socket.on('rematch-declined', function (data) {
-    showModal('Rematch offer was declined.');
-  });
-
-  $socket.on('rematch-confirmed', function (data) {
-    hideModal();
-    $side = $side === 'w' ? 'b' : 'w'; //swap sides
-    $piece = null;
-    $chess = new Chess();
-    $gameOver = false;
-
-    $('#clock li').each(function () {
-      $(this).text($time + ':00');
-    });
-
-    if ($('#clock li.black').hasClass('ticking')) {
-      $('#clock li.black').removeClass('ticking');
-      $('#clock li.white').addClass('ticking');
-    }
-
-    $('#moves tbody tr').empty();
-    $('#captured-pieces ul').each(function () {
-      $(this).empty();
-    })
-
-    $('.rematch').hide();
-    $('.resign').show();
-
-    if ($side === 'w') {
-      $('.chessboard.black').remove();
-      $('#board-wrapper').append($chessboardWhite.clone());
-
-      $socket.emit('timer-white', {
-        'token': $token
-      });
-    } else {
-      $('.chessboard.white').remove();
-      $('#board-wrapper').append($chessboardBlack.clone());
-      $('.chessboard.black').show();
-    }
-
-    bindMoveHandlers();
-    $('#send-message').find('input').removeClass('white black').addClass($side === 'b' ? 'black' : 'white');
-  });
-
-  /* gameplay */
-
-  $('#clock li').each(function() {
-    $(this).text($time + ':00');
-  });
-  $('#game-type').text($time + '|' + $increment);
-
   function movePieceFromHandler(e) {
     var piece = $(this);
     if ((piece.hasClass('white') && $side !== 'w') ||
@@ -497,17 +54,6 @@ $(function() {
     }
   }
 
-  function movePieceToHandler(e) {
-    if ($piece) {
-      movePiece(
-        from=$piece.parent().data('id').toLowerCase(),
-        to=$(this).data('id').toLowerCase(),
-        promotion=$('#promotion option:selected').val()
-      )
-    }
-  }
-
-  bindMoveHandlers();
 
   function dndSupported() {
     return 'draggable' in document.createElement('span');
@@ -528,79 +74,4 @@ $(function() {
     movePieceToHandler.call(this, undefined);
   }
 
-  $('#modal-mask, #modal-ok').click(function (e) {
-    e.preventDefault();
-    hideModal();
-  });
-
-  $('#offer-accept').click(function (e) {
-    e.preventDefault();
-    hideOffer();
-    rematchAccepted();
-  });
-
-  $('#offer-decline').click(function (e) {
-    e.preventDefault();
-    hideOffer();
-    rematchDeclined();
-  });
-
-  $('#modal-window, #offer-window').click(function (e) {
-    e.stopPropagation();
-  });
-
-  // $('.resign').click(function (e) {
-  //   e.preventDefault();
-
-  //   $socket.emit('resign', {
-  //     'token': $token,
-  //     'color': $side
-  //   });
-  // });
-
-  // $('.rematch').click(function (e) {
-  //   e.preventDefault();
-  //   showModal('Your offer has been sent.');
-
-  //   $socket.emit('rematch-offer', {
-  //     'token': $token
-  //   });
-  // })
-
-  $('a.chat').click(function (e) {
-    $('#chat-wrapper').toggle();
-    $('.new-message').remove();
-    var chat_node = $('ul#chat')[0];
-    if (chat_node.scrollHeight > 300) {
-      setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
-    }
-  });
-
-  $('#chat-wrapper .close').click(function (e) {
-    $('#chat-wrapper').hide();
-  });
-
-  $('#send-message').submit(function (e) {
-    e.preventDefault();
-    var input = $(this).find('input');
-    var message = input.val();
-    var color = $side === 'b' ? 'black' : 'white';
-
-    if (!/^\W*$/.test(message)) {
-      input.val('');
-      $('ul#chat').append('<li class="' + color + ' right" >' + escapeHTML(message) + '</li>');
-
-      var chat_node = $('ul#chat')[0];
-      if (chat_node.scrollHeight > 300) {
-        setTimeout(function() { chat_node.scrollTop = chat_node.scrollHeight; }, 50);
-      }
-
-      $socket.emit('send-message', {
-        'message': message,
-        'color': color,
-        'token': $token
-      });
-    }
-  });
-
 });

+ 0 - 1
src/css/_chat.scss

@@ -1,5 +1,4 @@
 #chat-wrapper {
-  display: none;
   position: absolute;
   width: 270px;
   right: 10px;

+ 1 - 7
src/css/main.scss

@@ -36,12 +36,6 @@
   }
 }
 
-@media only screen and (min-width: 1400px) {
-  #chat-wrapper {
-    display: block;
-  }
-}
-
 @media only screen and (max-width: 999px) {
   #captured-pieces {
     display: none;
@@ -76,7 +70,7 @@
       }
     }
   }
-  span#game-type{
+  #game-type{
     display: none;
   }
   #container-wrapper {

+ 13 - 7
src/js/components/Chat.js

@@ -10,7 +10,9 @@ const Chat = React.createClass({
     io: React.PropTypes.object.isRequired,
     token: React.PropTypes.string.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired,
-    soundsEnabled: React.PropTypes.bool.isRequired
+    soundsEnabled: React.PropTypes.bool.isRequired,
+    isOpponentAvailable: React.PropTypes.bool.isRequired,
+    openModal: React.PropTypes.func.isRequired
   },
   mixins: [React.addons.PureRenderMixin],
 
@@ -28,6 +30,8 @@ const Chat = React.createClass({
       this._maybePlaySound();
     });
     ChatStore.on('change', this._onChatStoreChange);
+    
+    if (window.innerWidth > 1399) ChatActions.toggleChat();
   },
   componentWillUnmount() {
     ChatStore.off('change', this._onChatStoreChange);
@@ -38,7 +42,7 @@ const Chat = React.createClass({
            style={this.state.isChatHidden ? {display: 'none'} : null}>
         <h4>Chat</h4>
         <a className="close"
-           onClick={this._toggleChat}>
+           onClick={ChatActions.toggleChat}>
           x
         </a>
         <audio preload="auto" ref="msgSnd">
@@ -66,18 +70,20 @@ const Chat = React.createClass({
   _onChatStoreChange() {
     this.setState(ChatStore.getState(), this._scrollChat);
   },
-  _toggleChat(e) {
-    e.preventDefault();
-    ChatActions.toggleChat();
-  },
   _onChangeMessage(e) {
     this.setState({message: e.target.value});
   },
   _submitMessage(e) {
     e.preventDefault();
-    const {io, token, color} = this.props;
+    const {io, token, color, isOpponentAvailable} = this.props;
     const message = this.state.message;
 
+    if (!isOpponentAvailable) {
+      this.props.openModal('info', 'Sorry, your opponent is not connected. ' +
+        'You canโ€˜t send messages.');
+      return;
+    }
+
     ChatActions.submitMessage(message, color + ' right');
     this.setState({message: ''});
 

+ 13 - 8
src/js/components/Chessboard.js

@@ -19,7 +19,9 @@ const Chessboard = React.createClass({
     io: React.PropTypes.object.isRequired,
     token: React.PropTypes.string.isRequired,
     maybePlaySound: React.PropTypes.func.isRequired,
-    color: React.PropTypes.oneOf(['white', 'black']).isRequired
+    color: React.PropTypes.oneOf(['white', 'black']).isRequired,
+    gameOver: React.PropTypes.bool.isRequired,
+    isOpponentAvailable: React.PropTypes.bool.isRequired
   },
   mixins: [React.addons.PureRenderMixin, maybeReverse],
 
@@ -45,15 +47,18 @@ const Chessboard = React.createClass({
         this._runClock();
       }
     });
+
+    io.on('rematch-confirmed', () => this.setState({moveFrom: null}));
   },
   componentWillUnmount() {
     GameStore.off('change', this._onGameChange);
     GameStore.on('new-move', this._onNewMove);
   },
   render() {
+    const {color, isOpponentAvailable, gameOver} = this.props;
     const fenArray = this.state.fen.split(' ');
     const placement = fenArray[0];
-    const isItMyTurn = fenArray[1] === this.props.color.charAt(0);
+    const isItMyTurn = fenArray[1] === color.charAt(0);
     const rows = this._maybeReverse(placement.split('/'));
     const ranks = this._maybeReverse(RANKS, 'white');
 
@@ -64,8 +69,8 @@ const Chessboard = React.createClass({
             key={i}
             rank={ranks.get(i)}
             placement={placement}
-            color={this.props.color}
-            isItMyTurn={isItMyTurn}
+            color={color}
+            isMoveable={isItMyTurn && isOpponentAvailable && !gameOver}
             moveFrom={this.state.moveFrom}
             lastMove={this.state.lastMove}
             setMoveFrom={this._setMoveFrom} />)}
@@ -110,7 +115,7 @@ const Row = React.createClass({
     rank: React.PropTypes.oneOf(['1','2','3','4','5','6','7','8']).isRequired,
     placement: React.PropTypes.string.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired,
-    isItMyTurn: React.PropTypes.bool.isRequired,
+    isMoveable: React.PropTypes.bool.isRequired,
     moveFrom: React.PropTypes.string,
     lastMove: React.PropTypes.object,
     setMoveFrom: React.PropTypes.func.isRequired
@@ -147,7 +152,7 @@ const Column = React.createClass({
     square: React.PropTypes.string.isRequired,
     piece: React.PropTypes.string.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired,
-    isItMyTurn: React.PropTypes.bool.isRequired,
+    isMoveable: React.PropTypes.bool.isRequired,
     moveFrom: React.PropTypes.string,
     lastMove: React.PropTypes.object,
     setMoveFrom: React.PropTypes.func.isRequired
@@ -170,10 +175,10 @@ const Column = React.createClass({
           onClick={this._onClickSquare} />;
   },
   _onClickSquare() {
-    const {isItMyTurn, color, moveFrom, square, piece} = this.props;
+    const {isMoveable, color, moveFrom, square, piece} = this.props;
     const rgx = color === 'white' ? /^[KQRBNP]$/ : /^[kqrbnp]$/;
 
-    if (!isItMyTurn || (!moveFrom && !rgx.test(piece)))
+    if (!isMoveable || (!moveFrom && !rgx.test(piece)))
       return;
     else if (moveFrom && moveFrom === square)
       this.props.setMoveFrom(null);

+ 27 - 22
src/js/components/ChessboardInterface.js

@@ -8,6 +8,7 @@ const Chessboard = require('./Chessboard');
 const CapturedPieces = require('./CapturedPieces');
 const TableOfMoves = require('./TableOfMoves');
 const cx = require('classnames');
+const omit = require('lodash.omit');
 
 const ChessboardInterface = React.createClass({
   
@@ -15,13 +16,21 @@ const ChessboardInterface = React.createClass({
     io: React.PropTypes.object.isRequired,
     token: React.PropTypes.string.isRequired,
     soundsEnabled: React.PropTypes.bool.isRequired,
-    color: React.PropTypes.oneOf(['white', 'black']).isRequired
+    color: React.PropTypes.oneOf(['white', 'black']).isRequired,
+    gameOver: React.PropTypes.object.isRequired,
+    isOpponentAvailable: React.PropTypes.bool.isRequired
   },
   mixins: [React.addons.PureRenderMixin, onGameChange],
 
   getInitialState() {
     return GameStore.getState();
   },
+  componentDidUpdate(prevProps) {
+    if (this.props.gameOver.get('status') &&
+        !prevProps.gameOver.get('status')) {
+      this.props.openModal('info', this._getGameOverMessage());
+    }
+  },
   render() {
     const {promotion, turn, gameOver, check} = this.state;
     const cxFeedback = cx({
@@ -29,8 +38,6 @@ const ChessboardInterface = React.createClass({
       white: turn === 'w',
       black: turn === 'b'
     });
-    const goType = gameOver.get('type');
-    const loser = gameOver.get('winner') === 'White' ? 'Black' : 'White';
 
     return (
       <div id="board-moves-wrapper" className="clearfix">
@@ -45,10 +52,9 @@ const ChessboardInterface = React.createClass({
         <div id="board-wrapper">
           <CapturedPieces />
           <Chessboard
-            io={this.props.io}
-            token={this.props.token}
-            maybePlaySound={this._maybePlaySound}
-            color={this.props.color} />
+            {...omit(this.props, 'soundsEnabled', 'gameOver')}
+            gameOver={gameOver.get('status')}
+            maybePlaySound={this._maybePlaySound} />
         </div>
 
         <TableOfMoves />
@@ -74,21 +80,7 @@ const ChessboardInterface = React.createClass({
             </span> :
 
             <strong>
-              {goType === 'checkmate' ?
-                `Checkmate. ${gameOver.get('winner')} wins!`
-              :goType === 'timeout' ?
-                `${loser}โ€˜s time is out. ${gameOver.get('winner')} wins!`
-              :goType === 'resign' ?
-                `${loser} has resigned. ${gameOver.get('winner')} wins!`
-              :goType === 'draw' ?
-                'Draw.'
-              :goType === 'stalemate' ?
-                'Draw (Stalemate).'
-              :goType === 'threefoldRepetition' ?
-                'Draw (Threefold Repetition).'
-              :goType === 'insufficientMaterial' ?
-                'Draw (Insufficient Material)'
-              :null}
+              {this._getGameOverMessage()}
             </strong>
           }
         </span>
@@ -105,6 +97,19 @@ const ChessboardInterface = React.createClass({
     if (this.props.soundsEnabled) {
       this.refs[this.state.check ? 'checkSnd' : 'moveSnd'].getDOMNode().play();
     }
+  },
+  _getGameOverMessage() {
+    const type = this.props.gameOver.get('type');
+    const winner = this.props.gameOver.get('winner');
+    const loser = winner === 'White' ? 'Black' : 'White';
+
+    return type === 'checkmate' ? `Checkmate. ${winner} wins!` :
+      type === 'timeout' ? `${loser}โ€˜s time is out. ${winner} wins!` :
+      type === 'resign' ? `${loser} has resigned. ${winner} wins!` :
+      type === 'draw' ? 'Draw.' :
+      type === 'stalemate' ? 'Draw (Stalemate).' :
+      type === 'threefoldRepetition' ? 'Draw (Threefold Repetition).' :
+      type === 'insufficientMaterial' ? 'Draw (Insufficient Material)' : '';
   }
 });
 

+ 7 - 0
src/js/components/Clock.js

@@ -37,6 +37,13 @@ const Clock = React.createClass({
         winner: data.color === 'black' ? 'White' : 'Black'
       });
     });
+
+    io.on('rematch-confirmed', () => {
+      this.setState({
+        white: this.props.params[1] * 60,
+        black: this.props.params[1] * 60
+      });
+    });
   },
   render() {
     return (

+ 17 - 10
src/js/components/GameHeader.js

@@ -12,7 +12,8 @@ const GameHeader = React.createClass({
     params: React.PropTypes.array.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired,
     openModal: React.PropTypes.func.isRequired,
-    gameOver: React.PropTypes.bool.isRequired
+    gameOver: React.PropTypes.bool.isRequired,
+    isOpponentAvailable: React.PropTypes.bool.isRequired
   },
   mixins: [React.addons.PureRenderMixin],
 
@@ -36,32 +37,32 @@ const GameHeader = React.createClass({
     ChatStore.off('change', this._onChatStoreChange);
   },
   render() {
-    const [_, time, inc] = this.props.params;
+    const {io, params, gameOver, isOpponentAvailable} = this.props;
 
     return (
       <header className="clearfix">
 
         <Clock
-          io={this.props.io}
-          params={this.props.params} />
+          io={io}
+          params={params} />
 
         <span id="game-type">
-          {`${time}|${inc}`}
+          {`${params[1]}|${params[2]}`}
         </span>
 
         <a className="btn" href="/">New game</a>
 
-        {!this.props.gameOver ?
+        {!gameOver && isOpponentAvailable ?
           <a className="btn btn--red resign"
               onClick={this._onResign}>
             Resign
-          </a> :
-
+          </a>
+        :gameOver ?
           <a className="btn btn--red rematch"
              onClick={this._onRematch}>
             Rematch
           </a>
-        }
+        :null}
 
         <a id="chat-icon"
            onClick={this._toggleChat}>
@@ -94,7 +95,13 @@ const GameHeader = React.createClass({
     });
   },
   _onRematch() {
-    const {io, params, openModal} = this.props;
+    const {io, params, openModal, isOpponentAvailable} = this.props;
+
+    if (!isOpponentAvailable) {
+      openModal('info', 'Your opponent has disconnected. You need to ' +
+        'generate a new link.');
+      return;
+    }
 
     io.emit('rematch-offer', {
       token: params[0]

+ 30 - 21
src/js/components/GameInterface.js

@@ -19,6 +19,7 @@ const GameInterface = React.createClass({
 
   getInitialState() {
     return {
+      isOpponentAvailable: false,
       color: 'white',
       modal: Map({
         open: false,
@@ -59,6 +60,8 @@ const GameInterface = React.createClass({
       } else {
         this.setState({color: 'black'});
       }
+
+      this.setState({isOpponentAvailable: true});
     });
 
     io.on('full', () => {
@@ -68,25 +71,19 @@ const GameInterface = React.createClass({
     });
 
     io.on('player-resigned', data => {
-      const winner = data.color === 'black' ? 'White' : 'Black';
-      const loser = winner === 'Black' ? 'White' : 'Black';
-
       GameActions.gameOver({
         type: 'resign',
-        winner: winner
+        winner: data.color === 'black' ? 'White' : 'Black'
       });
-      this._openModal('info', `${loser} has resigned. ${winner} wins!`);
     });
 
-    io.on('rematch-offered', () => {
-      this._openModal('offer', 'Your opponent has sent you a rematch offer.');
-    });
+    io.on('rematch-offered', () =>
+      this._openModal('offer', 'Your opponent has sent you a rematch offer.'));
 
-    io.on('rematch-declined', () => {
-      this._openModal('info', 'Rematch offer has been declined.');
-    });
+    io.on('rematch-declined', () =>
+      this._openModal('info', 'Rematch offer has been declined.'));
 
-    io.on('rematch-confirmed', data => {
+    io.on('rematch-confirmed', () => {
       GameActions.rematch();
       this.setState({
         color: this.state.color === 'white' ? 'black' : 'white',
@@ -95,11 +92,20 @@ const GameInterface = React.createClass({
         if (this.state.color === 'white') {
           io.emit('clock-run', {
             token: this.props.params[0],
-            clock: 'white'
+            color: 'white'
           });
         }
       });
     });
+
+    io.on('opponent-disconnected', () =>  {
+      if (!this.state.gameOver.get('status')) {
+        this._openModal('info', 'Your opponent has disconnected.');
+      }
+
+      this.setState({isOpponentAvailable: false});
+    });
+
     GameStore.on('change', this._onGameChange);
   },
   componentWillUnmount() {
@@ -107,15 +113,19 @@ const GameInterface = React.createClass({
   },
   render() {
     const {io, params} = this.props;
-    const {color, soundsEnabled, gameOver} = this.state;
+    const {color, soundsEnabled, gameOver, isOpponentAvailable} = this.state;
+    const commonProps = {
+      io: io,
+      color: color,
+      openModal: this._openModal,
+      isOpponentAvailable: isOpponentAvailable
+    };
 
     return (
       <div>
         <GameHeader
-          io={io}
+          {...commonProps}
           params={params}
-          color={color}
-          openModal={this._openModal}
           gameOver={gameOver.get('status')} />
 
         <label id="sounds-label">
@@ -126,16 +136,15 @@ const GameInterface = React.createClass({
         </label>
 
         <Chat
-          io={io}
+          {...commonProps}
           token={params[0]}
-          color={color}
           soundsEnabled={soundsEnabled} />
 
         <ChessboardInterface
-          io={io}
+          {...commonProps}
           token={params[0]}
           soundsEnabled={soundsEnabled}
-          color={color} />
+          gameOver={gameOver} />
 
         <Modal data={this.state.modal} />
       </div>

+ 11 - 2
src/js/components/Modal.js

@@ -1,6 +1,7 @@
 'use strict';
 
 const React = require('react/addons');
+const cx = require('classnames');
 
 const Modal = React.createClass({
   
@@ -23,7 +24,11 @@ const Modal = React.createClass({
     const callbacks = data.get('callbacks');
 
     return (
-      <div className={'modal-mask' + (data.get('open') ? '' : ' hidden')}>
+      <div className={cx({
+             'modal-mask': true,
+             'hidden': !data.get('open')
+           })}
+           onClick={this._hideModal}>
         <p>
           <strong>Esc: </strong>
           <span>{type === 'info' ? 'OK' : 'Decline'}</span>
@@ -32,7 +37,8 @@ const Modal = React.createClass({
           <span>{type === 'info' ? 'OK' : 'Accept'}</span>
         </p>
 
-        <div className="modal">
+        <div className="modal"
+             onClick={e => e.stopPropagation()}>
           <p>{data.get('message')}</p>
 
           {type === 'info' ? 
@@ -73,6 +79,9 @@ const Modal = React.createClass({
         callbacks.decline();
       }
     }
+  },
+  _hideModal() {
+    this.props.data.get('callbacks').hide();
   }
 });
 

+ 1 - 1
src/js/stores/ChatStore.js

@@ -8,7 +8,7 @@ const {List, Map} = Immutable;
 const CHANGE_EVENT = 'change';
   
 var _messages = List();
-var _isChatHidden = false;
+var _isChatHidden = true;
 
 const ChatStore = Object.assign({}, EventEmitter.prototype, {
   getState() {

+ 6 - 6
src/js/stores/GameStore.js

@@ -91,15 +91,15 @@ function makeMove(from, to, capture, emitMove) {
 
   if (_chess.game_over()) {
     const type = _chess.in_checkmate() ? 'checkmate' :
-      _chess.in_draw() ? 'draw' :
       _chess.in_stalemate() ? 'stalemate' :
       _chess.in_threefold_repetition() ? 'threefoldRepetition' :
-      _chess.insufficient_material() ? 'insufficientMaterial' : null;
+      _chess.insufficient_material() ? 'insufficientMaterial' :
+      _chess.in_draw() ? 'draw' : null;
 
-    _gameOver = _gameOver
-      .set('status', true)
-      .set('winner', _turn === 'w' ? 'White' : 'Black')
-      .set('type', type);
+    gameOver({
+      winner: _turn === 'b' ? 'White' : 'Black',
+      type: type
+    });
   }
 
   if (emitMove) {