본문 바로가기

코딩 공부/파이썬

파이썬 지렁이 게임 코드 2025

 

파이썬 지렁이(Snake) 게임 만들기 — 한글 OK · 스코어/최고점 · 랜덤 스킨 · 사운드

Pygame으로 클래식 지렁이 게임을 한 파일로 구현합니다. 한글 폰트 깨짐 방지, 점수·최고점 저장, 랜덤 스킨, 간단 사운드 이펙트까지 모두 포함했어요. 아래 가이드를 따라가면 설치 → 실행 → 커스터마이즈까지 바로 끝낼 수 있습니다.

개요 & 목표

본 포스트는 Pygame을 사용해 지렁이(Snake) 게임을 만드는 과정을 정리했습니다. 단순한 완성본이 아니라, 블로그/수업/스터디에서 재사용하기 쉽도록 한글 폰트, 스코어/최고점, 랜덤 스킨, 사운드 등 “필수+재미” 요소를 기본 탑재했습니다.

핵심 기능 요약

  • 한글 렌더링 자동 선택: Windows/맥/리눅스에서 가능한 한 적합한 한글 폰트를 자동 선택
  • 점수 & 최고점 저장: highscore.txt로 최고점 자동 관리
  • 랜덤 스킨: 시작 시 머리/몸/먹이 색상 랜덤, 가끔 먹을 때도 색 변환
  • 사운드 이펙트: 먹기/일시정지/게임오버 음향 (외부 파일 없이 PCM 생성)
  • 난이도 상승: 일정 점수마다 속도 증가
  • 벽 옵션: 벽 충돌 게임오버(ON) vs 화면 반대편으로 이동(OFF)

개발 환경 준비

필수 설치

pip install pygame

참고: 일부 리눅스는 SDL 관련 패키지가 추가로 필요할 수 있습니다.

파일 이름

아래 “전체 코드”를 그대로 복사해 snake_kor.py로 저장하세요.

전체 코드

아래 전체를 복사해 snake_kor.py로 저장
# -*- coding: utf-8 -*-
# Snake (지렁이) - 한글 폰트/스코어/최고점/랜덤스킨/사운드 포함 단일 파일
# pip install pygame
import pygame
import random
import sys
import os
import math
import struct
from typing import List, Tuple, Optional

# ====== 전역 설정 ======
WIDTH, HEIGHT = 640, 480           # 창 크기
CELL = 20                           # 격자 크기
GRID_W, GRID_H = WIDTH // CELL, HEIGHT // CELL

WALLS = True                        # True: 벽충돌=게임오버 / False: 반대편으로 랩어라운드
SHOW_GRID = False                   # 격자 표시
START_SPEED = 10                    # 시작 FPS
SPEED_STEP_EVERY = 5                # n점마다 속도 +1

# 한글 폰트 경로 (원하면 직접 경로 지정) 예: r"C:\Windows\Fonts\malgun.ttf"
FONT_PATH: Optional[str] = None
# 폰트 이름 후보 (시스템 폰트에서 검색)
KOREAN_FONT_CANDIDATES = [
    "malgungothic", "맑은 고딕",
    "NanumGothic", "나눔고딕",
    "AppleGothic",  # macOS
    "Noto Sans CJK KR", "Noto Sans KR"
]

BG_COLOR = (18, 18, 22)   # 배경
GRID_COLOR = (40, 40, 48)
INFO_COLOR = (160, 160, 170)
TEXT_COLOR = (235, 235, 235)

# 방향 벡터
UP, DOWN, LEFT, RIGHT = (0, -1), (0, 1), (-1, 0), (1, 0)

# ====== 유틸 ======
def pick_korean_font(size: int) -> pygame.font.Font:
    """한글 렌더링 가능한 폰트를 최대한 자동으로 선택."""
    if FONT_PATH and os.path.exists(FONT_PATH):
        return pygame.font.Font(FONT_PATH, size)
    # 시스템 폰트 탐색
    for name in KOREAN_FONT_CANDIDATES:
        try:
            f = pygame.font.SysFont(name, size)
            # 간단 확인: 한글 문자열 렌더가 되는지 테스트
            test = f.render("가나다 한글 테스트", True, (255, 255, 255))
            if test.get_width() > 0:
                return f
        except Exception:
            continue
    # 최후의 보루 (한글이 깨질 수 있음)
    return pygame.font.Font(None, size)

def render_text_center(surf, text: str, size: int, color, center):
    font = pick_korean_font(size)
    s = font.render(text, True, color)
    rect = s.get_rect(center=center)
    surf.blit(s, rect)

def draw_grid(surf):
    for x in range(0, WIDTH, CELL):
        pygame.draw.line(surf, GRID_COLOR, (x, 0), (x, HEIGHT))
    for y in range(0, HEIGHT, CELL):
        pygame.draw.line(surf, GRID_COLOR, (0, y), (WIDTH, y))

def clamp_wrap(pos: Tuple[int,int]) -> Tuple[int,int]:
    x, y = pos
    x %= GRID_W
    y %= GRID_H
    return (x, y)

def random_empty_cell(snake: List[Tuple[int,int]]) -> Tuple[int,int]:
    while True:
        p = (random.randrange(GRID_W), random.randrange(GRID_H))
        if p not in snake:
            return p

def draw_cell(surf, pos, color):
    x, y = pos
    pygame.draw.rect(surf, color, (x*CELL, y*CELL, CELL, CELL), border_radius=4)

def load_high_score(path="highscore.txt") -> int:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return int(f.read().strip() or "0")
    except Exception:
        return 0

def save_high_score(score: int, path="highscore.txt"):
    try:
        with open(path, "w", encoding="utf-8") as f:
            f.write(str(score))
    except Exception:
        pass

# ====== 사운드 (외부 파일 없이 톤 생성) ======
def init_mixer():
    try:
        # 44.1kHz, 16bit signed, mono
        pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
        return True
    except Exception:
        return False

def make_tone(freq=880, ms=120, volume=0.3) -> Optional[pygame.mixer.Sound]:
    """사인파 톤 생성 (numpy 없이 raw PCM 생성)."""
    if not pygame.mixer.get_init():
        return None
    sample_rate = 44100
    length = int(sample_rate * (ms / 1000.0))
    amp = int(32767 * max(0.0, min(1.0, volume)))
    frames = bytearray()
    for i in range(length):
        t = i / sample_rate
        val = int(amp * math.sin(2 * math.pi * freq * t))
        frames += struct.pack("<h", val)  # 16-bit little-endian
    try:
        sound = pygame.mixer.Sound(buffer=frames)
        return sound
    except Exception:
        return None

# ====== 스킨 팔레트 ======
def random_skin():
    """머리/몸/먹이 색상 랜덤 팔레트 생성."""
    # 기본 팔레트 후보 + 완전 랜덤 혼합
    palettes = [
        ((60, 200, 255), (0, 170, 120), (250, 80, 60)),     # 기본
        ((255, 90, 90), (220, 40, 40), (255, 210, 70)),     # 레드
        ((120, 220, 120), (60, 180, 60), (255, 120, 0)),    # 그린
        ((255, 160, 30), (230, 120, 20), (120, 200, 255)),  # 오렌지/블루
        ((200, 120, 255), (140, 70, 220), (255, 120, 140)), # 퍼플/핑크
    ]
    if random.random() < 0.7:
        return random.choice(palettes)
    # 30% 확률로 완전 랜덤 팔레트
    def rc():
        return (random.randint(40,255), random.randint(40,255), random.randint(40,255))
    return (rc(), rc(), rc())

# ====== 메인 ======
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("지렁이 게임 (Snake) - 한글/스코어/사운드/스킨")
    clock = pygame.time.Clock()

    # 폰트 사전 준비(상단 정보창용)
    info_font = pick_korean_font(18)
    big_font = pick_korean_font(56)
    mid_font = pick_korean_font(24)

    # 사운드 초기화 및 기본 효과음
    sound_ok = init_mixer()
    s_eat = make_tone(freq=990, ms=90, volume=0.35) if sound_ok else None
    s_over = make_tone(freq=200, ms=300, volume=0.4) if sound_ok else None
    s_pause = make_tone(freq=660, ms=70, volume=0.25) if sound_ok else None

    high_score = load_high_score()

    def new_game():
        snake = [(GRID_W//2, GRID_H//2)]
        direction = RIGHT
        food = random_empty_cell(snake)
        score = 0
        speed = START_SPEED
        paused = False
        head_color, body_color, food_color = random_skin()
        return snake, direction, food, score, speed, paused, head_color, body_color, food_color

    snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
    pending_dir = direction
    game_over = False

    running = True
    while running:
        # ===== 입력 처리 =====
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            elif event.type == pygame.KEYDOWN:
                key = event.key
                if key in (pygame.K_ESCAPE, pygame.K_q):
                    running = False

                if game_over:
                    if key == pygame.K_r:
                        snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
                        pending_dir = direction
                        game_over = False
                    continue

                if key in (pygame.K_UP, pygame.K_w) and direction != DOWN:
                    pending_dir = UP
                elif key in (pygame.K_DOWN, pygame.K_s) and direction != UP:
                    pending_dir = DOWN
                elif key in (pygame.K_LEFT, pygame.K_a) and direction != RIGHT:
                    pending_dir = LEFT
                elif key in (pygame.K_RIGHT, pygame.K_d) and direction != LEFT:
                    pending_dir = RIGHT
                elif key == pygame.K_p:
                    paused = not paused
                    if s_pause: s_pause.play()
                elif key == pygame.K_r:
                    snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
                    pending_dir = direction

        # ===== 업데이트 =====
        if not game_over and not paused:
            direction = pending_dir
            hx, hy = snake[0]
            dx, dy = direction
            new_head = (hx + dx, hy + dy)

            if WALLS:
                if not (0 <= new_head[0] < GRID_W and 0 <= new_head[1] < GRID_H):
                    game_over = True
            else:
                new_head = clamp_wrap(new_head)

            if not game_over:
                if new_head in snake:
                    game_over = True
                else:
                    snake.insert(0, new_head)
                    if new_head == food:
                        score += 1
                        food = random_empty_cell(snake)
                        # 속도 증가
                        if score % SPEED_STEP_EVERY == 0:
                            speed += 1
                        if s_eat: s_eat.play()
                        # 가끔 스킨을 살짝 섞어주기(먹을 때 색상 변화 효과)
                        if random.random() < 0.2:
                            HEAD, BODY, _ = random_skin()
                    else:
                        snake.pop()

        # 최고점 갱신/사운드
        if game_over:
            if s_over: s_over.play()
            if score > high_score:
                high_score = score
                save_high_score(high_score)

        # ===== 그리기 =====
        screen.fill(BG_COLOR)
        if SHOW_GRID:
            draw_grid(screen)

        # 먹이
        draw_cell(screen, food, FOOD)

        # 뱀
        for i, p in enumerate(snake):
            draw_cell(screen, p, HEAD if i == 0 else BODY)

        # 상단 정보줄
        info_text = f"점수: {score}   최고점: {high_score}   속도: {speed}   벽: {'ON' if WALLS else 'OFF'}   P:일시정지  R:재시작  Q/Esc:종료"
        info_surf = info_font.render(info_text, True, INFO_COLOR)
        screen.blit(info_surf, (10, 8))

        if paused and not game_over:
            render_text_center(screen, "일시정지", 48, TEXT_COLOR, (WIDTH//2, HEIGHT//2))

        if game_over:
            overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 130))
            screen.blit(overlay, (0, 0))
            render_text_center(screen, "게임 오버", 56, TEXT_COLOR, (WIDTH//2, HEIGHT//2 - 40))
            render_text_center(screen, f"이번 점수: {score}   최고점: {high_score}", 26, (230, 210, 210), (WIDTH//2, HEIGHT//2 + 5))
            render_text_center(screen, "R: 다시 시작   Q/Esc: 종료", 22, INFO_COLOR, (WIDTH//2, HEIGHT//2 + 40))

        pygame.display.flip()
        clock.tick(speed if not game_over else 30)

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

코드 구조 해설

  • 설정 상수: 창/격자/속도/벽 옵션 등 상단에서 한 번에 제어
  • 폰트 선택: pick_korean_font()에서 OS별 한글 폰트를 자동 탐색
  • 사운드: init_mixer()로 믹서 초기화, make_tone()으로 사인파 PCM 생성
  • 랜덤 스킨: random_skin()에서 팔레트 또는 랜덤 RGB를 뽑아 머리/몸/먹이 색 지정
  • 게임 루프: 이벤트 처리 → 상태 업데이트 → 렌더링(정보줄·오버레이 포함)
  • 최고점: 게임 오버 시 highscore.txt 갱신

실행 & 조작법

python snake_kor.py
  • 방향키 또는 WASD : 이동
  • P : 일시정지 / 해제
  • R : 재시작
  • Q 또는 ESC : 종료

커스터마이즈 가이드

룰 & 난이도

WALLS = False        # 랩어라운드 모드
SHOW_GRID = True     # 격자선 표시
START_SPEED = 8      # 시작 속도 조정
SPEED_STEP_EVERY = 4 # 4점마다 속도 +1

폰트 강제 지정

FONT_PATH = r"C:\Windows\Fonts\malgun.ttf"   # Windows 예시
# macOS 예: FONT_PATH = "/System/Library/Fonts/AppleSDGothicNeo.ttc"

최고점 저장(파일) 설명

최고점은 실행 폴더의 highscore.txt에 숫자 형태로 보관합니다. 권한 이슈가 있는 경로나 읽기전용 폴더에서는 기록이 실패할 수 있으니, 문제 발생 시 다른 폴더에서 실행해 보세요.

사운드 이펙트 작동 원리

외부 음원 파일 없이 사인파를 즉석 생성합니다. make_tone()가 주파수·길이·볼륨을 받아 16비트 PCM을 만들어 pygame.mixer.Sound로 재생합니다. 환경에 따라 오디오 장치 초기화가 안 되면 자동으로 무음 동작합니다(게임플레이는 정상).

자주 발생하는 이슈 해결

  • ModuleNotFoundError: pygamepip install pygame
  • 한글 깨짐FONT_PATH를 명시하거나, 시스템에 한글 폰트를 설치
  • 사운드가 안 남 → 다른 오디오 장치를 선택하거나 관리자 권한으로 실행, 그래도 안 되면 무시해도 됨
  • 격자/창 크기 변경WIDTH, HEIGHT, CELL이 서로 나누어떨어지도록

배포(EXE) 팁

pip install pyinstaller
pyinstaller -F -w snake_kor.py

dist/snake_kor.exe가 생성됩니다. 배포 시 실행 방법과 조작법을 함께 안내하면 좋아요.

확장 아이디어

  • 맵·장애물/스테이지(벽 레이아웃 추가)
  • 랭킹 Top 5 파일 저장
  • 스킨 선택 화면 & 테마 프리셋
  • 효과음/배경음 다중 채널 믹싱

© 본 코드/글은 학습 및 개인 프로젝트용으로 자유롭게 활용하셔도 됩니다.

 

 

 

 

반응형