How to Build a Tetris Game in Jetpack Compose

How to Build a Tetris Game in Jetpack Compose

🧩 What is Tetris?

Tetris is a classic block-stacking puzzle game where players guide falling shapes, known as Tetrominoes, to fit perfectly into horizontal rows. Complete a line, and it disappears—giving you more room to stack. Fail to clear, and the screen fills up, leading to game over.

It’s simple, endlessly playable, and one of the most iconic games in history.

💥 Why is Tetris So Popular?

Despite being around for nearly four decades, Tetris is still wildly popular. Here's why:

✅ Easy to pick up: You don’t need a tutorial to start.
✅ Hard to master: High-level strategies like T-spins and combos add serious depth.
📱 Cross-platform: From arcade machines to Android phones, it runs everywhere.
🧠 Good for your brain: Studies suggest it helps with spatial reasoning and focus.
🔁 Infinite replayability: Every game is unique thanks to randomness and increasing difficulty.

“Tetris is the purest form of game design.” – Every game dev ever

🏛️ The past History of Tetris

Believe it or not, Tetris didn’t come from Silicon Valley or Japan.
Inspired by a puzzle game using pentominoes (shapes made of five squares), he simplified it to four-block pieces and called it Tetris—a blend of “tetra” (Greek for four) and tennis, his favorite sport.

The game started as a simple ASCII program on a Russian Electronika 60 computer. No graphics. Just addictive gameplay.

🌍 Global Boom: How Tetris Took Over the World

The game silently spread across Moscow’s computing circles and made its way to Hungary. In 1989,It was tried to licensed by a British developer and soon, Nintendo bundled it with the Game Boy in 1989.

That decision changed gaming forever. Kids played it in cars. Adults played it in waiting rooms. It transcended language, age, and platform.

📱 Tetris in the Modern Age (1990s to 2020s)

Tetris has been reinvented across every era:

  • 1990s–2000s: Versions on NES, Sega, Game Boy Advance, and PCs. Multiplayer and 3D Tetris arrived.
  • Mobile Era: Played on Nokia brick phones. Later, smartphones brought touch controls and vibrant redesigns.
  • Online & AI Era: Games like Tetris 99 introduced battle royale mechanics.
AI bots mastered infinite play through neural networks and reinforcement learning.
How to Build a Tetris Game in Jetpack Compose

🛠️ How to Build a Tetris Game in Jetpack Compose

Ready to build your own Tetris game on Android? Jetpack Compose makes it smoother than ever, offering a modern declarative UI framework with animations and recomposition out-of-the-box.

Let’s break down the process.

Tetra Fall Game in Jetpack Compose
In this tutorial, we’ll build a fully working Tetris Game using Jetpack Compose, Kotlin, and ViewModel. You’ll learn:
  • How to render a grid
  • How to move and rotate Tetris blocks (Tetrominoes)
  • How to manage score and game-over
  • How to build a real-time game loop using coroutines
  • Bonus: Add repeatable touch input and restart button

📐 Step 1: Define the Cell and Tetromino Classes

Cell.kt

package com.example.tetrafall

data class Cell(
var isFilled: Boolean = false,
var color: Int = 0xFF000000.toInt() // Default: black
)

Tetromino.kt

package com.example.tetrafall

enum class TetrominoType { I, O, T, S, Z, J, L }

data class Tetromino(
val type: TetrominoType,
val color: Int,
val shape: List<Pair<Int, Int>>
)

fun getTetromino(type: TetrominoType): Tetromino {
return when (type) {
TetrominoType.I -> Tetromino(type, 0xFF00BCD4.toInt(), listOf(0 to 0, 1 to 0, 2 to 0, 3 to 0))
TetrominoType.O -> Tetromino(type, 0xFFFFEB3B.toInt(), listOf(0 to 0, 1 to 0, 0 to 1, 1 to 1))
TetrominoType.T -> Tetromino(type, 0xFFE91E63.toInt(), listOf(1 to 0, 0 to 1, 1 to 1, 2 to 1))
TetrominoType.S -> Tetromino(type, 0xFF4CAF50.toInt(), listOf(1 to 0, 2 to 0, 0 to 1, 1 to 1))
TetrominoType.Z -> Tetromino(type, 0xFFF44336.toInt(), listOf(0 to 0, 1 to 0, 1 to 1, 2 to 1))
TetrominoType.J -> Tetromino(type, 0xFF3F51B5.toInt(), listOf(0 to 0, 0 to 1, 1 to 1, 2 to 1))
TetrominoType.L -> Tetromino(type, 0xFFFF9800.toInt(), listOf(2 to 0, 0 to 1, 1 to 1, 2 to 1))
}
}

🔁 Step 2: Create the GameViewModel

GameViewModel.kt

Handles grid, score, game logic, movement, and game loop.
package com.example.tetrafall

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class GameViewModel : ViewModel() {
val rows = 20
val cols = 10
val grid = List(rows) { MutableList(cols) { Cell() } }

var currentShape by mutableStateOf(getRandomShape())
var shapePositionX by mutableIntStateOf(3)
var shapePositionY by mutableIntStateOf(0)
var score by mutableIntStateOf(0)
private val _isPaused = MutableStateFlow(false)
val isPaused: StateFlow<Boolean> = _isPaused
private var gameOver by mutableStateOf(false)

init {
startGameLoop()
}

private fun startGameLoop() {
viewModelScope.launch {
while (true) {
delay(600)
if (!isPaused.value && !gameOver) {
moveDown()
}
}
}
}

fun togglePause() {
_isPaused.value = !_isPaused.value
}

fun moveDown() {
if (canMove(0, 1)) {
shapePositionY++
} else {
placeShape()
clearFilledRows()
spawnNewShape()

// Now check game over by scanning top rows
if (isGameOver()) {
gameOver = true
}
}
}

fun moveLeft() {
if (canMove(-1, 0)) shapePositionX--
}

fun moveRight() {
if (canMove(1, 0)) shapePositionX++
}

fun rotateCurrentShape() {
val rotated = rotateShape(currentShape.shape)
if (canMove(0, 0, rotated)) {
currentShape = currentShape.copy(shape = rotated)
}
}

fun instantDrop() {
while (canMove(0, 1)) shapePositionY++
moveDown()
}

private fun canMove(
dx: Int,
dy: Int,
shape: List<Pair<Int, Int>> = currentShape.shape
): Boolean {
return shape.all { (x, y) ->
val newX = shapePositionX + x + dx
val newY = shapePositionY + y + dy
newX in 0 until cols && newY in 0 until rows && !grid[newY][newX].isFilled
}
}

private fun placeShape() {
currentShape.shape.forEach { (x, y) ->
val gx = shapePositionX + x
val gy = shapePositionY + y
if (gx in 0 until cols && gy in 0 until rows) {
grid[gy][gx] = Cell(true, currentShape.color)
}
}
}

private fun clearFilledRows() {
val newGrid = mutableListOf<MutableList<Cell>>()

// Step 1: Keep only rows that are NOT completely filled
for (row in grid) {
if (!row.all { it.isFilled }) {
newGrid.add(row.toMutableList())
}
}

// Step 2: Calculate how many rows were cleared
val clearedRows = rows - newGrid.size

// Step 3: Add new empty rows at the top
repeat(clearedRows) {
newGrid.add(0, MutableList(cols) { Cell() })
}

// Step 4: Copy newGrid back to the actual grid
for (y in 0 until rows) {
for (x in 0 until cols) {
grid[y][x] = newGrid[y][x]
}
}

// Step 5: Update score
score += clearedRows * 100
}

fun isGameOver(): Boolean {
return currentShape.shape.any { (x, y) ->
val gx = 3 + x // the spawn position X
val gy = 0 + y // the spawn position Y
gx !in 0 until cols || gy !in 0 until rows || grid[gy][gx].isFilled
}
}

private fun spawnNewShape() {
currentShape = getRandomShape()
shapePositionX = 3
shapePositionY = 0
}

fun resetGame() {
for (y in 0 until rows) {
for (x in 0 until cols) {
grid[y][x] = Cell()
}
}
score = 0
gameOver = false
spawnNewShape()
}

private fun getRandomShape(): Tetromino {
val types = TetrominoType.entries.toTypedArray()
return getTetromino(types.random())
}
}

🔄 Shape Rotation Logic

package com.example.tetrafall

fun rotateShape(shape: List<Pair<Int, Int>>): List<Pair<Int, Int>> {
val rotated = shape.map { (x, y) -> -y to x }
val minX = rotated.minOf { it.first }
val minY = rotated.minOf { it.second }
return rotated.map { (x, y) -> (x - minX) to (y - minY) }
}

🧩 Step 3: Build the Tetris UI

TetrisScreen.kt

package com.example.tetrafall

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp


@Composable
fun TetrisScreen(viewModel: GameViewModel = remember { GameViewModel() }) {
val grid = viewModel.grid
val shape = viewModel.currentShape
val offsetX = viewModel.shapePositionX
val offsetY = viewModel.shapePositionY
val score = viewModel.score
val isGameOver = viewModel.isGameOver()

Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Tetris", style = MaterialTheme.typography.headlineLarge)
Text("Score: $score", style = MaterialTheme.typography.headlineMedium)

Spacer(modifier = Modifier.height(12.dp))

// Game Grid
Box(modifier = Modifier.background(Color.Black)) {
Column {
for (y in 0 until viewModel.rows) {
Row {
for (x in 0 until viewModel.cols) {
val isShapeCell = shape.shape.any { (dx, dy) ->
dx + offsetX == x && dy + offsetY == y
}
val cell = grid[y][x]
val color = when {
isShapeCell -> Color(shape.color)
cell.isFilled -> Color(cell.color)
else -> Color.DarkGray
}
Box(
modifier = Modifier.size(24.dp).padding(1.dp).background(color)
)
}
}
}
}
}

Spacer(modifier = Modifier.height(24.dp))
MovementControls(viewModel)

Row {
Button(onClick = { viewModel.rotateCurrentShape() }) { Text(" Rotate") }
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = { viewModel.instantDrop() }) { Text("Drop ") }
}
}
Spacer(modifier = Modifier.height(16.dp))

if (isGameOver) {

RewardScreen(
score = viewModel.score,
// highScore = viewModel.highScore,
onPlayAgain = {
// viewModel.scoreSaved = false
viewModel.resetGame()
},
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.95f)) // optional dimming
)

}
}

🎮 Step 4: Add Controls with Repeat Touch

package com.example.tetrafall

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay

@Composable
fun MovementControls(viewModel: GameViewModel) {
val isPaused by viewModel.isPaused.collectAsState() // Collect the StateFlow
Row {
RepeatPressButton("←") { viewModel.moveLeft() }
Spacer(modifier = Modifier.width(8.dp))
Text((if (isPaused) "▶️" else "⏸️") ,fontSize = 34.sp,
modifier = Modifier.clickable { viewModel.togglePause() })
Spacer(modifier = Modifier.width(8.dp))
RepeatPressButton("→") { viewModel.moveRight() }
}
}

@Composable
fun RepeatPressButton(text: String, onRepeat: () -> Unit) {
val isPressed = remember { mutableStateOf(false) }

LaunchedEffect(isPressed.value) {
if (isPressed.value) {
while (isPressed.value) {
onRepeat()
delay(100)
}
}
}

Box(
modifier = Modifier
.background(Color.Gray)
.padding(horizontal = 16.dp, vertical = 8.dp)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
isPressed.value = true
tryAwaitRelease()
isPressed.value = false
}
)
}
) {
Text(text = text, color = Color.White)
}
}

🧪Step 5: MaiinActiivity

MainActivity.kt

package com.example.tetrafall

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.tetrafall.ui.theme.TetraFallTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TetraFallTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/

TetrisScreen()
}
}
}
}

🧪Step 5: RewardScreen

package com.example.tetrafall

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun RewardScreen(score: Int, onPlayAgain: () -> Unit,modifier: Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("🎉 Game Over!", fontSize = 30.sp, fontWeight = FontWeight.Bold)
Text("Your Score: $score", fontSize = 24.sp)

Spacer(modifier = Modifier.height(20.dp))

Button(onClick = onPlayAgain) {
Text("Play Again")
}
}
}

Output:

Tetra Fall Game Screenshot
To show the next Tetris block (Tetromino) in your Jetpack Compose Tetris game, you can follow these steps:

✅ Step-by-Step: Add a "Next Block" Preview

🧠 Step 1: Update GameViewModel
Add a new variable to store the next shape:
var nextShape by mutableStateOf(getRandomShape())
Update spawnNewShape() to use it:
fun spawnNewShape() {
    currentShape = nextShape
    nextShape = getRandomShape()
    shapePositionX = 3
    shapePositionY = 0
}

🧩 Step 2: Add a Composable for Preview

Create a new composable:
@Composable
fun NextBlockPreview(nextShape: Tetromino) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Next", style = MaterialTheme.typography.bodyLarge)
        Box(
            modifier = Modifier
                .size(100.dp)
                .background(Color.LightGray)
                .padding(4.dp)
        ) {
            val blockSize = 20.dp
            val shape = nextShape.shape

            for ((x, y) in shape) {
                Box(
                    modifier = Modifier
                        .offset(x.dp * 20, y.dp * 20)
                        .size(blockSize)
                        .background(Color(nextShape.color))
                        .border(1.dp, Color.Black)
                )
            }
        }
    }
}

🖼️ Step 3: Place It in TetrisScreen

Above the main game grid, show the preview:
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween
) {
    Column {
        Text("Tetris", style = MaterialTheme.typography.headlineLarge)
        Text("Score: $score", style = MaterialTheme.typography.headlineMedium)
    }

    NextBlockPreview(viewModel.nextShape)
}
✅ Final Output
Your screen will now show:
  • Current score
  • Next block in a small box (top right)
  • Main Tetris grid
  • Controls
Tetra Fall Game Screenshot 2


Tetra Fall Game Screenshot 3Tetra Fall Game Screenshot 4


Previous Post Next Post

Contact Form