2015-02-01
Even More ReactJS - Slide Puzzle

One of the things I want to do with this blog (eventually) is to prototype a fun and educational version of Combinator Birds, a game or simulation inspired by To Dissect a Mockingbird. The AngularJS-based MyLittleMachines are part of that groundwork. However, in order to understand ReactJS more, I’m going to try to build out at least some of Combinator Birds using ReactJS.

I looked at several possible examples of ReactJS animated games and eventually settled on something really simple, the sliding block tile game. I’ll eventually replace the numbers with lambda-calculus, pi-calculus, or combinator ID expressions and have a more interesting interaction than simple swapping (which is a combinator!).

But that will be several blog posts later (if ever). Meanwhile, I’m going to try to incorporate and eventually hack upon a great code example of ReactJS sliding blocks puzzle published by Evan Henley. This post is all about embedding the code from reactjs-slide-puzzle which is described at Henley Edition. One of the reasons I chose this example to start is that it is very lean, without requiring a massive toolchain to use.

Minor Embedding Challenges

For the record, this integration didn’t work out of the box. Lots of path name goofiness and other hacks to embed this. But lots of learning about React in the process.

Blog modifications

My blog is really a web application, and sometimes when I add new facilities or use new libraries I must edit some of the files that are not part of the post’s content. In the case of this Slide Puzzle, I needed to add the .css file from the original example, because I’m having difficulty embedding <style> directly into my blog and I haven’t debugged that yet Under my new blog conventions, these files are stored in the /static/content/ directory, which provides non-Markdown content to supplement the /content directory.

I’ve adapted the .css from the original Sliding Puzzle example so that it doesn’t conflict with my blog’s styling and uses Twitter Bootstrap where possible.

Bootstrap Prettiness

I’m using Twitter Bootstrap throughout this blog, and I took the opportunity to use its styling to replace the styles from the original Henley version.

Stylesheet Changes

The original Sliding Puzzle provided a style sheet that defined .container in such a way that it made my blog post look bad (text-align:center;). So I deleted that style and applied the centering to the appropriate elements.

In addition, to reduce the stylesheet size so that I can embed it in my post, I’ve reformatted it slightly for consistency. I’ve also removed most of the vendor-prefixed versions of CSS identifiers (e.g., I only use box-sizing and not -moz-box-sizing or -webkit-box-sizing). Apparently I still need to use -webkit-XXX for some CSS constructs. Old browsers may not work properly because of the lack of vendor prefix, but I can always add a prefixing preprocessor someday.

React.js version 0.11 vs 0.12

One of the things I ran into was that the original sliding puzzle code was based upon React.js version 0.11, but that my blog is incorporating the latest (as of February 2015) version 0.12.2. This turned out to affect the use of this.key and this.props.key within the JSX code of Sliding Puzzle. So I applied the workaround suggested at the following links:

Basically, I added a position attribute to Tile instead of overloading key.

Do It!

Reminder: I’m just incorporating code some other person wrote at this point, although I intend to learn from it and use that knowledge to build more interesting apps like Combinator Birds.

#game-board { display: inline-block; width: 304px; height: 304px; padding: 0; margin: 0; border: 2px solid black; }

.tile, .button { text-align: center; user-select: none; }

.tile { width: 100px; height: 100px; float: left; line-height: 100px; font-size: 50px; background: #fff; }

.tile:hover:not(:empty) { cursor: pointer; -webkit-transition: -webkit-transform 0.2s, background 0.2s; transition: transform 0.2s, background 0.2s; background: #eee; }

.win { -webkit-animation: winner 2s infinite; animation: winner 2s infinite; }

.highlight, .move-up, .move-right, .move-down, .move-left { background: #fdd !important; }

.highlight { background: #fff; }

.move-up { -webkit-transform: translateY(-100px); transform: translateY(-100px); } .move-right { -webkit-transform: translateX(100px); transform: translateX(100px); } .move-down { -webkit-transform: translateY(100px); transform: translateY(100px); } .move-left { -webkit-transform: translateX(-100px); transform: translateX(-100px); }

.button { display: inline-block; padding: 4px 10px; color: black; border: 2px solid black; } .button:hover { cursor: pointer; }

@keyframes winner { 0% { background: #fdd; } 50% { background: #fff; } 100% { background: #fdd; } }

@keyframes highlight { 0% { background: #fdd; } 100% { background: #fff; } }


The source code (mostly from Henley’s original app.js) is below:

<div class="container well" style="text-align:center;">
    <h4 class="title">ReactJS Slide Puzzle</h4>
    <div id="game-container"></div>
</div>

<script type="text/jsx">
// ReactJS Slide Puzzle
// Author:     Evan Henley
// Author URI: henleyedition.com

(function() {

    var Game = createReactClass({

        shuffle: function(array) {

            // switches first two tiles
            function switchTiles(array) {
                var i = 0;

                // find the first two tiles in a row
                while (!array[i] || !array[i+1]) i++;

                // store tile value
                var tile = array[i];
                // switche values
                array[i] = array[i+1];
                array[i+1] = tile;

                return array;
            }

            // counts inversions
            function countInversions(array) {
                // make array of inversions
                var invArray = array.map(function(num, i) {
                    var inversions = 0;
                    for (j = i + 1; j < array.length; j++) {
                        if (array[j] && array[j] < num) {
                            inversions += 1;
                        }
                    }
                    return inversions;
                });
                // return sum of inversions array
                return invArray.reduce(function(a, b) {
                    return a + b;
                });
            }

            // fischer-yates shuffle algorithm
            function fischerYates(array) {
                var counter = array.length, temp, index;

                // While there are elements in the array
                while (counter > 0) {
                    // Pick a random index
                    index = Math.floor(Math.random() * counter);
                    // Decrease counter by 1
                    counter--;
                    // And swap the last element with it
                    temp = array[counter];
                    array[counter] = array[index];
                    array[index] = temp;
                }

                return array;
            }

            // Fischer-Yates shuffle
            array = fischerYates(array);

            // check for even number of inversions
            if (countInversions(array) % 2 !== 0) {
                // switch two tiles if odd
                array = switchTiles(array);
            }

            return array;
        },
        getInitialState: function() {
            return {
                // initial state of game board
                tiles: this.shuffle([
                    1,2,3,
                    4,5,6,
                    7,8,''
                ]),
                win: false
            };
        },
        checkBoard: function() {
            var tiles = this.state.tiles;

            for (var i = 0; i < tiles.length-1; i++) {
                if (tiles[i] !== i+1) return false;
            }

            return true;
        },
        tileClick: function(tileEl, position, status) {
            var tiles = this.state.tiles;
            // Possible moves
            // [up,right,down,left]
            // 9 = out of bounds
            var moves = [
                [null,1,3,null],[null,2,4,0],[null,null,5,1],
                [0,4,6,null],   [1,5,7,3],   [2,null,8,4],
                [3,7,null,null],[4,8,null,6],[5,null,null,7]
            ];

            function animateTiles(i, move) {
                var directions = ['up','right','down','left'];
                var moveToEl = document.querySelector('.tile:nth-child(' + (move + 1) + ')');
                direction = directions[i];
                tileEl.classList.add('move-' + direction);
                // this is all a little hackish.
                // css/js are used together to create the illusion of moving blocks
                setTimeout(function() {
                    moveToEl.classList.add('highlight');
                    tileEl.classList.remove('move-' + direction);
                    // time horribly linked with css transition
                    setTimeout(function() {
                        moveToEl.classList.remove('highlight');
                    }, 400);
                }, 200);
            }

            // called after tile is fully moved
            // sets new state
            function afterAnimate() {
                tiles[position] = '';
                tiles[move] = status;
                this.setState({
                    tiles: tiles,
                    moves: moves,
                    win: this.checkBoard()
                });
            };

            // return if they've already won
            if (this.state.win) return;

            // check possible moves
            for (var i = 0; i < moves[position].length; i++) {
                var move = moves[position][i];
                // if an adjacent tile is empty
                if (typeof move === 'number' && !tiles[move]) {
                    animateTiles(i, move);
                    setTimeout(afterAnimate.bind(this), 200);
                    break;
                }
            }
        },
        restartGame: function() {
            this.setState(this.getInitialState());
        },
        render: function() {
            return <div>
                <div id="game-board">
                    {this.state.tiles.map(function(tile, position) {
                        var t = <Tile key={position} position={position} status={tile} tileClick={this.tileClick} />;
                        return t;
                    }, this)}
                </div>
                <Menu winClass={this.state.win ? 'button win btn' : 'button btn'} status={this.state.win ? 'You win!' : 'Solve the puzzle.'} restart={this.restartGame} />
            </div>;
        }
    });

    var Tile = createReactClass({
        clickHandler: function(e) {
            this.props.tileClick(e.target, this.props.position, this.props.status);
        },
        render: function() {
            return <div className="tile button btn" onClick={this.clickHandler}>{this.props.status}</div>;
        }
    });

    var Menu = createReactClass({
        clickHandler: function() {
            this.props.restart();
        },
        render: function() {
            return  <div id="menu">
                        <h3 id="subtitle">{this.props.status}</h3>
                        <button className={this.props.winClass} onClick={this.clickHandler}>Restart</button>
                        <br/>
                        <br/>
                    </div>;
        }
    });

    // render Game to container
    ReactDOM.renderComponent(
        <Game />,
        document.getElementById('game-container')
    );
}());
</script>

And here’s the CSS used:

#game-board, #game-board * {
  box-sizing: border-box;
}

#game-board {
  display: inline-block;
  width: 304px;
  height: 304px;
  padding: 0;
  margin: 0;
  border: 2px solid black;
}

.tile,
.button {
  text-align: center;
  user-select: none;
}

.tile {
  width: 100px;
  height: 100px;
  float: left;
  line-height: 100px;
  font-size: 50px;
  background: #fff;
}

.tile:hover:not(:empty) {
  cursor: pointer;
  -webkit-transition: -webkit-transform 0.2s, background 0.2s;
          transition: transform 0.2s, background 0.2s;
  background: #eee;
}

.win {
  -webkit-animation: winner 2s infinite;
          animation: winner 2s infinite;
}

.highlight,
.move-up,
.move-right,
.move-down,
.move-left {
  background: #fdd !important;
}

.highlight {
  background: #fff;
 }

.move-up {
  -webkit-transform: translateY(-100px);
          transform: translateY(-100px);
}
.move-right {
  -webkit-transform: translateX(100px);
          transform: translateX(100px);
}
.move-down {
  -webkit-transform: translateY(100px);
          transform: translateY(100px);
}
.move-left {
  -webkit-transform: translateX(-100px);
          transform: translateX(-100px);
}

.button {
  display: inline-block;
  padding: 4px 10px;
  color: black;
  border: 2px solid black;
}
.button:hover {
  cursor: pointer;
}

@keyframes winner {
  0%    { background: #fdd; }
  50%   { background: #fff; }
  100%  { background: #fdd; }
}

@keyframes highlight {
  0%    { background: #fdd; }
  100%  { background: #fff; }
}

What about an AngularJS version?

Up until now, I’ve been presenting the AngularJS version of an app along with its ReactJS version. I’m not going to bother with this right now, leaving it as an exercise in the Reader’s mind. My expectation is that the AngularJS version will be less lines of code, and easier (in my opinion) to understand.

For an example of how a grid-type application can be built with AngularJS, see my The Mandel Bots post.