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, 100,
]; ];
scoreManager: ScoreManager; #scoreManager: ScoreManager;
gridManager: GridManager; #gridManager: GridManager;
height: number; #height: number;
width: number; #width: number;
gameRunning: boolean; #gameRunning: boolean;
gamePaused: boolean; #gamePaused: boolean;
gameTime: number; #gameTime: number;
currentObject: Piece; #currentObject: Piece;
gameTick: number; #gameTick: number;
gameTickInterval: IntervalID; #gameTickInterval: IntervalID;
gameTimeInterval: IntervalID; #gameTimeInterval: IntervalID;
pressInInterval: TimeoutID; #pressInInterval: TimeoutID;
isPressedIn: boolean; #isPressedIn: boolean;
autoRepeatActivationDelay: number; #autoRepeatActivationDelay: number;
autoRepeatDelay: number; #autoRepeatDelay: number;
nextPieces: Array<Piece>; #nextPieces: Array<Piece>;
nextPiecesCount: number; #nextPiecesCount: number;
onTick: Function; #onTick: Function;
onClock: Function; #onClock: Function;
endCallback: Function; endCallback: Function;
colors: Object; #colors: Object;
constructor(height: number, width: number, colors: Object) { constructor(height: number, width: number, colors: Object) {
this.height = height; this.#height = height;
this.width = width; this.#width = width;
this.gameRunning = false; this.#gameRunning = false;
this.gamePaused = false; this.#gamePaused = false;
this.colors = colors; this.#colors = colors;
this.autoRepeatActivationDelay = 300; this.#autoRepeatActivationDelay = 300;
this.autoRepeatDelay = 50; this.#autoRepeatDelay = 50;
this.nextPieces = []; this.#nextPieces = [];
this.nextPiecesCount = 3; this.#nextPiecesCount = 3;
this.scoreManager = new ScoreManager(); this.#scoreManager = new ScoreManager();
this.gridManager = new GridManager(this.getWidth(), this.getHeight(), this.colors); this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors);
} }
getHeight(): number { getHeight(): number {
return this.height; return this.#height;
} }
getWidth(): number { getWidth(): number {
return this.width; return this.#width;
} }
getCurrentGrid() { getCurrentGrid() {
return this.gridManager.getCurrentGrid(); return this.#gridManager.getCurrentGrid();
} }
isGameRunning(): boolean { isGameRunning(): boolean {
return this.gameRunning; return this.#gameRunning;
} }
isGamePaused(): boolean { isGamePaused(): boolean {
return this.gamePaused; return this.#gamePaused;
} }
onFreeze() { onFreeze() {
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager); this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager);
this.createTetromino(); this.createTetromino();
} }
setNewGameTick(level: number) { setNewGameTick(level: number) {
if (level >= GameLogic.levelTicks.length) if (level >= GameLogic.levelTicks.length)
return; return;
this.gameTick = GameLogic.levelTicks[level]; this.#gameTick = GameLogic.levelTicks[level];
clearInterval(this.gameTickInterval); clearInterval(this.#gameTickInterval);
this.gameTickInterval = setInterval(this.onTick, this.gameTick); this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
} }
onTick(callback: Function) { onTick(callback: Function) {
this.currentObject.tryMove(0, 1, this.#currentObject.tryMove(0, 1,
this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
() => this.onFreeze()); () => this.onFreeze());
callback( callback(
this.scoreManager.getScore(), this.#scoreManager.getScore(),
this.scoreManager.getLevel(), this.#scoreManager.getLevel(),
this.gridManager.getCurrentGrid()); this.#gridManager.getCurrentGrid());
if (this.scoreManager.canLevelUp()) if (this.#scoreManager.canLevelUp())
this.setNewGameTick(this.scoreManager.getLevel()); this.setNewGameTick(this.#scoreManager.getLevel());
} }
onClock(callback: Function) { onClock(callback: Function) {
this.gameTime++; this.#gameTime++;
callback(this.gameTime); callback(this.#gameTime);
} }
canUseInput() { canUseInput() {
return this.gameRunning && !this.gamePaused return this.#gameRunning && !this.#gamePaused
} }
rightPressed(callback: Function) { rightPressed(callback: Function) {
this.isPressedIn = true; this.#isPressedIn = true;
this.movePressedRepeat(true, callback, 1, 0); this.movePressedRepeat(true, callback, 1, 0);
} }
leftPressedIn(callback: Function) { leftPressedIn(callback: Function) {
this.isPressedIn = true; this.#isPressedIn = true;
this.movePressedRepeat(true, callback, -1, 0); this.movePressedRepeat(true, callback, -1, 0);
} }
downPressedIn(callback: Function) { downPressedIn(callback: Function) {
this.isPressedIn = true; this.#isPressedIn = true;
this.movePressedRepeat(true, callback, 0, 1); this.movePressedRepeat(true, callback, 0, 1);
} }
movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) { movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) {
if (!this.canUseInput() || !this.isPressedIn) if (!this.canUseInput() || !this.#isPressedIn)
return; return;
const moved = this.currentObject.tryMove(x, y, const moved = this.#currentObject.tryMove(x, y,
this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
() => this.onFreeze()); () => this.onFreeze());
if (moved) { if (moved) {
if (y === 1) { if (y === 1) {
this.scoreManager.incrementScore(); this.#scoreManager.incrementScore();
callback(this.gridManager.getCurrentGrid(), this.scoreManager.getScore()); callback(this.#gridManager.getCurrentGrid(), this.#scoreManager.getScore());
} else } 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() { pressedOut() {
this.isPressedIn = false; this.#isPressedIn = false;
clearTimeout(this.pressInInterval); clearTimeout(this.#pressInInterval);
} }
rotatePressed(callback: Function) { rotatePressed(callback: Function) {
if (!this.canUseInput()) if (!this.canUseInput())
return; return;
if (this.currentObject.tryRotate(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
callback(this.gridManager.getCurrentGrid()); callback(this.#gridManager.getCurrentGrid());
} }
getNextPiecesPreviews() { getNextPiecesPreviews() {
let finalArray = []; let finalArray = [];
for (let i = 0; i < this.nextPieces.length; i++) { for (let i = 0; i < this.#nextPieces.length; i++) {
finalArray.push(this.gridManager.getEmptyGrid(4, 4)); finalArray.push(this.#gridManager.getEmptyGrid(4, 4));
this.nextPieces[i].toGrid(finalArray[i], true); this.#nextPieces[i].toGrid(finalArray[i], true);
} }
return finalArray; return finalArray;
} }
recoverNextPiece() { recoverNextPiece() {
this.currentObject = this.nextPieces.shift(); this.#currentObject = this.#nextPieces.shift();
this.generateNextPieces(); this.generateNextPieces();
} }
generateNextPieces() { generateNextPieces() {
while (this.nextPieces.length < this.nextPiecesCount) { while (this.#nextPieces.length < this.#nextPiecesCount) {
this.nextPieces.push(new Piece(this.colors)); this.#nextPieces.push(new Piece(this.#colors));
} }
} }
createTetromino() { createTetromino() {
this.pressedOut(); this.pressedOut();
this.recoverNextPiece(); 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); this.endGame(false);
} }
togglePause() { togglePause() {
if (!this.gameRunning) if (!this.#gameRunning)
return; return;
this.gamePaused = !this.gamePaused; this.#gamePaused = !this.#gamePaused;
if (this.gamePaused) { if (this.#gamePaused) {
clearInterval(this.gameTickInterval); clearInterval(this.#gameTickInterval);
clearInterval(this.gameTimeInterval); clearInterval(this.#gameTimeInterval);
} else { } else {
this.gameTickInterval = setInterval(this.onTick, this.gameTick); this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
this.gameTimeInterval = setInterval(this.onClock, 1000); this.#gameTimeInterval = setInterval(this.#onClock, 1000);
} }
} }
endGame(isRestart: boolean) { endGame(isRestart: boolean) {
this.gameRunning = false; this.#gameRunning = false;
this.gamePaused = false; this.#gamePaused = false;
clearInterval(this.gameTickInterval); clearInterval(this.#gameTickInterval);
clearInterval(this.gameTimeInterval); clearInterval(this.#gameTimeInterval);
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart); this.endCallback(this.#gameTime, this.#scoreManager.getScore(), isRestart);
} }
startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) { startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) {
if (this.gameRunning) if (this.#gameRunning)
this.endGame(true); this.endGame(true);
this.gameRunning = true; this.#gameRunning = true;
this.gamePaused = false; this.#gamePaused = false;
this.gameTime = 0; this.#gameTime = 0;
this.scoreManager = new ScoreManager(); this.#scoreManager = new ScoreManager();
this.gameTick = GameLogic.levelTicks[this.scoreManager.getLevel()]; this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()];
this.gridManager = new GridManager(this.getWidth(), this.getHeight(), this.colors); this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors);
this.nextPieces = []; this.#nextPieces = [];
this.generateNextPieces(); this.generateNextPieces();
this.createTetromino(); this.createTetromino();
tickCallback( tickCallback(
this.scoreManager.getScore(), this.#scoreManager.getScore(),
this.scoreManager.getLevel(), this.#scoreManager.getLevel(),
this.gridManager.getCurrentGrid()); this.#gridManager.getCurrentGrid());
clockCallback(this.gameTime); clockCallback(this.#gameTime);
this.onTick = this.onTick.bind(this, tickCallback); this.#onTick = this.onTick.bind(this, tickCallback);
this.onClock = this.onClock.bind(this, clockCallback); this.#onClock = this.onClock.bind(this, clockCallback);
this.gameTickInterval = setInterval(this.onTick, this.gameTick); this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
this.gameTimeInterval = setInterval(this.onClock, 1000); this.#gameTimeInterval = setInterval(this.#onClock, 1000);
this.endCallback = endCallback; this.endCallback = endCallback;
} }
} }

View file

@ -2,24 +2,49 @@
import Piece from "./Piece"; import Piece from "./Piece";
import ScoreManager from "./ScoreManager"; 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 { export default class GridManager {
#currentGrid: grid; #currentGrid: grid;
#colors: Object; #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) { constructor(width: number, height: number, colors: Object) {
this.#colors = colors; this.#colors = colors;
this.#currentGrid = this.getEmptyGrid(height, width); this.#currentGrid = this.getEmptyGrid(height, width);
} }
getCurrentGrid() { /**
* Get the current grid
*
* @return {grid} The current grid
*/
getCurrentGrid(): grid {
return this.#currentGrid; 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 = []; let line = [];
for (let col = 0; col < width; col++) { for (let col = 0; col < width; col++) {
line.push({ line.push({
@ -30,7 +55,14 @@ export default class GridManager {
return line; 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 = []; let grid = [];
for (let row = 0; row < height; row++) { for (let row = 0; row < height; row++) {
grid.push(this.getEmptyLine(width)); grid.push(this.getEmptyLine(width));
@ -38,6 +70,13 @@ export default class GridManager {
return grid; 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) { clearLines(lines: Array<number>, scoreManager: ScoreManager) {
lines.sort(); lines.sort();
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@ -47,7 +86,14 @@ export default class GridManager {
scoreManager.addLinesRemovedPoints(lines.length); 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 = []; let rows = [];
for (let i = 0; i < coord.length; i++) { for (let i = 0; i < coord.length; i++) {
let isLineFull = true; let isLineFull = true;
@ -63,6 +109,12 @@ export default class GridManager {
return rows; 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) { freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), 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 ShapeS from "./Shapes/ShapeS";
import ShapeT from "./Shapes/ShapeT"; import ShapeT from "./Shapes/ShapeT";
import ShapeZ from "./Shapes/ShapeZ"; 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 { export default class Piece {
#shapes = [ #shapes = [
@ -20,17 +27,32 @@ export default class Piece {
#currentShape: Object; #currentShape: Object;
#colors: Object; #colors: Object;
/**
* Initializes this piece's color and shape
*
* @param colors Object containing current theme colors
*/
constructor(colors: Object) { constructor(colors: Object) {
this.#currentShape = this.getRandomShape(colors); this.#currentShape = this.getRandomShape(colors);
this.#colors = colors; this.#colors = colors;
} }
/**
* Gets a random shape object
*
* @param colors Object containing current theme colors
*/
getRandomShape(colors: Object) { getRandomShape(colors: Object) {
return new this.#shapes[Math.floor(Math.random() * 7)](colors); 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++) { for (let i = 0; i < coord.length; i++) {
grid[coord[i].y][coord[i].x] = { grid[coord[i].y][coord[i].x] = {
color: this.#colors.tetrisBackground, 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++) { for (let i = 0; i < coord.length; i++) {
grid[coord[i].y][coord[i].x] = { grid[coord[i].y][coord[i].x] = {
color: this.#currentShape.getColor(), 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; 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++) { for (let i = 0; i < coord.length; i++) {
if (coord[i].x >= width if (coord[i].x >= width
|| coord[i].x < 0 || coord[i].x < 0
@ -65,7 +101,18 @@ export default class Piece {
return isValid; 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; // Prevent moving from more than one tile
if (x < -1) x = -1; if (x < -1) x = -1;
if (y > 1) y = 1; if (y > 1) y = 1;
@ -75,19 +122,26 @@ export default class Piece {
this.removeFromGrid(grid); this.removeFromGrid(grid);
this.#currentShape.move(x, y); this.#currentShape.move(x, y);
let isValid = this.isPositionValid(grid, width, height); let isValid = this.isPositionValid(grid, width, height);
let shouldFreeze = false;
if (!isValid) if (!isValid)
this.#currentShape.move(-x, -y); this.#currentShape.move(-x, -y);
shouldFreeze = !isValid && y !== 0; let shouldFreeze = !isValid && y !== 0;
this.toGrid(grid, false); this.toGrid(grid, false);
if (shouldFreeze) if (shouldFreeze)
freezeCallback(); freezeCallback();
return isValid; 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.removeFromGrid(grid);
this.#currentShape.rotate(true); this.#currentShape.rotate(true);
if (!this.isPositionValid(grid, width, height)) { if (!this.isPositionValid(grid, width, height)) {
@ -99,7 +153,12 @@ export default class Piece {
return true; return true;
} }
getCoordinates() { /**
* Gets this piece used cells coordinates
*
* @return {Array<coordinates>} An array of coordinates
*/
getCoordinates(): Array<coordinates> {
return this.#currentShape.getCellsCoordinates(true); return this.#currentShape.getCellsCoordinates(true);
} }
} }

View file

@ -1,5 +1,8 @@
// @flow // @flow
/**
* Class used to manage game score
*/
export default class ScoreManager { export default class ScoreManager {
#scoreLinesModifier = [40, 100, 300, 1200]; #scoreLinesModifier = [40, 100, 300, 1200];
@ -8,31 +11,60 @@ export default class ScoreManager {
#level: number; #level: number;
#levelProgression: number; #levelProgression: number;
/**
* Initializes score to 0
*/
constructor() { constructor() {
this.#score = 0; this.#score = 0;
this.#level = 0; this.#level = 0;
this.#levelProgression = 0; this.#levelProgression = 0;
} }
/**
* Gets the current score
*
* @return {number} The current score
*/
getScore(): number { getScore(): number {
return this.#score; return this.#score;
} }
/**
* Gets the current level
*
* @return {number} The current level
*/
getLevel(): number { getLevel(): number {
return this.#level; return this.#level;
} }
/**
* Gets the current level progression
*
* @return {number} The current level progression
*/
getLevelProgression(): number { getLevelProgression(): number {
return this.#levelProgression; return this.#levelProgression;
} }
/**
* Increments the score by one
*/
incrementScore() { incrementScore() {
this.#score++; 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) { addLinesRemovedPoints(numberRemoved: number) {
if (numberRemoved < 1 || numberRemoved > 4) if (numberRemoved < 1 || numberRemoved > 4)
return 0; return;
this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1); this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1);
switch (numberRemoved) { switch (numberRemoved) {
case 1: 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() { canLevelUp() {
let canLevel = this.#levelProgression > this.#level * 5; let canLevel = this.#levelProgression > this.#level * 5;
if (canLevel){ if (canLevel){

View file

@ -1,5 +1,10 @@
// @flow // @flow
export type coordinates = {
x: number,
y: number,
}
/** /**
* Abstract class used to represent a BaseShape. * 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 * 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>>; #currentShape: Array<Array<number>>;
#rotation: number; #rotation: number;
position: Object; position: coordinates;
/**
* Prevent instantiation if classname is BaseShape to force class to be abstract
*/
constructor() { constructor() {
if (this.constructor === BaseShape) if (this.constructor === BaseShape)
throw new Error("Abstract class can't be instantiated"); throw new Error("Abstract class can't be instantiated");
@ -19,19 +27,41 @@ export default class BaseShape {
this.#currentShape = this.getShapes()[this.#rotation]; this.#currentShape = this.getShapes()[this.#rotation];
} }
/**
* Gets this shape's color.
* Must be implemented by child class
*/
getColor(): string { getColor(): string {
throw new Error("Method 'getColor()' must be implemented"); 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>>> { getShapes(): Array<Array<Array<number>>> {
throw new Error("Method 'getShapes()' must be implemented"); 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; 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 = []; let coordinates = [];
for (let row = 0; row < this.#currentShape.length; row++) { for (let row = 0; row < this.#currentShape.length; row++) {
for (let col = 0; col < this.#currentShape[row].length; col++) { for (let col = 0; col < this.#currentShape[row].length; col++) {
@ -45,6 +75,11 @@ export default class BaseShape {
return coordinates; return coordinates;
} }
/**
* Rotate this object
*
* @param isForward Should we rotate clockwise?
*/
rotate(isForward: boolean) { rotate(isForward: boolean) {
if (isForward) if (isForward)
this.#rotation++; this.#rotation++;
@ -57,6 +92,12 @@ export default class BaseShape {
this.#currentShape = this.getShapes()[this.#rotation]; 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) { move(x: number, y: number) {
this.position.x += x; this.position.x += x;
this.position.y += y; this.position.y += y;