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:
- Stackoverflow - This.key in React.js 0.12
- React - this.key seems to always be undefined inside a React component
- React - Upgrading to 0.12
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.