From 3aaf56a660bdbfc73d0943e9437c10ee9b6446a3 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Sun, 15 Mar 2020 18:44:32 +0100 Subject: [PATCH] Added basic tetris functionality --- components/Sidebar.js | 11 ++- navigation/DrawerNavigator.js | 29 ++++++ screens/Tetris/GameLogic.js | 158 ++++++++++++++++++++++++++++++ screens/Tetris/TetrisScreen.js | 116 ++++++++++++++++++++++ screens/Tetris/Tetromino.js | 82 ++++++++++++++++ screens/Tetris/components/Cell.js | 21 ++++ screens/Tetris/components/Grid.js | 63 ++++++++++++ 7 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 screens/Tetris/GameLogic.js create mode 100644 screens/Tetris/TetrisScreen.js create mode 100644 screens/Tetris/Tetromino.js create mode 100644 screens/Tetris/components/Cell.js create mode 100644 screens/Tetris/components/Grid.js diff --git a/components/Sidebar.js b/components/Sidebar.js index 4f8fb9f..3fca4cd 100644 --- a/components/Sidebar.js +++ b/components/Sidebar.js @@ -6,6 +6,7 @@ import i18n from "i18n-js"; import * as WebBrowser from 'expo-web-browser'; import SidebarDivider from "./SidebarDivider"; import SidebarItem from "./SidebarItem"; +import {TouchableRipple} from "react-native-paper"; const deviceWidth = Dimensions.get("window").width; @@ -154,9 +155,17 @@ export default class SideBar extends React.PureComponent { } render() { + const onPress = this.onListItemPress.bind(this, {route: 'TetrisScreen'}); return ( - + + + + { + const openDrawer = getDrawerButton.bind(this, navigation); + return { + title: 'Tetris', + headerLeft: openDrawer + }; + }} + /> + + ); +} + const Drawer = createDrawerNavigator(); function getDrawerContent(props) { @@ -202,6 +227,10 @@ export default function DrawerNavigator() { name="BibScreen" component={BibStackComponent} /> + ); } diff --git a/screens/Tetris/GameLogic.js b/screens/Tetris/GameLogic.js new file mode 100644 index 0000000..a9b8263 --- /dev/null +++ b/screens/Tetris/GameLogic.js @@ -0,0 +1,158 @@ +// @flow + +import Tetromino from "./Tetromino"; + +export default class GameLogic { + + currentGrid: Array>; + + height: number; + width: number; + + gameRunning: boolean; + gameTime: number; + score: number; + + currentObject: Tetromino; + + gameTick: number; + gameTickInterval: IntervalID; + + onTick: Function; + + constructor(height: number, width: number) { + this.height = height; + this.width = width; + this.gameRunning = false; + this.gameTick = 250; + } + + getHeight(): number { + return this.height; + } + + getWidth(): number { + return this.width; + } + + isGameRunning(): boolean { + return this.gameRunning; + } + + getEmptyGrid() { + let grid = []; + for (let row = 0; row < this.getHeight(); row++) { + grid.push([]); + for (let col = 0; col < this.getWidth(); col++) { + grid[row].push({ + color: '#fff', + isEmpty: true, + }); + } + } + return grid; + } + + getGridCopy() { + return JSON.parse(JSON.stringify(this.currentGrid)); + } + + getFinalGrid() { + let coord = this.currentObject.getCellsCoordinates(); + let finalGrid = this.getGridCopy(); + for (let i = 0; i < coord.length; i++) { + finalGrid[coord[i].y][coord[i].x] = { + color: this.currentObject.getColor(), + isEmpty: false, + }; + } + return finalGrid; + } + + freezeTetromino() { + let coord = this.currentObject.getCellsCoordinates(); + for (let i = 0; i < coord.length; i++) { + this.currentGrid[coord[i].y][coord[i].x] = { + color: this.currentObject.getColor(), + isEmpty: false, + }; + } + } + + isTetrominoPositionValid() { + let isValid = true; + let coord = this.currentObject.getCellsCoordinates(); + for (let i = 0; i < coord.length; i++) { + if (coord[i].x >= this.getWidth() + || coord[i].x < 0 + || coord[i].y >= this.getHeight() + || coord[i].y < 0 + || !this.currentGrid[coord[i].y][coord[i].x].isEmpty) { + isValid = false; + break; + } + } + return isValid; + } + + tryMoveTetromino(x: number, y: number) { + if (x > 1) x = 1; // Prevent moving from more than one tile + if (x < -1) x = -1; + if (y > 1) y = 1; + if (y < -1) y = -1; + if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement + + this.currentObject.move(x, y); + let isValid = this.isTetrominoPositionValid(); + + if (!isValid && x !== 0) + this.currentObject.move(-x, 0); + else if (!isValid && y !== 0) { + this.currentObject.move(0, -y); + this.freezeTetromino(); + this.createTetromino(); + } + } + + onTick(callback: Function) { + this.gameTime++; + this.score++; + this.tryMoveTetromino(0, 1); + callback(this.gameTime, this.score, this.getFinalGrid()); + } + + rightPressed(callback: Function) { + this.tryMoveTetromino(1, 0); + callback(this.getFinalGrid()); + } + + leftPressed(callback: Function) { + this.tryMoveTetromino(-1, 0); + callback(this.getFinalGrid()); + } + + createTetromino() { + let shape = Math.floor(Math.random() * 7); + this.currentObject = new Tetromino(shape); + if (!this.isTetrominoPositionValid()) + this.endGame(); + } + + endGame() { + console.log('Game Over!'); + clearInterval(this.gameTickInterval); + } + + startGame(callback: Function) { + if (this.gameRunning) + return; + this.gameRunning = true; + this.gameTime = 0; + this.score = 0; + this.currentGrid = this.getEmptyGrid(); + this.createTetromino(); + this.onTick = this.onTick.bind(this, callback); + this.gameTickInterval = setInterval(this.onTick, this.gameTick); + } + +} diff --git a/screens/Tetris/TetrisScreen.js b/screens/Tetris/TetrisScreen.js new file mode 100644 index 0000000..a405e05 --- /dev/null +++ b/screens/Tetris/TetrisScreen.js @@ -0,0 +1,116 @@ +// @flow + +import * as React from 'react'; +import {View} from 'react-native'; +import {IconButton, Text, withTheme} from 'react-native-paper'; +import GameLogic from "./GameLogic"; +import Grid from "./components/Grid"; + +type Props = { + navigation: Object, +} + +type State = { + grid: Array>, + gameTime: number, + gameScore: number +} + +class TetrisScreen extends React.Component { + + colors: Object; + + logic: GameLogic; + onTick: Function; + updateGrid: Function; + + constructor(props) { + super(props); + this.colors = props.theme.colors; + this.logic = new GameLogic(20, 10); + this.state = { + grid: this.logic.getEmptyGrid(), + gameTime: 0, + gameScore: 0, + }; + this.onTick = this.onTick.bind(this); + this.updateGrid = this.updateGrid.bind(this); + const onScreenBlur = this.onScreenBlur.bind(this); + this.props.navigation.addListener('blur', onScreenBlur); + } + + + /** + * Remove any interval on un-focus + */ + onScreenBlur() { + this.logic.endGame(); + } + + onTick(time: number, score: number, newGrid: Array>) { + this.setState({ + gameTime: time, + gameScore: score, + grid: newGrid, + }); + } + + updateGrid(newGrid: Array>) { + this.setState({ + grid: newGrid, + }); + } + + startGame() { + if (!this.logic.isGameRunning()) { + this.logic.startGame(this.onTick); + } + } + + render() { + return ( + + + Score: {this.state.gameScore} + + + time: {this.state.gameTime} + + + + this.logic.rightPressed(this.updateGrid)} + /> + this.logic.leftPressed(this.updateGrid)} + /> + this.startGame()} + /> + + + ); + } + +} + +export default withTheme(TetrisScreen); diff --git a/screens/Tetris/Tetromino.js b/screens/Tetris/Tetromino.js new file mode 100644 index 0000000..1b15293 --- /dev/null +++ b/screens/Tetris/Tetromino.js @@ -0,0 +1,82 @@ +export default class Tetromino { + + static types = { + 'I': 0, + 'O': 1, + 'T': 2, + 'S': 3, + 'Z': 4, + 'J': 5, + 'L': 6, + }; + + static shapes = { + 0: [ + [1, 1, 1, 1] + ], + 1: [ + [1, 1], + [1, 1] + ], + 2: [ + [0, 1, 0], + [1, 1, 1], + ], + 3: [ + [0, 1, 1], + [1, 1, 0], + ], + 4: [ + [1, 1, 0], + [0, 1, 1], + ], + 5: [ + [1, 0, 0], + [1, 1, 1], + ], + 6: [ + [0, 0, 1], + [1, 1, 1], + ], + }; + + static colors = { + 0: '#00f8ff', + 1: '#ffe200', + 2: '#b817ff', + 3: '#0cff34', + 4: '#ff000b', + 5: '#1000ff', + 6: '#ff9400', + } + + + currentType: Tetromino.types; + position: Object; + + constructor(type: Tetromino.types) { + this.currentType = type; + this.position = {x: 0, y: 0}; + } + + getColor() { + return Tetromino.colors[this.currentType]; + } + + getCellsCoordinates() { + let coordinates = []; + for (let row = 0; row < Tetromino.shapes[this.currentType].length; row++) { + for (let col = 0; col < Tetromino.shapes[this.currentType][row].length; col++) { + if (Tetromino.shapes[this.currentType][row][col] === 1) + coordinates.push({x: this.position.x + col, y: this.position.y + row}); + } + } + return coordinates; + } + + move(x: number, y: number) { + this.position.x += x; + this.position.y += y; + } + +} diff --git a/screens/Tetris/components/Cell.js b/screens/Tetris/components/Cell.js new file mode 100644 index 0000000..6ebb28e --- /dev/null +++ b/screens/Tetris/components/Cell.js @@ -0,0 +1,21 @@ +// @flow + +import * as React from 'react'; +import {View} from 'react-native'; +import {withTheme} from 'react-native-paper'; + +function Cell(props) { + const colors = props.theme.colors; + return ( + + ); +} + +export default withTheme(Cell); diff --git a/screens/Tetris/components/Grid.js b/screens/Tetris/components/Grid.js new file mode 100644 index 0000000..5acaa26 --- /dev/null +++ b/screens/Tetris/components/Grid.js @@ -0,0 +1,63 @@ +// @flow + +import * as React from 'react'; +import {View} from 'react-native'; +import {withTheme} from 'react-native-paper'; +import Cell from "./Cell"; + +type Props = { + navigation: Object, + grid: Array>, + height: number, + width: number, +} + +class Grid extends React.Component{ + + colors: Object; + + constructor(props) { + super(props); + this.colors = props.theme.colors; + } + + getRow(rowNumber: number) { + let cells = []; + for (let i = 0; i < this.props.width; i++) { + let cell = this.props.grid[rowNumber][i]; + cells.push(); + } + return( + + {cells} + + ); + } + + getGrid() { + let rows = []; + for (let i = 0; i < this.props.height; i++) { + rows.push(this.getRow(i)); + } + return rows; + } + + render() { + return ( + + {this.getGrid()} + + ); + } +} + +export default withTheme(Grid);