| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
- <title>俄罗斯方块</title>
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
- html, body { height: 100%; }
- body {
- background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
- color: #e8e8f0;
- font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- overflow: auto;
- user-select: none;
- -webkit-user-select: none;
- touch-action: manipulation;
- }
- .app {
- display: flex;
- gap: 24px;
- padding: 20px;
- align-items: flex-start;
- flex-wrap: wrap;
- justify-content: center;
- }
- .board-wrap {
- position: relative;
- background: rgba(0,0,0,0.35);
- border: 2px solid rgba(255,255,255,0.12);
- border-radius: 12px;
- padding: 12px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.5), inset 0 0 30px rgba(0,0,0,0.4);
- }
- canvas#board {
- display: block;
- background: #0a0a18;
- border-radius: 6px;
- image-rendering: pixelated;
- }
- .side {
- display: flex;
- flex-direction: column;
- gap: 16px;
- min-width: 180px;
- }
- .panel {
- background: rgba(0,0,0,0.35);
- border: 2px solid rgba(255,255,255,0.12);
- border-radius: 12px;
- padding: 14px 16px;
- box-shadow: 0 10px 30px rgba(0,0,0,0.35);
- }
- .panel h3 {
- font-size: 12px;
- letter-spacing: 2px;
- color: #8a8ab0;
- text-transform: uppercase;
- margin-bottom: 10px;
- }
- .score {
- font-size: 28px;
- font-weight: 700;
- color: #ffd86b;
- text-shadow: 0 0 12px rgba(255,216,107,0.4);
- font-variant-numeric: tabular-nums;
- }
- .stats-row {
- display: flex;
- justify-content: space-between;
- font-size: 14px;
- margin-top: 8px;
- }
- .stats-row span:last-child {
- color: #7ee8ff;
- font-weight: 600;
- font-variant-numeric: tabular-nums;
- }
- .next-canvas-wrap {
- display: flex;
- justify-content: center;
- align-items: center;
- background: #0a0a18;
- border-radius: 6px;
- height: 110px;
- }
- canvas#next {
- background: #0a0a18;
- border-radius: 6px;
- }
- .keys {
- font-size: 12px;
- line-height: 1.9;
- }
- .keys kbd {
- display: inline-block;
- min-width: 22px;
- text-align: center;
- background: rgba(255,255,255,0.08);
- border: 1px solid rgba(255,255,255,0.18);
- border-bottom-width: 2px;
- border-radius: 4px;
- padding: 1px 6px;
- margin-right: 6px;
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 11px;
- color: #fff;
- }
- .keys .row { display: flex; align-items: center; }
- .overlay {
- position: absolute;
- inset: 12px;
- background: rgba(10, 10, 24, 0.85);
- backdrop-filter: blur(4px);
- border-radius: 6px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding: 20px;
- z-index: 10;
- transition: opacity 0.2s;
- }
- .overlay.hidden { display: none; }
- .overlay h2 {
- font-size: 32px;
- margin-bottom: 14px;
- letter-spacing: 4px;
- }
- .overlay p {
- color: #b0b0c8;
- margin-bottom: 8px;
- font-size: 14px;
- }
- .overlay .final-score {
- font-size: 40px;
- color: #ffd86b;
- font-weight: 700;
- margin: 10px 0;
- text-shadow: 0 0 20px rgba(255,216,107,0.5);
- }
- .btn {
- background: linear-gradient(135deg, #7ee8ff, #5b8cff);
- color: #0a0a18;
- border: none;
- padding: 10px 24px;
- font-size: 14px;
- font-weight: 700;
- border-radius: 6px;
- cursor: pointer;
- margin-top: 14px;
- letter-spacing: 1px;
- box-shadow: 0 4px 14px rgba(91,140,255,0.4);
- }
- .btn:hover { transform: translateY(-1px); }
- .btn:active { transform: translateY(1px); }
- .touch {
- display: none;
- margin-top: 16px;
- grid-template-columns: repeat(5, 1fr);
- gap: 8px;
- width: 100%;
- max-width: 420px;
- }
- .touch button {
- background: rgba(255,255,255,0.08);
- border: 1px solid rgba(255,255,255,0.18);
- color: #fff;
- padding: 14px 0;
- border-radius: 8px;
- font-size: 18px;
- font-weight: 700;
- cursor: pointer;
- min-height: 48px;
- }
- .touch button:active { background: rgba(126,232,255,0.25); }
- .touch .wide { grid-column: span 2; }
- .touch .full { grid-column: 1 / -1; }
- .combo {
- position: absolute;
- pointer-events: none;
- font-size: 22px;
- font-weight: 700;
- color: #ffd86b;
- text-shadow: 0 0 10px rgba(255,216,107,0.6);
- animation: float 1s ease-out forwards;
- }
- @keyframes float {
- from { opacity: 1; transform: translateY(0); }
- to { opacity: 0; transform: translateY(-40px); }
- }
- .title {
- position: absolute;
- top: -2px;
- left: 50%;
- transform: translate(-50%, -50%);
- background: linear-gradient(135deg, #7ee8ff, #ff7eb3);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- font-weight: 800;
- font-size: 18px;
- letter-spacing: 6px;
- padding: 0 14px;
- }
- .flash {
- animation: flash 0.4s ease;
- }
- @keyframes flash {
- 0%, 100% { background: #0a0a18; }
- 50% { background: #2a2a4a; }
- }
- @media (max-width: 720px) {
- .touch { display: grid; }
- .side { min-width: 140px; }
- .keys { display: none; }
- }
- @media (hover: none) and (pointer: coarse) {
- .touch { display: grid; }
- .keys { display: none; }
- }
- </style>
- </head>
- <body>
- <div class="app">
- <div class="board-wrap">
- <div class="title">TETRIS</div>
- <canvas id="board" width="300" height="600"></canvas>
- <div class="overlay hidden" id="overlay">
- <h2 id="overlay-title">已暂停</h2>
- <p id="overlay-text">按 空格 继续游戏</p>
- <button class="btn" id="overlay-btn">继续</button>
- </div>
- </div>
- <div class="side">
- <div class="panel">
- <h3>分数</h3>
- <div class="score" id="score">0</div>
- <div class="stats-row"><span>等级</span><span id="level">1</span></div>
- <div class="stats-row"><span>行数</span><span id="lines">0</span></div>
- </div>
- <div class="panel">
- <h3>下一个</h3>
- <div class="next-canvas-wrap">
- <canvas id="next" width="100" height="100"></canvas>
- </div>
- </div>
- <div class="panel keys">
- <h3>操作</h3>
- <div class="row"><kbd>←</kbd><kbd>→</kbd>移动</div>
- <div class="row"><kbd>↓</kbd>快速下落</div>
- <div class="row"><kbd>Enter</kbd>直接落地</div>
- <div class="row"><kbd>Z</kbd><kbd>X</kbd>旋转</div>
- <div class="row"><kbd>Space</kbd>暂停</div>
- <div class="row"><kbd>Q</kbd>重开</div>
- </div>
- </div>
- </div>
- <div class="touch" id="touch">
- <button data-act="left">◀</button>
- <button data-act="rotateL">Z</button>
- <button data-act="down">▼</button>
- <button data-act="rotateR">X</button>
- <button data-act="right">▶</button>
- <button class="wide" data-act="drop">落地</button>
- <button class="wide" data-act="pause">暂停</button>
- <button class="full" data-act="restart">重新开始 (Q)</button>
- </div>
- <script>
- (() => {
- // ====== Constants ======
- const COLS = 10, ROWS = 20, CELL = 30;
- const board = document.getElementById('board');
- const ctx = board.getContext('2d');
- const next = document.getElementById('next');
- const nctx = next.getContext('2d');
- // Piece definitions: each is a 4x4 matrix with rotation states
- // Using a simpler representation: list of 4 rotation states, each is list of [x,y] cells
- const SHAPES = {
- I: {
- color: '#00f0f0',
- rotations: [
- [[0,1],[1,1],[2,1],[3,1]],
- [[2,0],[2,1],[2,2],[2,3]],
- [[0,2],[1,2],[2,2],[3,2]],
- [[1,0],[1,1],[1,2],[1,3]],
- ]
- },
- O: {
- color: '#f0f000',
- rotations: [
- [[1,0],[2,0],[1,1],[2,1]],
- [[1,0],[2,0],[1,1],[2,1]],
- [[1,0],[2,0],[1,1],[2,1]],
- [[1,0],[2,0],[1,1],[2,1]],
- ]
- },
- T: {
- color: '#a000f0',
- rotations: [
- [[1,0],[0,1],[1,1],[2,1]],
- [[1,0],[1,1],[2,1],[1,2]],
- [[0,1],[1,1],[2,1],[1,2]],
- [[1,0],[0,1],[1,1],[1,2]],
- ]
- },
- S: {
- color: '#00f000',
- rotations: [
- [[1,0],[2,0],[0,1],[1,1]],
- [[1,0],[1,1],[2,1],[2,2]],
- [[1,1],[2,1],[0,2],[1,2]],
- [[0,0],[0,1],[1,1],[1,2]],
- ]
- },
- Z: {
- color: '#f00000',
- rotations: [
- [[0,0],[1,0],[1,1],[2,1]],
- [[2,0],[1,1],[2,1],[1,2]],
- [[0,1],[1,1],[1,2],[2,2]],
- [[1,0],[0,1],[1,1],[0,2]],
- ]
- },
- J: {
- color: '#0000f0',
- rotations: [
- [[0,0],[0,1],[1,1],[2,1]],
- [[1,0],[2,0],[1,1],[1,2]],
- [[0,1],[1,1],[2,1],[2,2]],
- [[1,0],[1,1],[0,2],[1,2]],
- ]
- },
- L: {
- color: '#f0a000',
- rotations: [
- [[2,0],[0,1],[1,1],[2,1]],
- [[1,0],[1,1],[1,2],[2,2]],
- [[0,1],[1,1],[2,1],[0,2]],
- [[0,0],[1,0],[1,1],[1,2]],
- ]
- },
- };
- const TYPES = Object.keys(SHAPES);
- // SRS-style wall kick data
- const KICKS_JLSTZ = {
- '0->1': [[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]],
- '1->0': [[0,0],[1,0],[1,-1],[0,2],[1,2]],
- '1->2': [[0,0],[1,0],[1,-1],[0,2],[1,2]],
- '2->1': [[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]],
- '2->3': [[0,0],[1,0],[1,1],[0,-2],[1,-2]],
- '3->2': [[0,0],[-1,0],[-1,-1],[0,2],[-1,2]],
- '3->0': [[0,0],[-1,0],[-1,-1],[0,2],[-1,2]],
- '0->3': [[0,0],[1,0],[1,1],[0,-2],[1,-2]],
- };
- const KICKS_I = {
- '0->1': [[0,0],[-2,0],[1,0],[-2,-1],[1,2]],
- '1->0': [[0,0],[2,0],[-1,0],[2,1],[-1,-2]],
- '1->2': [[0,0],[-1,0],[2,0],[-1,2],[2,-1]],
- '2->1': [[0,0],[1,0],[-2,0],[1,-2],[-2,1]],
- '2->3': [[0,0],[2,0],[-1,0],[2,1],[-1,-2]],
- '3->2': [[0,0],[-2,0],[1,0],[-2,-1],[1,2]],
- '3->0': [[0,0],[1,0],[-2,0],[1,-2],[-2,1]],
- '0->3': [[0,0],[-1,0],[2,0],[-1,2],[2,-1]],
- };
- // ====== State ======
- let grid, current, nextType, score, level, lines, dropInterval, dropCounter, lastTime, gameOver, paused;
- const overlay = document.getElementById('overlay');
- const overlayTitle = document.getElementById('overlay-title');
- const overlayText = document.getElementById('overlay-text');
- const overlayBtn = document.getElementById('overlay-btn');
- const scoreEl = document.getElementById('score');
- const levelEl = document.getElementById('level');
- const linesEl = document.getElementById('lines');
- function init() {
- grid = Array.from({length: ROWS}, () => Array(COLS).fill(null));
- score = 0; level = 1; lines = 0;
- dropInterval = 1000; dropCounter = 0; lastTime = 0;
- gameOver = false; paused = false;
- nextType = randomType();
- spawn();
- hideOverlay();
- updateUI();
- draw();
- drawNext();
- }
- function randomType() {
- return TYPES[Math.floor(Math.random() * TYPES.length)];
- }
- function spawn() {
- const type = nextType;
- nextType = randomType();
- current = {
- type,
- rotation: 0,
- x: 3,
- y: -1,
- };
- drawNext();
- if (collides(current, 0, 0)) {
- // game over
- gameOver = true;
- showOverlay('游戏结束', `最终得分: ${score}`, '重新开始');
- }
- }
- function collides(piece, dx, dy, rot = piece.rotation) {
- const cells = SHAPES[piece.type].rotations[rot];
- for (const [cx, cy] of cells) {
- const x = piece.x + cx + dx;
- const y = piece.y + cy + dy;
- if (x < 0 || x >= COLS || y >= ROWS) return true;
- if (y >= 0 && grid[y][x]) return true;
- }
- return false;
- }
- function tryMove(dx, dy) {
- if (collides(current, dx, dy)) return false;
- current.x += dx;
- current.y += dy;
- return true;
- }
- function rotate(dir) {
- if (current.type === 'O') return;
- const from = current.rotation;
- const to = (from + (dir > 0 ? 1 : 3)) % 4;
- const kickTable = (current.type === 'I') ? KICKS_I : KICKS_JLSTZ;
- const key = `${from}->${to}`;
- const kicks = kickTable[key] || [[0,0]];
- for (const [kx, ky] of kicks) {
- // For I piece, kicks are applied as offsets; for others, also
- if (!collides(current, kx, -ky, to)) {
- current.x += kx;
- current.y += -ky;
- current.rotation = to;
- return;
- }
- }
- }
- function softDrop() {
- if (tryMove(0, 1)) {
- score += 1;
- updateUI();
- } else {
- lock();
- }
- }
- function hardDrop() {
- let dist = 0;
- while (tryMove(0, 1)) dist++;
- score += dist * 2;
- lock();
- }
- function lock() {
- const cells = SHAPES[current.type].rotations[current.rotation];
- for (const [cx, cy] of cells) {
- const x = current.x + cx;
- const y = current.y + cy;
- if (y >= 0 && y < ROWS && x >= 0 && x < COLS) {
- grid[y][x] = SHAPES[current.type].color;
- }
- }
- const cleared = clearLines();
- if (cleared > 0) {
- const base = [0, 100, 300, 500, 800][cleared];
- const gained = base * level;
- score += gained;
- lines += cleared;
- showCombo(cleared, gained);
- // Level up every 10 lines
- const newLevel = Math.floor(lines / 10) + 1;
- if (newLevel > level) {
- level = newLevel;
- dropInterval = Math.max(80, 1000 - (level - 1) * 80);
- }
- updateUI();
- // Flash effect
- board.classList.add('flash');
- setTimeout(() => board.classList.remove('flash'), 400);
- }
- spawn();
- }
- function clearLines() {
- let count = 0;
- for (let y = ROWS - 1; y >= 0; y--) {
- if (grid[y].every(c => c !== null)) {
- grid.splice(y, 1);
- grid.unshift(Array(COLS).fill(null));
- count++;
- y++; // recheck this row
- }
- }
- return count;
- }
- function showCombo(n, gained) {
- const labels = {1: '单消!', 2: '双消!', 3: '三消!', 4: 'TETRIS!'};
- const el = document.createElement('div');
- el.className = 'combo';
- el.textContent = `${labels[n] || ''} +${gained}`;
- el.style.left = (board.width / 2 - 60) + 'px';
- el.style.top = (board.height / 2) + 'px';
- board.parentElement.appendChild(el);
- setTimeout(() => el.remove(), 1000);
- }
- function updateUI() {
- scoreEl.textContent = score;
- levelEl.textContent = level;
- linesEl.textContent = lines;
- }
- // ====== Drawing ======
- function drawCell(ctx, x, y, color, size = CELL) {
- const px = x * size, py = y * size;
- // Glow
- ctx.fillStyle = color;
- ctx.shadowColor = color;
- ctx.shadowBlur = 8;
- ctx.fillRect(px + 1, py + 1, size - 2, size - 2);
- ctx.shadowBlur = 0;
- // Highlight
- ctx.fillStyle = 'rgba(255,255,255,0.25)';
- ctx.fillRect(px + 2, py + 2, size - 4, 3);
- ctx.fillRect(px + 2, py + 2, 3, size - 4);
- // Shadow
- ctx.fillStyle = 'rgba(0,0,0,0.3)';
- ctx.fillRect(px + 2, py + size - 5, size - 4, 3);
- ctx.fillRect(px + size - 5, py + 2, 3, size - 4);
- }
- function drawGrid() {
- ctx.strokeStyle = 'rgba(255,255,255,0.04)';
- ctx.lineWidth = 1;
- for (let x = 1; x < COLS; x++) {
- ctx.beginPath();
- ctx.moveTo(x * CELL + 0.5, 0);
- ctx.lineTo(x * CELL + 0.5, ROWS * CELL);
- ctx.stroke();
- }
- for (let y = 1; y < ROWS; y++) {
- ctx.beginPath();
- ctx.moveTo(0, y * CELL + 0.5);
- ctx.lineTo(COLS * CELL, y * CELL + 0.5);
- ctx.stroke();
- }
- }
- function drawGhost() {
- if (!current || gameOver) return;
- const cells = SHAPES[current.type].rotations[current.rotation];
- let gy = current.y;
- while (!collides({...current, y: gy + 1}, 0, 0)) gy++;
- ctx.fillStyle = 'rgba(255,255,255,0.12)';
- for (const [cx, cy] of cells) {
- const x = current.x + cx;
- const y = gy + cy;
- if (y >= 0) {
- ctx.fillRect(x * CELL + 2, y * CELL + 2, CELL - 4, CELL - 4);
- }
- }
- }
- function draw() {
- ctx.fillStyle = '#0a0a18';
- ctx.fillRect(0, 0, board.width, board.height);
- drawGrid();
- for (let y = 0; y < ROWS; y++) {
- for (let x = 0; x < COLS; x++) {
- if (grid[y][x]) drawCell(ctx, x, y, grid[y][x]);
- }
- }
- if (current && !gameOver) {
- drawGhost();
- const cells = SHAPES[current.type].rotations[current.rotation];
- for (const [cx, cy] of cells) {
- const x = current.x + cx;
- const y = current.y + cy;
- if (y >= 0) drawCell(ctx, x, y, SHAPES[current.type].color);
- }
- }
- }
- function drawNext() {
- nctx.fillStyle = '#0a0a18';
- nctx.fillRect(0, 0, next.width, next.height);
- const cells = SHAPES[nextType].rotations[0];
- const cs = 20;
- // Find bounds to center
- let minX = 4, maxX = 0, minY = 4, maxY = 0;
- for (const [x, y] of cells) {
- if (x < minX) minX = x;
- if (x > maxX) maxX = x;
- if (y < minY) minY = y;
- if (y > maxY) maxY = y;
- }
- const w = (maxX - minX + 1) * cs;
- const h = (maxY - minY + 1) * cs;
- const ox = (next.width - w) / 2 - minX * cs;
- const oy = (next.height - h) / 2 - minY * cs;
- for (const [x, y] of cells) {
- nctx.shadowColor = SHAPES[nextType].color;
- nctx.shadowBlur = 8;
- nctx.fillStyle = SHAPES[nextType].color;
- nctx.fillRect(ox + x * cs + 1, oy + y * cs + 1, cs - 2, cs - 2);
- }
- nctx.shadowBlur = 0;
- }
- // ====== Overlay ======
- function showOverlay(title, text, btn) {
- overlayTitle.textContent = title;
- overlayText.textContent = text;
- overlayBtn.textContent = btn;
- overlay.classList.remove('hidden');
- }
- function hideOverlay() {
- overlay.classList.add('hidden');
- }
- overlayBtn.addEventListener('click', () => {
- if (gameOver) {
- init();
- } else {
- paused = false;
- hideOverlay();
- lastTime = performance.now();
- }
- });
- // ====== Input ======
- function togglePause() {
- if (gameOver) return;
- paused = !paused;
- if (paused) showOverlay('已暂停', '按 空格 继续', '继续');
- else { hideOverlay(); lastTime = performance.now(); }
- }
- function restart() {
- init();
- }
- document.addEventListener('keydown', (e) => {
- if (gameOver && e.key.toLowerCase() !== 'q') return;
- switch (e.key) {
- case 'ArrowLeft':
- if (!paused) tryMove(-1, 0);
- e.preventDefault(); break;
- case 'ArrowRight':
- if (!paused) tryMove(1, 0);
- e.preventDefault(); break;
- case 'ArrowDown':
- if (!paused) softDrop();
- e.preventDefault(); break;
- case 'Enter':
- if (!paused) hardDrop();
- e.preventDefault(); break;
- case 'z': case 'Z':
- if (!paused) rotate(-1);
- e.preventDefault(); break;
- case 'x': case 'X':
- if (!paused) rotate(1);
- e.preventDefault(); break;
- case ' ':
- togglePause();
- e.preventDefault(); break;
- case 'q': case 'Q':
- restart();
- e.preventDefault(); break;
- }
- });
- // Touch controls
- const touchHold = { ArrowLeft: false, ArrowRight: false, ArrowDown: false };
- let holdTimer = null;
- function pressHold(key, start) {
- if (gameOver || paused) return;
- if (start) {
- touchHold[key] = true;
- if (holdTimer) clearInterval(holdTimer);
- holdTimer = setInterval(() => {
- if (touchHold.ArrowLeft) tryMove(-1, 0);
- if (touchHold.ArrowRight) tryMove(1, 0);
- if (touchHold.ArrowDown) softDrop();
- }, 80);
- } else {
- touchHold[key] = false;
- if (!touchHold.ArrowLeft && !touchHold.ArrowRight && !touchHold.ArrowDown) {
- clearInterval(holdTimer); holdTimer = null;
- }
- }
- }
- document.querySelectorAll('#touch button').forEach(btn => {
- const act = btn.dataset.act;
- const fire = (e) => {
- e.preventDefault();
- if (gameOver && act !== 'restart') return;
- switch (act) {
- case 'left': if (!paused) tryMove(-1, 0); pressHold('ArrowLeft', true); break;
- case 'right': if (!paused) tryMove(1, 0); pressHold('ArrowRight', true); break;
- case 'down': if (!paused) softDrop(); pressHold('ArrowDown', true); break;
- case 'rotateL': if (!paused) rotate(-1); break;
- case 'rotateR': if (!paused) rotate(1); break;
- case 'drop': if (!paused) hardDrop(); break;
- case 'pause': togglePause(); break;
- case 'restart': restart(); break;
- }
- };
- const release = (e) => {
- e.preventDefault();
- const act = btn.dataset.act;
- if (act === 'left') pressHold('ArrowLeft', false);
- if (act === 'right') pressHold('ArrowRight', false);
- if (act === 'down') pressHold('ArrowDown', false);
- };
- btn.addEventListener('touchstart', fire, {passive: false});
- btn.addEventListener('touchend', release, {passive: false});
- btn.addEventListener('touchcancel', release, {passive: false});
- btn.addEventListener('mousedown', fire);
- btn.addEventListener('mouseup', release);
- btn.addEventListener('mouseleave', release);
- });
- // ====== Game loop ======
- function loop(time = 0) {
- if (!gameOver && !paused) {
- const delta = time - lastTime;
- dropCounter += delta;
- if (dropCounter > dropInterval) {
- if (!tryMove(0, 1)) lock();
- dropCounter = 0;
- }
- lastTime = time;
- draw();
- }
- requestAnimationFrame(loop);
- }
- init();
- requestAnimationFrame(loop);
- })();
- </script>
- </body>
- </html>
|