იდეები
რეალურ
პროგრამებად.
function main(canvas, options = {}) {
const game = createGame(options);
if (!canvas) return game;
const view = mountGame(canvas, game);
return { ...game, destroy: view.destroy };
}
// Board geometry includes two hidden spawn rows.
const WIDTH = 10;
const HEIGHT = 22;
const HIDDEN_ROWS = 2;
const LOCK_DELAY = 500;
const MAX_LOCK_RESETS = 15;
const LINE_POINTS = [0, 100, 300, 500, 800];
const TYPES = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];
const COLORS = {
I: '#67e8f9', O: '#fde68a', T: '#c4b5fd',
S: '#86efac', Z: '#fda4af', J: '#93c5fd', L: '#fdba74',
};
const SHAPES = {
I: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]],
O: [[1,1], [1,1]],
T: [[0,1,0], [1,1,1], [0,0,0]],
S: [[0,1,1], [1,1,0], [0,0,0]],
Z: [[1,1,0], [0,1,1], [0,0,0]],
J: [[1,0,0], [1,1,1], [0,0,0]],
L: [[0,0,1], [1,1,1], [0,0,0]],
};
// SRS kick offsets use screen coordinates: positive y is down.
const KICKS = {
'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 I_KICKS = {
'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]],
};
function createGame(options = {}) {
const random = options.random || Math.random;
const listeners = new Set();
let board;
let active;
let queue;
let held;
let holdUsed;
let score;
let lines;
let level;
let combo;
let backToBack;
let over;
let paused;
let gravityElapsed;
let lockElapsed;
let lockResets;
function reset() {
board = emptyBoard();
queue = [];
held = null;
holdUsed = false;
score = 0;
lines = 0;
level = 1;
combo = -1;
backToBack = false;
over = false;
paused = false;
gravityElapsed = 0;
lockElapsed = 0;
lockResets = 0;
refillQueue();
spawn();
notify();
}
function refillQueue() {
while (queue.length < 7) {
const bag = [...TYPES];
for (let i = bag.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]];
}
queue.push(...bag);
}
}
function spawn(type) {
refillQueue();
const nextType = type || queue.shift();
active = {
type: nextType,
shape: cloneMatrix(SHAPES[nextType]),
x: Math.floor((WIDTH - SHAPES[nextType].length) / 2),
y: 0,
rotation: 0,
};
gravityElapsed = 0;
lockElapsed = 0;
lockResets = 0;
refillQueue();
if (collides(board, active)) over = true;
}
function playable() {
return !over && !paused;
}
function grounded() {
return collides(board, { ...active, y: active.y + 1 });
}
function replacePiece(next, resetLock = true) {
if (collides(board, next)) return false;
const wasGrounded = grounded();
active = next;
if (resetLock && wasGrounded && lockResets < MAX_LOCK_RESETS) {
lockElapsed = 0;
lockResets++;
}
return true;
}
function move(dx, dy = 0) {
if (!playable()) return false;
const next = { ...active, x: active.x + dx, y: active.y + dy };
const moved = replacePiece(next);
if (moved) notify();
return moved;
}
function rotate(direction = 1) {
if (!playable() || active.type === 'O') return false;
const from = active.rotation;
const to = (from + direction + 4) % 4;
const shape = rotateMatrix(active.shape, direction);
const table = active.type === 'I' ? I_KICKS : KICKS;
const offsets = table[from + '>' + to];
for (const [dx, dy] of offsets) {
const next = {
...active, shape, rotation: to,
x: active.x + dx, y: active.y + dy,
};
if (replacePiece(next)) {
notify();
return true;
}
}
return false;
}
function softDrop() {
if (!playable()) return false;
const moved = replacePiece({ ...active, y: active.y + 1 }, false);
if (moved) {
score++;
gravityElapsed = 0;
notify();
}
return moved;
}
function hardDrop() {
if (!playable()) return 0;
let distance = 0;
while (!grounded()) {
active.y++;
distance++;
}
score += distance * 2;
lockPiece();
notify();
return distance;
}
function hold() {
if (!playable() || holdUsed) return false;
const previous = held;
held = active.type;
spawn(previous);
holdUsed = true;
notify();
return true;
}
function lockPiece() {
const occupied = pieceCells(active);
if (occupied.some(([, y]) => y < 0)) {
over = true;
return;
}
for (const [x, y] of occupied) board[y][x] = active.type;
if (occupied.every(([, y]) => y < HIDDEN_ROWS)) {
over = true;
return;
}
clearLines();
holdUsed = false;
spawn();
}
function clearLines() {
const remaining = board.filter(row => row.some(cell => !cell));
const cleared = HEIGHT - remaining.length;
if (cleared === 0) {
combo = -1;
return;
}
combo++;
const tetris = cleared === 4;
const multiplier = tetris && backToBack ? 1.5 : 1;
score += LINE_POINTS[cleared] * level * multiplier;
score += Math.max(combo, 0) * 50 * level;
backToBack = tetris;
lines += cleared;
level = 1 + Math.floor(lines / 10);
board = Array.from({ length: cleared }, emptyRow).concat(remaining);
}
function tick(delta = 16) {
if (!playable()) return;
const elapsed = Math.min(Math.max(delta, 0), 100);
gravityElapsed += elapsed;
const interval = Math.max(60, 1000 * Math.pow(0.8, level - 1));
while (gravityElapsed >= interval) {
gravityElapsed -= interval;
if (!grounded()) active.y++;
}
if (grounded()) {
lockElapsed += elapsed;
if (lockElapsed >= LOCK_DELAY) lockPiece();
} else {
lockElapsed = 0;
}
notify();
}
function pause() {
if (over) return false;
paused = !paused;
notify();
return paused;
}
function getState() {
const ghost = { ...active, shape: cloneMatrix(active.shape) };
while (!collides(board, { ...ghost, y: ghost.y + 1 })) ghost.y++;
return {
board: cloneMatrix(board),
active: { ...active, shape: cloneMatrix(active.shape) },
ghost, next: queue.slice(0, 5), held,
score, lines, level, combo, over, paused, holdUsed,
};
}
function notify() {
if (listeners.size === 0) return;
const state = getState();
for (const listener of listeners) listener(state);
}
function subscribe(listener) {
listeners.add(listener);
listener(getState());
return () => listeners.delete(listener);
}
reset();
return {
tick, move, rotate, softDrop, hardDrop, hold,
pause, reset, getState, subscribe,
};
}
function emptyRow() {
return Array(WIDTH).fill(null);
}
function emptyBoard() {
return Array.from({ length: HEIGHT }, emptyRow);
}
function cloneMatrix(matrix) {
return matrix.map(row => [...row]);
}
function rotateMatrix(matrix, direction) {
const size = matrix.length;
return Array.from({ length: size }, (_, y) =>
Array.from({ length: size }, (_, x) =>
direction > 0 ? matrix[size - 1 - x][y] : matrix[x][size - 1 - y]));
}
function pieceCells(piece) {
const cells = [];
piece.shape.forEach((row, dy) => {
row.forEach((value, dx) => {
if (value) cells.push([piece.x + dx, piece.y + dy]);
});
});
return cells;
}
function collides(board, piece) {
return pieceCells(piece).some(([x, y]) =>
x < 0 || x >= WIDTH || y >= HEIGHT || (y >= 0 && board[y][x]));
}
// Canvas rendering is optional; the engine also runs without a DOM.
function mountGame(canvas, game) {
const context = canvas.getContext('2d');
if (!context) throw new Error('A 2D canvas is required.');
const cell = 24;
const width = WIDTH * cell;
const height = (HEIGHT - HIDDEN_ROWS) * cell + 104;
const scale = globalThis.devicePixelRatio || 1;
canvas.width = width * scale;
canvas.height = height * scale;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
canvas.tabIndex = 0;
canvas.setAttribute('aria-label', 'Tetris game');
context.scale(scale, scale);
let frame;
let previousTime;
let destroyed = false;
function drawBlock(x, y, type, opacity = 1) {
if (y < HIDDEN_ROWS) return;
const top = (y - HIDDEN_ROWS) * cell;
context.globalAlpha = opacity;
context.fillStyle = COLORS[type];
context.fillRect(x * cell + 1, top + 1, cell - 2, cell - 2);
context.fillStyle = 'rgba(255,255,255,0.2)';
context.fillRect(x * cell + 2, top + 2, cell - 4, 2);
context.globalAlpha = 1;
}
function drawPiece(piece, opacity = 1) {
for (const [x, y] of pieceCells(piece)) {
drawBlock(x, y, piece.type, opacity);
}
}
function drawGrid() {
context.strokeStyle = 'rgba(255,255,255,0.06)';
context.lineWidth = 1;
context.beginPath();
for (let x = 0; x <= WIDTH; x++) {
context.moveTo(x * cell, 0);
context.lineTo(x * cell, height - 104);
}
for (let y = 0; y <= HEIGHT - HIDDEN_ROWS; y++) {
context.moveTo(0, y * cell);
context.lineTo(width, y * cell);
}
context.stroke();
}
function drawPreview(type, left, top) {
if (!type) return;
SHAPES[type].forEach((row, y) => {
row.forEach((value, x) => {
if (!value) return;
context.fillStyle = COLORS[type];
context.fillRect(left + x * 8, top + y * 8, 7, 7);
});
});
}
function drawStatus(state) {
const top = height - 88;
context.fillStyle = '#cbd5e1';
context.font = '12px monospace';
context.fillText('SCORE ' + state.score, 8, top);
context.fillText('LEVEL ' + state.level, 8, top + 20);
context.fillText('LINES ' + state.lines, 8, top + 40);
context.fillText('HOLD', width - 56, top);
drawPreview(state.held, width - 52, top + 12);
context.fillText('NEXT', 104, top);
state.next.slice(0, 3).forEach((type, index) => {
drawPreview(type, 102 + index * 38, top + 12);
});
}
function drawOverlay(label) {
const fieldHeight = height - 104;
context.fillStyle = 'rgba(11,15,16,0.82)';
context.fillRect(0, 0, width, fieldHeight);
context.fillStyle = '#f8fafc';
context.font = 'bold 22px monospace';
context.textAlign = 'center';
context.fillText(label, width / 2, fieldHeight / 2);
context.font = '11px monospace';
const hint = label === 'PAUSED' ? 'P to resume' : 'Enter to restart';
context.fillText(hint, width / 2, fieldHeight / 2 + 28);
context.textAlign = 'left';
}
function render(state) {
context.clearRect(0, 0, width, height);
context.fillStyle = '#0b0f10';
context.fillRect(0, 0, width, height);
drawGrid();
state.board.forEach((row, y) => {
row.forEach((type, x) => {
if (type) drawBlock(x, y, type);
});
});
if (!state.over) {
drawPiece(state.ghost, 0.18);
drawPiece(state.active);
}
drawStatus(state);
if (state.paused) drawOverlay('PAUSED');
if (state.over) drawOverlay('GAME OVER');
}
function onKey(event) {
const actions = {
ArrowLeft: () => game.move(-1),
ArrowRight: () => game.move(1),
ArrowDown: game.softDrop,
ArrowUp: () => game.rotate(1),
KeyX: () => game.rotate(1),
KeyZ: () => game.rotate(-1),
Space: game.hardDrop,
KeyC: game.hold,
KeyP: game.pause,
Escape: game.pause,
Enter: game.reset,
};
const action = actions[event.code];
if (!action) return;
event.preventDefault();
if (event.repeat && ['Space', 'KeyC', 'Enter'].includes(event.code)) return;
action();
}
function focusCanvas() {
canvas.focus();
}
function animate(time) {
if (destroyed) return;
const delta = previousTime === undefined ? 0 : time - previousTime;
previousTime = time;
game.tick(delta);
frame = requestAnimationFrame(animate);
}
// Suspend gravity while the browser tab is hidden.
let resumeOnVisible = false;
function onVisibilityChange() {
if (document.hidden) {
resumeOnVisible = !game.getState().paused && !game.getState().over;
if (resumeOnVisible) game.pause();
} else if (resumeOnVisible) {
if (game.getState().paused) game.pause();
resumeOnVisible = false;
previousTime = undefined;
}
}
document.addEventListener('visibilitychange', onVisibilityChange);
const unsubscribe = game.subscribe(render);
canvas.addEventListener('keydown', onKey);
canvas.addEventListener('pointerdown', focusCanvas);
frame = requestAnimationFrame(animate);
function destroy() {
if (destroyed) return;
destroyed = true;
cancelAnimationFrame(frame);
canvas.removeEventListener('keydown', onKey);
canvas.removeEventListener('pointerdown', focusCanvas);
document.removeEventListener('visibilitychange', onVisibilityChange);
unsubscribe();
}
return { destroy };
}
// Keyboard: arrows to move, Z/X to rotate, Space to drop, C to hold.
// Usage: const game = main(document.querySelector('canvas'));
// Headless: const engine = main(); engine.tick(16);ახალი ერა
ხარისხიანი პროგრამებისთვის.
იდეები
რეალური
შედეგი
ჩვენი ტექნოლოგიები.
გაიცანით AVALANCHE.
AVALANCHE, LLC არის პროგრამული უზრუნველყოფის შემქმნელი კომპანია საქართველოში. ჩვენ ვქმნით მობილურ აპლიკაციებს, ვებპროდუქტებსა და მათ ინფრასტრუქტურას.
