diff --git a/screens/Tetris/GameLogic.js b/screens/Tetris/GameLogic.js index b3de919..98c1257 100644 --- a/screens/Tetris/GameLogic.js +++ b/screens/Tetris/GameLogic.js @@ -17,218 +17,221 @@ export default class GameLogic { 100, ]; - scoreManager: ScoreManager; - gridManager: GridManager; + #scoreManager: ScoreManager; + #gridManager: GridManager; - height: number; - width: number; + #height: number; + #width: number; - gameRunning: boolean; - gamePaused: boolean; - gameTime: number; + #gameRunning: boolean; + #gamePaused: boolean; + #gameTime: number; - currentObject: Piece; + #currentObject: Piece; - gameTick: number; - gameTickInterval: IntervalID; - gameTimeInterval: IntervalID; + #gameTick: number; + #gameTickInterval: IntervalID; + #gameTimeInterval: IntervalID; - pressInInterval: TimeoutID; - isPressedIn: boolean; - autoRepeatActivationDelay: number; - autoRepeatDelay: number; + #pressInInterval: TimeoutID; + #isPressedIn: boolean; + #autoRepeatActivationDelay: number; + #autoRepeatDelay: number; - nextPieces: Array; - nextPiecesCount: number; + #nextPieces: Array; + #nextPiecesCount: number; - onTick: Function; - onClock: Function; + #onTick: Function; + #onClock: Function; endCallback: Function; - colors: Object; + #colors: Object; constructor(height: number, width: number, colors: Object) { - this.height = height; - this.width = width; - this.gameRunning = false; - this.gamePaused = false; - this.colors = colors; - this.autoRepeatActivationDelay = 300; - this.autoRepeatDelay = 50; - this.nextPieces = []; - this.nextPiecesCount = 3; - this.scoreManager = new ScoreManager(); - this.gridManager = new GridManager(this.getWidth(), this.getHeight(), this.colors); + this.#height = height; + this.#width = width; + this.#gameRunning = false; + this.#gamePaused = false; + this.#colors = colors; + this.#autoRepeatActivationDelay = 300; + this.#autoRepeatDelay = 50; + this.#nextPieces = []; + this.#nextPiecesCount = 3; + this.#scoreManager = new ScoreManager(); + this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors); } getHeight(): number { - return this.height; + return this.#height; } getWidth(): number { - return this.width; + return this.#width; } getCurrentGrid() { - return this.gridManager.getCurrentGrid(); + return this.#gridManager.getCurrentGrid(); } isGameRunning(): boolean { - return this.gameRunning; + return this.#gameRunning; } isGamePaused(): boolean { - return this.gamePaused; + return this.#gamePaused; } onFreeze() { - this.gridManager.freezeTetromino(this.currentObject, this.scoreManager); + this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager); this.createTetromino(); } setNewGameTick(level: number) { if (level >= GameLogic.levelTicks.length) return; - this.gameTick = GameLogic.levelTicks[level]; - clearInterval(this.gameTickInterval); - this.gameTickInterval = setInterval(this.onTick, this.gameTick); + this.#gameTick = GameLogic.levelTicks[level]; + clearInterval(this.#gameTickInterval); + this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); } onTick(callback: Function) { - this.currentObject.tryMove(0, 1, - this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), + 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()); + 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); + this.#gameTime++; + callback(this.#gameTime); } canUseInput() { - return this.gameRunning && !this.gamePaused + return this.#gameRunning && !this.#gamePaused } rightPressed(callback: Function) { - this.isPressedIn = true; + this.#isPressedIn = true; this.movePressedRepeat(true, callback, 1, 0); } leftPressedIn(callback: Function) { - this.isPressedIn = true; + this.#isPressedIn = true; this.movePressedRepeat(true, callback, -1, 0); } downPressedIn(callback: Function) { - this.isPressedIn = true; + this.#isPressedIn = true; this.movePressedRepeat(true, callback, 0, 1); } movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) { - if (!this.canUseInput() || !this.isPressedIn) + if (!this.canUseInput() || !this.#isPressedIn) return; - const moved = this.currentObject.tryMove(x, y, - this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), + 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()); + this.#scoreManager.incrementScore(); + callback(this.#gridManager.getCurrentGrid(), this.#scoreManager.getScore()); } else - callback(this.gridManager.getCurrentGrid()); + callback(this.#gridManager.getCurrentGrid()); } - this.pressInInterval = setTimeout(() => this.movePressedRepeat(false, callback, x, y), isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay); + this.#pressInInterval = setTimeout(() => + this.movePressedRepeat(false, callback, x, y), + isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay + ); } pressedOut() { - this.isPressedIn = false; - clearTimeout(this.pressInInterval); + this.#isPressedIn = false; + clearTimeout(this.#pressInInterval); } rotatePressed(callback: Function) { if (!this.canUseInput()) return; - if (this.currentObject.tryRotate(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) - callback(this.gridManager.getCurrentGrid()); + if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) + callback(this.#gridManager.getCurrentGrid()); } getNextPiecesPreviews() { let finalArray = []; - for (let i = 0; i < this.nextPieces.length; i++) { - finalArray.push(this.gridManager.getEmptyGrid(4, 4)); - this.nextPieces[i].toGrid(finalArray[i], true); + for (let i = 0; i < this.#nextPieces.length; i++) { + finalArray.push(this.#gridManager.getEmptyGrid(4, 4)); + this.#nextPieces[i].toGrid(finalArray[i], true); } return finalArray; } recoverNextPiece() { - this.currentObject = this.nextPieces.shift(); + this.#currentObject = this.#nextPieces.shift(); this.generateNextPieces(); } generateNextPieces() { - while (this.nextPieces.length < this.nextPiecesCount) { - this.nextPieces.push(new Piece(this.colors)); + while (this.#nextPieces.length < this.#nextPiecesCount) { + this.#nextPieces.push(new Piece(this.#colors)); } } createTetromino() { this.pressedOut(); this.recoverNextPiece(); - if (!this.currentObject.isPositionValid(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) + if (!this.#currentObject.isPositionValid(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) this.endGame(false); } togglePause() { - if (!this.gameRunning) + if (!this.#gameRunning) return; - this.gamePaused = !this.gamePaused; - if (this.gamePaused) { - clearInterval(this.gameTickInterval); - clearInterval(this.gameTimeInterval); + 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); + this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); + this.#gameTimeInterval = setInterval(this.#onClock, 1000); } } endGame(isRestart: boolean) { - this.gameRunning = false; - this.gamePaused = false; - clearInterval(this.gameTickInterval); - clearInterval(this.gameTimeInterval); - this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart); + this.#gameRunning = false; + this.#gamePaused = false; + clearInterval(this.#gameTickInterval); + clearInterval(this.#gameTimeInterval); + this.endCallback(this.#gameTime, this.#scoreManager.getScore(), isRestart); } startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) { - if (this.gameRunning) + 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.colors); - this.nextPieces = []; + 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.#colors); + 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.#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; } } diff --git a/screens/Tetris/GridManager.js b/screens/Tetris/GridManager.js index ced9a07..4f0b0ca 100644 --- a/screens/Tetris/GridManager.js +++ b/screens/Tetris/GridManager.js @@ -2,24 +2,49 @@ import Piece from "./Piece"; import ScoreManager from "./ScoreManager"; +import type {coordinates} from './Shapes/BaseShape'; -export type grid = Array>; +export type cell = {color: string, isEmpty: boolean}; +export type grid = Array>; + +/** + * Class used to manage the game grid + * + */ export default class GridManager { #currentGrid: grid; #colors: Object; + /** + * Initializes a grid of the given size + * + * @param width The grid width + * @param height The grid height + * @param colors Object containing current theme colors + */ constructor(width: number, height: number, colors: Object) { this.#colors = colors; this.#currentGrid = this.getEmptyGrid(height, width); } - getCurrentGrid() { + /** + * Get the current grid + * + * @return {grid} The current grid + */ + getCurrentGrid(): grid { return this.#currentGrid; } - getEmptyLine(width: number) { + /** + * 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({ @@ -30,7 +55,14 @@ export default class GridManager { return line; } - getEmptyGrid(height: number, width: number) { + /** + * 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)); @@ -38,6 +70,13 @@ export default class GridManager { 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++) { @@ -47,7 +86,14 @@ export default class GridManager { scoreManager.addLinesRemovedPoints(lines.length); } - getLinesToClear(coord: Object) { + /** + * 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 coord The piece's coordinates to check lines at + * @return {Array} An array containing the line numbers to clear + */ + getLinesToClear(coord: Array): Array { let rows = []; for (let i = 0; i < coord.length; i++) { let isLineFull = true; @@ -63,6 +109,12 @@ export default class GridManager { 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); } diff --git a/screens/Tetris/Piece.js b/screens/Tetris/Piece.js index 86cbc68..88d83d9 100644 --- a/screens/Tetris/Piece.js +++ b/screens/Tetris/Piece.js @@ -5,7 +5,14 @@ import ShapeO from "./Shapes/ShapeO"; import ShapeS from "./Shapes/ShapeS"; import ShapeT from "./Shapes/ShapeT"; import ShapeZ from "./Shapes/ShapeZ"; +import type {coordinates} from './Shapes/BaseShape'; +import type {grid} from './GridManager'; +/** + * Class used as an abstraction layer for shapes. + * Use this class to manipulate pieces rather than Shapes directly + * + */ export default class Piece { #shapes = [ @@ -20,17 +27,32 @@ export default class Piece { #currentShape: Object; #colors: Object; + /** + * Initializes this piece's color and shape + * + * @param colors Object containing current theme colors + */ constructor(colors: Object) { this.#currentShape = this.getRandomShape(colors); this.#colors = colors; } + /** + * Gets a random shape object + * + * @param colors Object containing current theme colors + */ getRandomShape(colors: Object) { return new this.#shapes[Math.floor(Math.random() * 7)](colors); } - removeFromGrid(grid) { - const coord = this.#currentShape.getCellsCoordinates(true); + /** + * Removes the piece from the given grid + * + * @param grid The grid to remove the piece from + */ + removeFromGrid(grid: grid) { + const coord: Array = this.#currentShape.getCellsCoordinates(true); for (let i = 0; i < coord.length; i++) { grid[coord[i].y][coord[i].x] = { color: this.#colors.tetrisBackground, @@ -39,8 +61,14 @@ export default class Piece { } } - toGrid(grid: Array>, isPreview: boolean) { - const coord = this.#currentShape.getCellsCoordinates(!isPreview); + /** + * Adds this piece to the given grid + * + * @param grid The grid to add the piece to + * @param isPreview Should we use this piece's current position to determine the cells? + */ + toGrid(grid: grid, isPreview: boolean) { + const coord: Array = this.#currentShape.getCellsCoordinates(!isPreview); for (let i = 0; i < coord.length; i++) { grid[coord[i].y][coord[i].x] = { color: this.#currentShape.getColor(), @@ -49,9 +77,17 @@ export default class Piece { } } - isPositionValid(grid, width, height) { + /** + * Checks if the piece's current position is valid + * + * @param grid The current game grid + * @param width The grid's width + * @param height The grid's height + * @return {boolean} If the position is valid + */ + isPositionValid(grid: grid, width: number, height: number) { let isValid = true; - const coord = this.#currentShape.getCellsCoordinates(true); + const coord: Array = this.#currentShape.getCellsCoordinates(true); for (let i = 0; i < coord.length; i++) { if (coord[i].x >= width || coord[i].x < 0 @@ -65,7 +101,18 @@ export default class Piece { return isValid; } - tryMove(x: number, y: number, grid, width, height, freezeCallback: Function) { + /** + * Tries to move the piece by the given offset on the given grid + * + * @param x Position X offset + * @param y Position Y offset + * @param grid The grid to move the piece on + * @param width The grid's width + * @param height The grid's height + * @param freezeCallback Callback to use if the piece should freeze itself + * @return {boolean} True if the move was valid, false otherwise + */ + tryMove(x: number, y: number, grid: grid, width: number, height: number, freezeCallback: Function) { if (x > 1) x = 1; // Prevent moving from more than one tile if (x < -1) x = -1; if (y > 1) y = 1; @@ -75,19 +122,26 @@ export default class Piece { this.removeFromGrid(grid); this.#currentShape.move(x, y); let isValid = this.isPositionValid(grid, width, height); - let shouldFreeze = false; if (!isValid) this.#currentShape.move(-x, -y); - shouldFreeze = !isValid && y !== 0; + let shouldFreeze = !isValid && y !== 0; this.toGrid(grid, false); if (shouldFreeze) freezeCallback(); return isValid; } - tryRotate(grid, width, height) { + /** + * Tries to rotate the piece + * + * @param grid The grid to rotate the piece on + * @param width The grid's width + * @param height The grid's height + * @return {boolean} True if the rotation was valid, false otherwise + */ + tryRotate(grid: grid, width: number, height: number) { this.removeFromGrid(grid); this.#currentShape.rotate(true); if (!this.isPositionValid(grid, width, height)) { @@ -99,7 +153,12 @@ export default class Piece { return true; } - getCoordinates() { + /** + * Gets this piece used cells coordinates + * + * @return {Array} An array of coordinates + */ + getCoordinates(): Array { return this.#currentShape.getCellsCoordinates(true); } } diff --git a/screens/Tetris/ScoreManager.js b/screens/Tetris/ScoreManager.js index 263d27b..e000202 100644 --- a/screens/Tetris/ScoreManager.js +++ b/screens/Tetris/ScoreManager.js @@ -1,5 +1,8 @@ // @flow +/** + * Class used to manage game score + */ export default class ScoreManager { #scoreLinesModifier = [40, 100, 300, 1200]; @@ -8,31 +11,60 @@ export default class ScoreManager { #level: number; #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++; } + /** + * 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 0; + return; this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1); switch (numberRemoved) { case 1: @@ -50,6 +82,13 @@ export default class ScoreManager { } } + /** + * 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){ diff --git a/screens/Tetris/Shapes/BaseShape.js b/screens/Tetris/Shapes/BaseShape.js index 717ef8a..e1890ac 100644 --- a/screens/Tetris/Shapes/BaseShape.js +++ b/screens/Tetris/Shapes/BaseShape.js @@ -1,5 +1,10 @@ // @flow +export type coordinates = { + x: number, + y: number, +} + /** * Abstract class used to represent a BaseShape. * Abstract classes do not exist by default in Javascript: we force it by throwing errors in the constructor @@ -9,8 +14,11 @@ export default class BaseShape { #currentShape: Array>; #rotation: number; - position: Object; + position: coordinates; + /** + * Prevent instantiation if classname is BaseShape to force class to be abstract + */ constructor() { if (this.constructor === BaseShape) throw new Error("Abstract class can't be instantiated"); @@ -19,19 +27,41 @@ export default class BaseShape { this.#currentShape = this.getShapes()[this.#rotation]; } + /** + * Gets this shape's color. + * Must be implemented by child class + */ getColor(): string { throw new Error("Method 'getColor()' must be implemented"); } + /** + * 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"); } - getCurrentShape() { + /** + * Gets this object's current shape. + * + * Used by tests to read private fields + */ + getCurrentShape(): Array> { return this.#currentShape; } - getCellsCoordinates(isAbsolute: boolean) { + /** + * 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++) { @@ -45,6 +75,11 @@ export default class BaseShape { return coordinates; } + /** + * Rotate this object + * + * @param isForward Should we rotate clockwise? + */ rotate(isForward: boolean) { if (isForward) this.#rotation++; @@ -57,6 +92,12 @@ export default class BaseShape { 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;