From 3d45bc62b830c145dc0609e5ab333395f7f04b55 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Tue, 24 Mar 2020 10:43:05 +0100 Subject: [PATCH] Added more tests and improved performance by not making deep copies of the grid --- screens/Tetris/GameLogic.js | 10 ++-- screens/Tetris/GridManager.js | 15 ++---- screens/Tetris/Piece.js | 32 +++++++++--- screens/Tetris/__tests__/GridManager.test.js | 34 ++++++++++++ screens/Tetris/__tests__/Piece.test.js | 54 ++++++++++++++++++-- 5 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 screens/Tetris/__tests__/GridManager.test.js diff --git a/screens/Tetris/GameLogic.js b/screens/Tetris/GameLogic.js index 7c46513..b3de919 100644 --- a/screens/Tetris/GameLogic.js +++ b/screens/Tetris/GameLogic.js @@ -101,7 +101,7 @@ export default class GameLogic { callback( this.scoreManager.getScore(), this.scoreManager.getLevel(), - this.gridManager.getFinalGrid(this.currentObject)); + this.gridManager.getCurrentGrid()); if (this.scoreManager.canLevelUp()) this.setNewGameTick(this.scoreManager.getLevel()); } @@ -139,9 +139,9 @@ export default class GameLogic { if (moved) { if (y === 1) { this.scoreManager.incrementScore(); - callback(this.gridManager.getFinalGrid(this.currentObject), this.scoreManager.getScore()); + callback(this.gridManager.getCurrentGrid(), this.scoreManager.getScore()); } else - callback(this.gridManager.getFinalGrid(this.currentObject)); + callback(this.gridManager.getCurrentGrid()); } this.pressInInterval = setTimeout(() => this.movePressedRepeat(false, callback, x, y), isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay); } @@ -156,7 +156,7 @@ export default class GameLogic { return; if (this.currentObject.tryRotate(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) - callback(this.gridManager.getFinalGrid(this.currentObject)); + callback(this.gridManager.getCurrentGrid()); } getNextPiecesPreviews() { @@ -223,7 +223,7 @@ export default class GameLogic { tickCallback( this.scoreManager.getScore(), this.scoreManager.getLevel(), - this.gridManager.getFinalGrid(this.currentObject)); + this.gridManager.getCurrentGrid()); clockCallback(this.gameTime); this.onTick = this.onTick.bind(this, tickCallback); this.onClock = this.onClock.bind(this, clockCallback); diff --git a/screens/Tetris/GridManager.js b/screens/Tetris/GridManager.js index 21d6ae4..ced9a07 100644 --- a/screens/Tetris/GridManager.js +++ b/screens/Tetris/GridManager.js @@ -3,9 +3,11 @@ import Piece from "./Piece"; import ScoreManager from "./ScoreManager"; +export type grid = Array>; + export default class GridManager { - #currentGrid: Array>; + #currentGrid: grid; #colors: Object; constructor(width: number, height: number, colors: Object) { @@ -36,16 +38,6 @@ export default class GridManager { return grid; } - getGridCopy() { - return JSON.parse(JSON.stringify(this.#currentGrid)); - } - - getFinalGrid(currentObject: Piece) { - let finalGrid = this.getGridCopy(); - currentObject.toGrid(finalGrid, false); - return finalGrid; - } - clearLines(lines: Array, scoreManager: ScoreManager) { lines.sort(); for (let i = 0; i < lines.length; i++) { @@ -72,7 +64,6 @@ export default class GridManager { } freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) { - currentObject.toGrid(this.#currentGrid, false); this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), scoreManager); } } diff --git a/screens/Tetris/Piece.js b/screens/Tetris/Piece.js index 306b9ef..86cbc68 100644 --- a/screens/Tetris/Piece.js +++ b/screens/Tetris/Piece.js @@ -17,17 +17,28 @@ export default class Piece { ShapeT, ShapeZ, ]; - #currentShape: Object; + #colors: Object; constructor(colors: Object) { this.#currentShape = this.getRandomShape(colors); + this.#colors = colors; } getRandomShape(colors: Object) { return new this.#shapes[Math.floor(Math.random() * 7)](colors); } + removeFromGrid(grid) { + const coord = this.#currentShape.getCellsCoordinates(true); + for (let i = 0; i < coord.length; i++) { + grid[coord[i].y][coord[i].x] = { + color: this.#colors.tetrisBackground, + isEmpty: true, + }; + } + } + toGrid(grid: Array>, isPreview: boolean) { const coord = this.#currentShape.getCellsCoordinates(!isPreview); for (let i = 0; i < coord.length; i++) { @@ -61,25 +72,30 @@ export default class Piece { if (y < -1) y = -1; if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement + this.removeFromGrid(grid); this.#currentShape.move(x, y); let isValid = this.isPositionValid(grid, width, height); + let shouldFreeze = false; - if (!isValid && x !== 0) - this.#currentShape.move(-x, 0); - else if (!isValid && y !== 0) { - this.#currentShape.move(0, -y); + if (!isValid) + this.#currentShape.move(-x, -y); + + shouldFreeze = !isValid && y !== 0; + this.toGrid(grid, false); + if (shouldFreeze) freezeCallback(); - } else - return true; - return false; + return isValid; } tryRotate(grid, width, height) { + this.removeFromGrid(grid); this.#currentShape.rotate(true); if (!this.isPositionValid(grid, width, height)) { this.#currentShape.rotate(false); + this.toGrid(grid, false); return false; } + this.toGrid(grid, false); return true; } diff --git a/screens/Tetris/__tests__/GridManager.test.js b/screens/Tetris/__tests__/GridManager.test.js new file mode 100644 index 0000000..43d1f0e --- /dev/null +++ b/screens/Tetris/__tests__/GridManager.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import GridManager from "../GridManager"; + +let colors = { + tetrisBackground: "#000002" +}; + +test('getEmptyLine', () => { + let g = new GridManager(2, 2, colors); + expect(g.getEmptyLine(2)).toStrictEqual([ + {color: colors.tetrisBackground, isEmpty: true}, + {color: colors.tetrisBackground, isEmpty: true}, + ]); + + expect(g.getEmptyLine(-1)).toStrictEqual([]); +}); + +test('getEmptyGrid', () => { + let g = new GridManager(2, 2, colors); + expect(g.getEmptyGrid(2, 2)).toStrictEqual([ + [ + {color: colors.tetrisBackground, isEmpty: true}, + {color: colors.tetrisBackground, isEmpty: true}, + ], + [ + {color: colors.tetrisBackground, isEmpty: true}, + {color: colors.tetrisBackground, isEmpty: true}, + ], + ]); + + expect(g.getEmptyGrid(-1, 2)).toStrictEqual([]); + expect(g.getEmptyGrid(2, -1)).toStrictEqual([[], []]); +}); + diff --git a/screens/Tetris/__tests__/Piece.test.js b/screens/Tetris/__tests__/Piece.test.js index f750848..da5d7b2 100644 --- a/screens/Tetris/__tests__/Piece.test.js +++ b/screens/Tetris/__tests__/Piece.test.js @@ -53,8 +53,12 @@ test('tryMove', () => { let p = new Piece(colors); const callbackMock = jest.fn(); let isValid = true; - let spy = jest.spyOn(Piece.prototype, 'isPositionValid') + let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid') .mockImplementation(() => {return isValid;}); + let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid') + .mockImplementation(() => {}); + let spy3 = jest.spyOn(Piece.prototype, 'toGrid') + .mockImplementation(() => {}); expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue(); isValid = false; @@ -67,20 +71,34 @@ test('tryMove', () => { expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse(); expect(callbackMock).toBeCalledTimes(1); - spy.mockRestore(); + expect(spy2).toBeCalledTimes(4); + expect(spy3).toBeCalledTimes(4); + + spy1.mockRestore(); + spy2.mockRestore(); + spy3.mockRestore(); }); test('tryRotate', () => { let p = new Piece(colors); let isValid = true; - let spy = jest.spyOn(Piece.prototype, 'isPositionValid') + let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid') .mockImplementation(() => {return isValid;}); + let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid') + .mockImplementation(() => {}); + let spy3 = jest.spyOn(Piece.prototype, 'toGrid') + .mockImplementation(() => {}); expect(p.tryRotate( null, null, null)).toBeTrue(); isValid = false; expect(p.tryRotate( null, null, null)).toBeFalse(); - spy.mockRestore(); + expect(spy2).toBeCalledTimes(2); + expect(spy3).toBeCalledTimes(2); + + spy1.mockRestore(); + spy2.mockRestore(); + spy3.mockRestore(); }); @@ -107,3 +125,31 @@ test('toGrid', () => { spy1.mockRestore(); spy2.mockRestore(); }); + +test('removeFromGrid', () => { + let gridOld = [ + [ + {color: colors.tetrisI, isEmpty: false}, + {color: colors.tetrisI, isEmpty: false}, + {color: colors.tetrisBackground, isEmpty: true}, + ], + ]; + let gridNew = [ + [ + {color: colors.tetrisBackground, isEmpty: true}, + {color: colors.tetrisBackground, isEmpty: true}, + {color: colors.tetrisBackground, isEmpty: true}, + ], + ]; + let oldCoord = [{x: 0, y: 0}, {x: 1, y: 0}]; + let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates') + .mockImplementation(() => {return oldCoord;}); + let spy2 = jest.spyOn(ShapeI.prototype, 'getColor') + .mockImplementation(() => {return colors.tetrisI;}); + let p = new Piece(colors); + p.removeFromGrid(gridOld); + expect(gridOld).toStrictEqual(gridNew); + + spy1.mockRestore(); + spy2.mockRestore(); +});