rjgMtIfGYu4OB4QkmjHAeAZy7ixF2fuByIYhJHQr

Membuat Game Tetris

Cara Membuat Game Tetris
Nostalgia Belajar Ngoding: Bikin Game Tetris Klasik Sendiri! (Pakai HTML, CSS, & JS)

Setelah kemarin kita sukses bikin game Ular, saatnya naik level! Proyek apa yang lebih klasik dan lebih menantang dari Tetris? Game legendaris ini adalah salah satu "ujian" yang cocok untuk melatih logika array dan manipulasi data.

Sama seperti game Ular, kita nggak akan pakai engine atau library. Kita akan bangun dari nol pakai tiga senjata andalan kita: HTML, CSS, dan JavaScript murni.

Siap? Kodenya bakal jauh lebih kompleks dari Ular, jadi siapkan kopi dan mari kita bedah!


Langkah 1: Canvas & Info (index.html)

Pertama, kita siapkan "panggung"-nya. Mirip dengan game Ular, kita butuh <canvas> sebagai layar game. Tapi kali ini, kita tambahkan satu area lagi di sampingnya untuk nampilin skor dan tombol "Mulai".

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Game Tetris</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Game Tetris</h1>
    
    <div id="game-container">
        <!-- Layar game kita -->
        <canvas id="tetrisCanvas" width="300" height="600"></canvas>
        
        <!-- Info game di samping -->
        <div id="game-info">
            <b>SKOR</b>
            <div id="score">0</div>
            <button id="startButton">Mulai Game</button>
            <p>Panah: Gerak/Rotasi</p>
            <p>Bawah: Percepat</p>
        </div>
    </div>
    
    <script src="script.js"></script>
</body>
</html>

Langkah 2: Tampilan CSS (style.css)

Sekarang, kita percantik tampilannya. Kita akan pakai flexbox untuk menata canvas dan #game-info agar tampil rapi bersebelahan. Sisanya cuma dandanin biar kelihatan kayak game (tema gelap, border keren, dll).

body {
    background-color: #282c34;
    color: #ffffff;
    font-family: 'Arial', sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    margin: 0;
}

h1 {
    color: #61dafb;
    margin-bottom: 20px;
}

#game-container {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
}

canvas {
    background-color: #000000;
    border: 5px solid #61dafb;
    display: block;
}

#game-info {
    margin-left: 20px;
    min-width: 150px;
    background-color: #20232a;
    padding: 20px;
    border-radius: 8px;
}

#game-info b {
    margin-top: 0;
    color: #61dafb;
    text-align: center;
}

#score {
    font-size: 2.5em;
    font-weight: bold;
    color: #ffffff;
    text-align: center;
    margin-bottom: 20px;
}

#startButton {
    background-color: #21a1f1;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    font-size: 1.1em;
    cursor: pointer;
    transition: background-color 0.3s ease;
    width: 100%;
}

#startButton:hover {
    background-color: #1a7bbd;
}

#game-info p {
    font-size: 0.9em;
    color: #ccc;
    text-align: center;
    margin-top: 20px;
}

Langkah 3: Otak Game (script.js)

Oke, ini adalah bagian terbesar dan paling rumit. Kodenya jauh lebih panjang dari game Ular, jadi saya akan jelaskan konsep-konsep utamanya dulu sebelum kita lihat kode lengkapnya.

Konsep Inti Logika Tetris

Gimana cara kerja Tetris? Kuncinya ada di dua "papan" yang berbeda:

  1. arena (Arena): Ini adalah "papan" utama (grid 10x20). Isinya adalah semua balok yang sudah mendarat dan nggak bisa gerak lagi. Bayangin ini kayak "cap" permanen di papan.
  2. player (Pemain): Ini adalah objek yang nyimpen data balok yang sedang jatuh. Objek ini punya bentuk baloknya (misal, bentuk 'L') dan posisinya saat ini (x dan y).

Game ini bekerja dengan cara:

  1. draw(): Setiap frame, kita bersihkan kanvas, gambar semua balok di arena, lalu gambar balok player di atasnya.
  2. playerDrop(): Setiap detik (atau pas tombol bawah ditekan), kita gerakin player ke bawah satu kotak.
  3. checkCollision(): Ini fungsi paling penting. Setiap player mau gerak (kiri, kanan, bawah, atau mutar), fungsi ini ngecek: "Kalau aku pindah ke sana, apa aku bakal nabrak dinding ATAU nabrak balok yang udah ada di arena?"
  4. Mendarat: Kalau playerDrop() ngecek dan ternyata "nabrak" di bawah, itu artinya baloknya mendarat.
  5. mergeToArena(): Pas mendarat, kita "cap" bentuk balok player ke dalam arena. Balok player sekarang jadi bagian permanen dari arena.
  6. sweepArena(): Setelah "nge-cap", kita cek arena. Ada baris yang penuh nggak? Kalau ada, hapus baris itu, kasih skor, dan turunin semua baris di atasnya.
  7. resetPlayer(): Setelah itu, kita bikin balok player baru di paling atas. Ulangi terus!
  8. Rotasi: Ini bagian paling "matematis". Kita memutar bentuk balok (yang disimpan sebagai array 2D) pakai trik "transpose + reverse".
Kode script.js Lengkap

Ini dia kode lengkapnya. Saya sudah tambahkan banyak komentar di dalam kode untuk membantu menjelaskan setiap langkahnya.

// --- Inisialisasi Elemen ---
const canvas = document.getElementById('tetrisCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const startButton = document.getElementById('startButton');

// --- Pengaturan Game ---
const COLS = 10; // 10 kolom
const ROWS = 20; // 20 baris
const GRID_SIZE = 30; // Ukuran setiap kotak (30px)
const EMPTY_COLOR = '#000';

// Atur ukuran kanvas sesuai grid
canvas.width = COLS * GRID_SIZE;
canvas.height = ROWS * GRID_SIZE;

// --- Bentuk-Bentuk Balok (Tetrominoes) ---
const SHAPES = [
    // I
    { shape: [[1, 1, 1, 1]], color: 'cyan' },
    // J
    { shape: [[1, 0, 0], [1, 1, 1]], color: 'blue' },
    // L
    { shape: [[0, 0, 1], [1, 1, 1]], color: 'orange' },
    // O
    { shape: [[1, 1], [1, 1]], color: 'yellow' },
    // S
    { shape: [[0, 1, 1], [1, 1, 0]], color: 'green' },
    // T
    { shape: [[0, 1, 0], [1, 1, 1]], color: 'purple' },
    // Z
    { shape: [[1, 1, 0], [0, 1, 1]], color: 'red' }
];

// --- Variabel Game ---
let arena = []; // 'Arena' adalah grid yang menyimpan balok yang sudah mendarat
let player = {}; // Balok yang sedang dikontrol pemain
let score = 0;
let gameOver = true;
let gameInterval; // Untuk game loop (auto-drop)

// === FUNGSI INTI ===

/**
 * Membuat arena (grid) baru yang kosong.
 */
function createArena(width, height) {
    const matrix = [];
    while (height--) {
        matrix.push(new Array(width).fill(0)); // 0 = kotak kosong
    }
    return matrix;
}

/**
 * Mendapatkan balok baru secara acak.
 */
function resetPlayer() {
    const randomIdx = Math.floor(Math.random() * SHAPES.length);
    const piece = SHAPES[randomIdx];

    player = {
        shape: piece.shape,
        color: piece.color,
        pos: { 
            x: Math.floor(COLS / 2) - Math.floor(piece.shape[0].length / 2), // Mulai di tengah
            y: 0 // Mulai di atas
        }
    };

    // Cek Game Over: Jika balok baru langsung bertabrakan, game selesai.
    if (checkCollision(arena, player)) {
        gameOver = true;
        clearInterval(gameInterval);
        alert('GAME OVER! Skor Anda: ' + score);
    }
}

/**
 * Memeriksa tabrakan antara balok pemain dan arena.
 * @returns {boolean} True jika ada tabrakan.
 */
function checkCollision(arena, player) {
    for (let y = 0; y < player.shape.length; y++) {
        for (let x = 0; x < player.shape[y].length; x++) {
            // Cek jika ini adalah bagian dari balok (bukan kotak kosong di matriks balok)
            if (player.shape[y][x] !== 0) {
                let newX = player.pos.x + x;
                let newY = player.pos.y + y;

                // Cek 1: Tabrakan dengan dinding (kiri/kanan/bawah)
                if (newX < 0 || newX >= COLS || newY >= ROWS) {
                    return true;
                }
                
                // Cek 2: Tabrakan dengan balok lain yang sudah ada di arena
                // Pastikan arena[newY] ada (tidak di atas layar)
                if (arena[newY] && arena[newY][newX] !== 0) {
                    return true;
                }
            }
        }
    }
    return false; // Tidak ada tabrakan
}

/**
 * "Mencap" atau menggabungkan balok pemain ke arena setelah mendarat.
 */
function mergeToArena(arena, player) {
    for (let y = 0; y < player.shape.length; y++) {
        for (let x = 0; x < player.shape[y].length; x++) {
            if (player.shape[y][x] !== 0) {
                // Set warna balok ke grid arena
                arena[player.pos.y + y][player.pos.x + x] = player.color;
            }
        }
    }
}

/**
 * Memeriksa dan membersihkan baris yang penuh.
 */
function sweepArena() {
    let rowsCleared = 0;
    
    // Iterasi dari bawah ke atas
    for (let y = arena.length - 1; y >= 0; y--) {
        // Cek apakah satu baris penuh (tidak ada '0')
        if (arena[y].every(value => value !== 0)) {
            // Hapus baris ini
            const removedRow = arena.splice(y, 1)[0];
            
            // Tambahkan baris kosong baru di atas
            const newRow = new Array(COLS).fill(0);
            arena.unshift(newRow);
            
            rowsCleared++;
            
            // Kita perlu cek baris yang sama lagi karena baris di atasnya sudah turun
            y++;
        }
    }
    
    // Update Skor (contoh: 100 poin per baris)
    score += rowsCleared * 100; 
    // Bonus untuk Tetris (4 baris)
    if (rowsCleared >= 4) {
        score += 400; // Bonus
    }
    scoreElement.innerText = score;
}

// === FUNGSI GAMBAR ===

/**
 * Menggambar seluruh kanvas (arena dan balok pemain).
 */
function draw() {
    // 1. Bersihkan kanvas (dengan warna latar belakang hitam)
    ctx.fillStyle = EMPTY_COLOR;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // 2. Gambar arena (balok yang sudah mendarat)
    drawMatrix(arena, { x: 0, y: 0 });
    
    // 3. Gambar balok pemain
    drawMatrix(player.shape, player.pos, player.color);
}

/**
 * Helper function untuk menggambar matriks (baik arena maupun balok).
 */
function drawMatrix(matrix, offset, specificColor = null) {
    for (let y = 0; y < matrix.length; y++) {
        for (let x = 0; x < matrix[y].length; x++) {
            let value = matrix[y][x];
            if (value !== 0) {
                // Jika specificColor diberikan (untuk balok pemain), gunakan itu.
                // Jika tidak (untuk arena), gunakan nilai dari matriks (yang sudah kita simpan warnanya).
                ctx.fillStyle = specificColor || value;
                
                // Gambar kotak
                ctx.fillRect((offset.x + x) * GRID_SIZE, 
                             (offset.y + y) * GRID_SIZE, 
                             GRID_SIZE, GRID_SIZE);
                             
                // Beri garis pinggir
                ctx.strokeStyle = '#222';
                ctx.strokeRect((offset.x + x) * GRID_SIZE, 
                             (offset.y + y) * GRID_SIZE, 
                             GRID_SIZE, GRID_SIZE);
            }
        }
    }
}

// === FUNGSI KONTROL PEMAIN ===

/**
 * Menjatuhkan balok pemain satu langkah ke bawah.
 */
function playerDrop() {
    player.pos.y++;
    
    if (checkCollision(arena, player)) {
        // Jika bertabrakan, kembalikan ke posisi sebelumnya
        player.pos.y--;
        // Gabungkan balok ke arena
        mergeToArena(arena, player);
        // Cek apakah ada baris yang penuh
        sweepArena();
        // Dapatkan balok baru
        resetPlayer();
    }
}

/**
 * Menggerakkan pemain ke kiri atau kanan.
 * @param {number} dir -1 untuk Kiri, 1 untuk Kanan.
 */
function playerMove(dir) {
    player.pos.x += dir;
    
    if (checkCollision(arena, player)) {
        // Jika bertabrakan, kembalikan
        player.pos.x -= dir;
    }
}

/**
 * Memutar balok pemain.
 */
function playerRotate() {
    const originalShape = player.shape;
    const rotatedShape = rotateMatrix(player.shape);
    player.shape = rotatedShape;
    
    // Cek tabrakan setelah rotasi
    if (checkCollision(arena, player)) {
        // Jika nabrak, kembalikan ke bentuk asli
        player.shape = originalShape;
    }
}

/**
 * Helper function untuk memutar matriks (logika inti Tetris).
 */
function rotateMatrix(matrix) {
    // 1. Transpose (ubah baris jadi kolom dan sebaliknya)
    const transposed = [];
    for (let y = 0; y < matrix[0].length; y++) {
        transposed.push(new Array(matrix.length).fill(0));
    }
    
    for (let y = 0; y < matrix.length; y++) {
        for (let x = 0; x < matrix[y].length; x++) {
            transposed[x][y] = matrix[y][x];
        }
    }
    
    // 2. Reverse (balik urutan setiap baris) -> ini untuk rotasi searah jarum jam
    transposed.forEach(row => row.reverse());
    
    return transposed;
}

// === KONTROL GAME ===

/**
 * Game Loop Utama
 */
function gameLoop() {
    if (gameOver) {
        return;
    }
    
    playerDrop(); // Jatuhkan balok
    draw();       // Gambar ulang
}

/**
 * Memulai game baru.
 */
function startGame() {
    // Reset semuanya
    arena = createArena(COLS, ROWS);
    score = 0;
    scoreElement.innerText = score;
    gameOver = false;
    
    resetPlayer(); // Dapatkan balok pertama
    
    // Hentikan loop lama jika ada
    if (gameInterval) {
        clearInterval(gameInterval);
    }
    
    // Mulai game loop (balok turun setiap 1 detik)
    // Ubah angka 1000 jadi lebih kecil (misal 500) biar lebih cepat
    gameInterval = setInterval(gameLoop, 1000);
    
    draw(); // Gambar game awal
}

// --- Event Listeners ---

// Kontrol Keyboard
document.addEventListener('keydown', (event) => {
    if (gameOver) return;

    if (event.key === 'ArrowLeft') {
        playerMove(-1); // Kiri
    } else if (event.key === 'ArrowRight') {
        playerMove(1); // Kanan
    } else if (event.key === 'ArrowDown') {
        playerDrop(); // Percepat ke bawah
    } else if (event.key === 'ArrowUp') {
        playerRotate(); // Putar
    }
    
    draw(); // Gambar ulang setelah setiap gerakan
});

// Tombol Mulai
startButton.addEventListener('click', startGame);

// Gambar tampilan awal
draw();

Selesai! Apa Selanjutnya?

Gimana? Lumayan bikin pusing tapi seru banget, kan? Kamu baru aja bikin salah satu game paling ikonik sepanjang masa pakai JavaScript murni. Logika yang kamu pelajari di sini (manipulasi array 2D, deteksi tabrakan, game state) itu super berharga.

Kalau kamu masih haus tantangan, ini beberapa ide "PR" buat ngembangin game ini:

  1. Level & Kecepatan: Bikin game makin ngebut (kurangi interval setInterval) setiap pemain dapet 1000 poin.
  2. "Next Piece" Preview: Buat satu kanvas kecil lagi di #game-info untuk nunjukin balok apa yang bakal keluar selanjutnya.
  3. High Score: Simpan skor tertinggi pakai localStorage biar nggak hilang pas di-refresh.
  4. Suara: Tambahin efek suara pas balok mendarat atau pas baris hancur!

Selamat mencoba dan selamat ngoding!

Posting Komentar