Updated doc and used private class members

This commit is contained in:
Arnaud Vergnet 2020-03-28 12:08:08 +01:00
parent 931d7b0fe6
commit cded72137e
5 changed files with 313 additions and 119 deletions

View file

@ -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<Piece>;
nextPiecesCount: number;
#nextPieces: Array<Piece>;
#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;
}
}

View file

@ -2,24 +2,49 @@
import Piece from "./Piece";
import ScoreManager from "./ScoreManager";
import type {coordinates} from './Shapes/BaseShape';
export type grid = Array<Array<{color: string, isEmpty: boolean}>>;
export type cell = {color: string, isEmpty: boolean};
export type grid = Array<Array<cell>>;
/**
* 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<cell>}
*/
getEmptyLine(width: number): Array<cell> {
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<number>, 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<number>} An array containing the line numbers to clear
*/
getLinesToClear(coord: Array<coordinates>): Array<number> {
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);
}

View file

@ -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<coordinates> = 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<Array<Object>>, 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<coordinates> = 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<coordinates> = 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<coordinates>} An array of coordinates
*/
getCoordinates(): Array<coordinates> {
return this.#currentShape.getCellsCoordinates(true);
}
}

View file

@ -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){

View file

@ -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<Array<number>>;
#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<Array<Array<number>>> {
throw new Error("Method 'getShapes()' must be implemented");
}
getCurrentShape() {
/**
* Gets this object's current shape.
*
* Used by tests to read private fields
*/
getCurrentShape(): Array<Array<number>> {
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<coordinates>} This object cells coordinates
*/
getCellsCoordinates(isAbsolute: boolean): Array<coordinates> {
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;