Browse Source

add make move action

romanmatiasko 9 years ago
parent
commit
fd855cf7e8

+ 2 - 1
gulpfile.js

@@ -30,7 +30,8 @@ var dependencies = [
   'immutable',
   'flux',
   'eventemitter2',
-  'chess.js'
+  'chess.js',
+  'lodash.omit'
 ];
 
 var browserifyTask = function() {

+ 4 - 9
io.js

@@ -89,21 +89,16 @@ io.sockets.on('connection', function (socket) {
     runTimer('black', data.token, socket);
   });
 
-  socket.on('timer-clear-interval', function (data) {
-    if (data.token in games) {
-      clearInterval(games[data.token].interval);
-    }
-  });
-
   socket.on('new-move', function (data) {
     var opponent;
 
     if (data.token in games) {
       opponent = getOpponent(data.token, socket);
       if (opponent) {
-        opponent.socket.emit('move', {
-          'move': data.move
-        });
+        opponent.socket.emit('move', data.move);
+      }
+      if (data.move.gameOver) {
+        clearInterval(games[data.token].interval);
       }
     }
   });

+ 1 - 0
package.json

@@ -37,6 +37,7 @@
     "gulp-streamify": "0.0.5",
     "gulp-uglify": "^1.1.0",
     "gulp-util": "^3.0.4",
+    "lodash.omit": "^3.0.0",
     "react": "^0.12.2",
     "socket.io-client": "^1.3.4",
     "vinyl-source-stream": "^1.0.0",

+ 4 - 7
src/css/_chessboard.scss

@@ -75,9 +75,10 @@ table#moves {
       display: block;
       float: left;
 
-      &.moving {
-        background: lighten($red, 30%) !important;
-      }
+      &.moving { background: lighten($red, 30%) !important; }
+      &.selected { background: lighten($red, 30%) !important; }
+      &.from { background: lighten($blue, 25%) !important; }
+      &.to { background: lighten($blue, 20%) !important; }
 
       a {
         width: 6ch2.5px;
@@ -125,8 +126,4 @@ span.promotion {
   margin-top: 1em;
   display: block;
   float: right;
-}
-
-.chessboard td.selected {
-  background: lighten($red, 30%) !important;
 }

+ 0 - 2
src/css/main.scss

@@ -5,8 +5,6 @@
 
 .alpha { font-size: 1.125rem; }
 .center { text-align: center !important; }
-.last-origin { background: lighten($blue, 25%) !important; }
-.last-target { background: lighten($blue, 20%) !important; }
 
 @media only screen and (min-width: 760px) and (max-width: 900px) {
   header, #board-moves-wrapper {

+ 9 - 0
src/js/actions/GameActions.js

@@ -2,6 +2,15 @@ const GameConstants = require('../constants/GameConstants');
 const AppDispatcher = require('../dispatcher/AppDispatcher');
 
 const GameActions = {
+  makeMove(from, to, capture, emitMove) {
+    AppDispatcher.handleViewAction({
+      actionType: GameConstants.MAKE_MOVE,
+      from: from,
+      to: to,
+      capture: capture,
+      emitMove: emitMove
+    });
+  },
   rematch() {
     AppDispatcher.handleViewAction({
       actionType: GameConstants.REMATCH

+ 116 - 26
src/js/components/Chessboard.js

@@ -6,39 +6,91 @@ const GameActions = require('../actions/GameActions');
 const ChessPieces = require('../constants/ChessPieces');
 const onGameChange = require('../mixins/onGameChange');
 const maybeReverse = require('../mixins/maybeReverse');
+const omit = require('lodash.omit');
+const cx = require('classnames');
 const Immutable = require('immutable');
-const {Seq, Repeat} = Immutable;
-const PureRenderMixin = React.addons.PureRenderMixin;
+const {Seq, Repeat, List} = Immutable;
+const FILES = Seq.Indexed('abcdefgh');
+const RANKS = Seq.Indexed('12345678');
 
 const Chessboard = React.createClass({
   
   propTypes: {
     io: React.PropTypes.object.isRequired,
+    token: React.PropTypes.string.isRequired,
     maybePlaySound: React.PropTypes.func.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired
   },
-  mixins: [PureRenderMixin, onGameChange, maybeReverse],
+  mixins: [React.addons.PureRenderMixin, maybeReverse],
 
   getInitialState() {
+    const state = GameStore.getChessboardState();
+
     return {
-      fen: GameStore.getFEN()
+      fen: state.fen,
+      moveFrom: null,
+      lastMove: state.lastMove
     };
   },
+  componentDidMount() {
+    GameStore.on('change', this._onGameChange);
+    GameStore.on('new-move', this._onNewMove);
+
+    this.props.io.on('move', data => {
+      console.log(data);
+      GameActions.makeMove(data.from, data.to, data.capture, false);
+    });
+  },
+  componentWillUnmount() {
+    GameStore.off('change', this._onGameChange);
+    GameStore.on('new-move', this._onNewMove);
+  },
   render() {
-    const fen = this.state.fen;
-    const placement = fen.split(' ')[0];
+    const fenArray = this.state.fen.split(' ');
+    const placement = fenArray[0];
+    const isItMyTurn = fenArray[1] === this.props.color.charAt(0);
     const rows = this._maybeReverse(placement.split('/'));
+    const ranks = this._maybeReverse(RANKS, 'white');
 
     return (
       <table className="chessboard">
         {rows.map((placement, i) =>
-          <Row placement={placement} color={this.props.color} key={i} />)}
+          <Row
+            key={i}
+            rank={ranks.get(i)}
+            placement={placement}
+            color={this.props.color}
+            isItMyTurn={isItMyTurn}
+            moveFrom={this.state.moveFrom}
+            lastMove={this.state.lastMove}
+            setMoveFrom={this._setMoveFrom} />)}
       </table>
     );
   },
   _onGameChange() {
+    const state = GameStore.getChessboardState();
     this.setState({
-      fen: GameStore.getFEN()
+      fen: state.fen,
+      lastMove: state.lastMove
+    });
+  },
+  _setMoveFrom(square) {
+    this.setState({
+      moveFrom: square
+    });
+  },
+  _onNewMove(move) {
+    const {io, token} = this.props;
+
+    io.emit('new-move', {
+      token: token,
+      move: omit(move, 'turn')
+    });
+
+    if (move.gameOver) return;
+
+    io.emit(move.turn === 'b' ? 'timer-black' : 'timer-white', {
+      token: token
     });
   }
 });
@@ -46,28 +98,35 @@ const Chessboard = React.createClass({
 const Row = React.createClass({
 
   propTypes: {
+    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
+    color: React.PropTypes.oneOf(['white', 'black']).isRequired,
+    isItMyTurn: React.PropTypes.bool.isRequired,
+    moveFrom: React.PropTypes.string,
+    lastMove: React.PropTypes.object,
+    setMoveFrom: React.PropTypes.func.isRequired
   },
-  mixins: [PureRenderMixin, maybeReverse],
+  mixins: [maybeReverse],
 
   render() {
-    const placement = this.props.placement;
-    let pieces;
-
-    if (placement.length < 8) {
-      pieces = this._maybeReverse(
-        Seq(placement).flatMap(piece => (
-          /^\d$/.test(piece) ? Repeat('-', parseInt(piece, 10)) : piece
-        ))
-      ).toArray();
-    } else {
-      pieces = this._maybeReverse(placement.split(''));
-    }
+    const {rank, placement, color} = this.props;
+    const files = this._maybeReverse(FILES);
+    const pieces = this._maybeReverse(placement.length < 8 ?
+      Seq(placement).flatMap(piece => (
+        /^\d$/.test(piece) ? Repeat('-', parseInt(piece, 10)) : piece
+      )).toArray() :
+
+      placement.split('')
+    );
+
     return (
       <tr>
         {pieces.map((piece, i) =>
-          <Column piece={piece} key={i} />)}
+          <Column
+            key={i}
+            square={files.get(i) + rank}
+            piece={piece}
+            {...omit(this.props, 'rank', 'placement')} />)}
       </tr>
     );
   }
@@ -76,12 +135,43 @@ const Row = React.createClass({
 const Column = React.createClass({
 
   propTypes: {
-    piece: React.PropTypes.string.isRequired
+    square: React.PropTypes.string.isRequired,
+    piece: React.PropTypes.string.isRequired,
+    color: React.PropTypes.oneOf(['white', 'black']).isRequired,
+    isItMyTurn: React.PropTypes.bool.isRequired,
+    moveFrom: React.PropTypes.string,
+    lastMove: React.PropTypes.object,
+    setMoveFrom: React.PropTypes.func.isRequired
   },
-  mixins: [PureRenderMixin],
 
   render() {
-    return <td>{ChessPieces[this.props.piece]}</td>;
+    const {moveFrom, lastMove, square} = this.props;
+    const piece = ChessPieces[this.props.piece];
+
+    return piece ? 
+      <td className={cx({
+            selected: moveFrom === square,
+            to: lastMove.get('to') === square
+          })}>
+        <a onClick={this._onClickSquare}>
+          {piece}
+        </a>
+      </td> :
+      <td className={lastMove.get('from') === square ? 'from' : null}
+          onClick={this._onClickSquare} />;
+  },
+  _onClickSquare() {
+    const {isItMyTurn, color, moveFrom, square, piece} = this.props;
+    const rgx = color === 'white' ? /^[KQRBNP]$/ : /^[kqrbnp]$/;
+
+    if (!isItMyTurn || (!moveFrom && !rgx.test(piece)))
+      return;
+    else if (moveFrom && moveFrom === square)
+      this.props.setMoveFrom(null);
+    else if (rgx.test(piece))
+      this.props.setMoveFrom(square);
+    else
+      GameActions.makeMove(moveFrom, square, ChessPieces[piece], true);
   }
 });
 

+ 3 - 1
src/js/components/ChessboardInterface.js

@@ -13,6 +13,7 @@ const ChessboardInterface = React.createClass({
   
   propTypes: {
     io: React.PropTypes.object.isRequired,
+    token: React.PropTypes.string.isRequired,
     soundsEnabled: React.PropTypes.bool.isRequired,
     color: React.PropTypes.oneOf(['white', 'black']).isRequired
   },
@@ -42,6 +43,7 @@ const ChessboardInterface = React.createClass({
           <CapturedPieces />
           <Chessboard
             io={this.props.io}
+            token={this.props.token}
             maybePlaySound={this._maybePlaySound}
             color={this.props.color} />
         </div>
@@ -55,8 +57,8 @@ const ChessboardInterface = React.createClass({
                     onChange={this._onPromotionChange}>
               <option value="q">Queen</option>
               <option value="r">Rook</option>
-              <option value="n">Knight</option>
               <option value="b">Bishop</option>
+              <option value="n">Knight</option>
             </select>
           </label>
         </span>

+ 1 - 0
src/js/components/GameInterface.js

@@ -131,6 +131,7 @@ const GameInterface = React.createClass({
 
         <ChessboardInterface
           io={io}
+          token={params[0]}
           soundsEnabled={soundsEnabled}
           color={color} />
 

+ 1 - 1
src/js/constants/ChessPieces.js

@@ -14,5 +14,5 @@ module.exports = {
   'n': '\u265e',
   'p': '\u265f',
   // empty square
-  '-': ''
+  '-': undefined
 };

+ 1 - 0
src/js/constants/GameConstants.js

@@ -1,6 +1,7 @@
 const keyMirror = require('react/lib/keyMirror');
 
 module.exports = keyMirror({
+  MAKE_MOVE: null,
   REMATCH: null,
   GAME_OVER: null,
   CHANGE_PROMOTION: null

+ 3 - 2
src/js/mixins/maybeReverse.js

@@ -1,5 +1,6 @@
 module.exports = {
-  _maybeReverse(iterable) {
-    return this.props.color === 'black' ? iterable.reverse() : iterable;
+  _maybeReverse(iterable, color) {
+    return this.props.color === (color || 'black') ?
+      iterable.reverse() : iterable;
   }
 };

+ 93 - 22
src/js/stores/GameStore.js

@@ -7,20 +7,18 @@ const Chess = require('chess.js').Chess;
 const Immutable = require('immutable');
 const {List, Map, OrderedMap, Set} = Immutable;
 const CHANGE_EVENT = 'change';
+const MOVE_EVENT = 'new-move';
   
-var _gameOver = Map({
-  status: false,
-  type: null,
-  winner: null
-});
-var _capturedPieces = OrderedMap([
-  ['white', List()],
-  ['black', List()]
-]);
-var _moves = List();
-var _promotion = 'q';
-var _turn = 'w';
-var _chess = new Chess();
+var _gameOver;
+var _capturedPieces;
+var _moves;
+var _promotion;
+var _turn;
+var _check;
+var _lastMove;
+var _chess;
+
+setInitialState();
 
 const GameStore = Object.assign({}, EventEmitter.prototype, {
   getState() {
@@ -36,16 +34,82 @@ const GameStore = Object.assign({}, EventEmitter.prototype, {
   getMoves() {
     return _moves;
   },
-  getFEN() {
-    return _chess.fen();
+  getChessboardState() {
+    return {
+      fen: _chess.fen(),
+      lastMove: _lastMove
+    };
   }
 });
 
-function rematch() {
-  _gameOver = _gameOver
-    .set('status', false)
-    .set('winner', null)
-    .set('type', null);
+function setInitialState() {
+  _gameOver = Map({
+    status: false,
+    type: null,
+    winner: null
+  });
+  _capturedPieces = OrderedMap([
+    ['w', List()],
+    ['b', List()]
+  ]);
+  _moves = List([List()]);
+  _promotion = 'q';
+  _turn = 'w';
+  _check = false;
+  _lastMove = Map();
+  _chess = new Chess();
+}
+
+function makeMove(from, to, capture, emitMove) {
+  const move = _chess.move({
+    from: from,
+    to: to,
+    promotion: _promotion
+  });
+
+  if (!move) {
+    // move is not valid, return false and don't emit any event.
+    return false;
+  }
+
+  _turn = _chess.turn();
+  _check = _chess.in_check();
+  _lastMove = _lastMove.set('from', from).set('to', to);
+  _moves = _moves.last().size === 2 ?
+    _moves.push(List([move.san])) :
+    _moves.update(_moves.size - 1, list => list.push(move.san));
+
+  if (capture || move.flags === 'e') {
+    const capturedPiece = capture || _turn === 'w' ? 'P' : 'p';
+
+    _capturedPieces = _capturedPieces
+      .update(_turn, list => list.push(capturedPiece));
+  }
+
+  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;
+
+    _gameOver = _gameOver
+      .set('status', true)
+      .set('winner', _turn === 'w' ? 'White' : 'Black')
+      .set('type', type);
+  }
+
+  if (emitMove) {
+    GameStore.emit(MOVE_EVENT, {
+      from: from,
+      to: to,
+      capture: capture,
+      gameOver: _chess.game_over(),
+      turn: _turn
+    });
+  }
+
+  return true;
 }
 
 function gameOver(options) {
@@ -57,11 +121,16 @@ function gameOver(options) {
 
 AppDispatcher.register(payload => {
   var action = payload.action;
+  var emitEvent = true;
 
   switch (action.actionType) {
+    case GameConstants.MAKE_MOVE:
+      emitEvent = makeMove(
+        action.from, action.to, action.capture, action.emitMove);
+      break;
 
     case GameConstants.REMATCH:
-      rematch();
+      setInitialState();
       break;
 
     case GameConstants.GAME_OVER:
@@ -76,7 +145,9 @@ AppDispatcher.register(payload => {
       return true;
   }
 
-  GameStore.emit(CHANGE_EVENT);
+  if (emitEvent) {
+    GameStore.emit(CHANGE_EVENT);
+  }
   return true;
 });