Trois: a game
2018.09.14I’d like to introduce you to my first React application and my first ever game.
See it live / View the source code
Background
Trois is a React.js implementation and adaptation of Threes!, the iOS puzzle game. Gameplay starts with 6 tiles randomly distributed across a 4x4 grid. Clicking the arrow buttons in the dashboard slides all tiles across the board by one spot. Pushing tiles together makes larger numbers, but be careful! After every move, a new tile with a random value gets added to the board.
Trois locally stores the history of the current game, which allows a player to undo their last move (and the move before that, and the move before that, until the game is in its start configuration). Restarting the game clears the game history and randomly generates a new start configuration.
Tile movements are animated using CSS classes and keyframes. Keyboard shortcuts are implemented for the directional and undo buttons, and the player can toggle the visibility of letter mappings. Instructions and gameplay statistics can be accessed by expanding the appropriate tabs. Statistical information is displayed using programmatically generated SVG graphs.
What is React?
React is a JavaScript library for building applications and user interfaces. The official React documentation has a good writeup on how to think about React applications, but I’ve summarized some of what I consider the key points (and how they apply to my game) below.
React components
A core principle of React is its components. Components are functions that return JSX (which get translated into elements in an HTML webpage). Following the React way of thinking, a single application is broken down into components, which are in turn comprised of more components. Each component can receive information (props) from its parent (the entity that invokes it), and can use that information to customize how it gets rendered. A component can also store state: updatable and persistent values that are tied to the component. A component will usually output JSX, which gets translated into DOM nodes.
In Trois, I use a Game
component to track the game’s history, possible moves remaining, and user input; all of these values are stored as state within this component. Game
renders a Board
component and passes Board
the information needed to draw the game board. This includes the current and previous game tile locations, the user’s move direction, whether the previous user move was “Undo”, and whether the game is over. Board
determines the types of animations needed for each tile, and tells each of its children (which are GameTile
elements) the values to display. If the game is over, Board
tells all GameTile
components to display the score. A GameTile
component creates a DOM element for each requested layer of the animation and assigns each layer a CSS class name.
A good practice when creating components is to try and make them reusable, thus minimizing duplicate logic. For example, I use the GameTile
component to draw the tiles in both the board grid and the “Next tile” dashboard section. Because I use the same component, the components are automatically drawn with the same colors; tiles with a “1” have an orange background, tiles with a “2” have a blue background, and all other tiles are white with a dark grey border.
The DOM and JSX
The DOM (Document Object Model) is the representation of an HTML document that a web browser creates when it loads a page. JavaScript code within the page can modify that representation, and nearly all modern web apps rely on this ability.
JSX (JavaScript XML) is a lightweight DOM-like data language. React components return JSX, and React converts that JSX into real DOM nodes.
React looks at the JSX from each component and figures out the minimum set of changes to make to the DOM in order to bring the two into sync. If React components returned DOM nodes directly, they would have to recreate those DOM nodes from scratch each time they were called. This would make React slower, because creating DOM nodes is expensive (in compute power) compared to creating JSX.
JSX-to-HTML example
This is a snippet of JSX that I use to draw the 4 arrow buttons on the Trois dashboard (excuse all the parenthesis and curly braces, they’re necessary and are more readable in a color-coded text editor):
const directions = [
{ name: "up", featherName: "arrow-up", shortcut: "W", canMove: true },
{ name: "right", featherName: "arrow-right", shortcut: "D", canMove: true },
{ name: "down", featherName: "arrow-down", shortcut: "S", canMove: false },
{ name: "left", featherName: "arrow-left", shortcut: "A", canMove: true }
];
<div className='GameControl'>
{directions.map(dir => (
<button
className={dir.name}
onClick={() => handleClick(dir.name)}
disabled={!dir.canMove}
title={`Move tiles ${dir.name}`}
key={dir}
>
<Icon name={dir.featherName}/>
<KeyboardShortcut shortcut={dir.shortcut}/>
</button>
))}
</div>
Here is the HTML code that gets generated from the JSX for one of the buttons:
<div class="GameControl">
<button class="up" title="Move tiles up">
<svg class="Icon">
<use xlink:href="./static/feather-sprite.svg#arrow-up"></use>
</svg>
<span class="KeyboardShortcut">W</span>
</button>
[[ ... other buttons ... ]]
</div>
Some notes on the JSX code:
Array.map
is a JavaScript function that runs a command for every element of the array it gets called on. In this example, a<button>
element gets generated for each of the four directions.- JSX contains keywords (ex:
className
) that can be assigned values (ex: the value of variabledir.name
). These get translated into attributes in the HTML element (ex:class="up"
). key
is an identifier that allows React to distinguish between similar components to identify what’s changed, and therefore which DOM elements need to be updated.- Curly braces
{}
contain JavaScript that gets run. Icon
andKeyboardShortcut
are both developer-defined React components that output additional JSX (an SVG icon and a shortcut letter, respectively). They both accept props (ex:name
,shortcut
).
Offline functionality
When a React project is prepared for web deployment, all of the JavaScript gets bundled up into a couple of files. This bundle of code (typically minified so it requires less bandwidth to download) is served up to visitors of the site.
Trois is designed to run offline after files are downloaded: the game’s JavaScript bundle contains all game logic, instructions, and statistics needed to play the game. Once the page is loaded, no further requests are made to the web server.
Try it for yourself: open up the game then turn off Wi-Fi and/or disconnect your ethernet cable. You will still be able to play the entire game (including game restart). Just don’t refresh or reload the page: doing so will issue a request to the server for the web page, but since you are offline, this will fail.
Traditional apps rely heavily on a web server to construct pages for the browser. Typically, these pages are HTML documents that includes both the page content and information about how to display it. But with React, it’s common to define all of the “presentation logic” of the app in JavaScript, and rely on the server only to deliver the content, usually through an API.
For example, in order to render an SVG chart, a React app might request a dataset in JSON format from the server. The server returns the JSON data as requested, but doesn’t have any idea how to turn that data into a chart. This responsibility is left to the client, which implements the chart “presentation logic” in JavaScript.
What is an API?
An API (Application Programming Interface) is an interface that allows a computer program to communicate with other programs or users. Here are a few kinds of computer interfaces you may have run across:
Acronym | Name | Definition |
---|---|---|
API | Application Programming Interface | Interface that one program uses to interact with another program |
CLI | Command-Line Interface | Command-line (text-based) interface that a human user uses to interact with a program (typically using a keyboard) |
GUI | Graphical User Interface | Graphical interface that a human user uses to interact with a program (typically using a mouse and a keyboard) |
React libraries
There are many packages and libraries that provide useful functionality for JavaScript programs. These packages can be as varied as capturing user keyboard presses (keybindings), compiling Markdown text to HTML, and minifying JavaScript files to allow for fast downloads. Many of these modules can be installed via npm (Node Package Manager), a package manager for JavaScript code.
npm allows a developer to install (usually via the command line) a particular set of package dependencies and versions that is unique and local to a project. For example, Trois uses version 1.2.0
of the package gh-pages
. This local package installation will not affect or corrupt a different project and development environment that installs gh-pages
version 1.1.0
, even if both projects are being developed on the same computer.
All user-installed packages are listed in a file called package.json
that lives at a project’s root directory. A second file, package-lock.json
, is a list of all packages used by the project, and all of the package dependencies. For example, the package.json
file for Trois contains the line "gh-pages": "^1.2.0"
(version 1.2.0 or higher), but the generated package-lock.json
specifies the exact software version of gh-pages
as well as the exact software versions of all packages that are required for gh-pages
to run.
Implementation notes
I learned the basics of React by completing the official React tutorial. The tutorial walked through the steps of implementing tic-tac-toe, which inspired me to try building my own more complicated game.
I built this game in the development environment set up by Create React App.
Libraries and tools
Feather is a library of SVG icons. Trois uses arrows and the open/closed eyes from this library, imported using an SVG sprite sheet. An SVG sprite sheet is a single SVG file that contains multiple icons or sprites. Every icon can be individually addressed and included into the webpage.
gh-pages
is a package that enables easy deployment of a Github project to Github Pages.
Lodash is a JavaScript library that adds math functions; I use it to import functions that act on numbers in a 2D array, such as finding the maximum number in an array.
Mousetrap is a JavaScript library that adds keybindings to the project. I use it to bind user keyboard input (specifically, the “W”, “A”, “S”, “D”, and “U” keys) to the appropriate functions that update game state.
React Developer Tools is a Chrome extension that adds React debugging tools to the web inspector environment. It is incredibly useful for debugging component state and props (values passed to a child component from its parent).
SASS is a powerful CSS extension that gives a developer tools to organize and eliminate redundancy in style sheets. To compile SASS (*.scss
) files into CSS (*.css
) files, I added the node-sass-chokidar
package and followed the Create React App instructions. Only CSS style sheets can be served with a website.
Tile animations
I used the FLIP animation technique to animate tiles on the board. FLIP stands for “First Last Invert Play”, and it deals with transitioning an object’s appearance in response to a state change.
- First: Calculate or record the position of the object prior to the state change—this is the pre-animation position.
- Last: Update the state of the object—this matches its post-animation position.
- Invert: Using CSS,
transform
the object’s position relative to its last position (and current state) to make it appear in its first position. - Play: Run the animation to transition the object from its first to its last position.
This animation technique allows for instantaneous update of an object’s state. In other words, the duration of an animation does not delay game state updates. For example, in Trois, when a user clicks an arrow key, the position of all tiles in game memory immediately updates to reflect the post-move location. However, using CSS, I make the tiles appear to be in their pre-input position, and then animate a slide to the new position. The animation (which takes 0.2 seconds to run) does not affect any aspect of game memory. Therefore, if the user clicks a new arrow key while the previous animation is still running, that action does not corrupt or confuse the state of the game. The new position is saved, a new animation is applied, and the old animation is terminated; the only indication of this termination may be a small jump in the tiles’ positions.
When updating game state, I assign tiles one of four classifications:
move-X
(whereX
isup
,right
,left
, ordown
): the tile slides in the specified directionfade-in
: the tile gradually appearsfade-out
: the tile gradually disappearsno-animate
: the tile’s position does not change
When a user makes a move, each grid on the board falls into one of the following situations, and its appearance is implemented using a combination of tile types:
Situation | Tile combination |
---|---|
A tile slides into place | move-X |
2 tiles combine: the sliding tile overlaps the existing tile, and after they overlap, a tile with the combined value appears | no-animate , move-X , fade-in (with a delay) |
A tile is added to the board | fade-in |
A tile is removed from the board (when the user clicks “Undo”) | fade-out |
A tile does not move | no-animate |
If no tile moves into a game board spot and there is no stationary tile at that spot, then no tile gets drawn.
Get a favicon to show up (almost) everywhere
The favicon is the little icon that is associated with a website. It gets displayed in Safari’s bookmarks and on Chrome’s tabs, and sometimes appears in the browser’s nav bar (and in various places on other browsers).
To generate the favicon, I first created an SVG icon using Inkscape. I exported this square SVG to PNG at two different resolutions: 32x32 and 64x64 pixels. To combine the two images into one file with extension .ico
, I used the command line tool ImageMagick.
Safari also uses a mask-icon
, which takes the form of a single-color SVG. This icon is displayed if the tab is pinned to the browser’s nav bar and appears on the touch bar of new MacBooks.
There are still a couple of bugs to work out regarding the favicon (for example, no image displays on Safari’s favorites page), but this is an issue that I will iron out at a later time. For reference, Real Favicon Generator knows what it’s doing and works; I just want to know WHY everything works, so I’ll do it all manually.
Website aesthetics
I designed and coded every part of the website and its appearance.
Trois does not use externally-loaded fonts; it only uses fonts available on the visitor’s machine. The website requests Century Gothic, but if that font isn’t available, the website resorts to one of the web safe fonts.
In a modern browser, the main page layout is implemented using CSS grid elements; this allowed me to specify the exact layout of elements on different sized screens or windows. I included graceful fallbacks in case CSS grid is unavailable.
Hosting details
The site is hosted using GitHub Pages, a free solution for hosting static sites. Content for the site is served directly from the Trois GitHub project, using a branch (gh-pages
) that contains the compiled site files. The project’s uncompiled source code is on the master
branch.
Concluding thought
Give Trois a play!
When in doubt, remember: 1+2=3
, 3+3=6
, … How high can you go?