Improved game organization

This commit is contained in:
Arnaud Vergnet 2020-03-23 11:32:50 +01:00
parent 965ddd3cb2
commit c75a7dc8fc
13 changed files with 579 additions and 309 deletions

View file

@ -1,6 +1,6 @@
// @flow // @flow
import Tetromino from "./Tetromino"; import Piece from "./Piece";
export default class GameLogic { export default class GameLogic {
@ -28,7 +28,7 @@ export default class GameLogic {
score: number; score: number;
level: number; level: number;
currentObject: Tetromino; currentObject: Piece;
gameTick: number; gameTick: number;
gameTickInterval: IntervalID; gameTickInterval: IntervalID;
@ -39,7 +39,7 @@ export default class GameLogic {
autoRepeatActivationDelay: number; autoRepeatActivationDelay: number;
autoRepeatDelay: number; autoRepeatDelay: number;
nextPieces: Array<Tetromino>; nextPieces: Array<Piece>;
nextPiecesCount: number; nextPiecesCount: number;
onTick: Function; onTick: Function;
@ -62,12 +62,11 @@ export default class GameLogic {
this.nextPiecesCount = 3; this.nextPiecesCount = 3;
} }
getNextPieces() { 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.getEmptyGrid(4, 4)); finalArray.push(this.getEmptyGrid(4, 4));
let coord = this.nextPieces[i].getCellsCoordinates(false); this.nextPieces[i].toGrid(finalArray[i], true);
this.tetrominoToGrid(this.nextPieces[i], coord, finalArray[i]);
} }
return finalArray; return finalArray;
@ -113,14 +112,8 @@ export default class GameLogic {
} }
getFinalGrid() { getFinalGrid() {
let coord = this.currentObject.getCellsCoordinates(true);
let finalGrid = this.getGridCopy(); let finalGrid = this.getGridCopy();
for (let i = 0; i < coord.length; i++) { this.currentObject.toGrid(finalGrid, false);
finalGrid[coord[i].y][coord[i].x] = {
color: this.currentObject.getColor(),
isEmpty: false,
};
}
return finalGrid; return finalGrid;
} }
@ -137,19 +130,9 @@ export default class GameLogic {
return canLevel; return canLevel;
} }
tetrominoToGrid(object: Object, coord : Array<Object>, grid: Array<Array<Object>>) {
for (let i = 0; i < coord.length; i++) {
grid[coord[i].y][coord[i].x] = {
color: object.getColor(),
isEmpty: false,
};
}
}
freezeTetromino() { freezeTetromino() {
let coord = this.currentObject.getCellsCoordinates(true); this.currentObject.toGrid(this.currentGrid, false);
this.tetrominoToGrid(this.currentObject, coord, this.currentGrid); this.clearLines(this.getLinesToClear(this.currentObject.getCoordinates()));
this.clearLines(this.getLinesToClear(coord));
} }
clearLines(lines: Array<number>) { clearLines(lines: Array<number>) {
@ -191,52 +174,6 @@ export default class GameLogic {
return rows; 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) { setNewGameTick(level: number) {
if (level >= GameLogic.levelTicks.length) if (level >= GameLogic.levelTicks.length)
return; return;
@ -245,8 +182,15 @@ export default class GameLogic {
this.gameTickInterval = setInterval(this.onTick, this.gameTick); this.gameTickInterval = setInterval(this.onTick, this.gameTick);
} }
onFreeze() {
this.freezeTetromino();
this.createTetromino();
}
onTick(callback: Function) { 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()); callback(this.score, this.level, this.getFinalGrid());
if (this.canLevelUp()) { if (this.canLevelUp()) {
this.level++; this.level++;
@ -281,8 +225,10 @@ export default class GameLogic {
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,
if (this.tryMoveTetromino(x, y)) { this.currentGrid, this.getWidth(), this.getHeight(),
() => this.onFreeze());
if (moved) {
if (y === 1) { if (y === 1) {
this.score++; this.score++;
callback(this.getFinalGrid(), this.score); callback(this.getFinalGrid(), this.score);
@ -301,7 +247,7 @@ export default class GameLogic {
if (!this.canUseInput()) if (!this.canUseInput())
return; return;
if (this.tryRotateTetromino()) if (this.currentObject.tryRotate(this.currentGrid, this.getWidth(), this.getHeight()))
callback(this.getFinalGrid()); callback(this.getFinalGrid());
} }
@ -312,15 +258,14 @@ export default class GameLogic {
generateNextPieces() { generateNextPieces() {
while (this.nextPieces.length < this.nextPiecesCount) { while (this.nextPieces.length < this.nextPiecesCount) {
let shape = Math.floor(Math.random() * 7); this.nextPieces.push(new Piece(this.colors));
this.nextPieces.push(new Tetromino(shape, this.colors));
} }
} }
createTetromino() { createTetromino() {
this.pressedOut(); this.pressedOut();
this.recoverNextPiece(); this.recoverNextPiece();
if (!this.isTetrominoPositionValid()) if (!this.currentObject.isPositionValid(this.currentGrid, this.getWidth(), this.getHeight()))
this.endGame(false); this.endGame(false);
} }

85
screens/Tetris/Piece.js Normal file
View file

@ -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<Array<Object>>, 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);
}
}

View file

@ -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<Array<number>>;
#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<Array<Array<number>>> {
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;
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -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],
],
];
}
}

View file

@ -253,7 +253,7 @@ class TetrisScreen extends React.Component<Props, State> {
right: 5, right: 5,
}}> }}>
<Preview <Preview
next={this.logic.getNextPieces()} next={this.logic.getNextPiecesPreviews()}
/> />
</View> </View>
<View style={{ <View style={{

View file

@ -1,230 +0,0 @@
// @flow
export default class Tetromino {
static types = {
'I': 0,
'O': 1,
'T': 2,
'S': 3,
'Z': 4,
'J': 5,
'L': 6,
};
static shapes = [
[
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[1, 1],
[1, 1],
],
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0],
],
[
[0, 1, 1],
[1, 1, 0],
[0, 0, 0],
],
[
[1, 1, 0],
[0, 1, 1],
[0, 0, 0],
],
[
[1, 0, 0],
[1, 1, 1],
[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],
],
[
[1, 1],
[1, 1],
],
[
[0, 1, 0],
[0, 1, 1],
[0, 1, 0],
],
[
[0, 1, 0],
[0, 1, 1],
[0, 0, 1],
],
[
[0, 0, 1],
[0, 1, 1],
[0, 1, 0],
],
[
[0, 1, 1],
[0, 1, 0],
[0, 1, 0],
],
[
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
],
],
[
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[1, 1],
[1, 1],
],
[
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
],
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
],
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
],
[
[0, 0, 0],
[1, 1, 1],
[0, 0, 1],
],
[
[0, 0, 0],
[1, 1, 1],
[1, 0, 0],
],
],
[
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
],
[
[1, 1],
[1, 1],
],
[
[0, 1, 0],
[1, 1, 0],
[0, 1, 0],
],
[
[1, 0, 0],
[1, 1, 0],
[0, 1, 0],
],
[
[0, 1, 0],
[1, 1, 0],
[1, 0, 0],
],
[
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
],
[
[1, 1, 0],
[0, 1, 0],
[0, 1, 0],
],
],
];
static colors: Object;
currentType: number;
currentShape: Object;
currentRotation: number;
position: Object;
colors: Object;
constructor(type: number, colors: Object) {
this.currentType = type;
this.currentRotation = 0;
this.currentShape = Tetromino.shapes[this.currentRotation][type];
this.position = {x: 0, y: 0};
if (this.currentType === Tetromino.types.O)
this.position.x = 4;
else
this.position.x = 3;
this.colors = colors;
Tetromino.colors = [
colors.tetrisI,
colors.tetrisO,
colors.tetrisT,
colors.tetrisS,
colors.tetrisZ,
colors.tetrisJ,
colors.tetrisL,
];
}
getColor() {
return Tetromino.colors[this.currentType];
}
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.currentRotation++;
else
this.currentRotation--;
if (this.currentRotation > 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;
}
}

View file

@ -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},
]);
});