From 62d1a0f83d7403f68f5ebd511ccfbaa16a07ba34 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Wed, 16 Mar 2016 00:47:53 -0700 Subject: [PATCH] Add Game2048 example --- examples/2048/Game2048.js | 324 +++++++++++++++++++++++++++++++++++++ examples/2048/GameBoard.js | 201 +++++++++++++++++++++++ examples/index.html | 2 +- examples/index.js | 22 +-- 4 files changed, 529 insertions(+), 20 deletions(-) create mode 100644 examples/2048/Game2048.js create mode 100644 examples/2048/GameBoard.js diff --git a/examples/2048/Game2048.js b/examples/2048/Game2048.js new file mode 100644 index 00000000..3be6adbd --- /dev/null +++ b/examples/2048/Game2048.js @@ -0,0 +1,324 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule Game2048 + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Animated, + AppRegistry, + StyleSheet, + Text, + TouchableBounce, + View, +} = React; + +var GameBoard = require('./GameBoard'); + +var BOARD_PADDING = 3; +var CELL_MARGIN = 4; +var CELL_SIZE = 60; + +class Cell extends React.Component { + render() { + return ; + } +} + +class Board extends React.Component { + render() { + return ( + + + + + + {this.props.children} + + ); + } +} + +class Tile extends React.Component { + state: any; + + static _getPosition(index): number { + return BOARD_PADDING + (index * (CELL_SIZE + CELL_MARGIN * 2) + CELL_MARGIN); + } + + constructor(props: {}) { + super(props); + + var tile = this.props.tile; + + this.state = { + opacity: new Animated.Value(0), + top: new Animated.Value(Tile._getPosition(tile.toRow())), + left: new Animated.Value(Tile._getPosition(tile.toColumn())), + }; + } + + calculateOffset(): {top: number; left: number; opacity: number} { + var tile = this.props.tile; + + var offset = { + top: this.state.top, + left: this.state.left, + opacity: this.state.opacity, + }; + + if (tile.isNew()) { + Animated.timing(this.state.opacity, { + duration: 100, + toValue: 1, + }).start(); + } else { + Animated.parallel([ + Animated.timing(offset.top, { + duration: 100, + toValue: Tile._getPosition(tile.toRow()), + }), + Animated.timing(offset.left, { + duration: 100, + toValue: Tile._getPosition(tile.toColumn()), + }), + ]).start(); + } + return offset; + } + + render() { + var tile = this.props.tile; + + var tileStyles = [ + styles.tile, + styles['tile' + tile.value], + this.calculateOffset(), + ]; + + var textStyles = [ + styles.value, + tile.value > 4 && styles.whiteText, + tile.value > 100 && styles.threeDigits, + tile.value > 1000 && styles.fourDigits, + ]; + + return ( + + {tile.value} + + ); + } +} + +class GameEndOverlay extends React.Component { + render() { + var board = this.props.board; + + if (!board.hasWon() && !board.hasLost()) { + return ; + } + + var message = board.hasWon() ? + 'Good Job!' : 'Game Over'; + + return ( + + {message} + + Try Again? + + + ); + } +} + +class Game2048 extends React.Component { + startX: number; + startY: number; + state: any; + + constructor(props: {}) { + super(props); + this.state = { + board: new GameBoard(), + }; + this.startX = 0; + this.startY = 0; + } + + restartGame() { + this.setState({board: new GameBoard()}); + } + + handleTouchStart(event: Object) { + if (this.state.board.hasWon()) { + return; + } + + this.startX = event.nativeEvent.pageX; + this.startY = event.nativeEvent.pageY; + } + + handleTouchEnd(event: Object) { + if (this.state.board.hasWon()) { + return; + } + + var deltaX = event.nativeEvent.pageX - this.startX; + var deltaY = event.nativeEvent.pageY - this.startY; + + var direction = -1; + if (Math.abs(deltaX) > 3 * Math.abs(deltaY) && Math.abs(deltaX) > 30) { + direction = deltaX > 0 ? 2 : 0; + } else if (Math.abs(deltaY) > 3 * Math.abs(deltaX) && Math.abs(deltaY) > 30) { + direction = deltaY > 0 ? 3 : 1; + } + + if (direction !== -1) { + this.setState({board: this.state.board.move(direction)}); + } + } + + render() { + var tiles = this.state.board.tiles + .filter((tile) => tile.value) + .map((tile) => ); + + return ( + this.handleTouchStart(event)} + onTouchEnd={(event) => this.handleTouchEnd(event)}> + + {tiles} + + this.restartGame()} /> + + ); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + board: { + padding: BOARD_PADDING, + backgroundColor: '#bbaaaa', + borderRadius: 5, + }, + overlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(221, 221, 221, 0.5)', + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + overlayMessage: { + fontSize: 40, + marginBottom: 20, + }, + tryAgain: { + backgroundColor: '#887761', + padding: 20, + borderRadius: 5, + }, + tryAgainText: { + color: '#ffffff', + fontSize: 20, + fontWeight: '500', + }, + cell: { + width: CELL_SIZE, + height: CELL_SIZE, + borderRadius: 5, + backgroundColor: '#ddccbb', + margin: CELL_MARGIN, + }, + row: { + flexDirection: 'row', + }, + tile: { + position: 'absolute', + width: CELL_SIZE, + height: CELL_SIZE, + backgroundColor: '#ddccbb', + borderRadius: 5, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + value: { + fontSize: 24, + color: '#776666', + fontFamily: 'Verdana', + fontWeight: '500', + }, + tile2: { + backgroundColor: '#eeeeee', + }, + tile4: { + backgroundColor: '#eeeecc', + }, + tile8: { + backgroundColor: '#ffbb87', + }, + tile16: { + backgroundColor: '#ff9966', + }, + tile32: { + backgroundColor: '#ff7755', + }, + tile64: { + backgroundColor: '#ff5533', + }, + tile128: { + backgroundColor: '#eecc77', + }, + tile256: { + backgroundColor: '#eecc66', + }, + tile512: { + backgroundColor: '#eecc55', + }, + tile1024: { + backgroundColor: '#eecc33', + }, + tile2048: { + backgroundColor: '#eecc22', + }, + whiteText: { + color: '#ffffff', + }, + threeDigits: { + fontSize: 20, + }, + fourDigits: { + fontSize: 18, + }, +}); + +AppRegistry.registerComponent('Game2048', () => Game2048); + +module.exports = Game2048; diff --git a/examples/2048/GameBoard.js b/examples/2048/GameBoard.js new file mode 100644 index 00000000..6ed92e6b --- /dev/null +++ b/examples/2048/GameBoard.js @@ -0,0 +1,201 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule GameBoard + * @flow + */ +'use strict'; + +// NB: Taken straight from: https://github.com/IvanVergiliev/2048-react/blob/master/src/board.js +// with no modification except to format it for CommonJS and fix lint/flow errors + +var rotateLeft = function (matrix) { + var rows = matrix.length; + var columns = matrix[0].length; + var res = []; + for (var row = 0; row < rows; ++row) { + res.push([]); + for (var column = 0; column < columns; ++column) { + res[row][column] = matrix[column][columns - row - 1]; + } + } + return res; +}; + +var Tile = function (value?: number, row?: number, column?: number) { + this.value = value || 0; + this.row = row || -1; + + this.column = column || -1; + this.oldRow = -1; + this.oldColumn = -1; + this.markForDeletion = false; + this.mergedInto = null; + this.id = Tile.id++; +}; + +Tile.id = 0; + +Tile.prototype.moveTo = function (row, column) { + this.oldRow = this.row; + this.oldColumn = this.column; + this.row = row; + this.column = column; +}; + +Tile.prototype.isNew = function () { + return this.oldRow === -1 && !this.mergedInto; +}; + +Tile.prototype.hasMoved = function () { + return (this.fromRow() !== -1 && (this.fromRow() !== this.toRow() || this.fromColumn() !== this.toColumn())) || + this.mergedInto; +}; + +Tile.prototype.fromRow = function () { + return this.mergedInto ? this.row : this.oldRow; +}; + +Tile.prototype.fromColumn = function () { + return this.mergedInto ? this.column : this.oldColumn; +}; + +Tile.prototype.toRow = function () { + return this.mergedInto ? this.mergedInto.row : this.row; +}; + +Tile.prototype.toColumn = function () { + return this.mergedInto ? this.mergedInto.column : this.column; +}; + +var Board = function () { + this.tiles = []; + this.cells = []; + for (var i = 0; i < Board.size; ++i) { + this.cells[i] = [this.addTile(), this.addTile(), this.addTile(), this.addTile()]; + } + this.addRandomTile(); + this.setPositions(); + this.won = false; +}; + +Board.prototype.addTile = function () { + var res = new Tile(); + Tile.apply(res, arguments); + this.tiles.push(res); + return res; +}; + +Board.size = 4; + +Board.prototype.moveLeft = function () { + var hasChanged = false; + for (var row = 0; row < Board.size; ++row) { + var currentRow = this.cells[row].filter(function (tile) { return tile.value !== 0; }); + var resultRow = []; + for (var target = 0; target < Board.size; ++target) { + var targetTile = currentRow.length ? currentRow.shift() : this.addTile(); + if (currentRow.length > 0 && currentRow[0].value === targetTile.value) { + var tile1 = targetTile; + targetTile = this.addTile(targetTile.value); + tile1.mergedInto = targetTile; + var tile2 = currentRow.shift(); + tile2.mergedInto = targetTile; + targetTile.value += tile2.value; + } + resultRow[target] = targetTile; + this.won = this.won || (targetTile.value === 2048); + hasChanged = hasChanged || (targetTile.value !== this.cells[row][target].value); + } + this.cells[row] = resultRow; + } + return hasChanged; +}; + +Board.prototype.setPositions = function () { + this.cells.forEach(function (row, rowIndex) { + row.forEach(function (tile, columnIndex) { + tile.oldRow = tile.row; + tile.oldColumn = tile.column; + tile.row = rowIndex; + tile.column = columnIndex; + tile.markForDeletion = false; + }); + }); +}; + +Board.fourProbability = 0.1; + +Board.prototype.addRandomTile = function () { + var emptyCells = []; + for (var r = 0; r < Board.size; ++r) { + for (var c = 0; c < Board.size; ++c) { + if (this.cells[r][c].value === 0) { + emptyCells.push({r: r, c: c}); + } + } + } + var index = Math.floor(Math.random() * emptyCells.length); + var cell = emptyCells[index]; + var newValue = Math.random() < Board.fourProbability ? 4 : 2; + this.cells[cell.r][cell.c] = this.addTile(newValue); +}; + +Board.prototype.move = function (direction) { + // 0 -> left, 1 -> up, 2 -> right, 3 -> down + this.clearOldTiles(); + for (var i = 0; i < direction; ++i) { + this.cells = rotateLeft(this.cells); + } + var hasChanged = this.moveLeft(); + for (var i = direction; i < 4; ++i) { + this.cells = rotateLeft(this.cells); + } + if (hasChanged) { + this.addRandomTile(); + } + this.setPositions(); + return this; +}; + +Board.prototype.clearOldTiles = function () { + this.tiles = this.tiles.filter(function (tile) { return tile.markForDeletion === false; }); + this.tiles.forEach(function (tile) { tile.markForDeletion = true; }); +}; + +Board.prototype.hasWon = function () { + return this.won; +}; + +Board.deltaX = [-1, 0, 1, 0]; +Board.deltaY = [0, -1, 0, 1]; + +Board.prototype.hasLost = function () { + var canMove = false; + for (var row = 0; row < Board.size; ++row) { + for (var column = 0; column < Board.size; ++column) { + canMove = canMove || (this.cells[row][column].value === 0); + for (var dir = 0; dir < 4; ++dir) { + var newRow = row + Board.deltaX[dir]; + var newColumn = column + Board.deltaY[dir]; + if (newRow < 0 || newRow >= Board.size || newColumn < 0 || newColumn >= Board.size) { + continue; + } + canMove = canMove || (this.cells[row][column].value === this.cells[newRow][newColumn].value); + } + } + } + return !canMove; +}; + +module.exports = Board; diff --git a/examples/index.html b/examples/index.html index 50b48f6e..6ee40eea 100644 --- a/examples/index.html +++ b/examples/index.html @@ -3,4 +3,4 @@ React Native for Web
- + diff --git a/examples/index.js b/examples/index.js index da3cc22a..74af3f62 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,23 +1,7 @@ -import { MediaProvider, matchMedia } from 'react-media-queries' -import App from './components/App' -import createGetter from 'react-media-queries/lib/createMediaQueryGetter' -import createListener from 'react-media-queries/lib/createMediaQueryListener' import React, { AppRegistry } from 'react-native' +import Game2048 from './2048/Game2048' +import TicTacToeApp from './TicTacToe/TicTacToe' -const mediaQueries = { - small: '(min-width: 300px)', - medium: '(min-width: 400px)', - large: '(min-width: 500px)' -} -const ResponsiveApp = matchMedia()(App) -const WrappedApp = () => ( - - - -) - -AppRegistry.registerComponent('Example', () => WrappedApp) - -AppRegistry.runApplication('Example', { +AppRegistry.runApplication('Game2048', { rootTag: document.getElementById('react-root') })