Update game to use TypeScript

This commit is contained in:
Arnaud Vergnet 2020-09-22 23:13:09 +02:00
parent c198a40148
commit fde9a12ef9
18 changed files with 326 additions and 313 deletions

View file

@ -19,11 +19,9 @@
// @flow // @flow
import type {CustomThemeType} from '../../../managers/ThemeManager';
export type CoordinatesType = { export type CoordinatesType = {
x: number, x: number;
y: number, y: number;
}; };
export type ShapeType = Array<Array<number>>; export type ShapeType = Array<Array<number>>;
@ -40,14 +38,15 @@ export default class BaseShape {
position: CoordinatesType; position: CoordinatesType;
theme: CustomThemeType; theme: ReactNativePaper.Theme;
/** /**
* Prevent instantiation if classname is BaseShape to force class to be abstract * Prevent instantiation if classname is BaseShape to force class to be abstract
*/ */
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
if (this.constructor === BaseShape) if (this.constructor === BaseShape) {
throw new Error("Abstract class can't be instantiated"); throw new Error("Abstract class can't be instantiated");
}
this.theme = theme; this.theme = theme;
this.#rotation = 0; this.#rotation = 0;
this.position = {x: 0, y: 0}; this.position = {x: 0, y: 0};
@ -58,7 +57,6 @@ export default class BaseShape {
* Gets this shape's color. * Gets this shape's color.
* Must be implemented by child class * Must be implemented by child class
*/ */
// eslint-disable-next-line class-methods-use-this
getColor(): string { getColor(): string {
throw new Error("Method 'getColor()' must be implemented"); throw new Error("Method 'getColor()' must be implemented");
} }
@ -69,7 +67,6 @@ export default class BaseShape {
* *
* Used by tests to read private fields * Used by tests to read private fields
*/ */
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
throw new Error("Method 'getShapes()' must be implemented"); throw new Error("Method 'getShapes()' must be implemented");
} }
@ -98,7 +95,9 @@ export default class BaseShape {
x: this.position.x + col, x: this.position.x + col,
y: this.position.y + row, y: this.position.y + row,
}); });
} else coordinates.push({x: col, y: row}); } else {
coordinates.push({x: col, y: row});
}
} }
} }
} }
@ -111,10 +110,16 @@ export default class BaseShape {
* @param isForward Should we rotate clockwise? * @param isForward Should we rotate clockwise?
*/ */
rotate(isForward: boolean) { rotate(isForward: boolean) {
if (isForward) this.#rotation += 1; if (isForward) {
else this.#rotation -= 1; this.#rotation += 1;
if (this.#rotation > 3) this.#rotation = 0; } else {
else if (this.#rotation < 0) this.#rotation = 3; this.#rotation -= 1;
}
if (this.#rotation > 3) {
this.#rotation = 0;
} else if (this.#rotation < 0) {
this.#rotation = 3;
}
this.#currentShape = this.getShapes()[this.#rotation]; this.#currentShape = this.getShapes()[this.#rotation];
} }

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeI extends BaseShape { export default class ShapeI extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeI extends BaseShape {
return this.theme.colors.tetrisI; return this.theme.colors.tetrisI;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeJ extends BaseShape { export default class ShapeJ extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeJ extends BaseShape {
return this.theme.colors.tetrisJ; return this.theme.colors.tetrisJ;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeL extends BaseShape { export default class ShapeL extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeL extends BaseShape {
return this.theme.colors.tetrisL; return this.theme.colors.tetrisL;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeO extends BaseShape { export default class ShapeO extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 4; this.position.x = 4;
} }
@ -33,7 +30,6 @@ export default class ShapeO extends BaseShape {
return this.theme.colors.tetrisO; return this.theme.colors.tetrisO;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeS extends BaseShape { export default class ShapeS extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeS extends BaseShape {
return this.theme.colors.tetrisS; return this.theme.colors.tetrisS;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeT extends BaseShape { export default class ShapeT extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeT extends BaseShape {
return this.theme.colors.tetrisT; return this.theme.colors.tetrisT;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import BaseShape from './BaseShape'; import BaseShape from './BaseShape';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape'; import type {ShapeType} from './BaseShape';
export default class ShapeZ extends BaseShape { export default class ShapeZ extends BaseShape {
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
super(theme); super(theme);
this.position.x = 3; this.position.x = 3;
} }
@ -33,7 +30,6 @@ export default class ShapeZ extends BaseShape {
return this.theme.colors.tetrisZ; return this.theme.colors.tetrisZ;
} }
// eslint-disable-next-line class-methods-use-this
getShapes(): Array<ShapeType> { getShapes(): Array<ShapeType> {
return [ return [
[ [

View file

@ -17,35 +17,29 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
export type CellType = {color: string, isEmpty: boolean, key: string}; export type CellType = {color: string; isEmpty: boolean; key: string};
type PropsType = { type PropsType = {
cell: CellType, cell: CellType;
}; };
class CellComponent extends React.PureComponent<PropsType> { function CellComponent(props: PropsType) {
render(): React.Node { const item = props.cell;
const {props} = this; return (
const item = props.cell; <View
return ( style={{
<View flex: 1,
style={{ backgroundColor: item.isEmpty ? 'transparent' : item.color,
flex: 1, borderColor: 'transparent',
backgroundColor: item.isEmpty ? 'transparent' : item.color, borderRadius: 4,
borderColor: 'transparent', borderWidth: 1,
borderRadius: 4, aspectRatio: 1,
borderWidth: 1, }}
aspectRatio: 1, />
}} );
/>
);
}
} }
export default withTheme(CellComponent); export default CellComponent;

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import type {CellType} from './CellComponent';
import CellComponent from './CellComponent';
export type GridType = Array<Array<CellComponent>>;
type PropsType = {
grid: Array<Array<CellType>>,
height: number,
width: number,
style: ViewStyle,
};
class GridComponent extends React.Component<PropsType> {
getRow(rowNumber: number): React.Node {
const {grid} = this.props;
return (
<View style={{flexDirection: 'row'}} key={rowNumber.toString()}>
{grid[rowNumber].map(this.getCellRender)}
</View>
);
}
getCellRender = (item: CellType): React.Node => {
return <CellComponent cell={item} key={item.key} />;
};
getGrid(): React.Node {
const {height} = this.props;
const rows = [];
for (let i = 0; i < height; i += 1) {
rows.push(this.getRow(i));
}
return rows;
}
render(): React.Node {
const {style, width, height} = this.props;
return (
<View
style={{
aspectRatio: width / height,
borderRadius: 4,
...style,
}}>
{this.getGrid()}
</View>
);
}
}
export default withTheme(GridComponent);

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {View, ViewStyle} from 'react-native';
import type {CellType} from './CellComponent';
import CellComponent from './CellComponent';
export type GridType = Array<Array<CellType>>;
type PropsType = {
grid: GridType;
height: number;
width: number;
style: ViewStyle;
};
const getCellRender = (item: CellType) => {
return <CellComponent cell={item} key={item.key} />;
};
function getRow(grid: GridType, rowNumber: number) {
return (
<View style={{flexDirection: 'row'}} key={rowNumber.toString()}>
{grid[rowNumber].map(getCellRender)}
</View>
);
}
function getGrid(grid: GridType, height: number) {
const rows = [];
for (let i = 0; i < height; i += 1) {
rows.push(getRow(grid, i));
}
return rows;
}
function GridComponent(props: PropsType) {
const {style, width, height, grid} = props;
return (
<View
style={{
aspectRatio: width / height,
borderRadius: 4,
...style,
}}>
{getGrid(grid, height)}
</View>
);
}
export default GridComponent;

View file

@ -17,53 +17,48 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View, ViewStyle} from 'react-native';
import {withTheme} from 'react-native-paper';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import type {GridType} from './GridComponent'; import type {GridType} from './GridComponent';
import GridComponent from './GridComponent'; import GridComponent from './GridComponent';
type PropsType = { type PropsType = {
items: Array<GridType>, items: Array<GridType>;
style: ViewStyle, style: ViewStyle;
}; };
function getGridRender(item: GridType, index: number) {
return (
<GridComponent
width={item[0].length}
height={item.length}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
key={index.toString()}
/>
);
}
function getGrids(items: Array<GridType>) {
const grids: Array<React.ReactNode> = [];
items.forEach((item: GridType, index: number) => {
grids.push(getGridRender(item, index));
});
return grids;
}
class Preview extends React.PureComponent<PropsType> { class Preview extends React.PureComponent<PropsType> {
getGrids(): React.Node { render() {
const {items} = this.props;
const grids = [];
items.forEach((item: GridType, index: number) => {
grids.push(Preview.getGridRender(item, index));
});
return grids;
}
static getGridRender(item: GridType, index: number): React.Node {
return (
<GridComponent
width={item[0].length}
height={item.length}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
key={index.toString()}
/>
);
}
render(): React.Node {
const {style, items} = this.props; const {style, items} = this.props;
if (items.length > 0) { if (items.length > 0) {
return <View style={style}>{this.getGrids()}</View>; return <View style={style}>{getGrids(items)}</View>;
} }
return null; return null;
} }
} }
export default withTheme(Preview); export default Preview;

View file

@ -17,12 +17,9 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import Piece from './Piece'; import Piece from './Piece';
import ScoreManager from './ScoreManager'; import ScoreManager from './ScoreManager';
import GridManager from './GridManager'; import GridManager from './GridManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {GridType} from '../components/GridComponent'; import type {GridType} from '../components/GridComponent';
export type TickCallbackType = ( export type TickCallbackType = (
@ -58,15 +55,15 @@ export default class GameLogic {
gameTime: number; gameTime: number;
currentObject: Piece; currentObject?: Piece;
gameTick: number; gameTick: number;
gameTickInterval: IntervalID; gameTickInterval?: NodeJS.Timeout;
gameTimeInterval: IntervalID; gameTimeInterval?: NodeJS.Timeout;
pressInInterval: TimeoutID; pressInInterval?: NodeJS.Timeout;
isPressedIn: boolean; isPressedIn: boolean;
@ -78,15 +75,19 @@ export default class GameLogic {
nextPiecesCount: number; nextPiecesCount: number;
tickCallback: TickCallbackType; tickCallback?: TickCallbackType;
clockCallback: ClockCallbackType; clockCallback?: ClockCallbackType;
endCallback: EndCallbackType; endCallback?: EndCallbackType;
theme: CustomThemeType; theme: ReactNativePaper.Theme;
constructor(height: number, width: number, theme: ReactNativePaper.Theme) {
this.gameTime = 0;
this.gameTick = 0;
this.isPressedIn = false;
constructor(height: number, width: number, theme: CustomThemeType) {
this.height = height; this.height = height;
this.width = width; this.width = width;
this.gameRunning = false; this.gameRunning = false;
@ -121,12 +122,16 @@ export default class GameLogic {
} }
onFreeze = () => { onFreeze = () => {
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager); if (this.currentObject) {
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager);
}
this.createTetromino(); this.createTetromino();
}; };
setNewGameTick(level: number) { setNewGameTick(level: number) {
if (level >= GameLogic.levelTicks.length) return; if (level >= GameLogic.levelTicks.length) {
return;
}
this.gameTick = GameLogic.levelTicks[level]; this.gameTick = GameLogic.levelTicks[level];
this.stopTick(); this.stopTick();
this.startTick(); this.startTick();
@ -145,11 +150,15 @@ export default class GameLogic {
} }
stopClock() { stopClock() {
clearInterval(this.gameTimeInterval); if (this.gameTimeInterval) {
clearInterval(this.gameTimeInterval);
}
} }
stopTick() { stopTick() {
clearInterval(this.gameTickInterval); if (this.gameTickInterval) {
clearInterval(this.gameTickInterval);
}
} }
stopGameTime() { stopGameTime() {
@ -162,27 +171,34 @@ export default class GameLogic {
this.startTick(); this.startTick();
} }
onTick(callback: TickCallbackType) { onTick(callback?: TickCallbackType) {
this.currentObject.tryMove( if (this.currentObject) {
0, this.currentObject.tryMove(
1, 0,
this.gridManager.getCurrentGrid(), 1,
this.getWidth(), this.gridManager.getCurrentGrid(),
this.getHeight(), this.getWidth(),
this.onFreeze, this.getHeight(),
); this.onFreeze,
callback( );
this.scoreManager.getScore(), }
this.scoreManager.getLevel(), if (callback) {
this.gridManager.getCurrentGrid(), callback(
); this.scoreManager.getScore(),
if (this.scoreManager.canLevelUp()) this.scoreManager.getLevel(),
this.gridManager.getCurrentGrid(),
);
}
if (this.scoreManager.canLevelUp()) {
this.setNewGameTick(this.scoreManager.getLevel()); this.setNewGameTick(this.scoreManager.getLevel());
}
} }
onClock(callback: ClockCallbackType) { onClock(callback?: ClockCallbackType) {
this.gameTime += 1; this.gameTime += 1;
callback(this.gameTime); if (callback) {
callback(this.gameTime);
}
} }
canUseInput(): boolean { canUseInput(): boolean {
@ -210,15 +226,19 @@ export default class GameLogic {
x: number, x: number,
y: number, y: number,
) { ) {
if (!this.canUseInput() || !this.isPressedIn) return; if (!this.canUseInput() || !this.isPressedIn) {
const moved = this.currentObject.tryMove( return;
x, }
y, const moved =
this.gridManager.getCurrentGrid(), this.currentObject &&
this.getWidth(), this.currentObject.tryMove(
this.getHeight(), x,
this.onFreeze, y,
); this.gridManager.getCurrentGrid(),
this.getWidth(),
this.getHeight(),
this.onFreeze,
);
if (moved) { if (moved) {
if (y === 1) { if (y === 1) {
this.scoreManager.incrementScore(); this.scoreManager.incrementScore();
@ -226,7 +246,9 @@ export default class GameLogic {
this.gridManager.getCurrentGrid(), this.gridManager.getCurrentGrid(),
this.scoreManager.getScore(), this.scoreManager.getScore(),
); );
} else callback(this.gridManager.getCurrentGrid()); } else {
callback(this.gridManager.getCurrentGrid());
}
} }
this.pressInInterval = setTimeout( this.pressInInterval = setTimeout(
() => { () => {
@ -238,20 +260,26 @@ export default class GameLogic {
pressedOut() { pressedOut() {
this.isPressedIn = false; this.isPressedIn = false;
clearTimeout(this.pressInInterval); if (this.pressInInterval) {
clearTimeout(this.pressInInterval);
}
} }
rotatePressed(callback: MovementCallbackType) { rotatePressed(callback: MovementCallbackType) {
if (!this.canUseInput()) return; if (!this.canUseInput()) {
return;
}
if ( if (
this.currentObject &&
this.currentObject.tryRotate( this.currentObject.tryRotate(
this.gridManager.getCurrentGrid(), this.gridManager.getCurrentGrid(),
this.getWidth(), this.getWidth(),
this.getHeight(), this.getHeight(),
) )
) ) {
callback(this.gridManager.getCurrentGrid()); callback(this.gridManager.getCurrentGrid());
}
} }
getNextPiecesPreviews(): Array<GridType> { getNextPiecesPreviews(): Array<GridType> {
@ -266,7 +294,10 @@ export default class GameLogic {
} }
recoverNextPiece() { recoverNextPiece() {
this.currentObject = this.nextPieces.shift(); const next = this.nextPieces.shift();
if (next) {
this.currentObject = next;
}
this.generateNextPieces(); this.generateNextPieces();
} }
@ -280,27 +311,36 @@ export default class GameLogic {
this.pressedOut(); this.pressedOut();
this.recoverNextPiece(); this.recoverNextPiece();
if ( if (
this.currentObject &&
!this.currentObject.isPositionValid( !this.currentObject.isPositionValid(
this.gridManager.getCurrentGrid(), this.gridManager.getCurrentGrid(),
this.getWidth(), this.getWidth(),
this.getHeight(), this.getHeight(),
) )
) ) {
this.endGame(false); this.endGame(false);
}
} }
togglePause() { togglePause() {
if (!this.gameRunning) return; if (!this.gameRunning) {
return;
}
this.gamePaused = !this.gamePaused; this.gamePaused = !this.gamePaused;
if (this.gamePaused) this.stopGameTime(); if (this.gamePaused) {
else this.startGameTime(); this.stopGameTime();
} else {
this.startGameTime();
}
} }
endGame(isRestart: boolean) { endGame(isRestart: boolean) {
this.gameRunning = false; this.gameRunning = false;
this.gamePaused = false; this.gamePaused = false;
this.stopGameTime(); this.stopGameTime();
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart); if (this.endCallback) {
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart);
}
} }
startGame( startGame(
@ -308,7 +348,9 @@ export default class GameLogic {
clockCallback: ClockCallbackType, clockCallback: ClockCallbackType,
endCallback: EndCallbackType, endCallback: EndCallbackType,
) { ) {
if (this.gameRunning) this.endGame(true); if (this.gameRunning) {
this.endGame(true);
}
this.gameRunning = true; this.gameRunning = true;
this.gamePaused = false; this.gamePaused = false;
this.gameTime = 0; this.gameTime = 0;

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import Piece from './Piece'; import Piece from './Piece';
import ScoreManager from './ScoreManager'; import ScoreManager from './ScoreManager';
import type {CoordinatesType} from '../Shapes/BaseShape'; import type {CoordinatesType} from '../Shapes/BaseShape';
import type {GridType} from '../components/GridComponent'; import type {GridType} from '../components/GridComponent';
import type {CellType} from '../components/CellComponent'; import type {CellType} from '../components/CellComponent';
import type {CustomThemeType} from '../../../managers/ThemeManager';
/** /**
* Class used to manage the game grid * Class used to manage the game grid
@ -32,7 +29,7 @@ import type {CustomThemeType} from '../../../managers/ThemeManager';
export default class GridManager { export default class GridManager {
#currentGrid: GridType; #currentGrid: GridType;
#theme: CustomThemeType; #theme: ReactNativePaper.Theme;
/** /**
* Initializes a grid of the given size * Initializes a grid of the given size
@ -41,7 +38,7 @@ export default class GridManager {
* @param height The grid height * @param height The grid height
* @param theme Object containing current theme * @param theme Object containing current theme
*/ */
constructor(width: number, height: number, theme: CustomThemeType) { constructor(width: number, height: number, theme: ReactNativePaper.Theme) {
this.#theme = theme; this.#theme = theme;
this.#currentGrid = this.getEmptyGrid(height, width); this.#currentGrid = this.getEmptyGrid(height, width);
} }
@ -121,7 +118,9 @@ export default class GridManager {
break; break;
} }
} }
if (isLineFull && rows.indexOf(pos[i].y) === -1) rows.push(pos[i].y); if (isLineFull && rows.indexOf(pos[i].y) === -1) {
rows.push(pos[i].y);
}
} }
return rows; return rows;
} }

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import ShapeL from '../Shapes/ShapeL'; import ShapeL from '../Shapes/ShapeL';
import ShapeI from '../Shapes/ShapeI'; import ShapeI from '../Shapes/ShapeI';
import ShapeJ from '../Shapes/ShapeJ'; import ShapeJ from '../Shapes/ShapeJ';
@ -29,7 +27,6 @@ import ShapeZ from '../Shapes/ShapeZ';
import type {CoordinatesType} from '../Shapes/BaseShape'; import type {CoordinatesType} from '../Shapes/BaseShape';
import BaseShape from '../Shapes/BaseShape'; import BaseShape from '../Shapes/BaseShape';
import type {GridType} from '../components/GridComponent'; import type {GridType} from '../components/GridComponent';
import type {CustomThemeType} from '../../../managers/ThemeManager';
/** /**
* Class used as an abstraction layer for shapes. * Class used as an abstraction layer for shapes.
@ -41,14 +38,14 @@ export default class Piece {
currentShape: BaseShape; currentShape: BaseShape;
theme: CustomThemeType; theme: ReactNativePaper.Theme;
/** /**
* Initializes this piece's color and shape * Initializes this piece's color and shape
* *
* @param theme Object containing current theme * @param theme Object containing current theme
*/ */
constructor(theme: CustomThemeType) { constructor(theme: ReactNativePaper.Theme) {
this.currentShape = this.getRandomShape(theme); this.currentShape = this.getRandomShape(theme);
this.theme = theme; this.theme = theme;
} }
@ -58,7 +55,7 @@ export default class Piece {
* *
* @param theme Object containing current theme * @param theme Object containing current theme
*/ */
getRandomShape(theme: CustomThemeType): BaseShape { getRandomShape(theme: ReactNativePaper.Theme): BaseShape {
return new this.shapes[Math.floor(Math.random() * 7)](theme); return new this.shapes[Math.floor(Math.random() * 7)](theme);
} }
@ -72,7 +69,6 @@ export default class Piece {
true, true,
); );
pos.forEach((coordinates: CoordinatesType) => { pos.forEach((coordinates: CoordinatesType) => {
// eslint-disable-next-line no-param-reassign
grid[coordinates.y][coordinates.x] = { grid[coordinates.y][coordinates.x] = {
color: this.theme.colors.tetrisBackground, color: this.theme.colors.tetrisBackground,
isEmpty: true, isEmpty: true,
@ -92,7 +88,6 @@ export default class Piece {
!isPreview, !isPreview,
); );
pos.forEach((coordinates: CoordinatesType) => { pos.forEach((coordinates: CoordinatesType) => {
// eslint-disable-next-line no-param-reassign
grid[coordinates.y][coordinates.x] = { grid[coordinates.y][coordinates.x] = {
color: this.currentShape.getColor(), color: this.currentShape.getColor(),
isEmpty: false, isEmpty: false,
@ -150,21 +145,35 @@ export default class Piece {
): boolean { ): boolean {
let newX = x; let newX = x;
let newY = y; let newY = y;
if (x > 1) newX = 1; // Prevent moving from more than one tile if (x > 1) {
if (x < -1) newX = -1; newX = 1;
if (y > 1) newY = 1; } // Prevent moving from more than one tile
if (y < -1) newY = -1; if (x < -1) {
if (x !== 0 && y !== 0) newY = 0; // Prevent diagonal movement newX = -1;
}
if (y > 1) {
newY = 1;
}
if (y < -1) {
newY = -1;
}
if (x !== 0 && y !== 0) {
newY = 0;
} // Prevent diagonal movement
this.removeFromGrid(grid); this.removeFromGrid(grid);
this.currentShape.move(newX, newY); this.currentShape.move(newX, newY);
const isValid = this.isPositionValid(grid, width, height); const isValid = this.isPositionValid(grid, width, height);
if (!isValid) this.currentShape.move(-newX, -newY); if (!isValid) {
this.currentShape.move(-newX, -newY);
}
const shouldFreeze = !isValid && newY !== 0; const shouldFreeze = !isValid && newY !== 0;
this.toGrid(grid, false); this.toGrid(grid, false);
if (shouldFreeze) freezeCallback(); if (shouldFreeze) {
freezeCallback();
}
return isValid; return isValid;
} }

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
/** /**
* Class used to manage game score * Class used to manage game score
*/ */
@ -83,7 +81,9 @@ export default class ScoreManager {
* @param numberRemoved The number of lines removed at the same time * @param numberRemoved The number of lines removed at the same time
*/ */
addLinesRemovedPoints(numberRemoved: number) { addLinesRemovedPoints(numberRemoved: number) {
if (numberRemoved < 1 || numberRemoved > 4) return; if (numberRemoved < 1 || numberRemoved > 4) {
return;
}
this.#score += this.#score +=
this.#scoreLinesModifier[numberRemoved - 1] * (this.#level + 1); this.#scoreLinesModifier[numberRemoved - 1] * (this.#level + 1);
switch (numberRemoved) { switch (numberRemoved) {

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Caption, IconButton, Text, withTheme} from 'react-native-paper'; import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
@ -32,27 +30,26 @@ import Preview from '../components/Preview';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../../components/Overrides/CustomHeaderButton'; } from '../../../components/Overrides/CustomHeaderButton';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog'; import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog';
import OptionsDialog from '../../../components/Dialogs/OptionsDialog'; import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
route: {params: {highScore: number}}, route: {params: {highScore: number}};
theme: CustomThemeType, theme: ReactNativePaper.Theme;
}; };
type StateType = { type StateType = {
grid: GridType, grid: GridType;
gameTime: number, gameTime: number;
gameScore: number, gameScore: number;
gameLevel: number, gameLevel: number;
dialogVisible: boolean, dialogVisible: boolean;
dialogTitle: string, dialogTitle: string;
dialogMessage: string, dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>, dialogButtons: Array<OptionsDialogButtonType>;
onDialogDismiss: () => void, onDialogDismiss: () => void;
}; };
class GameMainScreen extends React.Component<PropsType, StateType> { class GameMainScreen extends React.Component<PropsType, StateType> {
@ -62,11 +59,13 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
date.setMinutes(0); date.setMinutes(0);
date.setSeconds(seconds); date.setSeconds(seconds);
let format; let format;
if (date.getHours()) if (date.getHours()) {
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
else if (date.getMinutes()) } else if (date.getMinutes()) {
format = `${date.getMinutes()}:${date.getSeconds()}`; format = `${date.getMinutes()}:${date.getSeconds()}`;
else format = date.getSeconds().toString(); } else {
format = date.getSeconds().toString();
}
return format; return format;
} }
@ -76,6 +75,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.highScore = null;
this.logic = new GameLogic(20, 10, props.theme); this.logic = new GameLogic(20, 10, props.theme);
this.state = { this.state = {
grid: this.logic.getCurrentGrid(), grid: this.logic.getCurrentGrid(),
@ -88,8 +88,9 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
dialogButtons: [], dialogButtons: [],
onDialogDismiss: () => {}, onDialogDismiss: () => {},
}; };
if (props.route.params != null) if (props.route.params != null) {
this.highScore = props.route.params.highScore; this.highScore = props.route.params.highScore;
}
} }
componentDidMount() { componentDidMount() {
@ -104,7 +105,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
this.logic.endGame(true); this.logic.endGame(true);
} }
getRightButton = (): React.Node => { getRightButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={this.togglePause} /> <Item title="pause" iconName="pause" onPress={this.togglePause} />
@ -136,15 +137,16 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
gameTime: time, gameTime: time,
gameScore: score, gameScore: score,
}); });
if (!isRestart) if (!isRestart) {
props.navigation.replace('game-start', { props.navigation.replace('game-start', {
score: state.gameScore, score: state.gameScore,
level: state.gameLevel, level: state.gameLevel,
time: state.gameTime, time: state.gameTime,
}); });
}
}; };
getStatusIcons(): React.Node { getStatusIcons() {
const {props, state} = this; const {props, state} = this;
return ( return (
<View <View
@ -219,7 +221,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
); );
} }
getScoreIcon(): React.Node { getScoreIcon() {
const {props, state} = this; const {props, state} = this;
const highScore = const highScore =
this.highScore == null || state.gameScore > this.highScore this.highScore == null || state.gameScore > this.highScore
@ -285,7 +287,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
); );
} }
getControlButtons(): React.Node { getControlButtons() {
const {props} = this; const {props} = this;
return ( return (
<View <View
@ -353,8 +355,8 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
updateGridScore = (newGrid: GridType, score?: number) => { updateGridScore = (newGrid: GridType, score?: number) => {
this.setState((prevState: StateType): { this.setState((prevState: StateType): {
grid: GridType, grid: GridType;
gameScore: number, gameScore: number;
} => ({ } => ({
grid: newGrid, grid: newGrid,
gameScore: score != null ? score : prevState.gameScore, gameScore: score != null ? score : prevState.gameScore,
@ -363,7 +365,9 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
togglePause = () => { togglePause = () => {
this.logic.togglePause(); this.logic.togglePause();
if (this.logic.isGamePaused()) this.showPausePopup(); if (this.logic.isGamePaused()) {
this.showPausePopup();
}
}; };
showPausePopup = () => { showPausePopup = () => {
@ -415,7 +419,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd); this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import { import {
@ -35,7 +33,6 @@ import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot'; import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
import MascotPopup from '../../../components/Mascot/MascotPopup'; import MascotPopup from '../../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../../managers/AsyncStorageManager';
@ -47,17 +44,17 @@ import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type GameStatsType = { type GameStatsType = {
score: number, score: number;
level: number, level: number;
time: number, time: number;
}; };
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
route: { route: {
params: GameStatsType, params: GameStatsType;
}, };
theme: CustomThemeType, theme: ReactNativePaper.Theme;
}; };
class GameStartScreen extends React.Component<PropsType> { class GameStartScreen extends React.Component<PropsType> {
@ -65,21 +62,24 @@ class GameStartScreen extends React.Component<PropsType> {
scores: Array<number>; scores: Array<number>;
gameStats: GameStatsType | null; gameStats?: GameStatsType;
isHighScore: boolean; isHighScore: boolean;
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.isHighScore = false;
this.gridManager = new GridManager(4, 4, props.theme); this.gridManager = new GridManager(4, 4, props.theme);
this.scores = AsyncStorageManager.getObject( this.scores = AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.gameScores.key, AsyncStorageManager.PREFERENCES.gameScores.key,
); );
this.scores.sort((a: number, b: number): number => b - a); this.scores.sort((a: number, b: number): number => b - a);
if (props.route.params != null) this.recoverGameScore(); if (props.route.params != null) {
this.recoverGameScore();
}
} }
getPiecesBackground(): React.Node { getPiecesBackground() {
const {theme} = this.props; const {theme} = this.props;
const gridList = []; const gridList = [];
for (let i = 0; i < 18; i += 1) { for (let i = 0; i < 18; i += 1) {
@ -94,7 +94,7 @@ class GameStartScreen extends React.Component<PropsType> {
width: '100%', width: '100%',
height: '100%', height: '100%',
}}> }}>
{gridList.map((item: GridType, index: number): React.Node => { {gridList.map((item: GridType, index: number) => {
const size = 10 + Math.floor(Math.random() * 30); const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100); const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360); const rot = Math.floor(Math.random() * 360);
@ -129,7 +129,7 @@ class GameStartScreen extends React.Component<PropsType> {
); );
} }
getPostGameContent(stats: GameStatsType): React.Node { getPostGameContent(stats: GameStatsType) {
const {props} = this; const {props} = this;
return ( return (
<View <View
@ -141,8 +141,8 @@ class GameStartScreen extends React.Component<PropsType> {
animated={this.isHighScore} animated={this.isHighScore}
style={{ style={{
width: this.isHighScore ? '50%' : '30%', width: this.isHighScore ? '50%' : '30%',
marginLeft: this.isHighScore ? 'auto' : null, marginLeft: this.isHighScore ? 'auto' : undefined,
marginRight: this.isHighScore ? 'auto' : null, marginRight: this.isHighScore ? 'auto' : undefined,
}} }}
/> />
<SpeechArrow <SpeechArrow
@ -235,7 +235,7 @@ class GameStartScreen extends React.Component<PropsType> {
); );
} }
getWelcomeText(): React.Node { getWelcomeText() {
const {props} = this; const {props} = this;
return ( return (
<View> <View>
@ -281,7 +281,7 @@ class GameStartScreen extends React.Component<PropsType> {
); );
} }
getPodiumRender(place: 1 | 2 | 3, score: string): React.Node { getPodiumRender(place: 1 | 2 | 3, score: string) {
const {props} = this; const {props} = this;
let icon = 'podium-gold'; let icon = 'podium-gold';
let color = props.theme.colors.gameGold; let color = props.theme.colors.gameGold;
@ -338,7 +338,7 @@ class GameStartScreen extends React.Component<PropsType> {
<Text <Text
style={{ style={{
textAlign: 'center', textAlign: 'center',
fontWeight: place === 1 ? 'bold' : null, fontWeight: place === 1 ? 'bold' : undefined,
fontSize, fontSize,
}}> }}>
{score} {score}
@ -347,7 +347,7 @@ class GameStartScreen extends React.Component<PropsType> {
); );
} }
getTopScoresRender(): React.Node { getTopScoresRender() {
const gold = this.scores.length > 0 ? this.scores[0] : '-'; const gold = this.scores.length > 0 ? this.scores[0] : '-';
const silver = this.scores.length > 1 ? this.scores[1] : '-'; const silver = this.scores.length > 1 ? this.scores[1] : '-';
const bronze = this.scores.length > 2 ? this.scores[2] : '-'; const bronze = this.scores.length > 2 ? this.scores[2] : '-';
@ -371,7 +371,7 @@ class GameStartScreen extends React.Component<PropsType> {
); );
} }
getMainContent(): React.Node { getMainContent() {
const {props} = this; const {props} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
@ -415,7 +415,9 @@ class GameStartScreen extends React.Component<PropsType> {
break; break;
} }
} }
if (this.scores.length > 3) this.scores.splice(3, 1); if (this.scores.length > 3) {
this.scores.splice(3, 1);
}
AsyncStorageManager.set( AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.gameScores.key, AsyncStorageManager.PREFERENCES.gameScores.key,
this.scores, this.scores,
@ -423,7 +425,7 @@ class GameStartScreen extends React.Component<PropsType> {
} }
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
@ -444,7 +446,6 @@ class GameStartScreen extends React.Component<PropsType> {
message={i18n.t('screens.game.mascotDialog.message')} message={i18n.t('screens.game.mascotDialog.message')}
icon="gamepad-variant" icon="gamepad-variant"
buttons={{ buttons={{
action: null,
cancel: { cancel: {
message: i18n.t('screens.game.mascotDialog.button'), message: i18n.t('screens.game.mascotDialog.button'),
icon: 'check', icon: 'check',