diff --git a/index.html b/index.html
index fbf79f8..fbc4c88 100644
--- a/index.html
+++ b/index.html
@@ -85,6 +85,10 @@
+
+
+
+
diff --git a/js/arena.js b/js/arena.js
new file mode 100644
index 0000000..d95615d
--- /dev/null
+++ b/js/arena.js
@@ -0,0 +1,35 @@
+class Arena {
+ constructor(gameInfo, game) {
+ this.g = gameInfo;
+ this.game = game;
+ this.field = createMatrix(this.g.fieldSize.x, this.g.fieldSize.y);
+ }
+
+ clearScreen() {
+ this.g.context.clearRect(0, 0, this.g.canvas.width, this.g.canvas.height);
+ }
+
+ sweep() {
+ let rowCount = 1;
+ outer: for (let y = this.field.length - 1; y > 0; --y) {
+ for (let x = 0; x < this.field[y].length; ++x) {
+ if (this.field[y][x] === 0) {
+ continue outer;
+ }
+ }
+
+ const row = this.field.splice(y, 1)[0].fill(0);
+ this.field.unshift(row);
+ ++y;
+
+ this.p.score += rowCount * 10;
+ rowCount *= 2;
+ }
+ if (this.p.score - this.g.prevUpdateScore > 50) {
+ this.g.dropInterval -= 20;
+ this.g.dropInterval = this.g.dropInterval > 100 ? this.g.dropInterval : 100;
+ this.g.prevUpdateScore = this.p.score;
+ }
+ this.game.drawArena();
+ }
+}
\ No newline at end of file
diff --git a/js/game-info.js b/js/game-info.js
new file mode 100644
index 0000000..d6de3db
--- /dev/null
+++ b/js/game-info.js
@@ -0,0 +1,70 @@
+class GameInfo {
+ constructor(game) {
+ this.fieldSize = {x: 12, y: 20};
+ this.arena = new Arena(this, game);
+
+ this.player = new Player(this, game);
+
+ this.canvas = document.getElementById('tetris');
+ this.context = this.canvas.getContext('2d');
+
+ this.canvasBg = document.getElementById('tetris-background');
+ this.contextBg = this.canvasBg.getContext('2d');
+
+ this.canvasHold = document.getElementById('tetris-hold');
+ this.contextHold = this.canvasHold.getContext('2d');
+
+ this.canvasUpcoming = document.getElementById('tetris-upcoming');
+ this.contextUpcoming = this.canvasUpcoming.getContext('2d');
+
+ this.isPaused = true;
+
+ this.dropCounter = 0;
+ this.dropInterval = 1000;
+
+ this.keys = {
+ down: {
+ keys: [40, 83],
+ action: () => this.player.drop()
+ },
+ left: {
+ keys: [37, 65],
+ action: () => this.player.move(-1)
+ },
+ right: {
+ keys: [39, 68],
+ action: () => this.player.move(1)
+ },
+ rotateLeft: {
+ keys: [81],
+ action: () => this.player.rotate(-1)
+ },
+ rotateRight: {
+ keys: [69],
+ action: () => this.player.rotate(1)
+ },
+ holdTile: {
+ keys: [38, 87],
+ action: () => this.player.hold()
+ }
+ };
+
+ this.prevUpdateScore = 0;
+
+ this.lastScore = 0;
+ this.lastTime = 0;
+ this.lastTimeUpdate = Date.now();
+
+ this.startTime = 0;
+
+ this.upcomingTiles = [];
+
+ /*
+ default -> plain squares
+ retro -> original look
+ modern -> rounded corners
+ snake -> all tiles are connected
+ */
+ this.theme = 'default';
+ }
+}
\ No newline at end of file
diff --git a/js/game.js b/js/game.js
new file mode 100644
index 0000000..0227e35
--- /dev/null
+++ b/js/game.js
@@ -0,0 +1,178 @@
+class Game {
+ constructor() {
+ this.g = new GameInfo(this);
+ this.p = this.g.player;
+
+ this.registerListeners();
+ }
+
+ draw() {
+ this.g.context.clearRect(0, 0, this.g.canvas.width, this.g.canvas.height);
+ this.drawMatrix(this.p.matrix, this.p.pos);
+ }
+
+ drawArena() {
+ this.g.contextBg.fillStyle = '#000';
+ this.g.contextBg.fillRect(0, 0, this.g.canvas.width, this.g.canvas.height);
+ this.drawMatrix(this.g.arena.field, {x: 0, y: 0}, this.g.contextBg);
+ }
+
+ drawHolding() {
+ this.g.contextHold.clearRect(0, 0, this.g.canvasHold.width, this.g.canvasHold.height);
+ const offset = centerOffset(this.p.holdingTile);
+ const x = 3 - (this.p.holdingTile[0].length / 2) + offset.x;
+ const y = 3 - (this.p.holdingTile.length / 2) + offset.y;
+ this.drawMatrix(this.p.holdingTile, {x: x, y: y}, this.g.contextHold);
+ }
+
+ drawMatrix(matrix, offset, useContext = this.g.context) {
+ matrix.forEach((row, y) => {
+ row.forEach((value, x) => {
+ if (value !== 0) {
+ this.drawTile(x, y, offset, colors[value], matrix, useContext);
+ }
+ });
+ });
+ }
+
+ drawTile(x, y, offset, color, matrix, useContext = this.g.context) {
+ const ctx = useContext;
+ switch (this.g.theme) {
+ case "default":
+ ctx.fillStyle = color;
+ ctx.fillRect(x + offset.x + tileGap / 2, y + offset.y + tileGap / 2, 1 - tileGap, 1 - tileGap);
+ break;
+ case "modern":
+ ctx.fillStyle = color;
+ drawRoundRect(ctx, x + offset.x + tileGap / 2, y + offset.y + tileGap / 2, 1 - tileGap, 1 - tileGap, .15);
+ break;
+ case "snakes":
+ ctx.fillStyle = color;
+ let r1 = .15, // top right
+ r2 = .15, // bottom right
+ r3 = .15, // bottom left
+ r4 = .15; // top left
+ // Is there a tile to the left?
+ if (matrix[y][x - 1] > 0) {
+ r3 = 0;
+ r4 = 0;
+ }
+ // Is there a tile to the right?
+ if (matrix[y][x + 1] > 0) {
+ r1 = 0;
+ r2 = 0;
+ }
+ // Is there a tile to the top?
+ if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) {
+ r1 = 0;
+ r4 = 0;
+ }
+ // Is there a tile to the bottom?
+ if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) {
+ r2 = 0;
+ r3 = 0;
+ }
+ drawRoundRect(ctx, x + offset.x, y + offset.y, 1, 1, [r1, r2, r3, r4]);
+ break;
+ default:
+ this.g.theme = "default";
+ this.drawTile(x, y, offset, color, matrix, ctx);
+ break;
+ }
+ }
+
+ drawUpcoming() {
+ this.g.contextUpcoming.clearRect(0, 0, this.g.canvasUpcoming.width, this.g.canvasUpcoming.height);
+ let offsetY = 0;
+ let offset;
+ this.g.upcomingTiles.forEach((tile) => {
+ offset = centerOffset(tile);
+ const x = 3 - (tile[0].length / 2) + offset.x;
+ const y = offsetY + 3 - (tile.length / 2) + offset.y;
+ this.drawMatrix(tile, {x: x, y: y}, this.g.contextUpcoming);
+ offsetY += 6;
+ });
+ }
+
+ gameOver() {
+ this.g.arena.field.forEach(row => row.fill(0));
+ timePassed = 0;
+ this.g.lastTimeUpdate = Date.now();
+ this.updateTime();
+ this.p.score = 0;
+ this.g.dropInterval = 1000;
+ this.updateScore();
+ }
+
+ merge(arena, player) {
+ player.matrix.forEach((row, y) => {
+ row.forEach((value, x) => {
+ if (value !== 0) {
+ arena.field[y + player.pos.y][x + player.pos.x] = value;
+ }
+ });
+ });
+ this.drawArena();
+ }
+
+ registerListeners() {
+ // Keyboard controls
+ document.addEventListener('keydown', event => {
+ Object.keys(this.g.keys).map((objKey) => {
+ const keyBind = this.g.keys[objKey];
+ if (keyBind.keys.includes(event.keyCode)) {
+ keyBind.action();
+ }
+ });
+ });
+ }
+
+ saveHighscore() {
+ if (getCookie("highscore").value < this.p.score) {
+ document.cookie = "highscore=" + this.p.score + "; max-age=" + 60 * 60 * 24 * 365 * 1000 + "; path=/;";
+ }
+ }
+
+ start() {
+ this.drawArena();
+ this.p.addTile();
+ this.p.addTile();
+ this.p.addTile();
+ this.p.reset();
+ this.g.startTime = Date.now();
+ this.update();
+ this.updateScore();
+ }
+
+ update(time = 0) {
+ if (!this.g.isPaused) {
+ const deltaTime = time - this.g.lastTime;
+ this.g.lastTime = time;
+
+ this.g.dropCounter += deltaTime;
+ if (this.g.dropCounter > this.g.dropInterval) {
+ this.p.drop();
+ }
+
+ this.updateTime();
+
+ this.draw();
+ requestAnimationFrame(this.update.bind(this));
+ }
+ }
+
+ updateScore() {
+ if (this.g.lastScore !== this.p.score) {
+ scoreUpdateAni();
+ this.g.lastScore = this.p.score;
+ this.saveHighscore();
+ }
+ document.getElementById('score').innerText = this.p.score.toString();
+ }
+
+ updateTime() {
+ timePassed += Date.now() - this.g.lastTimeUpdate;
+ timeElement.innerHTML = formatMillis(timePassed);
+ this.g.lastTimeUpdate = Date.now();
+ }
+}
\ No newline at end of file
diff --git a/js/player.js b/js/player.js
new file mode 100644
index 0000000..56447da
--- /dev/null
+++ b/js/player.js
@@ -0,0 +1,86 @@
+class Player {
+ constructor(gameInfo, game) {
+ this.g = gameInfo;
+ this.game = game;
+ this.a = this.g.arena;
+ this.pos = {x: 0, y: 0};
+ this.matrix = null;
+ this.score = 0;
+
+ this.isHolding = false;
+ this.holdingTile = null;
+
+ this.a.p = this;
+ }
+
+ addTile() {
+ this.g.upcomingTiles.push(createPiece(pieces[pieces.length * Math.random() | 0]));
+ }
+
+ drop() {
+ this.pos.y++;
+ if (collide(this.a.field, this)) {
+ this.pos.y--;
+ this.game.merge(this.a, this);
+ this.reset();
+ this.a.sweep();
+ this.game.updateScore();
+ }
+ this.g.dropCounter = 0;
+ }
+
+ hold() {
+ if (this.isHolding)
+ return;
+ if (this.holdingTile === null) {
+ this.holdingTile = this.matrix;
+ this.reset(true);
+ } else {
+ this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0];
+ this.reset(true, false);
+ }
+ this.game.drawHolding();
+ }
+
+ move(dir) {
+ this.pos.x += dir;
+ if (collide(this.a.field, this)) {
+ this.pos.x -= dir;
+ }
+ this.g.dropCounter *= .75;
+ }
+
+ reset(resetHold = false, newTile = true) {
+ this.isHolding = resetHold;
+ if (newTile) {
+ this.matrix = this.g.upcomingTiles[0];
+ this.g.upcomingTiles.splice(0, 1);
+ this.addTile();
+ }
+
+ this.pos.y = 0;
+ this.pos.x = (this.a.field[0].length / 2 | 0) - (this.matrix[0].length / 2 | 0);
+
+ this.game.drawUpcoming();
+
+ if (collide(this.a.field, this)) {
+ this.game.gameOver();
+ }
+ }
+
+ rotate(dir) {
+ rotate(this.matrix, dir);
+
+ const pos = this.pos.x;
+ let offset = 1;
+ while (collide(this.a.field, this)) {
+ this.pos.x += offset;
+ offset = -(offset + (offset > 0 ? 1 : -1));
+ if (offset > this.matrix[0].length) {
+ rotate(this.matrix, -dir);
+ this.pos.x = pos;
+ return;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/tetris.js b/js/tetris.js
index 12107a3..79af12b 100644
--- a/js/tetris.js
+++ b/js/tetris.js
@@ -1,376 +1,3 @@
-class GameInfo {
- constructor(game) {
- this.fieldSize = {x: 12, y: 20};
- this.arena = new Arena(this, game);
-
- this.player = new Player(this, game);
-
- this.canvas = document.getElementById('tetris');
- this.context = this.canvas.getContext('2d');
-
- this.canvasBg = document.getElementById('tetris-background');
- this.contextBg = this.canvasBg.getContext('2d');
-
- this.canvasHold = document.getElementById('tetris-hold');
- this.contextHold = this.canvasHold.getContext('2d');
-
- this.canvasUpcoming = document.getElementById('tetris-upcoming');
- this.contextUpcoming = this.canvasUpcoming.getContext('2d');
-
- this.isPaused = true;
-
- this.dropCounter = 0;
- this.dropInterval = 1000;
-
- this.keys = {
- down: {
- keys: [40, 83],
- action: () => this.player.drop()
- },
- left: {
- keys: [37, 65],
- action: () => this.player.move(-1)
- },
- right: {
- keys: [39, 68],
- action: () => this.player.move(1)
- },
- rotateLeft: {
- keys: [81],
- action: () => this.player.rotate(-1)
- },
- rotateRight: {
- keys: [69],
- action: () => this.player.rotate(1)
- },
- holdTile: {
- keys: [38, 87],
- action: () => this.player.hold()
- }
- };
-
- this.prevUpdateScore = 0;
-
- this.lastScore = 0;
- this.lastTime = 0;
- this.lastTimeUpdate = Date.now();
-
- this.startTime = 0;
-
- this.upcomingTiles = [];
-
- /*
- default -> plain squares
- retro -> original look
- modern -> rounded corners
- snake -> all tiles are connected
- */
- this.theme = 'default';
- }
-}
-
-class Game {
- constructor() {
- this.g = new GameInfo(this);
- this.p = this.g.player;
-
- this.registerListeners();
- }
-
- draw() {
- this.g.context.clearRect(0, 0, this.g.canvas.width, this.g.canvas.height);
- this.drawMatrix(this.p.matrix, this.p.pos);
- }
-
- drawArena() {
- this.g.contextBg.fillStyle = '#000';
- this.g.contextBg.fillRect(0, 0, this.g.canvas.width, this.g.canvas.height);
- this.drawMatrix(this.g.arena.field, {x: 0, y: 0}, this.g.contextBg);
- }
-
- drawHolding() {
- this.g.contextHold.clearRect(0, 0, this.g.canvasHold.width, this.g.canvasHold.height);
- const offset = centerOffset(this.p.holdingTile);
- const x = 3 - (this.p.holdingTile[0].length / 2) + offset.x;
- const y = 3 - (this.p.holdingTile.length / 2) + offset.y;
- this.drawMatrix(this.p.holdingTile, {x: x, y: y}, this.g.contextHold);
- }
-
- drawMatrix(matrix, offset, useContext = this.g.context) {
- matrix.forEach((row, y) => {
- row.forEach((value, x) => {
- if (value !== 0) {
- this.drawTile(x, y, offset, colors[value], matrix, useContext);
- }
- });
- });
- }
-
- drawTile(x, y, offset, color, matrix, useContext = this.g.context) {
- const ctx = useContext;
- switch (this.g.theme) {
- case "default":
- ctx.fillStyle = color;
- ctx.fillRect(x + offset.x + tileGap / 2, y + offset.y + tileGap / 2, 1 - tileGap, 1 - tileGap);
- break;
- case "modern":
- ctx.fillStyle = color;
- drawRoundRect(ctx, x + offset.x + tileGap / 2, y + offset.y + tileGap / 2, 1 - tileGap, 1 - tileGap, .15);
- break;
- case "snakes":
- ctx.fillStyle = color;
- let r1 = .15, // top right
- r2 = .15, // bottom right
- r3 = .15, // bottom left
- r4 = .15; // top left
- // Is there a tile to the left?
- if (matrix[y][x - 1] > 0) {
- r3 = 0;
- r4 = 0;
- }
- // Is there a tile to the right?
- if (matrix[y][x + 1] > 0) {
- r1 = 0;
- r2 = 0;
- }
- // Is there a tile to the top?
- if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) {
- r1 = 0;
- r4 = 0;
- }
- // Is there a tile to the bottom?
- if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) {
- r2 = 0;
- r3 = 0;
- }
- drawRoundRect(ctx, x + offset.x, y + offset.y, 1, 1, [r1, r2, r3, r4]);
- break;
- default:
- this.g.theme = "default";
- this.drawTile(x, y, offset, color, matrix, ctx);
- break;
- }
- }
-
- drawUpcoming() {
- this.g.contextUpcoming.clearRect(0, 0, this.g.canvasUpcoming.width, this.g.canvasUpcoming.height);
- let offsetY = 0;
- let offset;
- this.g.upcomingTiles.forEach((tile) => {
- offset = centerOffset(tile);
- const x = 3 - (tile[0].length / 2) + offset.x;
- const y = offsetY + 3 - (tile.length / 2) + offset.y;
- this.drawMatrix(tile, {x: x, y: y}, this.g.contextUpcoming);
- offsetY += 6;
- });
- }
-
- gameOver() {
- this.g.arena.field.forEach(row => row.fill(0));
- timePassed = 0;
- this.g.lastTimeUpdate = Date.now();
- this.updateTime();
- this.p.score = 0;
- this.g.dropInterval = 1000;
- this.updateScore();
- }
-
- merge(arena, player) {
- player.matrix.forEach((row, y) => {
- row.forEach((value, x) => {
- if (value !== 0) {
- arena.field[y + player.pos.y][x + player.pos.x] = value;
- }
- });
- });
- this.drawArena();
- }
-
- registerListeners() {
- // Keyboard controls
- document.addEventListener('keydown', event => {
- Object.keys(this.g.keys).map((objKey) => {
- const keyBind = this.g.keys[objKey];
- if (keyBind.keys.includes(event.keyCode)) {
- keyBind.action();
- }
- });
- });
- }
-
- saveHighscore() {
- if (getCookie("highscore").value < this.p.score) {
- document.cookie = "highscore=" + this.p.score + "; max-age=" + 60 * 60 * 24 * 365 * 1000 + "; path=/;";
- }
- }
-
- start() {
- this.drawArena();
- this.p.addTile();
- this.p.addTile();
- this.p.addTile();
- this.p.reset();
- this.g.startTime = Date.now();
- this.update();
- this.updateScore();
- }
-
- update(time = 0) {
- if (!this.g.isPaused) {
- const deltaTime = time - this.g.lastTime;
- this.g.lastTime = time;
-
- this.g.dropCounter += deltaTime;
- if (this.g.dropCounter > this.g.dropInterval) {
- this.p.drop();
- }
-
- this.updateTime();
-
- this.draw();
- requestAnimationFrame(this.update.bind(this));
- }
- }
-
- updateScore() {
- if (this.g.lastScore !== this.p.score) {
- scoreUpdateAni();
- this.g.lastScore = this.p.score;
- this.saveHighscore();
- }
- document.getElementById('score').innerText = this.p.score.toString();
- }
-
- updateTime() {
- timePassed += Date.now() - this.g.lastTimeUpdate;
- timeElement.innerHTML = formatMillis(timePassed);
- this.g.lastTimeUpdate = Date.now();
- }
-}
-
-class Arena {
- constructor(gameInfo, game) {
- this.g = gameInfo;
- this.game = game;
- this.field = createMatrix(this.g.fieldSize.x, this.g.fieldSize.y);
- }
-
- clearScreen() {
- this.g.context.clearRect(0, 0, this.g.canvas.width, this.g.canvas.height);
- }
-
- sweep() {
- let rowCount = 1;
- outer: for (let y = this.field.length - 1; y > 0; --y) {
- for (let x = 0; x < this.field[y].length; ++x) {
- if (this.field[y][x] === 0) {
- continue outer;
- }
- }
-
- const row = this.field.splice(y, 1)[0].fill(0);
- this.field.unshift(row);
- ++y;
-
- this.p.score += rowCount * 10;
- rowCount *= 2;
- }
- if (this.p.score - this.g.prevUpdateScore > 50) {
- this.g.dropInterval -= 20;
- this.g.dropInterval = this.g.dropInterval > 100 ? this.g.dropInterval : 100;
- this.g.prevUpdateScore = this.p.score;
- }
- this.game.drawArena();
- }
-}
-
-class Player {
- constructor(gameInfo, game) {
- this.g = gameInfo;
- this.game = game;
- this.a = this.g.arena;
- this.pos = {x: 0, y: 0};
- this.matrix = null;
- this.score = 0;
-
- this.isHolding = false;
- this.holdingTile = null;
-
- this.a.p = this;
- }
-
- addTile() {
- this.g.upcomingTiles.push(createPiece(pieces[pieces.length * Math.random() | 0]));
- }
-
- drop() {
- this.pos.y++;
- if (collide(this.a.field, this)) {
- this.pos.y--;
- this.game.merge(this.a, this);
- this.reset();
- this.a.sweep();
- this.game.updateScore();
- }
- this.g.dropCounter = 0;
- }
-
- hold() {
- if (this.isHolding)
- return;
- if (this.holdingTile === null) {
- this.holdingTile = this.matrix;
- this.reset(true);
- } else {
- this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0];
- this.reset(true, false);
- }
- this.game.drawHolding();
- }
-
- move(dir) {
- this.pos.x += dir;
- if (collide(this.a.field, this)) {
- this.pos.x -= dir;
- }
- this.g.dropCounter *= .75;
- }
-
- reset(resetHold = false, newTile = true) {
- this.isHolding = resetHold;
- if (newTile) {
- this.matrix = this.g.upcomingTiles[0];
- this.g.upcomingTiles.splice(0, 1);
- this.addTile();
- }
-
- this.pos.y = 0;
- this.pos.x = (this.a.field[0].length / 2 | 0) - (this.matrix[0].length / 2 | 0);
-
- this.game.drawUpcoming();
-
- if (collide(this.a.field, this)) {
- this.game.gameOver();
- }
- }
-
- rotate(dir) {
- rotate(this.matrix, dir);
-
- const pos = this.pos.x;
- let offset = 1;
- while (collide(this.a.field, this)) {
- this.pos.x += offset;
- offset = -(offset + (offset > 0 ? 1 : -1));
- if (offset > this.matrix[0].length) {
- rotate(this.matrix, -dir);
- this.pos.x = pos;
- return;
- }
- }
- }
-}
-
const colors = [
null,
'#FF0D72',
diff --git a/style.css b/style.css
index 0f49110..33adaea 100644
--- a/style.css
+++ b/style.css
@@ -323,59 +323,6 @@ body {
transition: 0.2s background linear 0.1s, 0.2s top linear, 0.2s transform linear 0.2s, 0.2s -webkit-transform linear 0.2s;
}
-/*@media (max-width: 1400px) {*/
-/*#tetris, #tetris-background {*/
-/*height: 75%;*/
-/*}*/
-
-/*#game-stats {*/
-/*top: 10px;*/
-/*left: 50%;*/
-/*transform: translateX(-50%);*/
-/*}*/
-/**/
-/*#game-stats > div {*/
-/*display: inline;*/
-/*}*/
-/**/
-/*#time {*/
-/*margin-right: 50px;*/
-/*}*/
-/**/
-/*#controls {*/
-/*top: initial;*/
-/*bottom: 10px;*/
-/*left: 50%;*/
-/*transform: translateX(-50%);*/
-/*height: 10%;*/
-/*overflow: auto;*/
-/*}*/
-/**/
-/*#control-text {*/
-/*height: 100%;*/
-/*}*/
-/**/
-/*#language-selector {*/
-/*font-size: 20px;*/
-/*}*/
-/*}*/
-
-@media (max-width: 840px) {
- /*#game-stats {*/
- /*font-size: 20px;*/
- /*text-align: center;*/
- /*}*/
- /**/
- /*#game-stats > div {*/
- /*margin-right: 0;*/
- /*display: block;*/
- /*}*/
- /**/
- /*#control-text {*/
- /*font-size: 14px;*/
- /*}*/
-}
-
/*
Radio Buttons
*/