Files
grin-web-wallet/scripts/tetris.js
2024-12-20 18:08:44 -08:00

1582 lines
44 KiB
JavaScript
Executable File

// Use strict
"use strict";
// Classes
// Tetris class
class Tetris {
// Public
// Constructor
constructor(application, message, wakeLock) {
// Set application
this.application = application;
// Set message
this.message = message;
// Set wake lock
this.wakeLock = wakeLock;
// Get Tetris display
this.tetrisDisplay = $("section.tetris");
// Get body display
this.bodyDisplay = $("body");
// Get canvas
this.canvas = this.tetrisDisplay.find("canvas").get(0);
// Get context
this.context = this.canvas.getContext("2d");
// Reset
this.reset();
// Set self
var self = this;
// Window resize event
$(window).on("resize", function() {
// Check if running
if(self.running === true) {
// Update size
self.updateSize();
// Draw
self.draw();
}
});
// Document key down event
$(document).on("keydown", function(event) {
// Check if running and not game over
if(self.running === true && self.gameOver === false) {
// Check which key was pressed
switch(event["which"]) {
// Key left
case Tetris.KEY_LEFT:
// Move left
self.moveLeft();
// Break
break;
// Key up
case Tetris.KEY_UP:
// Rotate left
self.rotateLeft();
// Break
break;
// Key right
case Tetris.KEY_RIGHT:
// Move right
self.moveRight();
// Break
break;
// Key down
case Tetris.KEY_DOWN:
// Rotate right
self.rotateRight();
// Break
break;
}
}
});
// Initialize start touch position
var startTouchPositionX = Tetris.NO_TOUCH_POSITION;
var startTouchPositionY = Tetris.NO_TOUCH_POSITION;
// Initialize last touch position
var lastTouchPositionX = Tetris.NO_TOUCH_POSITION;
var lastTouchPositionY = Tetris.NO_TOUCH_POSITION;
// Document touch start event
$(document).on("touchstart", function(event) {
// Check if running and not game over
if(self.running === true && self.gameOver === false) {
// Set start touch position
startTouchPositionX = event["touches"][0]["screenX"];
startTouchPositionY = event["touches"][0]["screenY"];
// Set last touch position
lastTouchPositionX = event["touches"][0]["screenX"];
lastTouchPositionY = event["touches"][0]["screenY"];
}
// Document touch move event
}).on("touchmove", function() {
// Check if running and not game over
if(self.running === true && self.gameOver === false) {
// Set last touch position
lastTouchPositionX = event["touches"][0]["screenX"];
lastTouchPositionY = event["touches"][0]["screenY"];
}
// Document touch end event
}).on("touchend", function(event) {
// Check if running and not game over
if(self.running === true && self.gameOver === false) {
// Check if start position exists
if(startTouchPositionX !== Tetris.NO_TOUCH_POSITION && startTouchPositionY !== Tetris.NO_TOUCH_POSITION) {
// Get swipe length
var swipeLengthX = (lastTouchPositionX - startTouchPositionX) / screen["width"];
var swipeLengthY = (lastTouchPositionY - startTouchPositionY) / screen["height"];
// Check if swipe right
if(swipeLengthX >= Tetris.SWIPE_SCREEN_LENGTH_THRESHOLD_PERCENT) {
// Move right
self.moveRight();
}
// Otherwise check if swipe left
else if(swipeLengthX <= -Tetris.SWIPE_SCREEN_LENGTH_THRESHOLD_PERCENT) {
// Move left
self.moveLeft();
}
// Otherwise check if swipe down
else if(swipeLengthY >= Tetris.SWIPE_SCREEN_LENGTH_THRESHOLD_PERCENT) {
// Rotate right
self.rotateRight();
}
// Otherwise check if swipe up
else if(swipeLengthY <= -Tetris.SWIPE_SCREEN_LENGTH_THRESHOLD_PERCENT) {
// Rotate left
self.rotateLeft();
}
}
}
// Reset start touch position
startTouchPositionX = Tetris.NO_TOUCH_POSITION;
startTouchPositionY = Tetris.NO_TOUCH_POSITION;
// Document touch cancel event
}).on("touchcancel", function(event) {
// Reset start touch position
startTouchPositionX = Tetris.NO_TOUCH_POSITION;
startTouchPositionY = Tetris.NO_TOUCH_POSITION;
});
// Initialize last timestamp
var lastTimestamp = 0;
// On frame
var onFrame = function(timestamp) {
// Check if running and not game over
if(self.running === true) {
// Run
self.run(timestamp - lastTimestamp);
// Draw
self.draw();
}
// Update last timestamp
lastTimestamp = timestamp;
// Request animation frame
requestAnimationFrame(onFrame);
};
// On frame
onFrame(0);
}
// Show
show() {
// Keep device awake and catch errors
this.wakeLock.preventLock().catch(function(error) {
});
// Prevent showing messages
this.message.prevent();
// Blur focus
$(":focus").blur();
// Check if application's create display is shown
if(this.application.isCreateDisplayShown() === true) {
// Disable tabbing to everything in application's create display and disable everything in application's create display
this.application.createDisplay.find("*").disableTab().disable();
}
// Otherwise check if application's unlock display is shown
else if(this.application.isUnlockDisplayShown() === true) {
// Disable tabbing to everything in application's unlock display and disable everything in application's unlock display
this.application.unlockDisplay.find("*").disableTab().disable();
}
// Show loading
this.application.showLoading();
// Update size
this.updateSize();
// Draw
this.draw();
// Show Tetris display
this.tetrisDisplay.removeClass("hide");
// Set that body display is showing Tetris
this.bodyDisplay.addClass("tetris");
// Set self
var self = this;
// Tetris display transition end or timeout event
this.tetrisDisplay.transitionEndOrTimeout(function() {
// Hide loading
self.application.hideLoading();
// Set running
self.running = true;
}, "opacity");
}
// Is shown
isShown() {
// Return if Tetris display is shown
return this.tetrisDisplay.hasClass("hide") === false;
}
// Private
// Hide
hide() {
// Show loading
this.application.showLoading();
// Hide Tetris display
this.tetrisDisplay.addClass("hide");
// Set self
var self = this;
// Tetris display transition end or timeout event
this.tetrisDisplay.transitionEndOrTimeout(function() {
// Set that body display isn't showing Tetris
self.bodyDisplay.removeClass("tetris");
// Reset
self.reset();
// Allow device to sleep and catch errors
self.wakeLock.allowLock().catch(function(error) {
// Finally
}).finally(function() {
// Hide loading
self.application.hideLoading();
// Check if application's create display is shown
if(self.application.isCreateDisplayShown() === true) {
// Enable tabbing to everything in application's create display and enable everything in application's create display
self.application.createDisplay.find("*").enableTab().enable();
}
// Otherwise check if application's unlock display is shown
else if(self.application.isUnlockDisplayShown() === true) {
// Enable tabbing to everything in application's unlock display and enable everything in application's unlock display
self.application.unlockDisplay.find("*").enableTab().enable();
}
// Allow showing messages
self.message.allow();
});
}, "opacity");
}
// Update size
updateSize() {
// Remove canvas's width and height
$(this.canvas).height("").width("");
// Check if canvas's width can't be equal to the height's ratio
if(this.canvas["clientWidth"] < this.canvas["clientHeight"] * Tetris.WIDTH_TO_HEIGHT_RATIO) {
// Update canvas internal height
this.canvas["height"] = this.canvas["clientWidth"] / Tetris.WIDTH_TO_HEIGHT_RATIO;
}
// Otherwise
else {
// Update canvas internal height
this.canvas["height"] = this.canvas["clientHeight"];
}
// Set canvas's internal width
this.canvas["width"] = this.canvas["height"] * Tetris.WIDTH_TO_HEIGHT_RATIO;
// Set canvas's width and height
$(this.canvas).height(this.canvas["height"]).width(this.canvas["width"]);
}
// Draw
draw() {
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"]; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"]; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Set field at the position to the tetromino
this.field[this.positionX + j][this.positionY + i] = true;
}
}
}
}
// Clear
this.context.clearRect(0, 0, this.canvas["width"], this.canvas["height"]);
// Set fill color
this.context["fillStyle"] = Tetris.FILL_COLOR;
this.context["strokeStyle"] = Tetris.FILL_COLOR;
// Get unit width and height
var unitWidth = this.canvas["width"] / (Tetris.FIELD_WIDTH + Tetris.BORDER_WIDTH * 2);
var unitHeight = this.canvas["height"] / (Tetris.FIELD_HEIGHT + Tetris.BORDER_HEIGHT);
// Get corner angle radians
var cornerAngleRadians = Tetris.getUnitCornerAngleRadians(unitWidth);
// Go through all units in the field that can have a unit to its right
for(var i = 0; i < Tetris.FIELD_WIDTH - 1; ++i) {
for(var j = 0; j < Tetris.FIELD_HEIGHT; ++j) {
// Check if unit is set and the unit to its right is set
if(this.field[i][j] === true && this.field[i + 1][j] === true) {
// Draw rectangle
this.context.fillRect(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i + unitWidth / 2 + cornerAngleRadians, unitHeight * j + cornerAngleRadians, unitWidth - cornerAngleRadians * 2, unitHeight - cornerAngleRadians * 2);
// Mask out top and bottom of rectangle
this.context["globalCompositeOperation"] = "destination-out";
this.drawUnit(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i + unitWidth / 2, unitHeight * j - unitHeight / 2, unitWidth, unitHeight);
this.drawUnit(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i + unitWidth / 2, unitHeight * j + unitHeight / 2, unitWidth, unitHeight);
this.context["globalCompositeOperation"] = "source-over";
}
}
}
// Go through all units in the field that can have a unit under it
for(var i = 0; i < Tetris.FIELD_WIDTH; ++i) {
for(var j = 0; j < Tetris.FIELD_HEIGHT - 1; ++j) {
// Check if unit is set and the unit under it is set
if(this.field[i][j] === true && this.field[i][j + 1] === true) {
// Draw rectangle
this.context.fillRect(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i + cornerAngleRadians, unitHeight * j + unitHeight / 2 + cornerAngleRadians, unitWidth - cornerAngleRadians * 2, unitHeight - cornerAngleRadians * 2);
// Mask out left and right of rectangle
this.context["globalCompositeOperation"] = "destination-out";
this.drawUnit(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i + unitWidth / 2, unitHeight * j + unitHeight / 2, unitWidth, unitHeight);
this.drawUnit(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i - unitWidth / 2, unitHeight * j + unitHeight / 2, unitWidth, unitHeight);
this.context["globalCompositeOperation"] = "source-over";
}
}
}
// Go through all units in the field
for(var i = 0; i < Tetris.FIELD_WIDTH; ++i) {
for(var j = 0; j < Tetris.FIELD_HEIGHT; ++j) {
// Check if unit is set
if(this.field[i][j] === true) {
// Draw unit
this.context["globalCompositeOperation"] = "destination-over";
this.drawUnit(unitWidth * Tetris.BORDER_WIDTH + unitWidth * i, unitHeight * j, unitWidth, unitHeight);
this.context["globalCompositeOperation"] = "source-over";
}
}
}
// Draw Left border
this.context.fillRect(0, 0, unitWidth * Tetris.BORDER_WIDTH, this.canvas["height"]);
// Draw right border
this.context.fillRect(this.canvas["width"] - unitWidth * Tetris.BORDER_WIDTH, 0, unitWidth * Tetris.BORDER_WIDTH, this.canvas["height"]);
// Draw bottom border
this.context.fillRect(0, this.canvas["height"] - unitHeight * Tetris.BORDER_HEIGHT, this.canvas["width"], unitHeight * Tetris.BORDER_HEIGHT);
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"]; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"]; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Remove field at the position to the tetromino
this.field[this.positionX + j][this.positionY + i] = false;
}
}
}
}
}
// Reset
reset() {
// Set score to zero
this.score = 0;
// Set running to false;
this.running = false;
// Set run time to zero
this.runTime = 0;
// Set last move time to zero
this.lastMoveTime = 0;
// Set game over time to zero
this.gameOverTime = 0;
// Set move time milliseconds to the initial move speed
this.moveTimeMilliseconds = Tetris.INITIAL_MOVE_SPEED_MILLISECONDS;
// Set lines cleared to zero
this.linesCleared = 0;
// Set game over to false
this.gameOver = false;
// Set game over row
this.gameOverRow = Tetris.FIELD_HEIGHT - 1;
// Initialize field
this.field = new Array(Tetris.FIELD_WIDTH);
// Go through all units in field width
for(var i = 0; i < Tetris.FIELD_WIDTH; ++i) {
// Initialize field width
this.field[i] = new Array(Tetris.FIELD_HEIGHT);
// Go through all units in field height
for(var j = 0; j < Tetris.FIELD_HEIGHT; ++j) {
// Initialize field height
this.field[i][j] = false;
}
}
// Set random tetromino
this.tetromino = Object.keys(Tetris.TETROMINOES)[Common.randomNumber(0, Object.keys(Tetris.TETROMINOES)["length"] - 1)];
// Set random orientation
this.orientation = Common.randomNumber(0, Tetris.TETROMINOES[this.tetromino]["length"] - 1);
// Initialize starting offset found
var startingOffsetFound = false;
// Go through all rows in the orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation][0]["length"] && startingOffsetFound === false; ++i) {
// Go through all columns in the orientation
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && startingOffsetFound === false; ++j) {
// Check if unit at the orientation row and column is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][j][i] === true) {
// Set starting offset
var startingOffset = i;
// Set starting offset found
startingOffsetFound = true;
}
}
}
// Initialize ending offset found
var endingOffsetFound = false;
// Go through all rows in the orientation
for(var i = Tetris.TETROMINOES[this.tetromino][this.orientation][0]["length"] - 1; i >= 0 && endingOffsetFound === false; --i) {
// Go through all columns in the orientation
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && endingOffsetFound === false; ++j) {
// Check if unit at the orientation row and column is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][j][i] === true) {
// Set ending offset
var endingOffset = i;
// Set ending offset found
endingOffsetFound = true;
}
}
}
// Set additional offset
var additionalOffset = (this.tetromino === "I" && this.orientation === 1) || (this.tetromino === "S" && this.orientation === 0) || (this.tetromino === "Z" && this.orientation === 2) || (this.tetromino === "L" && this.orientation === 3) || (this.tetromino === "J" && this.orientation === 3) || (this.tetromino === "T" && this.orientation === 2);
// Set position X to be at the center
this.positionX = Math.floor((Tetris.FIELD_WIDTH - (endingOffset - startingOffset + 1)) / 2) - startingOffset + ((additionalOffset === true) ? 1 : 0);
// Initialize highest row found
var highestRowFound = false;
// Go through all rows in the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && highestRowFound === false; ++i) {
// Go through all units in the row
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && highestRowFound === false; ++j) {
// Check if unit is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Set position Y so that this row is on the top row
this.positionY = -i;
// Set highest row found
highestRowFound = true;
}
}
}
// Initialize collision occurred
var collisionOccurred = false;
// Loop until a collision doesn't occur
do {
// Clear collision occurred
collisionOccurred = false;
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && collisionOccurred === false; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if tetromino exists at the position and the position is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true && this.field[this.positionX + j][this.positionY + i] === true) {
// Move tetromino up
--this.positionY;
// Set collision occurred
collisionOccurred = true;
}
}
}
}
} while(collisionOccurred === true);
}
// Run
run(deltaTime) {
// Update run time
this.runTime += deltaTime;
// Check if not game over
if(this.gameOver === false) {
// Check if time to move
if(this.runTime - this.lastMoveTime >= this.moveTimeMilliseconds) {
// Update last move time
this.lastMoveTime = this.runTime;
// Initialize done moving
var doneMoving = false;
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && doneMoving === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && doneMoving === false; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i + 1 >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Set tetromino exists at the position and the position under it is the floor or is another tetromino
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true && (this.positionY + i + 1 === Tetris.FIELD_HEIGHT || this.field[this.positionX + j][this.positionY + i + 1] === true)) {
// Set done moving
doneMoving = true;
}
}
}
}
// Check if not done moving
if(doneMoving === false) {
// Update position Y
++this.positionY;
}
// Otherwise
else {
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && this.gameOver === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && this.gameOver === false; ++j) {
// Check of position is above the field
if(this.positionY + i < 0) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Set game over
this.gameOver = true;
// Set game over time
this.gameOverTime = this.runTime;
}
}
}
}
// Check if not game over
if(this.gameOver === false) {
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"]; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"]; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Set field at the position to the tetromino
this.field[this.positionX + j][this.positionY + i] = true;
}
}
}
}
// Initialize lines cleared
var linesCleared = 0;
// Go through all rows in the field
for(var i = 0; i < Tetris.FIELD_HEIGHT; ++i) {
for(var j = 0; j < Tetris.FIELD_WIDTH; ++j) {
// Check if unit isn't set
if(this.field[j][i] === false) {
// Break
break;
}
// Otherwise check if at the last unit in the row
else if(j === Tetris.FIELD_WIDTH - 1) {
// Increment lines cleared
++linesCleared;
// Go through all rows at and above the row
for(var k = i; k >= 0; --k) {
for(var l = 0; l < Tetris.FIELD_WIDTH; ++l) {
// Check if at the top row
if(k === 0) {
// Set unit to false
this.field[l][k] = false;
}
// Otherwise
else {
// Set unit to the one above it
this.field[l][k] = this.field[l][k - 1];
}
}
}
}
}
}
// Check if ten more lines were cleared
if(Math.floor((this.linesCleared + linesCleared) / 10) > Math.floor(this.linesCleared / 10)) {
// Update move speed
this.moveTimeMilliseconds -= Tetris.MOVE_SPEED_CHANGE_MILLISECONDS;
// Check if move speed is to fast
if(this.moveTimeMilliseconds < Tetris.MINIMUM_MOVE_SPEED_MILLISECONDS) {
// Set move speed to minimum move speed
this.moveTimeMilliseconds = Tetris.MINIMUM_MOVE_SPEED_MILLISECONDS;
}
}
// Update score
this.score += linesCleared * linesCleared;
// Update lines cleared
this.linesCleared += linesCleared;
// Set random tetromino
this.tetromino = Object.keys(Tetris.TETROMINOES)[Common.randomNumber(0, Object.keys(Tetris.TETROMINOES)["length"] - 1)];
// Set random orientation
this.orientation = Common.randomNumber(0, Tetris.TETROMINOES[this.tetromino]["length"] - 1);
// Initialize starting offset found
var startingOffsetFound = false;
// Go through all rows in the orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation][0]["length"] && startingOffsetFound === false; ++i) {
// Go through all columns in the orientation
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && startingOffsetFound === false; ++j) {
// Check if unit at the orientation row and column is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][j][i] === true) {
// Set starting offset
var startingOffset = i;
// Set starting offset found
startingOffsetFound = true;
}
}
}
// Initialize ending offset found
var endingOffsetFound = false;
// Go through all rows in the orientation
for(var i = Tetris.TETROMINOES[this.tetromino][this.orientation][0]["length"] - 1; i >= 0 && endingOffsetFound === false; --i) {
// Go through all columns in the orientation
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && endingOffsetFound === false; ++j) {
// Check if unit at the orientation row and column is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][j][i] === true) {
// Set ending offset
var endingOffset = i;
// Set ending offset found
endingOffsetFound = true;
}
}
}
// Set additional offset
var additionalOffset = (this.tetromino === "I" && this.orientation === 1) || (this.tetromino === "S" && this.orientation === 0) || (this.tetromino === "Z" && this.orientation === 2) || (this.tetromino === "L" && this.orientation === 3) || (this.tetromino === "J" && this.orientation === 3) || (this.tetromino === "T" && this.orientation === 2);
// Set position X to be at the center
this.positionX = Math.floor((Tetris.FIELD_WIDTH - (endingOffset - startingOffset + 1)) / 2) - startingOffset + ((additionalOffset === true) ? 1 : 0);
// Initialize highest row found
var highestRowFound = false;
// Go through all rows in the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && highestRowFound === false; ++i) {
// Go through all units in the row
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && highestRowFound === false; ++j) {
// Check if unit is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Set position Y so that this row is on the top row
this.positionY = -i;
// Set highest row found
highestRowFound = true;
}
}
}
// Initialize collision occurred
var collisionOccurred = false;
// Loop until a collision doesn't occur
do {
// Clear collision occurred
collisionOccurred = false;
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && collisionOccurred === false; ++j) {
// Check of position exists in the field
if(this.positionX + j >= 0 && this.positionX + j < Tetris.FIELD_WIDTH && this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if tetromino exists at the position and the position is set
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true && this.field[this.positionX + j][this.positionY + i] === true) {
// Move tetromino up
--this.positionY;
// Set collision occurred
collisionOccurred = true;
}
}
}
}
} while(collisionOccurred === true);
}
}
}
}
// Otherwise
else {
// Check if time to show game over row
if(this.runTime - this.gameOverTime >= Tetris.GAME_OVER_ROW_SPEED_MILLISECONDS) {
// Update game over time
this.gameOverTime = this.runTime;
// Go through all units in field width
for(var i = 0; i < Tetris.FIELD_WIDTH; ++i) {
// Check if game over row exists in the field
if(this.gameOverRow >= 0) {
// Set field in game over row
this.field[i][this.gameOverRow] = true;
}
}
// Check if all game over rows have been shown
if(this.gameOverRow === 0) {
// Set self
var self = this;
// Set timeout
setTimeout(function() {
// Hide
self.hide();
}, Tetris.GAME_OVER_DELAY_HIDE_MILLISECONDS);
}
// Check if there are more game over rows
if(this.gameOverRow >= 0) {
// Update game over row
--this.gameOverRow;
}
}
}
}
// Draw unit
drawUnit(x, y, width, height) {
// Save state
this.context.save();
// Set translation
this.context.translate(x + width / 2, y + height / 2);
// Set rotation
this.context.rotate(Tetris.UNIT_ANGLE_RADIANS);
// Get corner angle radians
var cornerAngleRadians = Tetris.getUnitCornerAngleRadians(width);
// Check if masking out shape
if(this.context["globalCompositeOperation"] === "destination-out") {
// Scale width and height
width *= Tetris.UNIT_MASK_DIMENSIONS_SCALE;
height *= Tetris.UNIT_MASK_DIMENSIONS_SCALE;
}
// Otherwise
else {
// Scale width and height
width *= Tetris.UNIT_DIMENSIONS_SCALE;
height *= Tetris.UNIT_DIMENSIONS_SCALE;
}
// Draw path
this.context.beginPath();
this.context.moveTo(-width / 2 + cornerAngleRadians, -height / 2);
this.context.lineTo(width / 2 - cornerAngleRadians, -height / 2);
this.context.quadraticCurveTo(width / 2, -height / 2, width / 2, -height / 2 + cornerAngleRadians);
this.context.lineTo(width / 2, height / 2 - cornerAngleRadians);
this.context.quadraticCurveTo(width / 2, height / 2, width / 2 - cornerAngleRadians, height / 2);
this.context.lineTo(-width / 2 + cornerAngleRadians, height / 2);
this.context.quadraticCurveTo(-width / 2, height / 2, -width / 2, height / 2 - cornerAngleRadians);
this.context.lineTo(-width / 2, -height / 2 + cornerAngleRadians);
this.context.quadraticCurveTo(-width / 2, -height / 2, -width / 2 + cornerAngleRadians, -height / 2);
this.context.closePath();
this.context.fill();
// Restore state
this.context.restore();
}
// Move left
moveLeft() {
// Initialize collision occurred
var collisionOccurred = false;
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && collisionOccurred === false; ++j) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Check of position to the left doesn't exists in the field
if(this.positionX + j - 1 < 0 || this.positionX + j - 1 >= Tetris.FIELD_WIDTH) {
// Set collision occurred
collisionOccurred = true;
}
// Otherwise check if the position exists in the field
else if(this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if the position is set
if(this.field[this.positionX + j - 1][this.positionY + i] === true) {
// Set collision occurred
collisionOccurred = true;
}
}
}
}
}
// Check if a collision didn't occur
if(collisionOccurred === false) {
// Update position X
--this.positionX;
}
}
// Move right
moveRight() {
// Initialize collision occurred
var collisionOccurred = false;
// Go through all heights of the tetromino's orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][this.orientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the tetromino's orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][this.orientation][i]["length"] && collisionOccurred === false; ++j) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][this.orientation][i][j] === true) {
// Check of position to the right doesn't exists in the field
if(this.positionX + j + 1 < 0 || this.positionX + j + 1 >= Tetris.FIELD_WIDTH) {
// Set collision occurred
collisionOccurred = true;
}
// Otherwise check if the position exists in the field
else if(this.positionY + i >= 0 && this.positionY + i < Tetris.FIELD_HEIGHT) {
// Check if the position is set
if(this.field[this.positionX + j + 1][this.positionY + i] === true) {
// Set collision occurred
collisionOccurred = true;
}
}
}
}
}
// Check if a collision didn't occur
if(collisionOccurred === false) {
// Update position X
++this.positionX;
}
}
// Rotate left
rotateLeft() {
// Check if can rotate left
if(this.orientation > 0) {
// Set new orientation
var newOrientation = this.orientation - 1;
}
// Otherwise
else {
// Set new orientation
var newOrientation = Tetris.TETROMINOES[this.tetromino]["length"] - 1;
}
// Initialize collision occurred
var collisionOccurred = false;
// Go through all heights of the new orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][newOrientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the new orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][newOrientation][i]["length"] && collisionOccurred === false; ++j) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][newOrientation][i][j] === true) {
// Check of position doesn't exists in the field
if(this.positionX + j < 0 || this.positionX + j >= Tetris.FIELD_WIDTH || this.positionY + i >= Tetris.FIELD_HEIGHT) {
// Set collision occurred
collisionOccurred = true;
}
// Otherwise check if the position exists in the field
else if(this.positionY + i >= 0) {
// Check if the position is set
if(this.field[this.positionX + j][this.positionY + i] === true) {
// Set collision occurred
collisionOccurred = true;
}
}
}
}
}
// Check if a collision didn't occur
if(collisionOccurred === false) {
// Update orientation
this.orientation = newOrientation;
}
}
// Rotate right
rotateRight() {
// Check if can rotate left
if(this.orientation < Tetris.TETROMINOES[this.tetromino]["length"] - 1) {
// Set new orientation
var newOrientation = this.orientation + 1;
}
// Otherwise
else {
// Set new orientation
var newOrientation = 0;
}
// Initialize collision occurred
var collisionOccurred = false;
// Go through all heights of the new orientation
for(var i = 0; i < Tetris.TETROMINOES[this.tetromino][newOrientation]["length"] && collisionOccurred === false; ++i) {
// Go through all widths of the new orientation height
for(var j = 0; j < Tetris.TETROMINOES[this.tetromino][newOrientation][i]["length"] && collisionOccurred === false; ++j) {
// Check if tetromino exists at the position
if(Tetris.TETROMINOES[this.tetromino][newOrientation][i][j] === true) {
// Check of position doesn't exists in the field
if(this.positionX + j < 0 || this.positionX + j >= Tetris.FIELD_WIDTH || this.positionY + i >= Tetris.FIELD_HEIGHT) {
// Set collision occurred
collisionOccurred = true;
}
// Otherwise check if the position exists in the field
else if(this.positionY + i >= 0) {
// Check if the position is set
if(this.field[this.positionX + j][this.positionY + i] === true) {
// Set collision occurred
collisionOccurred = true;
}
}
}
}
}
// Check if a collision didn't occur
if(collisionOccurred === false) {
// Update orientation
this.orientation = newOrientation;
}
}
// Unit corner angle radians
static getUnitCornerAngleRadians(unitWidth) {
// Return angle
return glMatrix["glMatrix"].toRadian(unitWidth * 8);
}
// Fill color
static get FILL_COLOR() {
// Return fill color
return "rgba(255, 255, 255, 1)";
}
// Field width
static get FIELD_WIDTH() {
// Return field width
return 10;
}
// Field height
static get FIELD_HEIGHT() {
// Return field height
return 20;
}
// Border width
static get BORDER_WIDTH() {
// Return border width
return 1;
}
// Border height
static get BORDER_HEIGHT() {
// Return border height
return 1;
}
// Width to height ratio
static get WIDTH_TO_HEIGHT_RATIO() {
// Return width to height ratio
return (Tetris.FIELD_WIDTH + Tetris.BORDER_WIDTH * 2) / (Tetris.FIELD_HEIGHT + Tetris.BORDER_HEIGHT);
}
// Unit angle radians
static get UNIT_ANGLE_RADIANS() {
// Return unit angle radians
return glMatrix["glMatrix"].toRadian(45);
}
// Unit dimensions scale
static get UNIT_DIMENSIONS_SCALE() {
// Return unit dimentions scale
return 0.8;
}
// Unit mask dimensions scale
static get UNIT_MASK_DIMENSIONS_SCALE() {
// Return unit mask dimentions scale
return 0.62;
}
// Key left
static get KEY_LEFT() {
// Return key left
return 37;
}
// Key up
static get KEY_UP() {
// Return key up
return 38;
}
// Key right
static get KEY_RIGHT() {
// Return key right
return 39;
}
// Key down
static get KEY_DOWN() {
// Return key down
return 40;
}
// Tetrominoes
static get TETROMINOES() {
// Return
return {
// I
"I": [
// First orientation
[
[false, false, false, false],
[false, false, false, false],
[true, true, true, true]
],
// Second orientation
[
[false, false, true],
[false, false, true],
[false, false, true],
[false, false, true]
],
// Third orientation
[
[false, false, false, false],
[false, false, false, false],
[true, true, true, true]
],
// Fourth orientation
[
[false, false],
[false, true],
[false, true],
[false, true],
[false, true]
]
],
// O
"O": [
// First orientation
[
[true, true],
[true, true]
]
],
// Z
"Z": [
// First orientation
[
[true, true, false],
[false, true, true]
],
// Second orientation
[
[false, false, true],
[false, true, true],
[false, true, false]
],
// Third orientation
[
[false, false, false],
[true, true, false],
[false, true, true]
],
// fourth orientation
[
[false, true, false],
[true, true, false],
[true, false, false]
]
],
// S
"S": [
// First orientation
[
[false, true, true],
[true, true, false]
],
// Second orientation
[
[false, true, false],
[false, true, true],
[false, false, true]
],
// Third orientation
[
[false, false, false],
[false, true, true],
[true, true, false]
],
// fourth orientation
[
[true, false, false],
[true, true, false],
[false, true, false]
]
],
// L
"L": [
// First orientation
[
[false, true, false],
[false, true, false],
[false, true, true]
],
// Second orientation
[
[false, false, false],
[true, true, true],
[true, false, false]
],
// Third orientation
[
[true, true, false],
[false, true, false],
[false, true, false]
],
// Fourth orientation
[
[false, false, true],
[true, true, true],
[false, false, false],
]
],
// J
"J": [
// First orientation
[
[false, true, false],
[false, true, false],
[true, true, false]
],
// Second orientation
[
[true, false, false],
[true, true, true],
[false, false, false]
],
// Third orientation
[
[false, true, true],
[false, true, false],
[false, true, false]
],
// Fourth orientation
[
[false, false, false],
[true, true, true],
[false, false, true]
]
],
// T
"T": [
// First orientation
[
[false, false, false],
[true, true, true],
[false, true, false]
],
// Second orientation
[
[false, true, false],
[true, true, false],
[false, true, false]
],
// Third orientation
[
[false, true, false],
[true, true, true],
[false, false, false]
],
// Fourth orientation
[
[false, true, false],
[false, true, true],
[false, true, false]
]
]
};
}
// Initial move speed milliseconds
static get INITIAL_MOVE_SPEED_MILLISECONDS() {
// Return initial move speed milliseconds
return 750;
}
// Move speed change milliseconds
static get MOVE_SPEED_CHANGE_MILLISECONDS() {
// Return move speed change milliseconds
return 100;
}
// Minimum move speed milliseconds
static get MINIMUM_MOVE_SPEED_MILLISECONDS() {
// Return minimum move speed milliseconds
return 100;
}
// No touch position
static get NO_TOUCH_POSITION() {
// Return no touch position
return null;
}
// Swipe screen length threshold percent
static get SWIPE_SCREEN_LENGTH_THRESHOLD_PERCENT() {
// Return swipe screen length threshold percent
return 0.04;
}
// Game over row speed milliseconds
static get GAME_OVER_ROW_SPEED_MILLISECONDS() {
// Return game over line speed milliseconds
return 25;
}
// Game over delay hide milliseconds
static get GAME_OVER_DELAY_HIDE_MILLISECONDS() {
// Return game over delay hide milliseconds
return 500;
}
}
// Main function
// Set global object's Tetris
globalThis["Tetris"] = Tetris;