Added basic tetris functionality
This commit is contained in:
parent
42731d26a1
commit
3aaf56a660
7 changed files with 479 additions and 1 deletions
|
@ -6,6 +6,7 @@ import i18n from "i18n-js";
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
import SidebarDivider from "./SidebarDivider";
|
import SidebarDivider from "./SidebarDivider";
|
||||||
import SidebarItem from "./SidebarItem";
|
import SidebarItem from "./SidebarItem";
|
||||||
|
import {TouchableRipple} from "react-native-paper";
|
||||||
|
|
||||||
const deviceWidth = Dimensions.get("window").width;
|
const deviceWidth = Dimensions.get("window").width;
|
||||||
|
|
||||||
|
@ -154,9 +155,17 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const onPress = this.onListItemPress.bind(this, {route: 'TetrisScreen'});
|
||||||
return (
|
return (
|
||||||
<View style={{height: '100%'}}>
|
<View style={{height: '100%'}}>
|
||||||
<Image source={require("../assets/drawer-cover.png")} style={styles.drawerCover}/>
|
<TouchableRipple
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={require("../assets/drawer-cover.png")}
|
||||||
|
style={styles.drawerCover}
|
||||||
|
/>
|
||||||
|
</TouchableRipple>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.dataSet}
|
data={this.dataSet}
|
||||||
extraData={this.state}
|
extraData={this.state}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
||||||
import SelfMenuScreen from '../screens/SelfMenuScreen';
|
import SelfMenuScreen from '../screens/SelfMenuScreen';
|
||||||
import AvailableRoomScreen from "../screens/Websites/AvailableRoomScreen";
|
import AvailableRoomScreen from "../screens/Websites/AvailableRoomScreen";
|
||||||
import BibScreen from "../screens/Websites/BibScreen";
|
import BibScreen from "../screens/Websites/BibScreen";
|
||||||
|
import TetrisScreen from "../screens/Tetris/TetrisScreen";
|
||||||
import DebugScreen from '../screens/About/DebugScreen';
|
import DebugScreen from '../screens/About/DebugScreen';
|
||||||
import Sidebar from "../components/Sidebar";
|
import Sidebar from "../components/Sidebar";
|
||||||
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
||||||
|
@ -161,6 +162,30 @@ function BibStackComponent() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TetrisStack = createStackNavigator();
|
||||||
|
|
||||||
|
function TetrisStackComponent() {
|
||||||
|
return (
|
||||||
|
<TetrisStack.Navigator
|
||||||
|
initialRouteName="TetrisScreen"
|
||||||
|
headerMode="float"
|
||||||
|
screenOptions={defaultScreenOptions}
|
||||||
|
>
|
||||||
|
<TetrisStack.Screen
|
||||||
|
name="TetrisScreen"
|
||||||
|
component={TetrisScreen}
|
||||||
|
options={({navigation}) => {
|
||||||
|
const openDrawer = getDrawerButton.bind(this, navigation);
|
||||||
|
return {
|
||||||
|
title: 'Tetris',
|
||||||
|
headerLeft: openDrawer
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TetrisStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Drawer = createDrawerNavigator();
|
const Drawer = createDrawerNavigator();
|
||||||
|
|
||||||
function getDrawerContent(props) {
|
function getDrawerContent(props) {
|
||||||
|
@ -202,6 +227,10 @@ export default function DrawerNavigator() {
|
||||||
name="BibScreen"
|
name="BibScreen"
|
||||||
component={BibStackComponent}
|
component={BibStackComponent}
|
||||||
/>
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="TetrisScreen"
|
||||||
|
component={TetrisStackComponent}
|
||||||
|
/>
|
||||||
</Drawer.Navigator>
|
</Drawer.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
158
screens/Tetris/GameLogic.js
Normal file
158
screens/Tetris/GameLogic.js
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import Tetromino from "./Tetromino";
|
||||||
|
|
||||||
|
export default class GameLogic {
|
||||||
|
|
||||||
|
currentGrid: Array<Array<Object>>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
screens/Tetris/TetrisScreen.js
Normal file
116
screens/Tetris/TetrisScreen.js
Normal file
|
@ -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<Array<Object>>,
|
||||||
|
gameTime: number,
|
||||||
|
gameScore: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class TetrisScreen extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
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<Array<Object>>) {
|
||||||
|
this.setState({
|
||||||
|
gameTime: time,
|
||||||
|
gameScore: score,
|
||||||
|
grid: newGrid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGrid(newGrid: Array<Array<Object>>) {
|
||||||
|
this.setState({
|
||||||
|
grid: newGrid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startGame() {
|
||||||
|
if (!this.logic.isGameRunning()) {
|
||||||
|
this.logic.startGame(this.onTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}>
|
||||||
|
<Text style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
Score: {this.state.gameScore}
|
||||||
|
</Text>
|
||||||
|
<Text style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
time: {this.state.gameTime}
|
||||||
|
</Text>
|
||||||
|
<Grid
|
||||||
|
width={this.logic.getWidth()}
|
||||||
|
height={this.logic.getHeight()}
|
||||||
|
grid={this.state.grid}
|
||||||
|
/>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
}}>
|
||||||
|
<IconButton
|
||||||
|
icon="arrow-right"
|
||||||
|
size={40}
|
||||||
|
onPress={() => this.logic.rightPressed(this.updateGrid)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="arrow-left"
|
||||||
|
size={40}
|
||||||
|
onPress={() => this.logic.leftPressed(this.updateGrid)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="format-rotate-90"
|
||||||
|
size={40}
|
||||||
|
onPress={() => this.startGame()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(TetrisScreen);
|
82
screens/Tetris/Tetromino.js
Normal file
82
screens/Tetris/Tetromino.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
screens/Tetris/components/Cell.js
Normal file
21
screens/Tetris/components/Cell.js
Normal file
|
@ -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 (
|
||||||
|
<View style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: props.color,
|
||||||
|
borderColor: props.isEmpty ? props.color : "#393939",
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
aspectRatio: 1,
|
||||||
|
}}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(Cell);
|
63
screens/Tetris/components/Grid.js
Normal file
63
screens/Tetris/components/Grid.js
Normal file
|
@ -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<Array<Object>>,
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Grid extends React.Component<Props>{
|
||||||
|
|
||||||
|
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(<Cell color={cell.color} isEmpty={cell.isEmpty}/>);
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#fff'
|
||||||
|
}}>
|
||||||
|
{cells}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGrid() {
|
||||||
|
let rows = [];
|
||||||
|
for (let i = 0; i < this.props.height; i++) {
|
||||||
|
rows.push(this.getRow(i));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '80%',
|
||||||
|
aspectRatio: this.props.width/this.props.height,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
}}>
|
||||||
|
{this.getGrid()}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(Grid);
|
Loading…
Reference in a new issue