2020-03-15 18:44:32 +01:00
|
|
|
// @flow
|
|
|
|
|
|
|
|
import Tetromino from "./Tetromino";
|
|
|
|
|
|
|
|
export default class GameLogic {
|
|
|
|
|
2020-03-16 20:10:54 +01:00
|
|
|
static levelTicks = {
|
|
|
|
'1': 1000,
|
|
|
|
'2': 900,
|
|
|
|
'3': 800,
|
|
|
|
'4': 700,
|
|
|
|
'5': 600,
|
|
|
|
'6': 500,
|
|
|
|
'7': 400,
|
|
|
|
'8': 300,
|
|
|
|
'9': 200,
|
|
|
|
'10': 150,
|
|
|
|
};
|
|
|
|
|
|
|
|
static levelThresholds = {
|
|
|
|
'1': 100,
|
|
|
|
'2': 300,
|
|
|
|
'3': 500,
|
|
|
|
'4': 700,
|
|
|
|
'5': 1000,
|
|
|
|
'7': 1500,
|
|
|
|
'8': 2000,
|
|
|
|
'9': 3000,
|
|
|
|
'10': 4000,
|
|
|
|
'11': 5000,
|
|
|
|
};
|
|
|
|
|
2020-03-15 18:44:32 +01:00
|
|
|
currentGrid: Array<Array<Object>>;
|
|
|
|
|
|
|
|
height: number;
|
|
|
|
width: number;
|
|
|
|
|
|
|
|
gameRunning: boolean;
|
2020-03-16 19:10:32 +01:00
|
|
|
gamePaused: boolean;
|
2020-03-15 18:44:32 +01:00
|
|
|
gameTime: number;
|
|
|
|
score: number;
|
2020-03-16 20:10:54 +01:00
|
|
|
level: number;
|
2020-03-15 18:44:32 +01:00
|
|
|
|
|
|
|
currentObject: Tetromino;
|
|
|
|
|
|
|
|
gameTick: number;
|
|
|
|
gameTickInterval: IntervalID;
|
2020-03-16 19:40:52 +01:00
|
|
|
gameTimeInterval: IntervalID;
|
2020-03-15 18:44:32 +01:00
|
|
|
|
|
|
|
onTick: Function;
|
2020-03-16 19:40:52 +01:00
|
|
|
onClock: Function;
|
2020-03-15 19:28:41 +01:00
|
|
|
endCallback: Function;
|
2020-03-15 18:44:32 +01:00
|
|
|
|
2020-03-15 20:34:20 +01:00
|
|
|
colors: Object;
|
|
|
|
|
|
|
|
constructor(height: number, width: number, colors: Object) {
|
2020-03-15 18:44:32 +01:00
|
|
|
this.height = height;
|
|
|
|
this.width = width;
|
|
|
|
this.gameRunning = false;
|
2020-03-16 19:10:32 +01:00
|
|
|
this.gamePaused = false;
|
2020-03-15 20:34:20 +01:00
|
|
|
this.colors = colors;
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
getHeight(): number {
|
|
|
|
return this.height;
|
|
|
|
}
|
|
|
|
|
|
|
|
getWidth(): number {
|
|
|
|
return this.width;
|
|
|
|
}
|
|
|
|
|
|
|
|
isGameRunning(): boolean {
|
|
|
|
return this.gameRunning;
|
|
|
|
}
|
|
|
|
|
2020-03-16 19:10:32 +01:00
|
|
|
isGamePaused(): boolean {
|
|
|
|
return this.gamePaused;
|
|
|
|
}
|
|
|
|
|
2020-03-16 14:58:13 +01:00
|
|
|
getEmptyLine() {
|
|
|
|
let line = [];
|
|
|
|
for (let col = 0; col < this.getWidth(); col++) {
|
|
|
|
line.push({
|
|
|
|
color: this.colors.tetrisBackground,
|
|
|
|
isEmpty: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
|
2020-03-15 18:44:32 +01:00
|
|
|
getEmptyGrid() {
|
|
|
|
let grid = [];
|
|
|
|
for (let row = 0; row < this.getHeight(); row++) {
|
2020-03-16 14:58:13 +01:00
|
|
|
grid.push(this.getEmptyLine());
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
return grid;
|
|
|
|
}
|
|
|
|
|
|
|
|
getGridCopy() {
|
|
|
|
return JSON.parse(JSON.stringify(this.currentGrid));
|
|
|
|
}
|
|
|
|
|
|
|
|
getFinalGrid() {
|
|
|
|
let coord = this.currentObject.getCellsCoordinates();
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return finalGrid;
|
|
|
|
}
|
|
|
|
|
|
|
|
freezeTetromino() {
|
|
|
|
let coord = this.currentObject.getCellsCoordinates();
|
|
|
|
for (let i = 0; i < coord.length; i++) {
|
|
|
|
this.currentGrid[coord[i].y][coord[i].x] = {
|
|
|
|
color: this.currentObject.getColor(),
|
|
|
|
isEmpty: false,
|
|
|
|
};
|
|
|
|
}
|
2020-03-16 14:58:13 +01:00
|
|
|
this.clearLines(this.getLinesToClear(coord));
|
|
|
|
}
|
|
|
|
|
|
|
|
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());
|
2020-03-16 19:26:42 +01:00
|
|
|
this.score += 100;
|
2020-03-16 14:58:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
isTetrominoPositionValid() {
|
|
|
|
let isValid = true;
|
|
|
|
let coord = this.currentObject.getCellsCoordinates();
|
|
|
|
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();
|
2020-03-16 19:26:42 +01:00
|
|
|
} else
|
|
|
|
return true;
|
|
|
|
return false;
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
2020-03-15 19:28:41 +01:00
|
|
|
tryRotateTetromino() {
|
|
|
|
this.currentObject.rotate(true);
|
2020-03-16 23:36:01 +01:00
|
|
|
if (!this.isTetrominoPositionValid()){
|
2020-03-15 19:28:41 +01:00
|
|
|
this.currentObject.rotate(false);
|
2020-03-16 23:36:01 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2020-03-15 19:28:41 +01:00
|
|
|
}
|
|
|
|
|
2020-03-16 20:10:54 +01:00
|
|
|
setNewGameTick(level: number) {
|
|
|
|
if (level > 10)
|
|
|
|
return;
|
|
|
|
this.gameTick = GameLogic.levelTicks[level];
|
|
|
|
clearInterval(this.gameTickInterval);
|
|
|
|
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
|
|
|
|
}
|
|
|
|
|
2020-03-15 18:44:32 +01:00
|
|
|
onTick(callback: Function) {
|
|
|
|
this.tryMoveTetromino(0, 1);
|
2020-03-16 20:10:54 +01:00
|
|
|
callback(this.score, this.level, this.getFinalGrid());
|
|
|
|
if (this.level <= 10 && this.score > GameLogic.levelThresholds[this.level]) {
|
|
|
|
this.level++;
|
|
|
|
this.setNewGameTick(this.level);
|
|
|
|
}
|
2020-03-16 19:40:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
onClock(callback: Function) {
|
|
|
|
this.gameTime++;
|
|
|
|
callback(this.gameTime);
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
2020-03-16 19:10:32 +01:00
|
|
|
canUseInput() {
|
|
|
|
return this.gameRunning && !this.gamePaused
|
|
|
|
}
|
|
|
|
|
2020-03-15 18:44:32 +01:00
|
|
|
rightPressed(callback: Function) {
|
2020-03-16 19:10:32 +01:00
|
|
|
if (!this.canUseInput())
|
|
|
|
return;
|
|
|
|
|
2020-03-16 19:26:42 +01:00
|
|
|
if (this.tryMoveTetromino(1, 0))
|
|
|
|
callback(this.getFinalGrid());
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
leftPressed(callback: Function) {
|
2020-03-16 19:10:32 +01:00
|
|
|
if (!this.canUseInput())
|
|
|
|
return;
|
|
|
|
|
2020-03-16 19:26:42 +01:00
|
|
|
if (this.tryMoveTetromino(-1, 0))
|
|
|
|
callback(this.getFinalGrid());
|
|
|
|
}
|
|
|
|
|
|
|
|
downPressed(callback: Function) {
|
|
|
|
if (!this.canUseInput())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this.tryMoveTetromino(0, 1)){
|
|
|
|
this.score++;
|
|
|
|
callback(this.getFinalGrid(), this.score);
|
|
|
|
}
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
2020-03-15 19:28:41 +01:00
|
|
|
rotatePressed(callback: Function) {
|
2020-03-16 19:10:32 +01:00
|
|
|
if (!this.canUseInput())
|
|
|
|
return;
|
|
|
|
|
2020-03-16 19:26:42 +01:00
|
|
|
if (this.tryRotateTetromino())
|
|
|
|
callback(this.getFinalGrid());
|
2020-03-15 19:28:41 +01:00
|
|
|
}
|
|
|
|
|
2020-03-15 18:44:32 +01:00
|
|
|
createTetromino() {
|
|
|
|
let shape = Math.floor(Math.random() * 7);
|
2020-03-15 20:34:20 +01:00
|
|
|
this.currentObject = new Tetromino(shape, this.colors);
|
2020-03-15 18:44:32 +01:00
|
|
|
if (!this.isTetrominoPositionValid())
|
2020-03-16 19:10:32 +01:00
|
|
|
this.endGame(false);
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
2020-03-16 19:10:32 +01:00
|
|
|
togglePause() {
|
|
|
|
if (!this.gameRunning)
|
|
|
|
return;
|
|
|
|
this.gamePaused = !this.gamePaused;
|
|
|
|
if (this.gamePaused) {
|
|
|
|
clearInterval(this.gameTickInterval);
|
2020-03-16 19:40:52 +01:00
|
|
|
clearInterval(this.gameTimeInterval);
|
2020-03-16 19:10:32 +01:00
|
|
|
} else {
|
|
|
|
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
|
2020-03-16 19:40:52 +01:00
|
|
|
this.gameTimeInterval = setInterval(this.onClock, 1000);
|
2020-03-16 19:10:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
endGame(isRestart: boolean) {
|
2020-03-15 19:28:41 +01:00
|
|
|
this.gameRunning = false;
|
2020-03-16 19:10:32 +01:00
|
|
|
this.gamePaused = false;
|
2020-03-15 18:44:32 +01:00
|
|
|
clearInterval(this.gameTickInterval);
|
2020-03-16 19:40:52 +01:00
|
|
|
clearInterval(this.gameTimeInterval);
|
2020-03-16 19:10:32 +01:00
|
|
|
this.endCallback(this.gameTime, this.score, isRestart);
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
2020-03-16 19:40:52 +01:00
|
|
|
startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) {
|
2020-03-15 18:44:32 +01:00
|
|
|
if (this.gameRunning)
|
2020-03-16 19:10:32 +01:00
|
|
|
this.endGame(true);
|
2020-03-15 18:44:32 +01:00
|
|
|
this.gameRunning = true;
|
2020-03-16 19:10:32 +01:00
|
|
|
this.gamePaused = false;
|
2020-03-15 18:44:32 +01:00
|
|
|
this.gameTime = 0;
|
|
|
|
this.score = 0;
|
2020-03-16 20:10:54 +01:00
|
|
|
this.level = 1;
|
|
|
|
this.gameTick = GameLogic.levelTicks[this.level];
|
2020-03-15 18:44:32 +01:00
|
|
|
this.currentGrid = this.getEmptyGrid();
|
|
|
|
this.createTetromino();
|
2020-03-16 20:10:54 +01:00
|
|
|
tickCallback(this.score, this.level, this.getFinalGrid());
|
2020-03-16 19:40:52 +01:00
|
|
|
clockCallback(this.gameTime);
|
2020-03-15 19:28:41 +01:00
|
|
|
this.onTick = this.onTick.bind(this, tickCallback);
|
2020-03-16 19:40:52 +01:00
|
|
|
this.onClock = this.onClock.bind(this, clockCallback);
|
2020-03-15 18:44:32 +01:00
|
|
|
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
|
2020-03-16 19:40:52 +01:00
|
|
|
this.gameTimeInterval = setInterval(this.onClock, 1000);
|
2020-03-15 19:28:41 +01:00
|
|
|
this.endCallback = endCallback;
|
2020-03-15 18:44:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|