Added basic tetris functionality

This commit is contained in:
Arnaud Vergnet 2020-03-15 18:44:32 +01:00
parent 42731d26a1
commit 3aaf56a660
7 changed files with 479 additions and 1 deletions

View file

@ -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<Props, State> {
}
render() {
const onPress = this.onListItemPress.bind(this, {route: 'TetrisScreen'});
return (
<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
data={this.dataSet}
extraData={this.state}

View file

@ -9,6 +9,7 @@ import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
import SelfMenuScreen from '../screens/SelfMenuScreen';
import AvailableRoomScreen from "../screens/Websites/AvailableRoomScreen";
import BibScreen from "../screens/Websites/BibScreen";
import TetrisScreen from "../screens/Tetris/TetrisScreen";
import DebugScreen from '../screens/About/DebugScreen';
import Sidebar from "../components/Sidebar";
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();
function getDrawerContent(props) {
@ -202,6 +227,10 @@ export default function DrawerNavigator() {
name="BibScreen"
component={BibStackComponent}
/>
<Drawer.Screen
name="TetrisScreen"
component={TetrisStackComponent}
/>
</Drawer.Navigator>
);
}

158
screens/Tetris/GameLogic.js Normal file
View 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);
}
}

View 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);

View 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;
}
}

View 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);

View 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);