diff --git a/src/screens/Game/Shapes/BaseShape.js b/src/screens/Game/Shapes/BaseShape.js index 0b891dd..e014ea4 100644 --- a/src/screens/Game/Shapes/BaseShape.js +++ b/src/screens/Game/Shapes/BaseShape.js @@ -1,13 +1,13 @@ // @flow -import type {CustomTheme} from "../../../managers/ThemeManager"; +import type {CustomThemeType} from '../../../managers/ThemeManager'; -export type Coordinates = { - x: number, - y: number, -} +export type CoordinatesType = { + x: number, + y: number, +}; -type Shape = Array>; +export type ShapeType = Array>; /** * Abstract class used to represent a BaseShape. @@ -15,96 +15,98 @@ type Shape = Array>; * and in methods to implement */ export default class BaseShape { + #currentShape: ShapeType; - #currentShape: Shape; - #rotation: number; - position: Coordinates; - theme: CustomTheme; + #rotation: number; - /** - * Prevent instantiation if classname is BaseShape to force class to be abstract - */ - constructor(theme: CustomTheme) { - if (this.constructor === BaseShape) - throw new Error("Abstract class can't be instantiated"); - this.theme = theme; - this.#rotation = 0; - this.position = {x: 0, y: 0}; - this.#currentShape = this.getShapes()[this.#rotation]; - } + position: CoordinatesType; - /** - * Gets this shape's color. - * Must be implemented by child class - */ - getColor(): string { - throw new Error("Method 'getColor()' must be implemented"); - } + theme: CustomThemeType; - /** - * Gets this object's all possible shapes as an array. - * Must be implemented by child class. - * - * Used by tests to read private fields - */ - getShapes(): Array { - throw new Error("Method 'getShapes()' must be implemented"); - } + /** + * Prevent instantiation if classname is BaseShape to force class to be abstract + */ + constructor(theme: CustomThemeType) { + if (this.constructor === BaseShape) + throw new Error("Abstract class can't be instantiated"); + this.theme = theme; + this.#rotation = 0; + this.position = {x: 0, y: 0}; + this.#currentShape = this.getShapes()[this.#rotation]; + } - /** - * Gets this object's current shape. - */ - getCurrentShape(): Shape { - return this.#currentShape; - } + /** + * Gets this shape's color. + * Must be implemented by child class + */ + // eslint-disable-next-line class-methods-use-this + getColor(): string { + throw new Error("Method 'getColor()' must be implemented"); + } - /** - * Gets this object's coordinates. - * This will return an array of coordinates representing the positions of the cells used by this object. - * - * @param isAbsolute Should we take into account the current position of the object? - * @return {Array} This object cells coordinates - */ - getCellsCoordinates(isAbsolute: boolean): Array { - let coordinates = []; - for (let row = 0; row < this.#currentShape.length; row++) { - for (let col = 0; col < this.#currentShape[row].length; col++) { - if (this.#currentShape[row][col] === 1) - if (isAbsolute) - coordinates.push({x: this.position.x + col, y: this.position.y + row}); - else - coordinates.push({x: col, y: row}); - } + /** + * Gets this object's all possible shapes as an array. + * Must be implemented by child class. + * + * Used by tests to read private fields + */ + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + throw new Error("Method 'getShapes()' must be implemented"); + } + + /** + * Gets this object's current shape. + */ + getCurrentShape(): ShapeType { + return this.#currentShape; + } + + /** + * Gets this object's coordinates. + * This will return an array of coordinates representing the positions of the cells used by this object. + * + * @param isAbsolute Should we take into account the current position of the object? + * @return {Array} This object cells coordinates + */ + getCellsCoordinates(isAbsolute: boolean): Array { + const coordinates = []; + for (let row = 0; row < this.#currentShape.length; row += 1) { + for (let col = 0; col < this.#currentShape[row].length; col += 1) { + if (this.#currentShape[row][col] === 1) { + if (isAbsolute) { + coordinates.push({ + x: this.position.x + col, + y: this.position.y + row, + }); + } else coordinates.push({x: col, y: row}); } - return coordinates; + } } + return coordinates; + } - /** - * Rotate this object - * - * @param isForward Should we rotate clockwise? - */ - rotate(isForward: boolean) { - if (isForward) - this.#rotation++; - else - this.#rotation--; - if (this.#rotation > 3) - this.#rotation = 0; - else if (this.#rotation < 0) - this.#rotation = 3; - this.#currentShape = this.getShapes()[this.#rotation]; - } - - /** - * Move this object - * - * @param x Position X offset to add - * @param y Position Y offset to add - */ - move(x: number, y: number) { - this.position.x += x; - this.position.y += y; - } + /** + * Rotate this object + * + * @param isForward Should we rotate clockwise? + */ + rotate(isForward: boolean) { + if (isForward) this.#rotation += 1; + else this.#rotation -= 1; + if (this.#rotation > 3) this.#rotation = 0; + else if (this.#rotation < 0) this.#rotation = 3; + this.#currentShape = this.getShapes()[this.#rotation]; + } + /** + * Move this object + * + * @param x Position X offset to add + * @param y Position Y offset to add + */ + move(x: number, y: number) { + this.position.x += x; + this.position.y += y; + } } diff --git a/src/screens/Game/Shapes/ShapeI.js b/src/screens/Game/Shapes/ShapeI.js index 5c1141c..450a8a6 100644 --- a/src/screens/Game/Shapes/ShapeI.js +++ b/src/screens/Game/Shapes/ShapeI.js @@ -1,45 +1,46 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeI extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisI; + } - getColor(): string { - return this.theme.colors.tetrisI; - } - - getShapes() { - return [ - [ - [0, 0, 0, 0], - [1, 1, 1, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - ], - [ - [0, 0, 1, 0], - [0, 0, 1, 0], - [0, 0, 1, 0], - [0, 0, 1, 0], - ], - [ - [0, 0, 0, 0], - [0, 0, 0, 0], - [1, 1, 1, 1], - [0, 0, 0, 0], - ], - [ - [0, 1, 0, 0], - [0, 1, 0, 0], - [0, 1, 0, 0], - [0, 1, 0, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + [ + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + ], + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + ], + [ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeJ.js b/src/screens/Game/Shapes/ShapeJ.js index 8262088..37e91ba 100644 --- a/src/screens/Game/Shapes/ShapeJ.js +++ b/src/screens/Game/Shapes/ShapeJ.js @@ -1,41 +1,42 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeJ extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisJ; + } - getColor(): string { - return this.theme.colors.tetrisJ; - } - - getShapes() { - return [ - [ - [1, 0, 0], - [1, 1, 1], - [0, 0, 0], - ], - [ - [0, 1, 1], - [0, 1, 0], - [0, 1, 0], - ], - [ - [0, 0, 0], - [1, 1, 1], - [0, 0, 1], - ], - [ - [0, 1, 0], - [0, 1, 0], - [1, 1, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [1, 0, 0], + [1, 1, 1], + [0, 0, 0], + ], + [ + [0, 1, 1], + [0, 1, 0], + [0, 1, 0], + ], + [ + [0, 0, 0], + [1, 1, 1], + [0, 0, 1], + ], + [ + [0, 1, 0], + [0, 1, 0], + [1, 1, 0], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeL.js b/src/screens/Game/Shapes/ShapeL.js index 0b65510..a153c74 100644 --- a/src/screens/Game/Shapes/ShapeL.js +++ b/src/screens/Game/Shapes/ShapeL.js @@ -1,41 +1,42 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeL extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisL; + } - getColor(): string { - return this.theme.colors.tetrisL; - } - - getShapes() { - return [ - [ - [0, 0, 1], - [1, 1, 1], - [0, 0, 0], - ], - [ - [0, 1, 0], - [0, 1, 0], - [0, 1, 1], - ], - [ - [0, 0, 0], - [1, 1, 1], - [1, 0, 0], - ], - [ - [1, 1, 0], - [0, 1, 0], - [0, 1, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [0, 0, 1], + [1, 1, 1], + [0, 0, 0], + ], + [ + [0, 1, 0], + [0, 1, 0], + [0, 1, 1], + ], + [ + [0, 0, 0], + [1, 1, 1], + [1, 0, 0], + ], + [ + [1, 1, 0], + [0, 1, 0], + [0, 1, 0], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeO.js b/src/screens/Game/Shapes/ShapeO.js index 9fec75f..ccec9bb 100644 --- a/src/screens/Game/Shapes/ShapeO.js +++ b/src/screens/Game/Shapes/ShapeO.js @@ -1,37 +1,38 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeO extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 4; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 4; - } + getColor(): string { + return this.theme.colors.tetrisO; + } - getColor(): string { - return this.theme.colors.tetrisO; - } - - getShapes() { - return [ - [ - [1, 1], - [1, 1], - ], - [ - [1, 1], - [1, 1], - ], - [ - [1, 1], - [1, 1], - ], - [ - [1, 1], - [1, 1], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeS.js b/src/screens/Game/Shapes/ShapeS.js index f62c30e..c6061b4 100644 --- a/src/screens/Game/Shapes/ShapeS.js +++ b/src/screens/Game/Shapes/ShapeS.js @@ -1,41 +1,42 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeS extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisS; + } - getColor(): string { - return this.theme.colors.tetrisS; - } - - getShapes() { - return [ - [ - [0, 1, 1], - [1, 1, 0], - [0, 0, 0], - ], - [ - [0, 1, 0], - [0, 1, 1], - [0, 0, 1], - ], - [ - [0, 0, 0], - [0, 1, 1], - [1, 1, 0], - ], - [ - [1, 0, 0], - [1, 1, 0], - [0, 1, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [0, 1, 1], + [1, 1, 0], + [0, 0, 0], + ], + [ + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + ], + [ + [0, 0, 0], + [0, 1, 1], + [1, 1, 0], + ], + [ + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeT.js b/src/screens/Game/Shapes/ShapeT.js index dd01b52..f2567ac 100644 --- a/src/screens/Game/Shapes/ShapeT.js +++ b/src/screens/Game/Shapes/ShapeT.js @@ -1,41 +1,42 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeT extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisT; + } - getColor(): string { - return this.theme.colors.tetrisT; - } - - getShapes() { - return [ - [ - [0, 1, 0], - [1, 1, 1], - [0, 0, 0], - ], - [ - [0, 1, 0], - [0, 1, 1], - [0, 1, 0], - ], - [ - [0, 0, 0], - [1, 1, 1], - [0, 1, 0], - ], - [ - [0, 1, 0], - [1, 1, 0], - [0, 1, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [0, 1, 0], + [1, 1, 1], + [0, 0, 0], + ], + [ + [0, 1, 0], + [0, 1, 1], + [0, 1, 0], + ], + [ + [0, 0, 0], + [1, 1, 1], + [0, 1, 0], + ], + [ + [0, 1, 0], + [1, 1, 0], + [0, 1, 0], + ], + ]; + } } diff --git a/src/screens/Game/Shapes/ShapeZ.js b/src/screens/Game/Shapes/ShapeZ.js index f7b8aaa..df45ac2 100644 --- a/src/screens/Game/Shapes/ShapeZ.js +++ b/src/screens/Game/Shapes/ShapeZ.js @@ -1,41 +1,42 @@ // @flow -import BaseShape from "./BaseShape"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import BaseShape from './BaseShape'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {ShapeType} from './BaseShape'; export default class ShapeZ extends BaseShape { + constructor(theme: CustomThemeType) { + super(theme); + this.position.x = 3; + } - constructor(theme: CustomTheme) { - super(theme); - this.position.x = 3; - } + getColor(): string { + return this.theme.colors.tetrisZ; + } - getColor(): string { - return this.theme.colors.tetrisZ; - } - - getShapes() { - return [ - [ - [1, 1, 0], - [0, 1, 1], - [0, 0, 0], - ], - [ - [0, 0, 1], - [0, 1, 1], - [0, 1, 0], - ], - [ - [0, 0, 0], - [1, 1, 0], - [0, 1, 1], - ], - [ - [0, 1, 0], - [1, 1, 0], - [1, 0, 0], - ], - ]; - } + // eslint-disable-next-line class-methods-use-this + getShapes(): Array { + return [ + [ + [1, 1, 0], + [0, 1, 1], + [0, 0, 0], + ], + [ + [0, 0, 1], + [0, 1, 1], + [0, 1, 0], + ], + [ + [0, 0, 0], + [1, 1, 0], + [0, 1, 1], + ], + [ + [0, 1, 0], + [1, 1, 0], + [1, 0, 0], + ], + ]; + } } diff --git a/src/screens/Game/components/CellComponent.js b/src/screens/Game/components/CellComponent.js index 015d0b1..e4aec0a 100644 --- a/src/screens/Game/components/CellComponent.js +++ b/src/screens/Game/components/CellComponent.js @@ -3,34 +3,30 @@ import * as React from 'react'; import {View} from 'react-native'; import {withTheme} from 'react-native-paper'; -import type {CustomTheme} from "../../../managers/ThemeManager"; -export type Cell = {color: string, isEmpty: boolean, key: string}; - -type Props = { - cell: Cell, - theme: CustomTheme, -} - -class CellComponent extends React.PureComponent { - - render() { - const item = this.props.cell; - return ( - - ); - } +export type CellType = {color: string, isEmpty: boolean, key: string}; +type PropsType = { + cell: CellType, +}; +class CellComponent extends React.PureComponent { + render(): React.Node { + const {props} = this; + const item = props.cell; + return ( + + ); + } } export default withTheme(CellComponent); diff --git a/src/screens/Game/components/GridComponent.js b/src/screens/Game/components/GridComponent.js index 9d3a271..16cf02b 100644 --- a/src/screens/Game/components/GridComponent.js +++ b/src/screens/Game/components/GridComponent.js @@ -3,56 +3,55 @@ import * as React from 'react'; import {View} from 'react-native'; import {withTheme} from 'react-native-paper'; -import type {Cell} from "./CellComponent"; -import CellComponent from "./CellComponent"; -import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet"; +import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type {CellType} from './CellComponent'; +import CellComponent from './CellComponent'; -export type Grid = Array>; +export type GridType = Array>; -type Props = { - grid: Array>, - height: number, - width: number, - style: ViewStyle, -} +type PropsType = { + grid: Array>, + height: number, + width: number, + style: ViewStyle, +}; -class GridComponent extends React.Component { +class GridComponent extends React.Component { + getRow(rowNumber: number): React.Node { + const {grid} = this.props; + return ( + + {grid[rowNumber].map(this.getCellRender)} + + ); + } - getRow(rowNumber: number) { - let cells = this.props.grid[rowNumber].map(this.getCellRender); - return ( - - {cells} - - ); + getCellRender = (item: CellType): React.Node => { + return ; + }; + + getGrid(): React.Node { + const {height} = this.props; + const rows = []; + for (let i = 0; i < height; i += 1) { + rows.push(this.getRow(i)); } + return rows; + } - getCellRender = (item: Cell) => { - return ; - }; - - getGrid() { - let rows = []; - for (let i = 0; i < this.props.height; i++) { - rows.push(this.getRow(i)); - } - return rows; - } - - render() { - return ( - - {this.getGrid()} - - ); - } + render(): React.Node { + const {style, width, height} = this.props; + return ( + + {this.getGrid()} + + ); + } } export default withTheme(GridComponent); diff --git a/src/screens/Game/components/Preview.js b/src/screens/Game/components/Preview.js index cf5559e..076787d 100644 --- a/src/screens/Game/components/Preview.js +++ b/src/screens/Game/components/Preview.js @@ -3,51 +3,48 @@ import * as React from 'react'; import {View} from 'react-native'; import {withTheme} from 'react-native-paper'; -import type {Grid} from "./GridComponent"; -import GridComponent from "./GridComponent"; -import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet"; +import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type {GridType} from './GridComponent'; +import GridComponent from './GridComponent'; -type Props = { - items: Array, - style: ViewStyle -} +type PropsType = { + items: Array, + style: ViewStyle, +}; -class Preview extends React.PureComponent { +class Preview extends React.PureComponent { + getGrids(): React.Node { + const {items} = this.props; + const grids = []; + items.forEach((item: GridType, index: number) => { + grids.push(Preview.getGridRender(item, index)); + }); + return grids; + } - getGrids() { - let grids = []; - for (let i = 0; i < this.props.items.length; i++) { - grids.push(this.getGridRender(this.props.items[i], i)); - } - return grids; + static getGridRender(item: GridType, index: number): React.Node { + return ( + + ); + } + + render(): React.Node { + const {style, items} = this.props; + if (items.length > 0) { + return {this.getGrids()}; } - - getGridRender(item: Grid, index: number) { - return ; - }; - - render() { - if (this.props.items.length > 0) { - return ( - - {this.getGrids()} - - ); - } else - return null; - } - - + return null; + } } export default withTheme(Preview); diff --git a/src/screens/Game/logic/GameLogic.js b/src/screens/Game/logic/GameLogic.js index 8c2292d..3b81af1 100644 --- a/src/screens/Game/logic/GameLogic.js +++ b/src/screens/Game/logic/GameLogic.js @@ -1,243 +1,318 @@ // @flow -import Piece from "./Piece"; -import ScoreManager from "./ScoreManager"; -import GridManager from "./GridManager"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import Piece from './Piece'; +import ScoreManager from './ScoreManager'; +import GridManager from './GridManager'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {GridType} from '../components/GridComponent'; + +export type TickCallbackType = ( + score: number, + level: number, + grid: GridType, +) => void; + +export type ClockCallbackType = (time: number) => void; + +export type EndCallbackType = ( + time: number, + score: number, + isRestart: boolean, +) => void; + +export type MovementCallbackType = (grid: GridType, score?: number) => void; export default class GameLogic { + static levelTicks = [1000, 800, 600, 400, 300, 200, 150, 100]; - static levelTicks = [ - 1000, - 800, - 600, - 400, - 300, - 200, - 150, - 100, - ]; + scoreManager: ScoreManager; - #scoreManager: ScoreManager; - #gridManager: GridManager; + gridManager: GridManager; - #height: number; - #width: number; + height: number; - #gameRunning: boolean; - #gamePaused: boolean; - #gameTime: number; + width: number; - #currentObject: Piece; + gameRunning: boolean; - #gameTick: number; - #gameTickInterval: IntervalID; - #gameTimeInterval: IntervalID; + gamePaused: boolean; - #pressInInterval: TimeoutID; - #isPressedIn: boolean; - #autoRepeatActivationDelay: number; - #autoRepeatDelay: number; + gameTime: number; - #nextPieces: Array; - #nextPiecesCount: number; + currentObject: Piece; - #onTick: Function; - #onClock: Function; - endCallback: Function; + gameTick: number; - #theme: CustomTheme; + gameTickInterval: IntervalID; - constructor(height: number, width: number, theme: CustomTheme) { - this.#height = height; - this.#width = width; - this.#gameRunning = false; - this.#gamePaused = false; - this.#theme = theme; - this.#autoRepeatActivationDelay = 300; - this.#autoRepeatDelay = 50; - this.#nextPieces = []; - this.#nextPiecesCount = 3; - this.#scoreManager = new ScoreManager(); - this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme); - } + gameTimeInterval: IntervalID; - getHeight(): number { - return this.#height; - } + pressInInterval: TimeoutID; - getWidth(): number { - return this.#width; - } + isPressedIn: boolean; - getCurrentGrid() { - return this.#gridManager.getCurrentGrid(); - } + autoRepeatActivationDelay: number; - isGameRunning(): boolean { - return this.#gameRunning; - } + autoRepeatDelay: number; - isGamePaused(): boolean { - return this.#gamePaused; - } + nextPieces: Array; - onFreeze() { - this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager); - this.createTetromino(); - } + nextPiecesCount: number; - setNewGameTick(level: number) { - if (level >= GameLogic.levelTicks.length) - return; - this.#gameTick = GameLogic.levelTicks[level]; - clearInterval(this.#gameTickInterval); - this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); - } + tickCallback: TickCallbackType; - onTick(callback: Function) { - this.#currentObject.tryMove(0, 1, - this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), - () => this.onFreeze()); + clockCallback: ClockCallbackType; + + endCallback: EndCallbackType; + + theme: CustomThemeType; + + constructor(height: number, width: number, theme: CustomThemeType) { + this.height = height; + this.width = width; + this.gameRunning = false; + this.gamePaused = false; + this.theme = theme; + this.autoRepeatActivationDelay = 300; + this.autoRepeatDelay = 50; + this.nextPieces = []; + this.nextPiecesCount = 3; + this.scoreManager = new ScoreManager(); + this.gridManager = new GridManager( + this.getWidth(), + this.getHeight(), + this.theme, + ); + } + + getHeight(): number { + return this.height; + } + + getWidth(): number { + return this.width; + } + + getCurrentGrid(): GridType { + return this.gridManager.getCurrentGrid(); + } + + isGamePaused(): boolean { + return this.gamePaused; + } + + onFreeze = () => { + this.gridManager.freezeTetromino(this.currentObject, this.scoreManager); + this.createTetromino(); + }; + + setNewGameTick(level: number) { + if (level >= GameLogic.levelTicks.length) return; + this.gameTick = GameLogic.levelTicks[level]; + this.stopTick(); + this.startTick(); + } + + startClock() { + this.gameTimeInterval = setInterval(() => { + this.onClock(this.clockCallback); + }, 1000); + } + + startTick() { + this.gameTickInterval = setInterval(() => { + this.onTick(this.tickCallback); + }, this.gameTick); + } + + stopClock() { + clearInterval(this.gameTimeInterval); + } + + stopTick() { + clearInterval(this.gameTickInterval); + } + + stopGameTime() { + this.stopClock(); + this.stopTick(); + } + + startGameTime() { + this.startClock(); + this.startTick(); + } + + onTick(callback: TickCallbackType) { + this.currentObject.tryMove( + 0, + 1, + this.gridManager.getCurrentGrid(), + this.getWidth(), + this.getHeight(), + this.onFreeze, + ); + callback( + this.scoreManager.getScore(), + this.scoreManager.getLevel(), + this.gridManager.getCurrentGrid(), + ); + if (this.scoreManager.canLevelUp()) + this.setNewGameTick(this.scoreManager.getLevel()); + } + + onClock(callback: ClockCallbackType) { + this.gameTime += 1; + callback(this.gameTime); + } + + canUseInput(): boolean { + return this.gameRunning && !this.gamePaused; + } + + rightPressed(callback: MovementCallbackType) { + this.isPressedIn = true; + this.movePressedRepeat(true, callback, 1, 0); + } + + leftPressedIn(callback: MovementCallbackType) { + this.isPressedIn = true; + this.movePressedRepeat(true, callback, -1, 0); + } + + downPressedIn(callback: MovementCallbackType) { + this.isPressedIn = true; + this.movePressedRepeat(true, callback, 0, 1); + } + + movePressedRepeat( + isInitial: boolean, + callback: MovementCallbackType, + x: number, + y: number, + ) { + if (!this.canUseInput() || !this.isPressedIn) return; + const moved = this.currentObject.tryMove( + x, + y, + this.gridManager.getCurrentGrid(), + this.getWidth(), + this.getHeight(), + this.onFreeze, + ); + if (moved) { + if (y === 1) { + this.scoreManager.incrementScore(); callback( - this.#scoreManager.getScore(), - this.#scoreManager.getLevel(), - this.#gridManager.getCurrentGrid()); - if (this.#scoreManager.canLevelUp()) - this.setNewGameTick(this.#scoreManager.getLevel()); - } - - onClock(callback: Function) { - this.#gameTime++; - callback(this.#gameTime); - } - - canUseInput() { - return this.#gameRunning && !this.#gamePaused - } - - rightPressed(callback: Function) { - this.#isPressedIn = true; - this.movePressedRepeat(true, callback, 1, 0); - } - - leftPressedIn(callback: Function) { - this.#isPressedIn = true; - this.movePressedRepeat(true, callback, -1, 0); - } - - downPressedIn(callback: Function) { - this.#isPressedIn = true; - this.movePressedRepeat(true, callback, 0, 1); - } - - movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) { - if (!this.canUseInput() || !this.#isPressedIn) - return; - const moved = this.#currentObject.tryMove(x, y, - this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), - () => this.onFreeze()); - if (moved) { - if (y === 1) { - this.#scoreManager.incrementScore(); - callback(this.#gridManager.getCurrentGrid(), this.#scoreManager.getScore()); - } else - callback(this.#gridManager.getCurrentGrid()); - } - this.#pressInInterval = setTimeout(() => - this.movePressedRepeat(false, callback, x, y), - isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay + this.gridManager.getCurrentGrid(), + this.scoreManager.getScore(), ); + } else callback(this.gridManager.getCurrentGrid()); } + this.pressInInterval = setTimeout( + () => { + this.movePressedRepeat(false, callback, x, y); + }, + isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay, + ); + } - pressedOut() { - this.#isPressedIn = false; - clearTimeout(this.#pressInInterval); + pressedOut() { + this.isPressedIn = false; + clearTimeout(this.pressInInterval); + } + + rotatePressed(callback: MovementCallbackType) { + if (!this.canUseInput()) return; + + if ( + this.currentObject.tryRotate( + this.gridManager.getCurrentGrid(), + this.getWidth(), + this.getHeight(), + ) + ) + callback(this.gridManager.getCurrentGrid()); + } + + getNextPiecesPreviews(): Array { + const finalArray = []; + for (let i = 0; i < this.nextPieces.length; i += 1) { + const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0] + .length; + finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize)); + this.nextPieces[i].toGrid(finalArray[i], true); } + return finalArray; + } - rotatePressed(callback: Function) { - if (!this.canUseInput()) - return; + recoverNextPiece() { + this.currentObject = this.nextPieces.shift(); + this.generateNextPieces(); + } - if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) - callback(this.#gridManager.getCurrentGrid()); + generateNextPieces() { + while (this.nextPieces.length < this.nextPiecesCount) { + this.nextPieces.push(new Piece(this.theme)); } + } - getNextPiecesPreviews() { - let finalArray = []; - for (let i = 0; i < this.#nextPieces.length; i++) { - const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length; - finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize)); - this.#nextPieces[i].toGrid(finalArray[i], true); - } + createTetromino() { + this.pressedOut(); + this.recoverNextPiece(); + if ( + !this.currentObject.isPositionValid( + this.gridManager.getCurrentGrid(), + this.getWidth(), + this.getHeight(), + ) + ) + this.endGame(false); + } - return finalArray; - } + togglePause() { + if (!this.gameRunning) return; + this.gamePaused = !this.gamePaused; + if (this.gamePaused) this.stopGameTime(); + else this.startGameTime(); + } - recoverNextPiece() { - this.#currentObject = this.#nextPieces.shift(); - this.generateNextPieces(); - } + endGame(isRestart: boolean) { + this.gameRunning = false; + this.gamePaused = false; + this.stopGameTime(); + this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart); + } - generateNextPieces() { - while (this.#nextPieces.length < this.#nextPiecesCount) { - this.#nextPieces.push(new Piece(this.#theme)); - } - } - - createTetromino() { - this.pressedOut(); - this.recoverNextPiece(); - if (!this.#currentObject.isPositionValid(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) - this.endGame(false); - } - - togglePause() { - if (!this.#gameRunning) - return; - this.#gamePaused = !this.#gamePaused; - if (this.#gamePaused) { - clearInterval(this.#gameTickInterval); - clearInterval(this.#gameTimeInterval); - } else { - this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); - this.#gameTimeInterval = setInterval(this.#onClock, 1000); - } - } - - stopGame() { - this.#gameRunning = false; - this.#gamePaused = false; - clearInterval(this.#gameTickInterval); - clearInterval(this.#gameTimeInterval); - } - - endGame(isRestart: boolean) { - this.stopGame(); - this.endCallback(this.#gameTime, this.#scoreManager.getScore(), isRestart); - } - - startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) { - if (this.#gameRunning) - this.endGame(true); - this.#gameRunning = true; - this.#gamePaused = false; - this.#gameTime = 0; - this.#scoreManager = new ScoreManager(); - this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()]; - this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme); - this.#nextPieces = []; - this.generateNextPieces(); - this.createTetromino(); - tickCallback( - this.#scoreManager.getScore(), - this.#scoreManager.getLevel(), - this.#gridManager.getCurrentGrid()); - clockCallback(this.#gameTime); - this.#onTick = this.onTick.bind(this, tickCallback); - this.#onClock = this.onClock.bind(this, clockCallback); - this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); - this.#gameTimeInterval = setInterval(this.#onClock, 1000); - this.endCallback = endCallback; - } + startGame( + tickCallback: TickCallbackType, + clockCallback: ClockCallbackType, + endCallback: EndCallbackType, + ) { + if (this.gameRunning) this.endGame(true); + this.gameRunning = true; + this.gamePaused = false; + this.gameTime = 0; + this.scoreManager = new ScoreManager(); + this.gameTick = GameLogic.levelTicks[this.scoreManager.getLevel()]; + this.gridManager = new GridManager( + this.getWidth(), + this.getHeight(), + this.theme, + ); + this.nextPieces = []; + this.generateNextPieces(); + this.createTetromino(); + tickCallback( + this.scoreManager.getScore(), + this.scoreManager.getLevel(), + this.gridManager.getCurrentGrid(), + ); + clockCallback(this.gameTime); + this.startTick(); + this.startClock(); + this.tickCallback = tickCallback; + this.clockCallback = clockCallback; + this.endCallback = endCallback; + } } diff --git a/src/screens/Game/logic/GridManager.js b/src/screens/Game/logic/GridManager.js index 0ea1819..48358b7 100644 --- a/src/screens/Game/logic/GridManager.js +++ b/src/screens/Game/logic/GridManager.js @@ -1,120 +1,122 @@ // @flow -import Piece from "./Piece"; -import ScoreManager from "./ScoreManager"; -import type {Coordinates} from '../Shapes/BaseShape'; -import type {Grid} from "../components/GridComponent"; -import type {Cell} from "../components/CellComponent"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import Piece from './Piece'; +import ScoreManager from './ScoreManager'; +import type {CoordinatesType} from '../Shapes/BaseShape'; +import type {GridType} from '../components/GridComponent'; +import type {CellType} from '../components/CellComponent'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; /** * Class used to manage the game grid */ export default class GridManager { + #currentGrid: GridType; - #currentGrid: Grid; - #theme: CustomTheme; + #theme: CustomThemeType; - /** - * Initializes a grid of the given size - * - * @param width The grid width - * @param height The grid height - * @param theme Object containing current theme - */ - constructor(width: number, height: number, theme: CustomTheme) { - this.#theme = theme; - this.#currentGrid = this.getEmptyGrid(height, width); + /** + * Initializes a grid of the given size + * + * @param width The grid width + * @param height The grid height + * @param theme Object containing current theme + */ + constructor(width: number, height: number, theme: CustomThemeType) { + this.#theme = theme; + this.#currentGrid = this.getEmptyGrid(height, width); + } + + /** + * Get the current grid + * + * @return {GridType} The current grid + */ + getCurrentGrid(): GridType { + return this.#currentGrid; + } + + /** + * Get a new empty grid line of the given size + * + * @param width The line size + * @return {Array} + */ + getEmptyLine(width: number): Array { + const line = []; + for (let col = 0; col < width; col += 1) { + line.push({ + color: this.#theme.colors.tetrisBackground, + isEmpty: true, + key: col.toString(), + }); } + return line; + } - /** - * Get the current grid - * - * @return {Grid} The current grid - */ - getCurrentGrid(): Grid { - return this.#currentGrid; + /** + * Gets a new empty grid + * + * @param width The grid width + * @param height The grid height + * @return {GridType} A new empty grid + */ + getEmptyGrid(height: number, width: number): GridType { + const grid = []; + for (let row = 0; row < height; row += 1) { + grid.push(this.getEmptyLine(width)); } + return grid; + } - /** - * Get a new empty grid line of the given size - * - * @param width The line size - * @return {Array} - */ - getEmptyLine(width: number): Array { - let line = []; - for (let col = 0; col < width; col++) { - line.push({ - color: this.#theme.colors.tetrisBackground, - isEmpty: true, - key: col.toString(), - }); + /** + * Removes the given lines from the grid, + * shifts down every line on top and adds new empty lines on top. + * + * @param lines An array of line numbers to remove + * @param scoreManager A reference to the score manager + */ + clearLines(lines: Array, scoreManager: ScoreManager) { + lines.sort(); + for (let i = 0; i < lines.length; i += 1) { + this.#currentGrid.splice(lines[i], 1); + this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length)); + } + scoreManager.addLinesRemovedPoints(lines.length); + } + + /** + * Gets the lines to clear around the given piece's coordinates. + * The piece's coordinates are used for optimization and to prevent checking the whole grid. + * + * @param pos The piece's coordinates to check lines at + * @return {Array} An array containing the line numbers to clear + */ + getLinesToClear(pos: Array): Array { + const rows = []; + for (let i = 0; i < pos.length; i += 1) { + let isLineFull = true; + for (let col = 0; col < this.#currentGrid[pos[i].y].length; col += 1) { + if (this.#currentGrid[pos[i].y][col].isEmpty) { + isLineFull = false; + break; } - return line; + } + if (isLineFull && rows.indexOf(pos[i].y) === -1) rows.push(pos[i].y); } + return rows; + } - /** - * Gets a new empty grid - * - * @param width The grid width - * @param height The grid height - * @return {Grid} A new empty grid - */ - getEmptyGrid(height: number, width: number): Grid { - let grid = []; - for (let row = 0; row < height; row++) { - grid.push(this.getEmptyLine(width)); - } - return grid; - } - - /** - * Removes the given lines from the grid, - * shifts down every line on top and adds new empty lines on top. - * - * @param lines An array of line numbers to remove - * @param scoreManager A reference to the score manager - */ - clearLines(lines: Array, scoreManager: ScoreManager) { - lines.sort(); - for (let i = 0; i < lines.length; i++) { - this.#currentGrid.splice(lines[i], 1); - this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length)); - } - scoreManager.addLinesRemovedPoints(lines.length); - } - - /** - * Gets the lines to clear around the given piece's coordinates. - * The piece's coordinates are used for optimization and to prevent checking the whole grid. - * - * @param pos The piece's coordinates to check lines at - * @return {Array} An array containing the line numbers to clear - */ - getLinesToClear(pos: Array): Array { - let rows = []; - for (let i = 0; i < pos.length; i++) { - let isLineFull = true; - for (let col = 0; col < this.#currentGrid[pos[i].y].length; col++) { - if (this.#currentGrid[pos[i].y][col].isEmpty) { - isLineFull = false; - break; - } - } - if (isLineFull && rows.indexOf(pos[i].y) === -1) - rows.push(pos[i].y); - } - return rows; - } - - /** - * Freezes the given piece to the grid - * - * @param currentObject The piece to freeze - * @param scoreManager A reference to the score manager - */ - freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) { - this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), scoreManager); - } + /** + * Freezes the given piece to the grid + * + * @param currentObject The piece to freeze + * @param scoreManager A reference to the score manager + */ + freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) { + this.clearLines( + this.getLinesToClear(currentObject.getCoordinates()), + scoreManager, + ); + } } diff --git a/src/screens/Game/logic/ScoreManager.js b/src/screens/Game/logic/ScoreManager.js index e000202..4900197 100644 --- a/src/screens/Game/logic/ScoreManager.js +++ b/src/screens/Game/logic/ScoreManager.js @@ -4,98 +4,100 @@ * Class used to manage game score */ export default class ScoreManager { + #scoreLinesModifier = [40, 100, 300, 1200]; - #scoreLinesModifier = [40, 100, 300, 1200]; + #score: number; - #score: number; - #level: number; - #levelProgression: number; + #level: number; - /** - * Initializes score to 0 - */ - constructor() { - this.#score = 0; - this.#level = 0; - this.#levelProgression = 0; + #levelProgression: number; + + /** + * Initializes score to 0 + */ + constructor() { + this.#score = 0; + this.#level = 0; + this.#levelProgression = 0; + } + + /** + * Gets the current score + * + * @return {number} The current score + */ + getScore(): number { + return this.#score; + } + + /** + * Gets the current level + * + * @return {number} The current level + */ + getLevel(): number { + return this.#level; + } + + /** + * Gets the current level progression + * + * @return {number} The current level progression + */ + getLevelProgression(): number { + return this.#levelProgression; + } + + /** + * Increments the score by one + */ + incrementScore() { + this.#score += 1; + } + + /** + * Add score corresponding to the number of lines removed at the same time. + * Also updates the level progression. + * + * The more lines cleared at the same time, the more points and level progression the player gets. + * + * @param numberRemoved The number of lines removed at the same time + */ + addLinesRemovedPoints(numberRemoved: number) { + if (numberRemoved < 1 || numberRemoved > 4) return; + this.#score += + this.#scoreLinesModifier[numberRemoved - 1] * (this.#level + 1); + switch (numberRemoved) { + case 1: + this.#levelProgression += 1; + break; + case 2: + this.#levelProgression += 3; + break; + case 3: + this.#levelProgression += 5; + break; + case 4: // Did a tetris ! + this.#levelProgression += 8; + break; + default: + break; } + } - /** - * Gets the current score - * - * @return {number} The current score - */ - getScore(): number { - return this.#score; + /** + * Checks if the player can go to the next level. + * + * If he can, change the level. + * + * @return {boolean} True if the current level has changed + */ + canLevelUp(): boolean { + const canLevel = this.#levelProgression > this.#level * 5; + if (canLevel) { + this.#levelProgression -= this.#level * 5; + this.#level += 1; } - - /** - * Gets the current level - * - * @return {number} The current level - */ - getLevel(): number { - return this.#level; - } - - /** - * Gets the current level progression - * - * @return {number} The current level progression - */ - getLevelProgression(): number { - return this.#levelProgression; - } - - /** - * Increments the score by one - */ - incrementScore() { - this.#score++; - } - - /** - * Add score corresponding to the number of lines removed at the same time. - * Also updates the level progression. - * - * The more lines cleared at the same time, the more points and level progression the player gets. - * - * @param numberRemoved The number of lines removed at the same time - */ - addLinesRemovedPoints(numberRemoved: number) { - if (numberRemoved < 1 || numberRemoved > 4) - return; - this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1); - switch (numberRemoved) { - case 1: - this.#levelProgression += 1; - break; - case 2: - this.#levelProgression += 3; - break; - case 3: - this.#levelProgression += 5; - break; - case 4: // Did a tetris ! - this.#levelProgression += 8; - break; - } - } - - /** - * Checks if the player can go to the next level. - * - * If he can, change the level. - * - * @return {boolean} True if the current level has changed - */ - canLevelUp() { - let canLevel = this.#levelProgression > this.#level * 5; - if (canLevel){ - this.#levelProgression -= this.#level * 5; - this.#level++; - } - return canLevel; - } - + return canLevel; + } } diff --git a/src/screens/Game/screens/GameMainScreen.js b/src/screens/Game/screens/GameMainScreen.js index acdd4cd..c8146f6 100644 --- a/src/screens/Game/screens/GameMainScreen.js +++ b/src/screens/Game/screens/GameMainScreen.js @@ -3,400 +3,447 @@ import * as React from 'react'; import {View} from 'react-native'; import {Caption, IconButton, Text, withTheme} from 'react-native-paper'; -import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; -import GameLogic from "../logic/GameLogic"; -import type {Grid} from "../components/GridComponent"; -import GridComponent from "../components/GridComponent"; -import Preview from "../components/Preview"; -import i18n from "i18n-js"; -import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog"; -import OptionsDialog from "../../../components/Dialogs/OptionsDialog"; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import i18n from 'i18n-js'; +import {StackNavigationProp} from '@react-navigation/stack'; +import GameLogic from '../logic/GameLogic'; +import type {GridType} from '../components/GridComponent'; +import GridComponent from '../components/GridComponent'; +import Preview from '../components/Preview'; +import MaterialHeaderButtons, { + Item, +} from '../../../components/Overrides/CustomHeaderButton'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog'; +import OptionsDialog from '../../../components/Dialogs/OptionsDialog'; -type Props = { - navigation: StackNavigationProp, - route: { params: { highScore: number }, ... }, - theme: CustomTheme, -} +type PropsType = { + navigation: StackNavigationProp, + route: {params: {highScore: number}}, + theme: CustomThemeType, +}; -type State = { - grid: Grid, - gameRunning: boolean, - gameTime: number, - gameScore: number, - gameLevel: number, +type StateType = { + grid: GridType, + gameTime: number, + gameScore: number, + gameLevel: number, - dialogVisible: boolean, - dialogTitle: string, - dialogMessage: string, - dialogButtons: Array, - onDialogDismiss: () => void, -} + dialogVisible: boolean, + dialogTitle: string, + dialogMessage: string, + dialogButtons: Array, + onDialogDismiss: () => void, +}; -class GameMainScreen extends React.Component { +class GameMainScreen extends React.Component { + static getFormattedTime(seconds: number): string { + const date = new Date(); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(seconds); + let format; + if (date.getHours()) + format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; + else if (date.getMinutes()) + format = `${date.getMinutes()}:${date.getSeconds()}`; + else format = date.getSeconds().toString(); + return format; + } - logic: GameLogic; - highScore: number | null; + logic: GameLogic; - constructor(props) { - super(props); - this.logic = new GameLogic(20, 10, this.props.theme); - this.state = { - grid: this.logic.getCurrentGrid(), - gameRunning: false, - gameTime: 0, - gameScore: 0, - gameLevel: 0, - dialogVisible: false, - dialogTitle: "", - dialogMessage: "", - dialogButtons: [], - onDialogDismiss: () => { - }, - }; - if (this.props.route.params != null) - this.highScore = this.props.route.params.highScore; - } + highScore: number | null; - componentDidMount() { - this.props.navigation.setOptions({ - headerRight: this.getRightButton, - }); - this.startGame(); - } + constructor(props: PropsType) { + super(props); + this.logic = new GameLogic(20, 10, props.theme); + this.state = { + grid: this.logic.getCurrentGrid(), + gameTime: 0, + gameScore: 0, + gameLevel: 0, + dialogVisible: false, + dialogTitle: '', + dialogMessage: '', + dialogButtons: [], + onDialogDismiss: () => {}, + }; + if (props.route.params != null) + this.highScore = props.route.params.highScore; + } - componentWillUnmount() { - this.logic.stopGame(); - } + componentDidMount() { + const {navigation} = this.props; + navigation.setOptions({ + headerRight: this.getRightButton, + }); + this.startGame(); + } - getRightButton = () => { - return - - ; - } + componentWillUnmount() { + this.logic.endGame(false); + } - getFormattedTime(seconds: number) { - let date = new Date(); - date.setHours(0); - date.setMinutes(0); - date.setSeconds(seconds); - let format; - if (date.getHours()) - format = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds(); - else if (date.getMinutes()) - format = date.getMinutes() + ':' + date.getSeconds(); - else - format = date.getSeconds(); - return format; - } + getRightButton = (): React.Node => { + return ( + + + + ); + }; - onTick = (score: number, level: number, newGrid: Grid) => { - this.setState({ - gameScore: score, - gameLevel: level, - grid: newGrid, - }); - } + onTick = (score: number, level: number, newGrid: GridType) => { + this.setState({ + gameScore: score, + gameLevel: level, + grid: newGrid, + }); + }; - onClock = (time: number) => { - this.setState({ - gameTime: time, - }); - } + onClock = (time: number) => { + this.setState({ + gameTime: time, + }); + }; - updateGrid = (newGrid: Grid) => { - this.setState({ - grid: newGrid, - }); - } + onDialogDismiss = () => { + this.setState({dialogVisible: false}); + }; - updateGridScore = (newGrid: Grid, score: number) => { - this.setState({ - grid: newGrid, - gameScore: score, - }); - } + onGameEnd = (time: number, score: number, isRestart: boolean) => { + const {props, state} = this; + this.setState({ + gameTime: time, + gameScore: score, + }); + if (!isRestart) + props.navigation.replace('game-start', { + score: state.gameScore, + level: state.gameLevel, + time: state.gameTime, + }); + }; - togglePause = () => { - this.logic.togglePause(); - if (this.logic.isGamePaused()) - this.showPausePopup(); - } + getStatusIcons(): React.Node { + const {props, state} = this; + return ( + + + + {i18n.t('screens.game.time')} + + + + + {GameMainScreen.getFormattedTime(state.gameTime)} + + + + + + {i18n.t('screens.game.level')} + + + + + {state.gameLevel} + + + + + ); + } - onDialogDismiss = () => this.setState({dialogVisible: false}); + getScoreIcon(): React.Node { + const {props, state} = this; + const highScore = + this.highScore == null || state.gameScore > this.highScore + ? state.gameScore + : this.highScore; + return ( + + + + {i18n.t('screens.game.score', {score: state.gameScore})} + + + + + + {i18n.t('screens.game.highScore', {score: highScore})} + + + + + ); + } - showPausePopup = () => { - const onDismiss = () => { - this.togglePause(); + getControlButtons(): React.Node { + const {props} = this; + return ( + + { + this.logic.rotatePressed(this.updateGrid); + }} + style={{flex: 1}} + /> + + { + this.logic.pressedOut(); + }} + onPressIn={() => { + this.logic.leftPressedIn(this.updateGrid); + }} + /> + { + this.logic.pressedOut(); + }} + onPressIn={() => { + this.logic.rightPressed(this.updateGrid); + }} + /> + + { + this.logic.downPressedIn(this.updateGridScore); + }} + onPress={() => { + this.logic.pressedOut(); + }} + style={{flex: 1}} + color={props.theme.colors.tetrisScore} + /> + + ); + } + + updateGrid = (newGrid: GridType) => { + this.setState({ + grid: newGrid, + }); + }; + + updateGridScore = (newGrid: GridType, score?: number) => { + this.setState((prevState: StateType): { + grid: GridType, + gameScore: number, + } => ({ + grid: newGrid, + gameScore: score != null ? score : prevState.gameScore, + })); + }; + + togglePause = () => { + this.logic.togglePause(); + if (this.logic.isGamePaused()) this.showPausePopup(); + }; + + showPausePopup = () => { + const onDismiss = () => { + this.togglePause(); + this.onDialogDismiss(); + }; + this.setState({ + dialogVisible: true, + dialogTitle: i18n.t('screens.game.pause'), + dialogMessage: i18n.t('screens.game.pauseMessage'), + dialogButtons: [ + { + title: i18n.t('screens.game.restart.text'), + onPress: this.showRestartConfirm, + }, + { + title: i18n.t('screens.game.resume'), + onPress: onDismiss, + }, + ], + onDialogDismiss: onDismiss, + }); + }; + + showRestartConfirm = () => { + this.setState({ + dialogVisible: true, + dialogTitle: i18n.t('screens.game.restart.confirm'), + dialogMessage: i18n.t('screens.game.restart.confirmMessage'), + dialogButtons: [ + { + title: i18n.t('screens.game.restart.confirmYes'), + onPress: () => { this.onDialogDismiss(); - }; - this.setState({ - dialogVisible: true, - dialogTitle: i18n.t("screens.game.pause"), - dialogMessage: i18n.t("screens.game.pauseMessage"), - dialogButtons: [ - { - title: i18n.t("screens.game.restart.text"), - onPress: this.showRestartConfirm - }, - { - title: i18n.t("screens.game.resume"), - onPress: onDismiss - } - ], - onDialogDismiss: onDismiss, - }); - } + this.startGame(); + }, + }, + { + title: i18n.t('screens.game.restart.confirmNo'), + onPress: this.showPausePopup, + }, + ], + onDialogDismiss: this.showPausePopup, + }); + }; - showRestartConfirm = () => { - this.setState({ - dialogVisible: true, - dialogTitle: i18n.t("screens.game.restart.confirm"), - dialogMessage: i18n.t("screens.game.restart.confirmMessage"), - dialogButtons: [ - { - title: i18n.t("screens.game.restart.confirmYes"), - onPress: () => { - this.onDialogDismiss(); - this.startGame(); - } - }, - { - title: i18n.t("screens.game.restart.confirmNo"), - onPress: this.showPausePopup - } - ], - onDialogDismiss: this.showPausePopup, - }); - } + startGame = () => { + this.logic.startGame(this.onTick, this.onClock, this.onGameEnd); + }; - startGame = () => { - this.logic.startGame(this.onTick, this.onClock, this.onGameEnd); - this.setState({ - gameRunning: true, - }); - } - - onGameEnd = (time: number, score: number, isRestart: boolean) => { - this.setState({ - gameTime: time, - gameScore: score, - gameRunning: false, - }); - if (!isRestart) - this.props.navigation.replace( - "game-start", - { - score: this.state.gameScore, - level: this.state.gameLevel, - time: this.state.gameTime, - } - ); - } - - getStatusIcons() { - return ( - + + {this.getStatusIcons()} + + {this.getScoreIcon()} + - - {i18n.t("screens.game.time")} - - - {this.getFormattedTime(this.state.gameTime)} - + marginLeft: 'auto', + marginRight: 'auto', + }} + /> + - - - {i18n.t("screens.game.level")} - - - {this.state.gameLevel} - - - - ); - } - - getScoreIcon() { - let highScore = this.highScore == null || this.state.gameScore > this.highScore - ? this.state.gameScore - : this.highScore; - return ( - + - - {i18n.t("screens.game.score", {score: this.state.gameScore})} - - - - {i18n.t("screens.game.highScore", {score: highScore})} - - - - - ); - } - - getControlButtons() { - return ( - - this.logic.rotatePressed(this.updateGrid)} - style={{flex: 1}} - /> - - this.logic.pressedOut()} - onPressIn={() => this.logic.leftPressedIn(this.updateGrid)} - - /> - this.logic.pressedOut()} - onPressIn={() => this.logic.rightPressed(this.updateGrid)} - /> - - this.logic.downPressedIn(this.updateGridScore)} - onPress={() => this.logic.pressedOut()} - style={{flex: 1}} - color={this.props.theme.colors.tetrisScore} - /> - - ); - } - - render() { - return ( - - - {this.getStatusIcons()} - - {this.getScoreIcon()} - - - - - - - - {this.getControlButtons()} - - - - ); - } + }} + /> + + + {this.getControlButtons()} + + + ); + } } export default withTheme(GameMainScreen); diff --git a/src/screens/Game/screens/GameStartScreen.js b/src/screens/Game/screens/GameStartScreen.js index c16f093..53f1b9d 100644 --- a/src/screens/Game/screens/GameStartScreen.js +++ b/src/screens/Game/screens/GameStartScreen.js @@ -1,427 +1,440 @@ // @flow -import * as React from "react"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper"; -import {View} from "react-native"; -import i18n from "i18n-js"; -import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; -import MascotPopup from "../../../components/Mascot/MascotPopup"; -import AsyncStorageManager from "../../../managers/AsyncStorageManager"; -import type {Grid} from "../components/GridComponent"; -import GridComponent from "../components/GridComponent"; -import GridManager from "../logic/GridManager"; -import Piece from "../logic/Piece"; -import * as Animatable from "react-native-animatable"; -import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; -import LinearGradient from "react-native-linear-gradient"; -import SpeechArrow from "../../../components/Mascot/SpeechArrow"; -import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; +import * as React from 'react'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + Button, + Card, + Divider, + Headline, + Paragraph, + Text, + withTheme, +} from 'react-native-paper'; +import {View} from 'react-native'; +import i18n from 'i18n-js'; +import * as Animatable from 'react-native-animatable'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import LinearGradient from 'react-native-linear-gradient'; +import type {CustomThemeType} from '../../../managers/ThemeManager'; +import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot'; +import MascotPopup from '../../../components/Mascot/MascotPopup'; +import AsyncStorageManager from '../../../managers/AsyncStorageManager'; +import type {GridType} from '../components/GridComponent'; +import GridComponent from '../components/GridComponent'; +import GridManager from '../logic/GridManager'; +import Piece from '../logic/Piece'; +import SpeechArrow from '../../../components/Mascot/SpeechArrow'; +import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; -type GameStats = { - score: number, - level: number, - time: number, -} +type GameStatsType = { + score: number, + level: number, + time: number, +}; -type Props = { - navigation: StackNavigationProp, - route: { - params: GameStats - }, - theme: CustomTheme, -} +type PropsType = { + navigation: StackNavigationProp, + route: { + params: GameStatsType, + }, + theme: CustomThemeType, +}; -class GameStartScreen extends React.Component { +class GameStartScreen extends React.Component { + gridManager: GridManager; - gridManager: GridManager; - scores: Array; + scores: Array; - gameStats: GameStats | null; - isHighScore: boolean; + gameStats: GameStatsType | null; - constructor(props: Props) { - super(props); - this.gridManager = new GridManager(4, 4, props.theme); - this.scores = AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.gameScores.key); - this.scores.sort((a, b) => b - a); - if (this.props.route.params != null) - this.recoverGameScore(); + isHighScore: boolean; + + constructor(props: PropsType) { + super(props); + this.gridManager = new GridManager(4, 4, props.theme); + this.scores = AsyncStorageManager.getObject( + AsyncStorageManager.PREFERENCES.gameScores.key, + ); + this.scores.sort((a: number, b: number): number => b - a); + if (props.route.params != null) this.recoverGameScore(); + } + + getPiecesBackground(): React.Node { + const {theme} = this.props; + const gridList = []; + for (let i = 0; i < 18; i += 1) { + gridList.push(this.gridManager.getEmptyGrid(4, 4)); + const piece = new Piece(theme); + piece.toGrid(gridList[i], true); } + return ( + + {gridList.map((item: GridType, index: number): React.Node => { + const size = 10 + Math.floor(Math.random() * 30); + const top = Math.floor(Math.random() * 100); + const rot = Math.floor(Math.random() * 360); + const left = (index % 6) * 20; + const animDelay = size * 20; + const animDuration = 2 * (2000 - size * 30); + return ( + + + + ); + })} + + ); + } - recoverGameScore() { - this.gameStats = this.props.route.params; - this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0]; - for (let i = 0; i < 3; i++) { - if (this.scores.length > i && this.gameStats.score > this.scores[i]) { - this.scores.splice(i, 0, this.gameStats.score); - break; - } else if (this.scores.length <= i) { - this.scores.push(this.gameStats.score); - break; - } - } - if (this.scores.length > 3) - this.scores.splice(3, 1); - AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameScores.key, this.scores); - } - - getPiecesBackground() { - let gridList = []; - for (let i = 0; i < 18; i++) { - gridList.push(this.gridManager.getEmptyGrid(4, 4)); - const piece = new Piece(this.props.theme); - piece.toGrid(gridList[i], true); - } - return ( - - {gridList.map((item: Grid, index: number) => { - const size = 10 + Math.floor(Math.random() * 30); - const top = Math.floor(Math.random() * 100); - const rot = Math.floor(Math.random() * 360); - const left = (index % 6) * 20; - const animDelay = size * 20; - const animDuration = 2 * (2000 - (size * 30)); - return ( - - - - ); - })} - - ); - } - - getPostGameContent(stats: GameStats) { - return ( - - - - + + + + + + {this.isHighScore + ? i18n.t('screens.game.newHighScore') + : i18n.t('screens.game.gameOver')} + + + + - - - {this.isHighScore - ? i18n.t("screens.game.newHighScore") - : i18n.t("screens.game.gameOver")} - - - - - {i18n.t("screens.game.score", {score: stats.score})} - - - - - {i18n.t("screens.game.level")} - - - {stats.level} - - - - {i18n.t("screens.game.time")} - - - {stats.time} - - - - + {i18n.t('screens.game.score', {score: stats.score})} + + - ) - } - - getWelcomeText() { - return ( - - - - - - - {i18n.t("screens.game.welcomeTitle")} - - - - {i18n.t("screens.game.welcomeMessage")} - - - + + {i18n.t('screens.game.level')} + + {stats.level} - ); - } + + {i18n.t('screens.game.time')} + + {stats.time} + + + + + ); + } - getPodiumRender(place: 1 | 2 | 3, score: string) { - let icon = "podium-gold"; - let color = this.props.theme.colors.gameGold; - let fontSize = 20; - let size = 70; - if (place === 2) { - icon = "podium-silver"; - color = this.props.theme.colors.gameSilver; - fontSize = 18; - size = 60; - } else if (place === 3) { - icon = "podium-bronze"; - color = this.props.theme.colors.gameBronze; - fontSize = 15; - size = 50; - } - return ( - + + + + + + {i18n.t('screens.game.welcomeTitle')} + + + + {i18n.t('screens.game.welcomeMessage')} + + + + + ); + } + + getPodiumRender(place: 1 | 2 | 3, score: string): React.Node { + const {props} = this; + let icon = 'podium-gold'; + let color = props.theme.colors.gameGold; + let fontSize = 20; + let size = 70; + if (place === 2) { + icon = 'podium-silver'; + color = props.theme.colors.gameSilver; + fontSize = 18; + size = 60; + } else if (place === 3) { + icon = 'podium-bronze'; + color = props.theme.colors.gameBronze; + fontSize = 15; + size = 50; + } + return ( + + {this.isHighScore && place === 1 ? ( + - { - this.isHighScore && place === 1 - ? - - - - - + + + + + ) : null} + + + {score} + + + ); + } - : null - } - - {score} - - ); + getTopScoresRender(): React.Node { + const gold = this.scores.length > 0 ? this.scores[0] : '-'; + const silver = this.scores.length > 1 ? this.scores[1] : '-'; + const bronze = this.scores.length > 2 ? this.scores[2] : '-'; + return ( + + {this.getPodiumRender(1, gold.toString())} + + {this.getPodiumRender(3, bronze.toString())} + {this.getPodiumRender(2, silver.toString())} + + + ); + } + + getMainContent(): React.Node { + const {props} = this; + return ( + + {this.gameStats != null + ? this.getPostGameContent(this.gameStats) + : this.getWelcomeText()} + + {this.getTopScoresRender()} + + ); + } + + keyExtractor = (item: number): string => item.toString(); + + recoverGameScore() { + const {route} = this.props; + this.gameStats = route.params; + this.isHighScore = + this.scores.length === 0 || this.gameStats.score > this.scores[0]; + for (let i = 0; i < 3; i += 1) { + if (this.scores.length > i && this.gameStats.score > this.scores[i]) { + this.scores.splice(i, 0, this.gameStats.score); + break; + } else if (this.scores.length <= i) { + this.scores.push(this.gameStats.score); + break; + } } + if (this.scores.length > 3) this.scores.splice(3, 1); + AsyncStorageManager.set( + AsyncStorageManager.PREFERENCES.gameScores.key, + this.scores, + ); + } - getTopScoresRender() { - const gold = this.scores.length > 0 - ? this.scores[0] - : "-"; - const silver = this.scores.length > 1 - ? this.scores[1] - : "-"; - const bronze = this.scores.length > 2 - ? this.scores[2] - : "-"; - return ( - - {this.getPodiumRender(1, gold.toString())} - - {this.getPodiumRender(3, bronze.toString())} - {this.getPodiumRender(2, silver.toString())} - - - ); - } - - getMainContent() { - return ( - - { - this.gameStats != null - ? this.getPostGameContent(this.gameStats) - : this.getWelcomeText() - } - - {this.getTopScoresRender()} - - ) - } - - keyExtractor = (item: number) => item.toString(); - - render() { - return ( - - {this.getPiecesBackground()} - - - {this.getMainContent()} - - - - - - ); - } + render(): React.Node { + const {props} = this; + return ( + + {this.getPiecesBackground()} + + + {this.getMainContent()} + + + + + ); + } } export default withTheme(GameStartScreen); -