Compare commits
No commits in common. "8dc620b987b3cf804cfda469c1631707f976f848" and "c75a7dc8fc9385bd4b5232b357902959b3ca7728" have entirely different histories.
8dc620b987
...
c75a7dc8fc
8 changed files with 140 additions and 296 deletions
12
.idea/runConfigurations/All_Tests__coverage_.xml
Normal file
12
.idea/runConfigurations/All_Tests__coverage_.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All Tests (coverage)" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="" />
|
||||
<jest-package value="$PROJECT_DIR$/node_modules/jest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<jest-options value="--coverage" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import Piece from "./Piece";
|
||||
import ScoreManager from "./ScoreManager";
|
||||
import GridManager from "./GridManager";
|
||||
|
||||
export default class GameLogic {
|
||||
|
||||
|
|
@ -17,8 +15,9 @@ export default class GameLogic {
|
|||
100,
|
||||
];
|
||||
|
||||
scoreManager: ScoreManager;
|
||||
gridManager: GridManager;
|
||||
static scoreLinesModifier = [40, 100, 300, 1200];
|
||||
|
||||
currentGrid: Array<Array<Object>>;
|
||||
|
||||
height: number;
|
||||
width: number;
|
||||
|
|
@ -26,6 +25,8 @@ export default class GameLogic {
|
|||
gameRunning: boolean;
|
||||
gamePaused: boolean;
|
||||
gameTime: number;
|
||||
score: number;
|
||||
level: number;
|
||||
|
||||
currentObject: Piece;
|
||||
|
||||
|
|
@ -47,6 +48,8 @@ export default class GameLogic {
|
|||
|
||||
colors: Object;
|
||||
|
||||
levelProgression: number;
|
||||
|
||||
constructor(height: number, width: number, colors: Object) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
|
|
@ -57,8 +60,16 @@ export default class GameLogic {
|
|||
this.autoRepeatDelay = 50;
|
||||
this.nextPieces = [];
|
||||
this.nextPiecesCount = 3;
|
||||
this.scoreManager = new ScoreManager();
|
||||
this.gridManager = new GridManager(this.getWidth(), this.getHeight(), this.colors);
|
||||
}
|
||||
|
||||
getNextPiecesPreviews() {
|
||||
let finalArray = [];
|
||||
for (let i = 0; i < this.nextPieces.length; i++) {
|
||||
finalArray.push(this.getEmptyGrid(4, 4));
|
||||
this.nextPieces[i].toGrid(finalArray[i], true);
|
||||
}
|
||||
|
||||
return finalArray;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
|
|
@ -69,10 +80,6 @@ export default class GameLogic {
|
|||
return this.width;
|
||||
}
|
||||
|
||||
getCurrentGrid() {
|
||||
return this.gridManager.getCurrentGrid();
|
||||
}
|
||||
|
||||
isGameRunning(): boolean {
|
||||
return this.gameRunning;
|
||||
}
|
||||
|
|
@ -81,9 +88,90 @@ export default class GameLogic {
|
|||
return this.gamePaused;
|
||||
}
|
||||
|
||||
onFreeze() {
|
||||
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager);
|
||||
this.createTetromino();
|
||||
getEmptyLine(width: number) {
|
||||
let line = [];
|
||||
for (let col = 0; col < width; col++) {
|
||||
line.push({
|
||||
color: this.colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
});
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
getEmptyGrid(height: number, width: number) {
|
||||
let grid = [];
|
||||
for (let row = 0; row < height; row++) {
|
||||
grid.push(this.getEmptyLine(width));
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
getGridCopy() {
|
||||
return JSON.parse(JSON.stringify(this.currentGrid));
|
||||
}
|
||||
|
||||
getFinalGrid() {
|
||||
let finalGrid = this.getGridCopy();
|
||||
this.currentObject.toGrid(finalGrid, false);
|
||||
return finalGrid;
|
||||
}
|
||||
|
||||
getLinesRemovedPoints(numberRemoved: number) {
|
||||
if (numberRemoved < 1 || numberRemoved > 4)
|
||||
return 0;
|
||||
return GameLogic.scoreLinesModifier[numberRemoved-1] * (this.level + 1);
|
||||
}
|
||||
|
||||
canLevelUp() {
|
||||
let canLevel = this.levelProgression > this.level * 5;
|
||||
if (canLevel)
|
||||
this.levelProgression -= this.level * 5;
|
||||
return canLevel;
|
||||
}
|
||||
|
||||
freezeTetromino() {
|
||||
this.currentObject.toGrid(this.currentGrid, false);
|
||||
this.clearLines(this.getLinesToClear(this.currentObject.getCoordinates()));
|
||||
}
|
||||
|
||||
clearLines(lines: Array<number>) {
|
||||
lines.sort();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
this.currentGrid.splice(lines[i], 1);
|
||||
this.currentGrid.unshift(this.getEmptyLine(this.getWidth()));
|
||||
}
|
||||
switch (lines.length) {
|
||||
case 1:
|
||||
this.levelProgression += 1;
|
||||
break;
|
||||
case 2:
|
||||
this.levelProgression += 3;
|
||||
break;
|
||||
case 3:
|
||||
this.levelProgression += 5;
|
||||
break;
|
||||
case 4: // Did a tetris !
|
||||
this.levelProgression += 8;
|
||||
break;
|
||||
}
|
||||
this.score += this.getLinesRemovedPoints(lines.length);
|
||||
}
|
||||
|
||||
getLinesToClear(coord: Object) {
|
||||
let rows = [];
|
||||
for (let i = 0; i < coord.length; i++) {
|
||||
let isLineFull = true;
|
||||
for (let col = 0; col < this.getWidth(); col++) {
|
||||
if (this.currentGrid[coord[i].y][col].isEmpty) {
|
||||
isLineFull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isLineFull && rows.indexOf(coord[i].y) === -1)
|
||||
rows.push(coord[i].y);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
setNewGameTick(level: number) {
|
||||
|
|
@ -94,16 +182,20 @@ export default class GameLogic {
|
|||
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
|
||||
}
|
||||
|
||||
onFreeze() {
|
||||
this.freezeTetromino();
|
||||
this.createTetromino();
|
||||
}
|
||||
|
||||
onTick(callback: Function) {
|
||||
this.currentObject.tryMove(0, 1,
|
||||
this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
|
||||
this.currentGrid, this.getWidth(), this.getHeight(),
|
||||
() => this.onFreeze());
|
||||
callback(
|
||||
this.scoreManager.getScore(),
|
||||
this.scoreManager.getLevel(),
|
||||
this.gridManager.getFinalGrid(this.currentObject));
|
||||
if (this.scoreManager.canLevelUp())
|
||||
this.setNewGameTick(this.scoreManager.getLevel());
|
||||
callback(this.score, this.level, this.getFinalGrid());
|
||||
if (this.canLevelUp()) {
|
||||
this.level++;
|
||||
this.setNewGameTick(this.level);
|
||||
}
|
||||
}
|
||||
|
||||
onClock(callback: Function) {
|
||||
|
|
@ -134,14 +226,14 @@ export default class GameLogic {
|
|||
if (!this.canUseInput() || !this.isPressedIn)
|
||||
return;
|
||||
const moved = this.currentObject.tryMove(x, y,
|
||||
this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
|
||||
this.currentGrid, this.getWidth(), this.getHeight(),
|
||||
() => this.onFreeze());
|
||||
if (moved) {
|
||||
if (y === 1) {
|
||||
this.scoreManager.incrementScore();
|
||||
callback(this.gridManager.getFinalGrid(this.currentObject), this.scoreManager.getScore());
|
||||
this.score++;
|
||||
callback(this.getFinalGrid(), this.score);
|
||||
} else
|
||||
callback(this.gridManager.getFinalGrid(this.currentObject));
|
||||
callback(this.getFinalGrid());
|
||||
}
|
||||
this.pressInInterval = setTimeout(() => this.movePressedRepeat(false, callback, x, y), isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay);
|
||||
}
|
||||
|
|
@ -155,18 +247,8 @@ export default class GameLogic {
|
|||
if (!this.canUseInput())
|
||||
return;
|
||||
|
||||
if (this.currentObject.tryRotate(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
|
||||
callback(this.gridManager.getFinalGrid(this.currentObject));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return finalArray;
|
||||
if (this.currentObject.tryRotate(this.currentGrid, this.getWidth(), this.getHeight()))
|
||||
callback(this.getFinalGrid());
|
||||
}
|
||||
|
||||
recoverNextPiece() {
|
||||
|
|
@ -183,7 +265,7 @@ export default class GameLogic {
|
|||
createTetromino() {
|
||||
this.pressedOut();
|
||||
this.recoverNextPiece();
|
||||
if (!this.currentObject.isPositionValid(this.gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
|
||||
if (!this.currentObject.isPositionValid(this.currentGrid, this.getWidth(), this.getHeight()))
|
||||
this.endGame(false);
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +287,7 @@ export default class GameLogic {
|
|||
this.gamePaused = false;
|
||||
clearInterval(this.gameTickInterval);
|
||||
clearInterval(this.gameTimeInterval);
|
||||
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart);
|
||||
this.endCallback(this.gameTime, this.score, isRestart);
|
||||
}
|
||||
|
||||
startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) {
|
||||
|
|
@ -214,16 +296,15 @@ export default class GameLogic {
|
|||
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.score = 0;
|
||||
this.level = 0;
|
||||
this.levelProgression = 0;
|
||||
this.gameTick = GameLogic.levelTicks[this.level];
|
||||
this.currentGrid = this.getEmptyGrid(this.getHeight(), this.getWidth());
|
||||
this.nextPieces = [];
|
||||
this.generateNextPieces();
|
||||
this.createTetromino();
|
||||
tickCallback(
|
||||
this.scoreManager.getScore(),
|
||||
this.scoreManager.getLevel(),
|
||||
this.gridManager.getFinalGrid(this.currentObject));
|
||||
tickCallback(this.score, this.level, this.getFinalGrid());
|
||||
clockCallback(this.gameTime);
|
||||
this.onTick = this.onTick.bind(this, tickCallback);
|
||||
this.onClock = this.onClock.bind(this, clockCallback);
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import Piece from "./Piece";
|
||||
import ScoreManager from "./ScoreManager";
|
||||
|
||||
export default class GridManager {
|
||||
|
||||
#currentGrid: Array<Array<Object>>;
|
||||
#colors: Object;
|
||||
|
||||
constructor(width: number, height: number, colors: Object) {
|
||||
this.#colors = colors;
|
||||
this.#currentGrid = this.getEmptyGrid(height, width);
|
||||
}
|
||||
|
||||
getCurrentGrid() {
|
||||
return this.#currentGrid;
|
||||
}
|
||||
|
||||
getEmptyLine(width: number) {
|
||||
let line = [];
|
||||
for (let col = 0; col < width; col++) {
|
||||
line.push({
|
||||
color: this.#colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
});
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
getEmptyGrid(height: number, width: number) {
|
||||
let grid = [];
|
||||
for (let row = 0; row < height; row++) {
|
||||
grid.push(this.getEmptyLine(width));
|
||||
}
|
||||
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<number>, scoreManager: ScoreManager) {
|
||||
lines.sort();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
this.#currentGrid.splice(lines[i], 1);
|
||||
this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length));
|
||||
}
|
||||
scoreManager.addLinesRemovedPoints(lines.length);
|
||||
}
|
||||
|
||||
getLinesToClear(coord: Object) {
|
||||
let rows = [];
|
||||
for (let i = 0; i < coord.length; i++) {
|
||||
let isLineFull = true;
|
||||
for (let col = 0; col < this.#currentGrid[coord[i].y].length; col++) {
|
||||
if (this.#currentGrid[coord[i].y][col].isEmpty) {
|
||||
isLineFull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isLineFull && rows.indexOf(coord[i].y) === -1)
|
||||
rows.push(coord[i].y);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
|
||||
currentObject.toGrid(this.#currentGrid, false);
|
||||
this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), scoreManager);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,11 +21,7 @@ export default class Piece {
|
|||
#currentShape: Object;
|
||||
|
||||
constructor(colors: Object) {
|
||||
this.#currentShape = this.getRandomShape(colors);
|
||||
}
|
||||
|
||||
getRandomShape(colors: Object) {
|
||||
return new this.#shapes[Math.floor(Math.random() * 7)](colors);
|
||||
this.#currentShape = new this.#shapes[Math.floor(Math.random() * 7)](colors);
|
||||
}
|
||||
|
||||
toGrid(grid: Array<Array<Object>>, isPreview: boolean) {
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
// @flow
|
||||
|
||||
export default class ScoreManager {
|
||||
|
||||
#scoreLinesModifier = [40, 100, 300, 1200];
|
||||
|
||||
#score: number;
|
||||
#level: number;
|
||||
#levelProgression: number;
|
||||
|
||||
constructor() {
|
||||
this.#score = 0;
|
||||
this.#level = 0;
|
||||
this.#levelProgression = 0;
|
||||
}
|
||||
|
||||
getScore(): number {
|
||||
return this.#score;
|
||||
}
|
||||
|
||||
getLevel(): number {
|
||||
return this.#level;
|
||||
}
|
||||
|
||||
incrementScore() {
|
||||
this.#score++;
|
||||
}
|
||||
|
||||
addLinesRemovedPoints(numberRemoved: number) {
|
||||
if (numberRemoved < 1 || numberRemoved > 4)
|
||||
return 0;
|
||||
this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1);
|
||||
switch (numberRemoved) {
|
||||
case 1:
|
||||
this.#levelProgression += 1;
|
||||
break;
|
||||
case 2:
|
||||
this.#levelProgression += 3;
|
||||
break;
|
||||
case 3:
|
||||
this.#levelProgression += 5;
|
||||
break;
|
||||
case 4: // Did a tetris !
|
||||
this.#levelProgression += 8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
canLevelUp() {
|
||||
let canLevel = this.#levelProgression > this.#level * 5;
|
||||
if (canLevel){
|
||||
this.#levelProgression -= this.#level * 5;
|
||||
this.#level++;
|
||||
}
|
||||
return canLevel;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ class TetrisScreen extends React.Component<Props, State> {
|
|||
this.colors = props.theme.colors;
|
||||
this.logic = new GameLogic(20, 10, this.colors);
|
||||
this.state = {
|
||||
grid: this.logic.getCurrentGrid(),
|
||||
grid: this.logic.getEmptyGrid(this.logic.getHeight(), this.logic.getWidth()),
|
||||
gameRunning: false,
|
||||
gameTime: 0,
|
||||
gameScore: 0,
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
import React from 'react';
|
||||
import Piece from "../Piece";
|
||||
import ShapeI from "../Shapes/ShapeI";
|
||||
|
||||
let colors = {
|
||||
tetrisI: "#000001",
|
||||
tetrisBackground: "#000002"
|
||||
};
|
||||
|
||||
jest.mock("../Shapes/ShapeI");
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(Piece.prototype, 'getRandomShape')
|
||||
.mockImplementation((colors: Object) => {return new ShapeI(colors);});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('isPositionValid', () => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {return [{x: x, y: y}];});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: false}],
|
||||
];
|
||||
let size = 2;
|
||||
|
||||
let p = new Piece(colors);
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 0; y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1; y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 2; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = -1; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0; y = 2;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0; y = -1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('tryMove', () => {
|
||||
let p = new Piece(colors);
|
||||
const callbackMock = jest.fn();
|
||||
let isValid = true;
|
||||
let spy = jest.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {return isValid;});
|
||||
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeFalse();
|
||||
isValid = true;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeTrue();
|
||||
expect(callbackMock).toBeCalledTimes(0);
|
||||
|
||||
isValid = false;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse();
|
||||
expect(callbackMock).toBeCalledTimes(1);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('tryRotate', () => {
|
||||
let p = new Piece(colors);
|
||||
let isValid = true;
|
||||
let spy = jest.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {return isValid;});
|
||||
|
||||
expect(p.tryRotate( null, null, null)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryRotate( null, null, null)).toBeFalse();
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
|
||||
test('toGrid', () => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {return [{x: x, y: y}];});
|
||||
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor')
|
||||
.mockImplementation(() => {return colors.tetrisI;});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
let expectedGrid = [
|
||||
[{color: colors.tetrisI, isEmpty: false}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
|
||||
let p = new Piece(colors);
|
||||
p.toGrid(grid, true);
|
||||
expect(grid).toStrictEqual(expectedGrid);
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
});
|
||||
Loading…
Reference in a new issue