- (() => {
- const T = window.APP_LOCALE === "en"
- ? {
- start: "Press <b>SPACE</b> to start",
- pause: "Paused — press <b>P</b> to continue",
- drop: "DROP",
- }
- : {
- start: "Pulsa <b>ESPACIO</b> para empezar",
- pause: "Pausa — pulsa <b>P</b> para continuar",
- drop: "SOLTAR",
- };
- const canvas = document.getElementById("gameCanvas");
- const shell = document.getElementById("canvasShell");
- const ctx = canvas.getContext("2d");
- const startOverlay = document.getElementById("startMessage");
- const gameOverOverlay = document.getElementById("gameOverMessage");
- const btnStart = document.getElementById("btnStartTetris");
- const resetBtn = document.getElementById("resetBtn");
- const playAgainBtn = document.getElementById("playAgainBtn");
- const scoreEl = document.getElementById("scoreValue");
- const linesEl = document.getElementById("linesValue");
- const levelEl = document.getElementById("levelValue");
- const mobileMount = document.getElementById("tetrisMobileMount");
- const COLS = 10;
- const ROWS = 20;
- const DROP_START_MS = 700;
- const COLORS = {
- I: "#00d2ff",
- O: "#ffd166",
- T: "#b388ff",
- S: "#7cfc00",
- Z: "#ff4d6d",
- J: "#4ea8de",
- L: "#ff9f1c",
- X: "rgba(255,255,255,.065)",
- };
- const SHAPES = {
- I: [
- [0, 0, 0, 0],
- [1, 1, 1, 1],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- O: [
- [0, 1, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- T: [
- [0, 1, 0, 0],
- [1, 1, 1, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- S: [
- [0, 1, 1, 0],
- [1, 1, 0, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- Z: [
- [1, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- J: [
- [1, 0, 0, 0],
- [1, 1, 1, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- L: [
- [0, 0, 1, 0],
- [1, 1, 1, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- };
- const TYPES = Object.keys(SHAPES);
- let grid;
- let current;
- let next;
- let score = 0;
- let lines = 0;
- let level = 1;
- let running = false;
- let paused = false;
- let gameOver = false;
- let dropMilliseconds = DROP_START_MS;
- let dropAccumulator = 0;
- let lastTime = 0;
- let dpr = 1;
- let viewportWidth = 0;
- let viewportHeight = 0;
- let cellSize = 20;
- let offsetX = 0;
- let offsetY = 0;
- let holdTimer = null;
- let mobileControlsCreated = false;
- function makeGrid() {
- return Array.from(
- { length: ROWS },
- () => Array(COLS).fill(null)
- );
- }
- function cloneMatrix(matrix) {
- return matrix.map((row) => row.slice());
- }
- function randomPiece() {
- const type = TYPES[Math.floor(Math.random() * TYPES.length)];
- return {
- type,
- mat: cloneMatrix(SHAPES[type]),
- x: 3,
- y: -1,
- };
- }
- function rotateMatrix(matrix) {
- const size = matrix.length;
- const rotated = Array.from(
- { length: size },
- () => Array(size).fill(0)
- );
- for (let y = 0; y < size; y++) {
- for (let x = 0; x < size; x++) {
- rotated[x][size - 1 - y] = matrix[y][x];
- }
- }
- return rotated;
- }
- function resizeCanvas() {
- const rect = shell.getBoundingClientRect();
- dpr = Math.max(
- 1,
- Math.min(2.5, window.devicePixelRatio || 1)
- );
- canvas.width = Math.max(1, Math.floor(rect.width * dpr));
- canvas.height = Math.max(1, Math.floor(rect.height * dpr));
- canvas.style.width = "100%";
- canvas.style.height = "100%";
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- viewportWidth = rect.width;
- viewportHeight = rect.height;
- /*
- * El canvas conserva EXACTAMENTE el tamaño visual de Break.
- * Solo centramos el tablero Tetris dentro.
- */
- const maxBoardHeight = viewportHeight * 0.90;
- const maxBoardWidth = viewportWidth * 0.44;
- const byHeight = Math.floor(maxBoardHeight / ROWS);
- const byWidth = Math.floor(maxBoardWidth / COLS);
- cellSize = Math.max(9, Math.min(byHeight, byWidth));
- const boardWidth = COLS * cellSize;
- const boardHeight = ROWS * cellSize;
- offsetX = Math.floor((viewportWidth - boardWidth) / 2);
- offsetY = Math.floor((viewportHeight - boardHeight) / 2);
- syncMobileControls();
- }
- function collide(piece, gridX, gridY) {
- for (let y = 0; y < 4; y++) {
- for (let x = 0; x < 4; x++) {
- if (!piece.mat[y][x]) {
- continue;
- }
- const x2 = gridX + x;
- const y2 = gridY + y;
- if (x2 < 0 || x2 >= COLS || y2 >= ROWS) {
- return true;
- }
- if (y2 >= 0 && grid[y2][x2]) {
- return true;
- }
- }
- }
- return false;
- }
- function merge(piece) {
- for (let y = 0; y < 4; y++) {
- for (let x = 0; x < 4; x++) {
- if (!piece.mat[y][x]) {
- continue;
- }
- const x2 = piece.x + x;
- const y2 = piece.y + y;
- if (
- y2 >= 0 &&
- y2 < ROWS &&
- x2 >= 0 &&
- x2 < COLS
- ) {
- grid[y2][x2] = piece.type;
- }
- }
- }
- }
- function clearLines() {
- let cleared = 0;
- for (let y = ROWS - 1; y >= 0; y--) {
- if (grid[y].every(Boolean)) {
- grid.splice(y, 1);
- grid.unshift(Array(COLS).fill(null));
- cleared++;
- y++;
- }
- }
- if (!cleared) {
- return;
- }
- lines += cleared;
- const points = {
- 1: 100,
- 2: 300,
- 3: 500,
- 4: 800,
- };
- score += (points[cleared] || 0) * level;
- level = Math.floor(lines / 10) + 1;
- dropMilliseconds = Math.max(
- 120,
- DROP_START_MS - (level - 1) * 60
- );
- updateHUD();
- }
- function spawn() {
- current = next;
- current.x = 3;
- current.y = -1;
- next = randomPiece();
- if (collide(current, current.x, current.y)) {
- gameOver = true;
- running = false;
- paused = false;
- gameOverOverlay.style.display = "grid";
- window.GameScores?.submit("tetris", score);
- }
- }
- function lockPiece() {
- merge(current);
- clearLines();
- spawn();
- }
- function moveLeft() {
- if (!running || gameOver) {
- return;
- }
- if (!collide(current, current.x - 1, current.y)) {
- current.x--;
- }
- }
- function moveRight() {
- if (!running || gameOver) {
- return;
- }
- if (!collide(current, current.x + 1, current.y)) {
- current.x++;
- }
- }
- function moveDown() {
- if (!running || gameOver) {
- return;
- }
- if (!collide(current, current.x, current.y + 1)) {
- current.y++;
- score++;
- updateHUD();
- }
- }
- function stepDown() {
- if (!running || gameOver) {
- return;
- }
- if (!collide(current, current.x, current.y + 1)) {
- current.y++;
- } else {
- lockPiece();
- }
- }
- function rotatePiece() {
- if (!running || gameOver) {
- return;
- }
- const previous = current.mat;
- current.mat = rotateMatrix(current.mat);
- if (collide(current, current.x, current.y)) {
- if (!collide(current, current.x - 1, current.y)) {
- current.x--;
- } else if (!collide(current, current.x + 1, current.y)) {
- current.x++;
- } else {
- current.mat = previous;
- }
- }
- }
- function hardDrop() {
- if (!running || gameOver) {
- return;
- }
- while (!collide(current, current.x, current.y + 1)) {
- current.y++;
- score++;
- }
- updateHUD();
- lockPiece();
- }
- function updateHUD() {
- scoreEl.textContent = String(score);
- linesEl.textContent = String(lines);
- levelEl.textContent = String(level);
- }
- function showStart(html) {
- startOverlay.innerHTML =
- '<div class="start-copy">' +
- html +
- "</div>";
- startOverlay.style.display = "grid";
- }
- function hideStart() {
- startOverlay.style.display = "none";
- }
- function startGame() {
- if (gameOver) {
- return;
- }
- running = true;
- paused = false;
- dropAccumulator = 0;
- hideStart();
- }
- function resetGame() {
- grid = makeGrid();
- score = 0;
- lines = 0;
- level = 1;
- running = false;
- paused = false;
- gameOver = false;
- dropMilliseconds = DROP_START_MS;
- dropAccumulator = 0;
- gameOverOverlay.style.display = "none";
- updateHUD();
- next = randomPiece();
- spawn();
- showStart(T.start);
- }
- function togglePause() {
- if (gameOver) {
- return;
- }
- if (paused) {
- paused = false;
- running = true;
- dropAccumulator = 0;
- hideStart();
- return;
- }
- if (!running) {
- startGame();
- return;
- }
- paused = true;
- running = false;
- showStart(T.pause);
- }
- /*
- * Teclas bloqueadas para que Chrome no mueva
- * la página ni navegue por los botones.
- */
- const gameKeys = new Set([
- "ArrowLeft",
- "ArrowRight",
- "ArrowUp",
- "ArrowDown",
- "Space",
- "KeyP",
- "KeyR",
- ]);
- function blockBrowserGameKeys(event) {
- if (gameKeys.has(event.code)) {
- event.preventDefault();
- }
- }
- window.addEventListener(
- "keydown",
- blockBrowserGameKeys,
- {
- capture: true,
- passive: false,
- }
- );
- window.addEventListener(
- "keyup",
- blockBrowserGameKeys,
- {
- capture: true,
- passive: false,
- }
- );
- window.addEventListener(
- "keydown",
- (event) => {
- if (event.code === "Space") {
- if (!running && !gameOver) {
- startGame();
- } else if (running && !gameOver) {
- hardDrop();
- }
- return;
- }
- if (event.code === "KeyP") {
- if (!event.repeat) {
- togglePause();
- }
- return;
- }
- if (event.code === "KeyR") {
- if (!event.repeat) {
- resetGame();
- }
- return;
- }
- if (!running || gameOver) {
- return;
- }
- if (event.code === "ArrowLeft") {
- moveLeft();
- } else if (event.code === "ArrowRight") {
- moveRight();
- } else if (event.code === "ArrowDown") {
- moveDown();
- } else if (event.code === "ArrowUp") {
- rotatePiece();
- }
- },
- {
- passive: false,
- }
- );
- btnStart?.addEventListener("click", () => {
- if (gameOver) {
- resetGame();
- }
- startGame();
- btnStart.blur();
- });
- resetBtn?.addEventListener("click", () => {
- resetGame();
- resetBtn.blur();
- });
- playAgainBtn?.addEventListener("click", () => {
- resetGame();
- startGame();
- playAgainBtn.blur();
- });
- function clearHold() {
- if (holdTimer) {
- clearInterval(holdTimer);
- holdTimer = null;
- }
- }
- function runMobileAction(action) {
- if (!running && !gameOver) {
- startGame();
- }
- if (!running || gameOver) {
- return;
- }
- if (action === "left") {
- moveLeft();
- } else if (action === "right") {
- moveRight();
- } else if (action === "down") {
- moveDown();
- } else if (action === "rotate") {
- rotatePiece();
- } else if (action === "drop") {
- hardDrop();
- }
- }
- function createMobileControls() {
- if (mobileControlsCreated || !mobileMount) {
- return;
- }
- const wrapper = document.createElement("div");
- wrapper.className = "tetris-mobile-controls";
- const dpad = document.createElement("div");
- dpad.className = "tetris-mobile-dpad";
- const rotate = document.createElement("button");
- rotate.type = "button";
- rotate.className = "tetris-mobile-key tetris-mobile-rotate";
- rotate.dataset.action = "rotate";
- rotate.textContent = "⟳";
- const left = document.createElement("button");
- left.type = "button";
- left.className = "tetris-mobile-key tetris-mobile-left";
- left.dataset.action = "left";
- left.textContent = "◀";
- const down = document.createElement("button");
- down.type = "button";
- down.className = "tetris-mobile-key tetris-mobile-down";
- down.dataset.action = "down";
- down.textContent = "▼";
- const right = document.createElement("button");
- right.type = "button";
- right.className = "tetris-mobile-key tetris-mobile-right";
- right.dataset.action = "right";
- right.textContent = "▶";
- dpad.append(
- rotate,
- left,
- down,
- right
- );
- const drop = document.createElement("button");
- drop.type = "button";
- drop.className = "tetris-mobile-drop";
- drop.dataset.action = "drop";
- drop.textContent = T.drop;
- wrapper.append(dpad, drop);
- mobileMount.appendChild(wrapper);
- wrapper.addEventListener(
- "pointerdown",
- (event) => {
- const button =
- event.target.closest("[data-action]");
- if (!button) {
- return;
- }
- event.preventDefault();
- const action =
- button.dataset.action;
- runMobileAction(action);
- if (
- action === "left" ||
- action === "right" ||
- action === "down"
- ) {
- clearHold();
- holdTimer = setInterval(
- () => runMobileAction(action),
- 70
- );
- }
- },
- {
- passive: false,
- }
- );
- wrapper.addEventListener(
- "pointerup",
- clearHold
- );
- wrapper.addEventListener(
- "pointercancel",
- clearHold
- );
- wrapper.addEventListener(
- "pointerleave",
- clearHold
- );
- mobileControlsCreated = true;
- }
- function destroyMobileControls() {
- clearHold();
- if (!mobileControlsCreated || !mobileMount) {
- return;
- }
- mobileMount.innerHTML = "";
- mobileControlsCreated = false;
- }
- function syncMobileControls() {
- if (window.matchMedia("(max-width: 820px)").matches) {
- createMobileControls();
- } else {
- destroyMobileControls();
- }
- }
- function drawCell(x, y, color) {
- const px = offsetX + x * cellSize;
- const py = offsetY + y * cellSize;
- ctx.fillStyle = color;
- ctx.fillRect(px, py, cellSize, cellSize);
- ctx.strokeStyle = "rgba(0,0,0,.30)";
- ctx.strokeRect(
- px + 0.5,
- py + 0.5,
- cellSize - 1,
- cellSize - 1
- );
- ctx.strokeStyle = "rgba(255,255,255,.10)";
- ctx.strokeRect(
- px + 2,
- py + 2,
- cellSize - 4,
- cellSize - 4
- );
- }
- function drawBackground() {
- ctx.fillStyle = "#03060c";
- ctx.fillRect(
- 0,
- 0,
- viewportWidth,
- viewportHeight
- );
- const gradient =
- ctx.createRadialGradient(
- viewportWidth * 0.5,
- viewportHeight * 0.45,
- 40,
- viewportWidth * 0.5,
- viewportHeight * 0.45,
- Math.max(viewportWidth, viewportHeight)
- );
- gradient.addColorStop(
- 0,
- "rgba(255,255,255,.03)"
- );
- gradient.addColorStop(
- 1,
- "rgba(0,0,0,.65)"
- );
- ctx.fillStyle = gradient;
- ctx.fillRect(
- 0,
- 0,
- viewportWidth,
- viewportHeight
- );
- }
- function drawBoard() {
- const boardWidth = COLS * cellSize;
- const boardHeight = ROWS * cellSize;
- ctx.save();
- ctx.fillStyle = "rgba(255,255,255,.018)";
- ctx.fillRect(
- offsetX,
- offsetY,
- boardWidth,
- boardHeight
- );
- ctx.strokeStyle = "rgba(74,163,255,.75)";
- ctx.lineWidth = 2;
- ctx.shadowBlur = 15;
- ctx.shadowColor = "rgba(74,163,255,.28)";
- ctx.strokeRect(
- offsetX - 1,
- offsetY - 1,
- boardWidth + 2,
- boardHeight + 2
- );
- ctx.restore();
- for (let y = 0; y < ROWS; y++) {
- for (let x = 0; x < COLS; x++) {
- drawCell(
- x,
- y,
- grid[y][x]
- ? COLORS[grid[y][x]]
- : COLORS.X
- );
- }
- }
- if (!current) {
- return;
- }
- for (let y = 0; y < 4; y++) {
- for (let x = 0; x < 4; x++) {
- if (!current.mat[y][x]) {
- continue;
- }
- const boardX = current.x + x;
- const boardY = current.y + y;
- if (boardY >= 0) {
- drawCell(
- boardX,
- boardY,
- COLORS[current.type]
- );
- }
- }
- }
- }
- function draw() {
- ctx.clearRect(
- 0,
- 0,
- viewportWidth,
- viewportHeight
- );
- drawBackground();
- drawBoard();
- }
- function loop(timestamp) {
- const delta =
- Math.min(
- 0.04,
- (timestamp - lastTime) / 1000
- );
- lastTime = timestamp;
- if (running && !gameOver) {
- dropAccumulator += delta * 1000;
- if (dropAccumulator >= dropMilliseconds) {
- stepDown();
- dropAccumulator = 0;
- }
- }
- draw();
- requestAnimationFrame(loop);
- }
- window.addEventListener("resize", resizeCanvas);
- window.addEventListener("blur", clearHold);
- resizeCanvas();
- resetGame();
- requestAnimationFrame(loop);
- })();