Improved game UI and state management

This commit is contained in:
Arnaud Vergnet 2020-03-16 19:10:32 +01:00
parent 8fc5cfb25e
commit 7f33c8376d
3 changed files with 162 additions and 32 deletions

View file

@ -10,6 +10,7 @@ export default class GameLogic {
width: number; width: number;
gameRunning: boolean; gameRunning: boolean;
gamePaused: boolean;
gameTime: number; gameTime: number;
score: number; score: number;
@ -27,6 +28,7 @@ export default class GameLogic {
this.height = height; this.height = height;
this.width = width; this.width = width;
this.gameRunning = false; this.gameRunning = false;
this.gamePaused = false;
this.gameTick = 250; this.gameTick = 250;
this.colors = colors; this.colors = colors;
} }
@ -43,6 +45,10 @@ export default class GameLogic {
return this.gameRunning; return this.gameRunning;
} }
isGamePaused(): boolean {
return this.gamePaused;
}
getEmptyLine() { getEmptyLine() {
let line = []; let line = [];
for (let col = 0; col < this.getWidth(); col++) { for (let col = 0; col < this.getWidth(); col++) {
@ -161,17 +167,30 @@ export default class GameLogic {
callback(this.gameTime, this.score, this.getFinalGrid()); callback(this.gameTime, this.score, this.getFinalGrid());
} }
canUseInput() {
return this.gameRunning && !this.gamePaused
}
rightPressed(callback: Function) { rightPressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryMoveTetromino(1, 0); this.tryMoveTetromino(1, 0);
callback(this.getFinalGrid()); callback(this.getFinalGrid());
} }
leftPressed(callback: Function) { leftPressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryMoveTetromino(-1, 0); this.tryMoveTetromino(-1, 0);
callback(this.getFinalGrid()); callback(this.getFinalGrid());
} }
rotatePressed(callback: Function) { rotatePressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryRotateTetromino(); this.tryRotateTetromino();
callback(this.getFinalGrid()); callback(this.getFinalGrid());
} }
@ -180,20 +199,32 @@ export default class GameLogic {
let shape = Math.floor(Math.random() * 7); let shape = Math.floor(Math.random() * 7);
this.currentObject = new Tetromino(shape, this.colors); this.currentObject = new Tetromino(shape, this.colors);
if (!this.isTetrominoPositionValid()) if (!this.isTetrominoPositionValid())
this.endGame(); this.endGame(false);
} }
endGame() { togglePause() {
console.log('Game Over!'); if (!this.gameRunning)
return;
this.gamePaused = !this.gamePaused;
if (this.gamePaused) {
clearInterval(this.gameTickInterval);
} else {
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
}
}
endGame(isRestart: boolean) {
this.gameRunning = false; this.gameRunning = false;
this.gamePaused = false;
clearInterval(this.gameTickInterval); clearInterval(this.gameTickInterval);
this.endCallback(this.gameTime, this.score); this.endCallback(this.gameTime, this.score, isRestart);
} }
startGame(tickCallback: Function, endCallback: Function) { startGame(tickCallback: Function, endCallback: Function) {
if (this.gameRunning) if (this.gameRunning)
return; this.endGame(true);
this.gameRunning = true; this.gameRunning = true;
this.gamePaused = false;
this.gameTime = 0; this.gameTime = 0;
this.score = 0; this.score = 0;
this.currentGrid = this.getEmptyGrid(); this.currentGrid = this.getEmptyGrid();

View file

@ -1,10 +1,12 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {Alert, View} from 'react-native';
import {IconButton, Text, withTheme} from 'react-native-paper'; import {IconButton, Text, withTheme} from 'react-native-paper';
import {MaterialCommunityIcons} from "@expo/vector-icons";
import GameLogic from "./GameLogic"; import GameLogic from "./GameLogic";
import Grid from "./components/Grid"; import Grid from "./components/Grid";
import HeaderButton from "../../components/HeaderButton";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -12,6 +14,7 @@ type Props = {
type State = { type State = {
grid: Array<Array<Object>>, grid: Array<Array<Object>>,
gameRunning: boolean,
gameTime: number, gameTime: number,
gameScore: number gameScore: number
} }
@ -31,22 +34,49 @@ class TetrisScreen extends React.Component<Props, State> {
this.logic = new GameLogic(20, 10, this.colors); this.logic = new GameLogic(20, 10, this.colors);
this.state = { this.state = {
grid: this.logic.getEmptyGrid(), grid: this.logic.getEmptyGrid(),
gameRunning: false,
gameTime: 0, gameTime: 0,
gameScore: 0, gameScore: 0,
}; };
this.onTick = this.onTick.bind(this); this.onTick = this.onTick.bind(this);
this.onGameEnd = this.onGameEnd.bind(this); this.onGameEnd = this.onGameEnd.bind(this);
this.updateGrid = this.updateGrid.bind(this); this.updateGrid = this.updateGrid.bind(this);
const onScreenBlur = this.onScreenBlur.bind(this); this.props.navigation.addListener('blur', this.onScreenBlur.bind(this));
this.props.navigation.addListener('blur', onScreenBlur); this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
} }
componentDidMount() {
const rightButton = this.getRightButton.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
this.startGame();
}
getRightButton() {
return (
<View
style={{
flexDirection: 'row',
}}>
<HeaderButton icon={'pause'} onPress={() => this.togglePause()}/>
</View>
);
}
/** /**
* Remove any interval on un-focus * Remove any interval on un-focus
*/ */
onScreenBlur() { onScreenBlur() {
this.logic.endGame(); if (!this.logic.isGamePaused())
this.logic.togglePause();
}
onScreenFocus() {
if (!this.logic.isGameRunning())
this.startGame();
else if (this.logic.isGamePaused())
this.showPausePopup();
} }
onTick(time: number, score: number, newGrid: Array<Array<Object>>) { onTick(time: number, score: number, newGrid: Array<Array<Object>>) {
@ -63,17 +93,63 @@ class TetrisScreen extends React.Component<Props, State> {
}); });
} }
startGame() { togglePause() {
if (!this.logic.isGameRunning()) { this.logic.togglePause();
this.logic.startGame(this.onTick, this.onGameEnd); if (this.logic.isGamePaused())
} this.showPausePopup();
} }
onGameEnd(time: number, score: number) { showPausePopup() {
Alert.alert(
'PAUSE',
'GAME PAUSED',
[
{text: 'RESTART', onPress: () => this.showRestartConfirm()},
{text: 'RESUME', onPress: () => this.togglePause()},
],
{cancelable: false},
);
}
showRestartConfirm() {
Alert.alert(
'RESTART?',
'WHOA THERE',
[
{text: 'NO', onPress: () => this.showPausePopup()},
{text: 'YES', onPress: () => this.startGame()},
],
{cancelable: false},
);
}
showGameOverConfirm() {
Alert.alert(
'GAME OVER',
'NOOB',
[
{text: 'LEAVE', onPress: () => this.props.navigation.goBack()},
{text: 'RESTART', onPress: () => this.startGame()},
],
{cancelable: false},
);
}
startGame() {
this.logic.startGame(this.onTick, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd(time: number, score: number, isRestart: boolean) {
this.setState({ this.setState({
gameTime: time, gameTime: time,
gameScore: score, gameScore: score,
}) gameRunning: false,
});
if (!isRestart)
this.showGameOverConfirm();
} }
render() { render() {
@ -82,28 +158,49 @@ class TetrisScreen extends React.Component<Props, State> {
width: '100%', width: '100%',
height: '100%', height: '100%',
}}> }}>
<Text style={{ <View style={{
textAlign: 'center', flexDirection: 'row',
position: 'absolute',
top: 10,
left: 10,
}}> }}>
Score: {this.state.gameScore} <MaterialCommunityIcons
</Text> name={'timer'}
<Text style={{ color={this.colors.subtitle}
textAlign: 'center', size={20}/>
<Text style={{
marginLeft: 5,
color: this.colors.subtitle
}}>{this.state.gameTime}</Text>
</View>
<View style={{
flexDirection: 'row',
marginRight: 'auto',
marginLeft: 'auto',
}}> }}>
time: {this.state.gameTime} <MaterialCommunityIcons
</Text> name={'star'}
color={this.colors.tetrisScore}
size={30}/>
<Text style={{
marginLeft: 5,
fontSize: 22,
}}>{this.state.gameScore}</Text>
</View>
<Grid <Grid
width={this.logic.getWidth()} width={this.logic.getWidth()}
height={this.logic.getHeight()} height={this.logic.getHeight()}
grid={this.state.grid} grid={this.state.grid}
/> />
<View style={{ <View style={{
flexDirection: 'row-reverse', flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
}}> }}>
<IconButton <IconButton
icon="arrow-right" icon="format-rotate-90"
size={40} size={40}
onPress={() => this.logic.rightPressed(this.updateGrid)} onPress={() => this.logic.rotatePressed(this.updateGrid)}
/> />
<IconButton <IconButton
icon="arrow-left" icon="arrow-left"
@ -111,15 +208,15 @@ class TetrisScreen extends React.Component<Props, State> {
onPress={() => this.logic.leftPressed(this.updateGrid)} onPress={() => this.logic.leftPressed(this.updateGrid)}
/> />
<IconButton <IconButton
icon="format-rotate-90" icon="arrow-right"
size={40} size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)} onPress={() => this.logic.rightPressed(this.updateGrid)}
/> />
<IconButton <IconButton
icon="power" icon="arrow-down"
size={40} size={40}
onPress={() => this.startGame()} onPress={() => this.logic.rightPressed(this.updateGrid)}
/> />
</View> </View>
</View> </View>
); );

View file

@ -58,6 +58,7 @@ export default class ThemeManager {
// Tetris // Tetris
tetrisBackground:'#d1d1d1', tetrisBackground:'#d1d1d1',
tetrisBorder:'#afafaf', tetrisBorder:'#afafaf',
tetrisScore:'#fff307',
tetrisI : '#42f1ff', tetrisI : '#42f1ff',
tetrisO : '#ffdd00', tetrisO : '#ffdd00',
tetrisT : '#ba19ff', tetrisT : '#ba19ff',
@ -109,6 +110,7 @@ export default class ThemeManager {
// Tetris // Tetris
tetrisBackground:'#2c2c2c', tetrisBackground:'#2c2c2c',
tetrisBorder:'#1b1b1b', tetrisBorder:'#1b1b1b',
tetrisScore:'#e2d707',
tetrisI : '#30b3be', tetrisI : '#30b3be',
tetrisO : '#c1a700', tetrisO : '#c1a700',
tetrisT : '#9114c7', tetrisT : '#9114c7',