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 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}
|
||||
|
|
|
@ -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
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