From c75a7dc8fc9385bd4b5232b357902959b3ca7728 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Mon, 23 Mar 2020 11:32:50 +0100 Subject: [PATCH] Improved game organization --- screens/Tetris/GameLogic.js | 101 +++------ screens/Tetris/Piece.js | 85 ++++++++ screens/Tetris/Shapes/BaseShape.js | 65 ++++++ screens/Tetris/Shapes/ShapeI.js | 47 +++++ screens/Tetris/Shapes/ShapeJ.js | 43 ++++ screens/Tetris/Shapes/ShapeL.js | 43 ++++ screens/Tetris/Shapes/ShapeO.js | 39 ++++ screens/Tetris/Shapes/ShapeS.js | 43 ++++ screens/Tetris/Shapes/ShapeT.js | 43 ++++ screens/Tetris/Shapes/ShapeZ.js | 43 ++++ screens/Tetris/TetrisScreen.js | 2 +- screens/Tetris/Tetromino.js | 230 --------------------- screens/Tetris/__tests__/Tetromino.test.js | 104 ++++++++++ 13 files changed, 579 insertions(+), 309 deletions(-) create mode 100644 screens/Tetris/Piece.js create mode 100644 screens/Tetris/Shapes/BaseShape.js create mode 100644 screens/Tetris/Shapes/ShapeI.js create mode 100644 screens/Tetris/Shapes/ShapeJ.js create mode 100644 screens/Tetris/Shapes/ShapeL.js create mode 100644 screens/Tetris/Shapes/ShapeO.js create mode 100644 screens/Tetris/Shapes/ShapeS.js create mode 100644 screens/Tetris/Shapes/ShapeT.js create mode 100644 screens/Tetris/Shapes/ShapeZ.js delete mode 100644 screens/Tetris/Tetromino.js create mode 100644 screens/Tetris/__tests__/Tetromino.test.js diff --git a/screens/Tetris/GameLogic.js b/screens/Tetris/GameLogic.js index 7682e29..684ac65 100644 --- a/screens/Tetris/GameLogic.js +++ b/screens/Tetris/GameLogic.js @@ -1,6 +1,6 @@ // @flow -import Tetromino from "./Tetromino"; +import Piece from "./Piece"; export default class GameLogic { @@ -28,7 +28,7 @@ export default class GameLogic { score: number; level: number; - currentObject: Tetromino; + currentObject: Piece; gameTick: number; gameTickInterval: IntervalID; @@ -39,7 +39,7 @@ export default class GameLogic { autoRepeatActivationDelay: number; autoRepeatDelay: number; - nextPieces: Array; + nextPieces: Array; nextPiecesCount: number; onTick: Function; @@ -62,12 +62,11 @@ export default class GameLogic { this.nextPiecesCount = 3; } - getNextPieces() { + getNextPiecesPreviews() { let finalArray = []; for (let i = 0; i < this.nextPieces.length; i++) { finalArray.push(this.getEmptyGrid(4, 4)); - let coord = this.nextPieces[i].getCellsCoordinates(false); - this.tetrominoToGrid(this.nextPieces[i], coord, finalArray[i]); + this.nextPieces[i].toGrid(finalArray[i], true); } return finalArray; @@ -113,14 +112,8 @@ export default class GameLogic { } getFinalGrid() { - let coord = this.currentObject.getCellsCoordinates(true); let finalGrid = this.getGridCopy(); - for (let i = 0; i < coord.length; i++) { - finalGrid[coord[i].y][coord[i].x] = { - color: this.currentObject.getColor(), - isEmpty: false, - }; - } + this.currentObject.toGrid(finalGrid, false); return finalGrid; } @@ -137,19 +130,9 @@ export default class GameLogic { return canLevel; } - tetrominoToGrid(object: Object, coord : Array, grid: Array>) { - for (let i = 0; i < coord.length; i++) { - grid[coord[i].y][coord[i].x] = { - color: object.getColor(), - isEmpty: false, - }; - } - } - freezeTetromino() { - let coord = this.currentObject.getCellsCoordinates(true); - this.tetrominoToGrid(this.currentObject, coord, this.currentGrid); - this.clearLines(this.getLinesToClear(coord)); + this.currentObject.toGrid(this.currentGrid, false); + this.clearLines(this.getLinesToClear(this.currentObject.getCoordinates())); } clearLines(lines: Array) { @@ -191,52 +174,6 @@ export default class GameLogic { return rows; } - isTetrominoPositionValid() { - let isValid = true; - let coord = this.currentObject.getCellsCoordinates(true); - for (let i = 0; i < coord.length; i++) { - if (coord[i].x >= this.getWidth() - || coord[i].x < 0 - || coord[i].y >= this.getHeight() - || coord[i].y < 0 - || !this.currentGrid[coord[i].y][coord[i].x].isEmpty) { - isValid = false; - break; - } - } - return isValid; - } - - tryMoveTetromino(x: number, y: number) { - if (x > 1) x = 1; // Prevent moving from more than one tile - if (x < -1) x = -1; - if (y > 1) y = 1; - if (y < -1) y = -1; - if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement - - this.currentObject.move(x, y); - let isValid = this.isTetrominoPositionValid(); - - if (!isValid && x !== 0) - this.currentObject.move(-x, 0); - else if (!isValid && y !== 0) { - this.currentObject.move(0, -y); - this.freezeTetromino(); - this.createTetromino(); - } else - return true; - return false; - } - - tryRotateTetromino() { - this.currentObject.rotate(true); - if (!this.isTetrominoPositionValid()) { - this.currentObject.rotate(false); - return false; - } - return true; - } - setNewGameTick(level: number) { if (level >= GameLogic.levelTicks.length) return; @@ -245,8 +182,15 @@ export default class GameLogic { this.gameTickInterval = setInterval(this.onTick, this.gameTick); } + onFreeze() { + this.freezeTetromino(); + this.createTetromino(); + } + onTick(callback: Function) { - this.tryMoveTetromino(0, 1); + this.currentObject.tryMove(0, 1, + this.currentGrid, this.getWidth(), this.getHeight(), + () => this.onFreeze()); callback(this.score, this.level, this.getFinalGrid()); if (this.canLevelUp()) { this.level++; @@ -281,8 +225,10 @@ export default class GameLogic { movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) { if (!this.canUseInput() || !this.isPressedIn) return; - - if (this.tryMoveTetromino(x, y)) { + const moved = this.currentObject.tryMove(x, y, + this.currentGrid, this.getWidth(), this.getHeight(), + () => this.onFreeze()); + if (moved) { if (y === 1) { this.score++; callback(this.getFinalGrid(), this.score); @@ -301,7 +247,7 @@ export default class GameLogic { if (!this.canUseInput()) return; - if (this.tryRotateTetromino()) + if (this.currentObject.tryRotate(this.currentGrid, this.getWidth(), this.getHeight())) callback(this.getFinalGrid()); } @@ -312,15 +258,14 @@ export default class GameLogic { generateNextPieces() { while (this.nextPieces.length < this.nextPiecesCount) { - let shape = Math.floor(Math.random() * 7); - this.nextPieces.push(new Tetromino(shape, this.colors)); + this.nextPieces.push(new Piece(this.colors)); } } createTetromino() { this.pressedOut(); this.recoverNextPiece(); - if (!this.isTetrominoPositionValid()) + if (!this.currentObject.isPositionValid(this.currentGrid, this.getWidth(), this.getHeight())) this.endGame(false); } diff --git a/screens/Tetris/Piece.js b/screens/Tetris/Piece.js new file mode 100644 index 0000000..8818d0c --- /dev/null +++ b/screens/Tetris/Piece.js @@ -0,0 +1,85 @@ +import ShapeL from "./Shapes/ShapeL"; +import ShapeI from "./Shapes/ShapeI"; +import ShapeJ from "./Shapes/ShapeJ"; +import ShapeO from "./Shapes/ShapeO"; +import ShapeS from "./Shapes/ShapeS"; +import ShapeT from "./Shapes/ShapeT"; +import ShapeZ from "./Shapes/ShapeZ"; + +export default class Piece { + + #shapes = [ + ShapeL, + ShapeI, + ShapeJ, + ShapeO, + ShapeS, + ShapeT, + ShapeZ, + ]; + + #currentShape: Object; + + constructor(colors: Object) { + this.#currentShape = new this.#shapes[Math.floor(Math.random() * 7)](colors); + } + + toGrid(grid: Array>, isPreview: boolean) { + const coord = this.#currentShape.getCellsCoordinates(!isPreview); + for (let i = 0; i < coord.length; i++) { + grid[coord[i].y][coord[i].x] = { + color: this.#currentShape.getColor(), + isEmpty: false, + }; + } + } + + isPositionValid(grid, width, height) { + let isValid = true; + const coord = this.#currentShape.getCellsCoordinates(true); + for (let i = 0; i < coord.length; i++) { + if (coord[i].x >= width + || coord[i].x < 0 + || coord[i].y >= height + || coord[i].y < 0 + || !grid[coord[i].y][coord[i].x].isEmpty) { + isValid = false; + break; + } + } + return isValid; + } + + tryMove(x: number, y: number, grid, width, height, freezeCallback: Function) { + if (x > 1) x = 1; // Prevent moving from more than one tile + if (x < -1) x = -1; + if (y > 1) y = 1; + if (y < -1) y = -1; + if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement + + this.#currentShape.move(x, y); + let isValid = this.isPositionValid(grid, width, height); + + if (!isValid && x !== 0) + this.#currentShape.move(-x, 0); + else if (!isValid && y !== 0) { + this.#currentShape.move(0, -y); + freezeCallback(); + } else + return true; + return false; + } + + tryRotate(grid, width, height) { + this.#currentShape.rotate(true); + if (!this.isPositionValid(grid, width, height)) { + this.#currentShape.rotate(false); + return false; + } + return true; + } + + getCoordinates() { + return this.#currentShape.getCellsCoordinates(true); + } +} diff --git a/screens/Tetris/Shapes/BaseShape.js b/screens/Tetris/Shapes/BaseShape.js new file mode 100644 index 0000000..717ef8a --- /dev/null +++ b/screens/Tetris/Shapes/BaseShape.js @@ -0,0 +1,65 @@ +// @flow + +/** + * 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 + * and in methods to implement + */ +export default class BaseShape { + + #currentShape: Array>; + #rotation: number; + position: Object; + + constructor() { + if (this.constructor === BaseShape) + throw new Error("Abstract class can't be instantiated"); + this.#rotation = 0; + this.position = {x: 0, y: 0}; + this.#currentShape = this.getShapes()[this.#rotation]; + } + + getColor(): string { + throw new Error("Method 'getColor()' must be implemented"); + } + + getShapes(): Array>> { + throw new Error("Method 'getShapes()' must be implemented"); + } + + getCurrentShape() { + return this.#currentShape; + } + + getCellsCoordinates(isAbsolute: boolean) { + 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}); + } + } + return coordinates; + } + + 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(x: number, y: number) { + this.position.x += x; + this.position.y += y; + } + +} diff --git a/screens/Tetris/Shapes/ShapeI.js b/screens/Tetris/Shapes/ShapeI.js new file mode 100644 index 0000000..d3d2205 --- /dev/null +++ b/screens/Tetris/Shapes/ShapeI.js @@ -0,0 +1,47 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeI extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeJ.js b/screens/Tetris/Shapes/ShapeJ.js new file mode 100644 index 0000000..391e180 --- /dev/null +++ b/screens/Tetris/Shapes/ShapeJ.js @@ -0,0 +1,43 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeJ extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeL.js b/screens/Tetris/Shapes/ShapeL.js new file mode 100644 index 0000000..77562cc --- /dev/null +++ b/screens/Tetris/Shapes/ShapeL.js @@ -0,0 +1,43 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeL extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeO.js b/screens/Tetris/Shapes/ShapeO.js new file mode 100644 index 0000000..e55b3aa --- /dev/null +++ b/screens/Tetris/Shapes/ShapeO.js @@ -0,0 +1,39 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeO extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 4; + this.#colors = colors; + } + + getColor(): string { + return this.#colors.tetrisO; + } + + getShapes() { + return [ + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + [ + [1, 1], + [1, 1], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeS.js b/screens/Tetris/Shapes/ShapeS.js new file mode 100644 index 0000000..2124a00 --- /dev/null +++ b/screens/Tetris/Shapes/ShapeS.js @@ -0,0 +1,43 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeS extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeT.js b/screens/Tetris/Shapes/ShapeT.js new file mode 100644 index 0000000..244bfae --- /dev/null +++ b/screens/Tetris/Shapes/ShapeT.js @@ -0,0 +1,43 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeT extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/Shapes/ShapeZ.js b/screens/Tetris/Shapes/ShapeZ.js new file mode 100644 index 0000000..05a619f --- /dev/null +++ b/screens/Tetris/Shapes/ShapeZ.js @@ -0,0 +1,43 @@ +// @flow + +import BaseShape from "./BaseShape"; + +export default class ShapeZ extends BaseShape { + + #colors: Object; + + constructor(colors: Object) { + super(); + this.position.x = 3; + this.#colors = colors; + } + + getColor(): string { + return this.#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], + ], + ]; + } +} diff --git a/screens/Tetris/TetrisScreen.js b/screens/Tetris/TetrisScreen.js index d3a4ef1..60d409d 100644 --- a/screens/Tetris/TetrisScreen.js +++ b/screens/Tetris/TetrisScreen.js @@ -253,7 +253,7 @@ class TetrisScreen extends React.Component { right: 5, }}> 3) - this.currentRotation = 0; - else if (this.currentRotation < 0) - this.currentRotation = 3; - this.currentShape = Tetromino.shapes[this.currentRotation][this.currentType]; - } - - move(x: number, y: number) { - this.position.x += x; - this.position.y += y; - } - -} diff --git a/screens/Tetris/__tests__/Tetromino.test.js b/screens/Tetris/__tests__/Tetromino.test.js new file mode 100644 index 0000000..3a4537e --- /dev/null +++ b/screens/Tetris/__tests__/Tetromino.test.js @@ -0,0 +1,104 @@ +import React from 'react'; +import BaseShape from "../Shapes/BaseShape"; +import ShapeI from "../Shapes/ShapeI"; + +const colors = { + tetrisI: '#000001', + tetrisO: '#000002', + tetrisT: '#000003', + tetrisS: '#000004', + tetrisZ: '#000005', + tetrisJ: '#000006', + tetrisL: '#000007', +}; + +test('constructor', () => { + expect(() => new BaseShape()).toThrow(Error); + + let T = new ShapeI(colors); + expect(T.position.y).toBe(0); + expect(T.position.x).toBe(3); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); + expect(T.getColor()).toBe(colors.tetrisI); +}); + +test("move", () => { + let T = new ShapeI(colors); + T.move(0, 1); + expect(T.position.x).toBe(3); + expect(T.position.y).toBe(1); + T.move(1, 0); + expect(T.position.x).toBe(4); + expect(T.position.y).toBe(1); + T.move(1, 1); + expect(T.position.x).toBe(5); + expect(T.position.y).toBe(2); + T.move(2, 2); + expect(T.position.x).toBe(7); + expect(T.position.y).toBe(4); + T.move(-1, -1); + expect(T.position.x).toBe(6); + expect(T.position.y).toBe(3); +}); + +test('rotate', () => { + let T = new ShapeI(colors); + T.rotate(true); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]); + T.rotate(true); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]); + T.rotate(true); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]); + T.rotate(true); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); + T.rotate(false); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]); + T.rotate(false); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]); + T.rotate(false); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]); + T.rotate(false); + expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); +}); + +test('getCellsCoordinates', () => { + let T = new ShapeI(colors); + expect(T.getCellsCoordinates(false)).toStrictEqual([ + {x: 0, y: 1}, + {x: 1, y: 1}, + {x: 2, y: 1}, + {x: 3, y: 1}, + ]); + expect(T.getCellsCoordinates(true)).toStrictEqual([ + {x: 3, y: 1}, + {x: 4, y: 1}, + {x: 5, y: 1}, + {x: 6, y: 1}, + ]); + T.move(1, 1); + expect(T.getCellsCoordinates(false)).toStrictEqual([ + {x: 0, y: 1}, + {x: 1, y: 1}, + {x: 2, y: 1}, + {x: 3, y: 1}, + ]); + expect(T.getCellsCoordinates(true)).toStrictEqual([ + {x: 4, y: 2}, + {x: 5, y: 2}, + {x: 6, y: 2}, + {x: 7, y: 2}, + ]); + T.rotate(true); + expect(T.getCellsCoordinates(false)).toStrictEqual([ + {x: 2, y: 0}, + {x: 2, y: 1}, + {x: 2, y: 2}, + {x: 2, y: 3}, + ]); + expect(T.getCellsCoordinates(true)).toStrictEqual([ + {x: 6, y: 1}, + {x: 6, y: 2}, + {x: 6, y: 3}, + {x: 6, y: 4}, + ]); +});