Final Project: Complete Your Tetris Game
Every piece from this course now exists on its own: the board, all 7 tetrominoes with rotation, gravity, collision, touch controls, locking, line-clearing, scoring, levels, game over, and a fair 7-bag piece queue. This final project assembles all of it into one complete, playable Tetris screen.
Requirements
Your finished app/tetris.tsx screen should satisfy every one of these:
- Creates a 10x20 board using
createEmptyBoard()and renders it with the nested-Viewgrid pattern. - Spawns tetrominoes from the 7-bag queue (
usePieceQueue), not plainMath.random(), and shows a "Next" preview of the upcoming piece. - Runs a game loop (
useGameLoop) that moves the active piece down automatically, at a speed that increases with the level. - Provides on-screen touch controls for move left, move right, rotate, and soft drop, each going through
hasCollision()before being applied. - Locks a piece into the board when it can no longer fall (
lockPiece), clears any completed rows (clearFullRows), and updates score/level based on lines cleared. - Detects game over when a freshly spawned piece immediately collides, stops the game loop, and shows a game over message with the final score.
Bringing every earlier lesson's pieces together into one screen (state, game loop, controls, and rendering) is the whole project:
import { useCallback, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function TetrisScreen() {
const [board, setBoard] = useState<Matrix>(createEmptyBoard);
const [piece, setPiece] = useState<ActivePiece>(() => spawnPiece('T'));
const [score, setScore] = useState(0);
const [totalLines, setTotalLines] = useState(0);
const [gameOver, setGameOver] = useState(false);
const { next, takeNext } = usePieceQueue();
const level = levelForLines(totalLines);
const tick = useCallback(() => {
if (gameOver) return;
if (!hasCollision(board, piece, piece.row + 1, piece.col)) {
setPiece((p) => ({ ...p, row: p.row + 1 }));
return;
}
const locked = lockPiece(board, piece);
const { board: cleared, linesCleared } = clearFullRows(locked);
const nextPiece = spawnPiece(takeNext());
if (isGameOver(cleared, nextPiece)) {
setBoard(cleared);
setGameOver(true);
return;
}
setBoard(cleared);
setPiece(nextPiece);
setScore((s) => s + scoreForLines(linesCleared, level));
setTotalLines((t) => t + linesCleared);
}, [board, piece, gameOver, level, takeNext]);
useGameLoop(speedForLevel(level), tick);
return (
<View style={styles.screen}>
<Text style={styles.score}>Score: {score}</Text>
<Board board={renderPieceOnBoard(board, piece)} />
<NextPreview shapeName={next} />
<Controls
onLeft={() => tryMove(0, -1)}
onRight={() => tryMove(0, 1)}
onRotate={tryRotate}
onDrop={() => tryMove(1, 0)}
/>
{gameOver && <Text style={styles.gameOver}>Game Over! Final score: {score}</Text>}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: 'center', paddingTop: 48, backgroundColor: '#0f172a' },
score: { color: '#fff', fontSize: 20, marginBottom: 12 },
gameOver: { color: '#f87171', fontSize: 18, marginTop: 16 },
});
renderPieceOnBoard(board, piece) (not shown, worth writing yourself) should return a copy of board with the active piece's cells overlaid on top, so the falling piece is visible before it locks, without mutating the real board state.
Stretch goals
- Add a hard drop: instantly move the piece straight down (loop
tryMove(1, 0)until it would collide) instead of waiting for gravity, common in modern Tetris as a fifth control. - Add a ghost piece: a faint outline showing where the current piece would land if hard-dropped, computed the same way as the hard drop, but only rendered, not actually moved.
- Add a hold piece: let the player swap the active piece into a "held" slot once per drop, swapping it back in later.
- Replace the on-screen buttons with swipe gestures using
react-native-gesture-handler, swipe left/right to move, swipe down for soft drop, tap to rotate. - Persist the high score locally with
@react-native-async-storage/async-storageso it survives closing the app. - Add sound effects for line clears and game over with
expo-av.
Submit a link to your finished project (a repo or gist) below, an instructor will review it before you can mark this lesson complete.