Python

파이썬, 테트리스 게임 소스 Tetris game source

coding-abc.tistory.com 2025. 8. 11. 13:56
반응형

파이썬으로 만든 테트리스 게임 소스입니다.

하단에서 소스 파일을 다운로드할 수 있습니다.

 

pygame이 설치되어 있어야 합니다.

pip install pygame

 

pip를 실행하는 방법을 모르면 아래 글을 읽어보세요.

https://coding-abc.tistory.com/349

 

파이썬, pip: 패키지 및 라이브러리를 설치하고 관리하기

pip는 **"Python Package Installer"**의 약자로, Python 패키지 및 라이브러리를 설치하고 관리하기 위한 표준 도구입니다. Python의 공식 패키지 저장소인 **PyPI (Python Package Index)**에서 패키지를 다운로드하

coding-abc.tistory.com

 


테트리스 게임 소스 Tetris game source

조작 키:

  • ← / → : 이동
  • ↓ : 소프트 드롭
  • Space : 하드 드롭
  • ↑ 또는 X : 시계 방향 회전
  • Z : 반시계 회전
  • C : 홀드
  • P : 일시정지
  • Esc : 종료

테트리스 게임 소스 Tetris game source

"""
Simple Tetris game in Python using pygame
Save as tetris.py and run with: python tetris.py
Requires: pip install pygame

Controls:
 - Left/Right arrow: move
 - Down arrow: soft drop
 - Space: hard drop
 - Up arrow / X: rotate clockwise
 - Z: rotate counter-clockwise
 - C: hold piece
 - P: pause
 - Esc: quit

This is a single-file implementation intended for learning and small projects.
"""
import pygame
import random
import sys

# Game constants
CELL_SIZE = 30
COLUMNS = 10
ROWS = 20
WIDTH = CELL_SIZE * COLUMNS
HEIGHT = CELL_SIZE * ROWS
FPS = 60

# Colors
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)
WHITE = (255, 255, 255)

# Tetromino shapes (4x4 matrices)
SHAPES = {
    'I': [[0,0,0,0],
          [1,1,1,1],
          [0,0,0,0],
          [0,0,0,0]],
    'J': [[1,0,0],
          [1,1,1],
          [0,0,0]],
    'L': [[0,0,1],
          [1,1,1],
          [0,0,0]],
    'O': [[1,1],
          [1,1]],
    'S': [[0,1,1],
          [1,1,0],
          [0,0,0]],
    'T': [[0,1,0],
          [1,1,1],
          [0,0,0]],
    'Z': [[1,1,0],
          [0,1,1],
          [0,0,0]],
}

COLORS = {
    'I': (0, 240, 240),
    'J': (0, 0, 240),
    'L': (240, 160, 0),
    'O': (240, 240, 0),
    'S': (0, 240, 0),
    'T': (160, 0, 240),
    'Z': (240, 0, 0),
}

# Helper functions

def rotate(shape):
    # rotate shape clockwise
    return [list(row) for row in zip(*shape[::-1])]


def create_grid():
    return [[None for _ in range(COLUMNS)] for _ in range(ROWS)]


class Piece:
    def __init__(self, kind=None):
        if kind is None:
            kind = random.choice(list(SHAPES.keys()))
        self.kind = kind
        self.shape = [row[:] for row in SHAPES[kind]]
        self.color = COLORS[kind]
        # start position (row, col) - top-left of shape matrix in the board
        self.row = 0
        self.col = COLUMNS // 2 - len(self.shape[0]) // 2

    def rotate(self):
        self.shape = rotate(self.shape)

    def width(self):
        return len(self.shape[0])

    def height(self):
        return len(self.shape)


class Tetris:
    def __init__(self):
        self.grid = create_grid()
        self.current = Piece()
        self.next_piece = Piece()
        self.hold_piece = None
        self.can_hold = True
        self.score = 0
        self.level = 1
        self.lines = 0
        self.game_over = False
        self.drop_counter = 0.0
        self.drop_speed = 0.8  # seconds per cell (will decrease as level increases)

    def new_piece(self):
        self.current = self.next_piece
        self.next_piece = Piece()
        self.can_hold = True
        if not self.valid_position(self.current, self.current.row, self.current.col):
            self.game_over = True

    def hold(self):
        if not self.can_hold:
            return
        self.can_hold = False
        if self.hold_piece is None:
            self.hold_piece = Piece(self.current.kind)
            self.new_piece()
        else:
            temp = Piece(self.current.kind)
            self.current = Piece(self.hold_piece.kind)
            self.hold_piece = temp
            # reset position of current piece
            self.current.row = 0
            self.current.col = COLUMNS // 2 - len(self.current.shape[0]) // 2
            if not self.valid_position(self.current, self.current.row, self.current.col):
                self.game_over = True

    def valid_position(self, piece, test_row, test_col):
        for r, row in enumerate(piece.shape):
            for c, val in enumerate(row):
                if val:
                    board_r = test_row + r
                    board_c = test_col + c
                    if (board_c < 0 or board_c >= COLUMNS or
                        board_r >= ROWS):
                        return False
                    if board_r >= 0 and self.grid[board_r][board_c] is not None:
                        return False
        return True

    def lock_piece(self):
        piece = self.current
        for r, row in enumerate(piece.shape):
            for c, val in enumerate(row):
                if val:
                    board_r = piece.row + r
                    board_c = piece.col + c
                    if 0 <= board_r < ROWS and 0 <= board_c < COLUMNS:
                        self.grid[board_r][board_c] = piece.color
        self.clear_lines()
        self.new_piece()

    def clear_lines(self):
        full_rows = [r for r in range(ROWS) if all(self.grid[r][c] is not None for c in range(COLUMNS))]
        lines_cleared = len(full_rows)
        if lines_cleared > 0:
            for r in reversed(full_rows):
                del self.grid[r]
                self.grid.insert(0, [None for _ in range(COLUMNS)])
            # scoring (classic tetris style)
            points = {1: 100, 2: 300, 3: 500, 4: 800}
            self.score += points.get(lines_cleared, lines_cleared * 200) * self.level
            self.lines += lines_cleared
            # level up per 10 lines
            self.level = 1 + self.lines // 10
            # increase speed
            self.drop_speed = max(0.05, 0.8 - (self.level - 1) * 0.06)

    def hard_drop(self):
        while self.valid_position(self.current, self.current.row + 1, self.current.col):
            self.current.row += 1
        self.lock_piece()

    def soft_drop(self):
        if self.valid_position(self.current, self.current.row + 1, self.current.col):
            self.current.row += 1
            self.score += 1  # small reward
        else:
            self.lock_piece()

    def move(self, dx):
        if self.valid_position(self.current, self.current.row, self.current.col + dx):
            self.current.col += dx

    def rotate_cw(self):
        original = [row[:] for row in self.current.shape]
        self.current.rotate()
        # wall kick simple: try offsets
        kicks = [(0,0), (0,-1), (0,1), (-1,0), (1,0), (0,-2), (0,2)]
        for dr, dc in kicks:
            if self.valid_position(self.current, self.current.row + dr, self.current.col + dc):
                self.current.row += dr
                self.current.col += dc
                return
        # if none valid, revert
        self.current.shape = original

    def rotate_ccw(self):
        # rotate counter-clockwise = 3 cw
        for _ in range(3):
            self.rotate_cw()

    def tick(self, dt):
        if self.game_over:
            return
        self.drop_counter += dt
        if self.drop_counter >= self.drop_speed:
            self.drop_counter = 0.0
            # gravity step
            if self.valid_position(self.current, self.current.row + 1, self.current.col):
                self.current.row += 1
            else:
                self.lock_piece()


# Pygame rendering

def draw_grid(surface, grid):
    for r in range(ROWS):
        for c in range(COLUMNS):
            rect = pygame.Rect(c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE, CELL_SIZE)
            pygame.draw.rect(surface, GRAY, rect, 1)
            if grid[r][c] is not None:
                pygame.draw.rect(surface, grid[r][c], rect.inflate(-2, -2))


def draw_piece(surface, piece):
    for r, row in enumerate(piece.shape):
        for c, val in enumerate(row):
            if val:
                board_r = piece.row + r
                board_c = piece.col + c
                if board_r >= 0:
                    rect = pygame.Rect(board_c * CELL_SIZE, board_r * CELL_SIZE, CELL_SIZE, CELL_SIZE)
                    pygame.draw.rect(surface, piece.color, rect.inflate(-2, -2))


def draw_next(surface, piece, x, y):
    font = pygame.font.SysFont(None, 20)
    txt = font.render('Next', True, WHITE)
    surface.blit(txt, (x, y - 24))
    for r, row in enumerate(piece.shape):
        for c, val in enumerate(row):
            if val:
                rect = pygame.Rect(x + c * CELL_SIZE, y + r * CELL_SIZE, CELL_SIZE, CELL_SIZE)
                pygame.draw.rect(surface, piece.color, rect.inflate(-2, -2))
                pygame.draw.rect(surface, GRAY, rect, 1)


def draw_hold(surface, piece, x, y):
    font = pygame.font.SysFont(None, 20)
    txt = font.render('Hold', True, WHITE)
    surface.blit(txt, (x, y - 24))
    if piece is None:
        return
    for r, row in enumerate(piece.shape):
        for c, val in enumerate(row):
            if val:
                rect = pygame.Rect(x + c * CELL_SIZE, y + r * CELL_SIZE, CELL_SIZE, CELL_SIZE)
                pygame.draw.rect(surface, piece.color, rect.inflate(-2, -2))
                pygame.draw.rect(surface, GRAY, rect, 1)


def draw_info(surface, game, x, y):
    font = pygame.font.SysFont(None, 24)
    lines = [f'Score: {game.score}', f'Level: {game.level}', f'Lines: {game.lines}']
    for i, line in enumerate(lines):
        txt = font.render(line, True, WHITE)
        surface.blit(txt, (x, y + i * 28))


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH + 200, HEIGHT))
    pygame.display.set_caption('Tetris')
    clock = pygame.time.Clock()

    game = Tetris()

    running = True
    paused = False
    key_down_time = 0
    move_delay = 0.12  # for holding left/right
    last_move_time = 0

    while running:
        dt = clock.tick(FPS) / 1000.0
        '''for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_p:
                    paused = not paused
                elif event.key == pygame.K_SPACE:
                    game.hard_drop()
                elif event.key == pygame.K_DOWN:
                    game.soft_drop()
                elif event.key == pygame.K_LEFT:
                    game.move(-1)
                elif event.key == pygame.K_RIGHT:
                    game.move(1)
                elif event.key == pygame.K_UP or event.key == pygame.K_x:
                    game.rotate_cw()
                elif event.key == pygame.K_z:
                    game.rotate_ccw()
                elif event.key == pygame.K_c:
                    game.hold()
        '''
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_p:
                    paused = not paused
                elif event.key == pygame.K_SPACE:
                    game.hard_drop()
                elif event.key == pygame.K_DOWN:
                    game.soft_drop()
                elif event.key == pygame.K_LEFT:
                    game.move(-1)
                    last_move_time = pygame.time.get_ticks() / 1000.0  # ← 키 누른 시점 저장
                elif event.key == pygame.K_RIGHT:
                    game.move(1)
                    last_move_time = pygame.time.get_ticks() / 1000.0
                elif event.key == pygame.K_UP or event.key == pygame.K_x:
                    game.rotate_cw()
                elif event.key == pygame.K_z:
                    game.rotate_ccw()
                elif event.key == pygame.K_c:
                    game.hold()
                    
        
        keys = pygame.key.get_pressed()
        if not paused and not game.game_over:
            if keys[pygame.K_LEFT]:
                # repeat behavior
                if pygame.time.get_ticks() / 1000.0 - last_move_time > move_delay:
                    game.move(-1)
                    last_move_time = pygame.time.get_ticks() / 1000.0
            if keys[pygame.K_RIGHT]:
                if pygame.time.get_ticks() / 1000.0 - last_move_time > move_delay:
                    game.move(1)
                    last_move_time = pygame.time.get_ticks() / 1000.0

        if not paused and not game.game_over:
            game.tick(dt)

        # draw
        screen.fill(BLACK)
        # playfield surface
        play_surf = pygame.Surface((WIDTH, HEIGHT))
        play_surf.fill((20, 20, 20))
        draw_grid(play_surf, game.grid)
        draw_piece(play_surf, game.current)
        screen.blit(play_surf, (0, 0))

        # side panel
        side_x = WIDTH + 20
        draw_next(screen, game.next_piece, side_x, 20)
        draw_hold(screen, game.hold_piece, side_x, 140)
        draw_info(screen, game, side_x, 260)

        if paused:
            font = pygame.font.SysFont(None, 48)
            txt = font.render('PAUSED', True, WHITE)
            screen.blit(txt, (WIDTH // 2 - 80, HEIGHT // 2 - 24))

        if game.game_over:
            font = pygame.font.SysFont(None, 48)
            txt = font.render('GAME OVER', True, WHITE)
            screen.blit(txt, (WIDTH // 2 - 140, HEIGHT // 2 - 24))

        pygame.display.flip()

    pygame.quit()
    sys.exit()


if __name__ == '__main__':
    main()

 

 

소스 파일 다운로드:

tetris.py
0.01MB

반응형