프로젝트 10: 터미널 뱀 게임
프로젝트 10: 터미널 뱀 게임¶
터미널에서 동작하는 클래식 뱀 게임을 만들어봅니다.
학습 목표¶
- 터미널 제어 (ANSI escape codes)
- 비동기 키보드 입력 처리
- 게임 루프 구현
- 타이머와 프레임 관리
사전 지식¶
- 구조체와 포인터
- 동적 메모리 관리
- 연결 리스트 (뱀 몸통 표현)
1단계: ANSI Escape Codes 이해¶
터미널에서 그래픽을 표현하기 위해 ANSI escape codes를 사용합니다.
기본 ANSI 코드¶
// ansi_demo.c
#include <stdio.h>
#include <unistd.h>
// ANSI Escape Codes
#define CLEAR_SCREEN "\033[2J"
#define CURSOR_HOME "\033[H"
#define HIDE_CURSOR "\033[?25l"
#define SHOW_CURSOR "\033[?25h"
// 커서 이동: \033[row;colH
#define MOVE_CURSOR(row, col) printf("\033[%d;%dH", row, col)
// 색상
#define COLOR_RESET "\033[0m"
#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_MAGENTA "\033[35m"
#define COLOR_CYAN "\033[36m"
int main(void) {
// 화면 지우기
printf(CLEAR_SCREEN);
printf(CURSOR_HOME);
// 커서 숨기기
printf(HIDE_CURSOR);
// 여러 위치에 출력
MOVE_CURSOR(5, 10);
printf(COLOR_RED "빨간색 텍스트" COLOR_RESET);
MOVE_CURSOR(7, 10);
printf(COLOR_GREEN "초록색 텍스트" COLOR_RESET);
MOVE_CURSOR(9, 10);
printf(COLOR_BLUE "파란색 텍스트" COLOR_RESET);
// 박스 그리기
MOVE_CURSOR(12, 5);
printf("┌────────────────────┐");
for (int i = 13; i < 18; i++) {
MOVE_CURSOR(i, 5);
printf("│ │");
}
MOVE_CURSOR(18, 5);
printf("└────────────────────┘");
MOVE_CURSOR(15, 10);
printf(COLOR_YELLOW "박스 안의 텍스트" COLOR_RESET);
sleep(3);
// 커서 보이기
printf(SHOW_CURSOR);
MOVE_CURSOR(20, 1);
return 0;
}
2단계: 비동기 키보드 입력¶
게임에서는 키 입력을 기다리지 않고 계속 실행되어야 합니다.
termios를 이용한 입력 처리¶
// input_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
// 원래 터미널 설정 저장
static struct termios original_termios;
// 터미널을 raw 모드로 설정
void enable_raw_mode(void) {
tcgetattr(STDIN_FILENO, &original_termios);
struct termios raw = original_termios;
// 입력 플래그: 에코 끄기, 라인 버퍼링 끄기
raw.c_lflag &= ~(ECHO | ICANON);
// 최소 입력 문자: 0 (non-blocking 가능)
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0; // 타임아웃 없음
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
// 터미널 설정 복원
void disable_raw_mode(void) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios);
}
// 키 입력 확인 (non-blocking)
int kbhit(void) {
int ch = getchar();
if (ch != EOF) {
ungetc(ch, stdin);
return 1;
}
return 0;
}
// 키 읽기
int getch(void) {
return getchar();
}
// 방향키 읽기 (escape sequence 처리)
typedef enum {
KEY_NONE = 0,
KEY_UP,
KEY_DOWN,
KEY_LEFT,
KEY_RIGHT,
KEY_QUIT,
KEY_OTHER
} KeyCode;
KeyCode read_key(void) {
int ch = getchar();
if (ch == EOF) return KEY_NONE;
if (ch == 'q' || ch == 'Q') return KEY_QUIT;
// Escape sequence (방향키)
if (ch == '\033') {
int ch2 = getchar();
if (ch2 == '[') {
int ch3 = getchar();
switch (ch3) {
case 'A': return KEY_UP;
case 'B': return KEY_DOWN;
case 'C': return KEY_RIGHT;
case 'D': return KEY_LEFT;
}
}
}
// WASD 키 지원
switch (ch) {
case 'w': case 'W': return KEY_UP;
case 's': case 'S': return KEY_DOWN;
case 'a': case 'A': return KEY_LEFT;
case 'd': case 'D': return KEY_RIGHT;
}
return KEY_OTHER;
}
int main(void) {
enable_raw_mode();
atexit(disable_raw_mode); // 프로그램 종료시 자동 복원
printf("\033[2J\033[H"); // 화면 지우기
printf("방향키 또는 WASD로 이동, Q로 종료\n\n");
int x = 10, y = 5;
while (1) {
KeyCode key = read_key();
if (key == KEY_QUIT) break;
// 이전 위치 지우기
printf("\033[%d;%dH ", y, x);
switch (key) {
case KEY_UP: if (y > 3) y--; break;
case KEY_DOWN: if (y < 20) y++; break;
case KEY_LEFT: if (x > 1) x--; break;
case KEY_RIGHT: if (x < 40) x++; break;
default: break;
}
// 새 위치에 출력
printf("\033[%d;%dH@", y, x);
fflush(stdout);
usleep(50000); // 50ms 대기
}
printf("\033[22;1H종료합니다.\n");
return 0;
}
3단계: 기본 게임 구조¶
게임 데이터 구조 정의¶
// snake_types.h
#ifndef SNAKE_TYPES_H
#define SNAKE_TYPES_H
#include <stdbool.h>
// 화면 크기
#define SCREEN_WIDTH 40
#define SCREEN_HEIGHT 20
// 게임 속도 (마이크로초)
#define GAME_SPEED 150000
// 방향
typedef enum {
DIR_UP,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT
} Direction;
// 좌표
typedef struct {
int x;
int y;
} Point;
// 뱀 몸통 노드
typedef struct SnakeNode {
Point pos;
struct SnakeNode* next;
} SnakeNode;
// 뱀
typedef struct {
SnakeNode* head;
SnakeNode* tail;
Direction dir;
int length;
} Snake;
// 게임 상태
typedef struct {
Snake snake;
Point food;
int score;
bool game_over;
bool paused;
} GameState;
#endif
뱀 관리 함수¶
// snake.c
#include <stdio.h>
#include <stdlib.h>
#include "snake_types.h"
// 뱀 생성
Snake* snake_create(int start_x, int start_y) {
Snake* snake = malloc(sizeof(Snake));
if (!snake) return NULL;
// 초기 몸통 3칸
snake->head = NULL;
snake->tail = NULL;
snake->length = 0;
snake->dir = DIR_RIGHT;
// 머리부터 꼬리 순서로 추가
for (int i = 0; i < 3; i++) {
SnakeNode* node = malloc(sizeof(SnakeNode));
node->pos.x = start_x - i;
node->pos.y = start_y;
node->next = NULL;
if (snake->head == NULL) {
snake->head = node;
snake->tail = node;
} else {
snake->tail->next = node;
snake->tail = node;
}
snake->length++;
}
return snake;
}
// 뱀 해제
void snake_destroy(Snake* snake) {
SnakeNode* current = snake->head;
while (current) {
SnakeNode* next = current->next;
free(current);
current = next;
}
free(snake);
}
// 방향 변경 (반대 방향 금지)
void snake_change_direction(Snake* snake, Direction new_dir) {
// 현재 진행방향의 반대로는 못 감
if ((snake->dir == DIR_UP && new_dir == DIR_DOWN) ||
(snake->dir == DIR_DOWN && new_dir == DIR_UP) ||
(snake->dir == DIR_LEFT && new_dir == DIR_RIGHT) ||
(snake->dir == DIR_RIGHT && new_dir == DIR_LEFT)) {
return;
}
snake->dir = new_dir;
}
// 다음 머리 위치 계산
Point snake_next_head(Snake* snake) {
Point next = snake->head->pos;
switch (snake->dir) {
case DIR_UP: next.y--; break;
case DIR_DOWN: next.y++; break;
case DIR_LEFT: next.x--; break;
case DIR_RIGHT: next.x++; break;
}
return next;
}
// 뱀 이동 (음식 먹으면 true)
bool snake_move(Snake* snake, Point food) {
Point next = snake_next_head(snake);
// 새 머리 노드 생성
SnakeNode* new_head = malloc(sizeof(SnakeNode));
new_head->pos = next;
new_head->next = snake->head;
snake->head = new_head;
snake->length++;
// 음식을 먹었는지 확인
if (next.x == food.x && next.y == food.y) {
return true; // 꼬리 유지 (길이 증가)
}
// 음식 안 먹었으면 꼬리 제거
SnakeNode* current = snake->head;
while (current->next != snake->tail) {
current = current->next;
}
free(snake->tail);
snake->tail = current;
snake->tail->next = NULL;
snake->length--;
return false;
}
// 충돌 검사: 벽
bool snake_hit_wall(Snake* snake, int width, int height) {
int x = snake->head->pos.x;
int y = snake->head->pos.y;
return (x < 1 || x >= width - 1 || y < 1 || y >= height - 1);
}
// 충돌 검사: 자기 몸
bool snake_hit_self(Snake* snake) {
SnakeNode* head = snake->head;
SnakeNode* current = head->next;
while (current) {
if (head->pos.x == current->pos.x &&
head->pos.y == current->pos.y) {
return true;
}
current = current->next;
}
return false;
}
// 특정 위치에 뱀이 있는지 확인
bool snake_occupies(Snake* snake, int x, int y) {
SnakeNode* current = snake->head;
while (current) {
if (current->pos.x == x && current->pos.y == y) {
return true;
}
current = current->next;
}
return false;
}
4단계: 완성된 뱀 게임¶
메인 게임 코드¶
// snake_game.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>
// ============ 설정 ============
#define WIDTH 40
#define HEIGHT 20
#define INITIAL_SPEED 150000 // 마이크로초
// ============ ANSI Codes ============
#define CLEAR "\033[2J"
#define HOME "\033[H"
#define HIDE_CURSOR "\033[?25l"
#define SHOW_CURSOR "\033[?25h"
#define MOVE(r,c) printf("\033[%d;%dH", r, c)
#define RESET "\033[0m"
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define RED "\033[31m"
#define CYAN "\033[36m"
#define BOLD "\033[1m"
// ============ 방향 ============
typedef enum { UP, DOWN, LEFT, RIGHT } Direction;
// ============ 좌표 ============
typedef struct {
int x, y;
} Point;
// ============ 뱀 노드 ============
typedef struct Node {
Point pos;
struct Node* next;
} Node;
// ============ 게임 상태 ============
typedef struct {
Node* head;
Node* tail;
Direction dir;
Point food;
int score;
int length;
bool game_over;
int speed;
} Game;
// ============ 터미널 설정 ============
static struct termios orig_termios;
void disable_raw_mode(void) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
printf(SHOW_CURSOR);
}
void enable_raw_mode(void) {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disable_raw_mode);
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
printf(HIDE_CURSOR);
}
// ============ 입력 처리 ============
Direction read_direction(Direction current) {
int ch = getchar();
if (ch == EOF) return current;
// ESC sequence (방향키)
if (ch == '\033') {
getchar(); // '['
switch (getchar()) {
case 'A': return (current != DOWN) ? UP : current;
case 'B': return (current != UP) ? DOWN : current;
case 'C': return (current != LEFT) ? RIGHT : current;
case 'D': return (current != RIGHT) ? LEFT : current;
}
}
// WASD
switch (ch) {
case 'w': case 'W': return (current != DOWN) ? UP : current;
case 's': case 'S': return (current != UP) ? DOWN : current;
case 'a': case 'A': return (current != RIGHT) ? LEFT : current;
case 'd': case 'D': return (current != LEFT) ? RIGHT : current;
case 'q': case 'Q': return -1; // 종료 신호
}
return current;
}
// ============ 뱀 함수 ============
bool snake_at(Node* head, int x, int y) {
for (Node* n = head; n; n = n->next) {
if (n->pos.x == x && n->pos.y == y) return true;
}
return false;
}
void spawn_food(Game* g) {
do {
g->food.x = 1 + rand() % (WIDTH - 2);
g->food.y = 1 + rand() % (HEIGHT - 2);
} while (snake_at(g->head, g->food.x, g->food.y));
}
Game* game_init(void) {
Game* g = malloc(sizeof(Game));
// 뱀 초기화 (길이 3)
g->head = NULL;
for (int i = 0; i < 3; i++) {
Node* n = malloc(sizeof(Node));
n->pos.x = WIDTH / 2 - i;
n->pos.y = HEIGHT / 2;
n->next = g->head;
g->head = n;
if (i == 0) g->tail = n;
}
// 꼬리 찾기
Node* curr = g->head;
while (curr->next) curr = curr->next;
g->tail = curr;
g->dir = RIGHT;
g->score = 0;
g->length = 3;
g->game_over = false;
g->speed = INITIAL_SPEED;
spawn_food(g);
return g;
}
void game_free(Game* g) {
Node* n = g->head;
while (n) {
Node* next = n->next;
free(n);
n = next;
}
free(g);
}
bool game_update(Game* g) {
// 다음 머리 위치 계산
Point next = g->head->pos;
switch (g->dir) {
case UP: next.y--; break;
case DOWN: next.y++; break;
case LEFT: next.x--; break;
case RIGHT: next.x++; break;
}
// 벽 충돌
if (next.x <= 0 || next.x >= WIDTH - 1 ||
next.y <= 0 || next.y >= HEIGHT - 1) {
g->game_over = true;
return false;
}
// 자기 몸 충돌
if (snake_at(g->head, next.x, next.y)) {
g->game_over = true;
return false;
}
// 새 머리 추가
Node* new_head = malloc(sizeof(Node));
new_head->pos = next;
new_head->next = g->head;
g->head = new_head;
// 음식 확인
if (next.x == g->food.x && next.y == g->food.y) {
g->score += 10;
g->length++;
spawn_food(g);
// 속도 증가 (최소 50ms)
if (g->speed > 50000) {
g->speed -= 5000;
}
return true;
}
// 꼬리 제거
Node* curr = g->head;
while (curr->next && curr->next->next) {
curr = curr->next;
}
free(curr->next);
curr->next = NULL;
g->tail = curr;
return false;
}
// ============ 화면 그리기 ============
void draw_border(void) {
// 상단
MOVE(1, 1);
printf(CYAN "╔");
for (int i = 1; i < WIDTH - 1; i++) printf("═");
printf("╗" RESET);
// 측면
for (int i = 2; i < HEIGHT; i++) {
MOVE(i, 1);
printf(CYAN "║" RESET);
MOVE(i, WIDTH);
printf(CYAN "║" RESET);
}
// 하단
MOVE(HEIGHT, 1);
printf(CYAN "╚");
for (int i = 1; i < WIDTH - 1; i++) printf("═");
printf("╝" RESET);
}
void draw_game(Game* g) {
printf(CLEAR HOME);
draw_border();
// 음식
MOVE(g->food.y + 1, g->food.x + 1);
printf(RED "●" RESET);
// 뱀
bool is_head = true;
for (Node* n = g->head; n; n = n->next) {
MOVE(n->pos.y + 1, n->pos.x + 1);
if (is_head) {
printf(BOLD GREEN "◆" RESET);
is_head = false;
} else {
printf(GREEN "■" RESET);
}
}
// 점수
MOVE(HEIGHT + 1, 1);
printf(YELLOW "점수: %d 길이: %d" RESET, g->score, g->length);
MOVE(HEIGHT + 2, 1);
printf("조작: ↑↓←→ 또는 WASD, Q: 종료");
fflush(stdout);
}
void draw_game_over(Game* g) {
MOVE(HEIGHT / 2, WIDTH / 2 - 5);
printf(BOLD RED "GAME OVER!" RESET);
MOVE(HEIGHT / 2 + 1, WIDTH / 2 - 6);
printf("최종 점수: %d", g->score);
MOVE(HEIGHT / 2 + 2, WIDTH / 2 - 8);
printf("R: 재시작, Q: 종료");
fflush(stdout);
}
// ============ 메인 ============
int main(void) {
srand(time(NULL));
enable_raw_mode();
Game* game = game_init();
draw_game(game);
while (1) {
// 입력 처리
Direction new_dir = read_direction(game->dir);
if (new_dir == (Direction)-1) break; // Q 종료
game->dir = new_dir;
if (!game->game_over) {
// 게임 업데이트
game_update(game);
draw_game(game);
if (game->game_over) {
draw_game_over(game);
}
} else {
// 게임 오버 상태에서 R 누르면 재시작
int ch = getchar();
if (ch == 'r' || ch == 'R') {
game_free(game);
game = game_init();
draw_game(game);
} else if (ch == 'q' || ch == 'Q') {
break;
}
}
usleep(game->speed);
}
game_free(game);
MOVE(HEIGHT + 4, 1);
printf("게임을 종료합니다.\n");
return 0;
}
컴파일 및 실행¶
gcc -o snake snake_game.c
./snake
5단계: 기능 확장¶
벽 통과 모드 추가¶
// 설정에 추가
#define WALL_WRAP true // true면 반대편으로 나옴
// game_update 함수의 벽 충돌 부분 수정
bool game_update(Game* g) {
Point next = g->head->pos;
switch (g->dir) {
case UP: next.y--; break;
case DOWN: next.y++; break;
case LEFT: next.x--; break;
case RIGHT: next.x++; break;
}
#if WALL_WRAP
// 벽 통과: 반대편으로 나옴
if (next.x <= 0) next.x = WIDTH - 2;
else if (next.x >= WIDTH - 1) next.x = 1;
if (next.y <= 0) next.y = HEIGHT - 2;
else if (next.y >= HEIGHT - 1) next.y = 1;
#else
// 벽 충돌: 게임 오버
if (next.x <= 0 || next.x >= WIDTH - 1 ||
next.y <= 0 || next.y >= HEIGHT - 1) {
g->game_over = true;
return false;
}
#endif
// ... 나머지 코드
}
장애물 추가¶
// 장애물 구조체
#define MAX_OBSTACLES 10
typedef struct {
Point obstacles[MAX_OBSTACLES];
int count;
} Obstacles;
// 장애물 생성
void spawn_obstacles(Game* g, Obstacles* obs, int count) {
obs->count = 0;
for (int i = 0; i < count && obs->count < MAX_OBSTACLES; i++) {
Point p;
do {
p.x = 2 + rand() % (WIDTH - 4);
p.y = 2 + rand() % (HEIGHT - 4);
} while (snake_at(g->head, p.x, p.y) ||
(p.x == g->food.x && p.y == g->food.y));
obs->obstacles[obs->count++] = p;
}
}
// 장애물 충돌 검사
bool hit_obstacle(Obstacles* obs, int x, int y) {
for (int i = 0; i < obs->count; i++) {
if (obs->obstacles[i].x == x && obs->obstacles[i].y == y) {
return true;
}
}
return false;
}
// 장애물 그리기
void draw_obstacles(Obstacles* obs) {
for (int i = 0; i < obs->count; i++) {
MOVE(obs->obstacles[i].y + 1, obs->obstacles[i].x + 1);
printf("\033[35m█\033[0m"); // 마젠타 색상
}
}
레벨 시스템¶
typedef struct {
int level;
int food_to_next; // 다음 레벨까지 필요한 음식
int food_eaten;
} LevelSystem;
void level_init(LevelSystem* ls) {
ls->level = 1;
ls->food_to_next = 5;
ls->food_eaten = 0;
}
bool level_eat_food(LevelSystem* ls) {
ls->food_eaten++;
if (ls->food_eaten >= ls->food_to_next) {
ls->level++;
ls->food_eaten = 0;
ls->food_to_next += 2; // 점점 더 많은 음식 필요
return true; // 레벨 업!
}
return false;
}
// 레벨에 따른 속도 계산
int get_speed_for_level(int level) {
int base_speed = 150000;
int speed = base_speed - (level - 1) * 15000;
return (speed < 50000) ? 50000 : speed;
}
점수 저장 (최고 기록)¶
#include <stdio.h>
#define SCORE_FILE "snake_highscore.dat"
int load_highscore(void) {
FILE* f = fopen(SCORE_FILE, "r");
if (!f) return 0;
int score;
if (fscanf(f, "%d", &score) != 1) {
score = 0;
}
fclose(f);
return score;
}
void save_highscore(int score) {
int current_high = load_highscore();
if (score > current_high) {
FILE* f = fopen(SCORE_FILE, "w");
if (f) {
fprintf(f, "%d", score);
fclose(f);
}
}
}
// 게임 종료시 호출
void game_end(Game* g) {
int highscore = load_highscore();
if (g->score > highscore) {
MOVE(HEIGHT / 2 + 3, WIDTH / 2 - 8);
printf("\033[33m★ 신기록! ★\033[0m");
save_highscore(g->score);
} else {
MOVE(HEIGHT / 2 + 3, WIDTH / 2 - 8);
printf("최고 기록: %d", highscore);
}
}
6단계: ncurses 버전 (선택사항)¶
ncurses 라이브러리를 사용하면 더 깔끔한 코드가 가능합니다.
ncurses 설치¶
# macOS
brew install ncurses
# Ubuntu/Debian
sudo apt install libncurses5-dev
# Fedora
sudo dnf install ncurses-devel
ncurses 버전 기본 구조¶
// snake_ncurses.c
#include <ncurses.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#define WIDTH 40
#define HEIGHT 20
// 방향
enum { UP, DOWN, LEFT, RIGHT };
// 노드
typedef struct Node {
int x, y;
struct Node* next;
} Node;
// 전역 상태
Node* head = NULL;
int dir = RIGHT;
int food_x, food_y;
int score = 0;
bool game_over = false;
void spawn_food(void) {
do {
food_x = 1 + rand() % (WIDTH - 2);
food_y = 1 + rand() % (HEIGHT - 2);
} while (/* 뱀 위치 체크 */ 0);
}
void init_game(void) {
// ncurses 초기화
initscr();
cbreak();
noecho();
nodelay(stdscr, TRUE); // non-blocking input
keypad(stdscr, TRUE); // 방향키 활성화
curs_set(0); // 커서 숨기기
// 색상 초기화
if (has_colors()) {
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 뱀
init_pair(2, COLOR_RED, COLOR_BLACK); // 음식
init_pair(3, COLOR_CYAN, COLOR_BLACK); // 벽
}
// 뱀 초기화
for (int i = 0; i < 3; i++) {
Node* n = malloc(sizeof(Node));
n->x = WIDTH / 2 - i;
n->y = HEIGHT / 2;
n->next = head;
head = n;
}
srand(time(NULL));
spawn_food();
}
void draw(void) {
clear();
// 벽
attron(COLOR_PAIR(3));
for (int i = 0; i < WIDTH; i++) {
mvaddch(0, i, ACS_HLINE);
mvaddch(HEIGHT - 1, i, ACS_HLINE);
}
for (int i = 0; i < HEIGHT; i++) {
mvaddch(i, 0, ACS_VLINE);
mvaddch(i, WIDTH - 1, ACS_VLINE);
}
mvaddch(0, 0, ACS_ULCORNER);
mvaddch(0, WIDTH - 1, ACS_URCORNER);
mvaddch(HEIGHT - 1, 0, ACS_LLCORNER);
mvaddch(HEIGHT - 1, WIDTH - 1, ACS_LRCORNER);
attroff(COLOR_PAIR(3));
// 음식
attron(COLOR_PAIR(2));
mvaddch(food_y, food_x, 'O');
attroff(COLOR_PAIR(2));
// 뱀
attron(COLOR_PAIR(1));
for (Node* n = head; n; n = n->next) {
mvaddch(n->y, n->x, n == head ? '@' : '#');
}
attroff(COLOR_PAIR(1));
// 점수
mvprintw(HEIGHT + 1, 0, "Score: %d", score);
refresh();
}
void input(void) {
int ch = getch();
switch (ch) {
case KEY_UP: if (dir != DOWN) dir = UP; break;
case KEY_DOWN: if (dir != UP) dir = DOWN; break;
case KEY_LEFT: if (dir != RIGHT) dir = LEFT; break;
case KEY_RIGHT: if (dir != LEFT) dir = RIGHT; break;
case 'q': game_over = true; break;
}
}
void update(void) {
// 다음 위치
int nx = head->x, ny = head->y;
switch (dir) {
case UP: ny--; break;
case DOWN: ny++; break;
case LEFT: nx--; break;
case RIGHT: nx++; break;
}
// 벽 충돌
if (nx <= 0 || nx >= WIDTH - 1 || ny <= 0 || ny >= HEIGHT - 1) {
game_over = true;
return;
}
// 새 머리
Node* new_head = malloc(sizeof(Node));
new_head->x = nx;
new_head->y = ny;
new_head->next = head;
head = new_head;
// 음식 확인
if (nx == food_x && ny == food_y) {
score += 10;
spawn_food();
} else {
// 꼬리 제거
Node* curr = head;
while (curr->next && curr->next->next) curr = curr->next;
free(curr->next);
curr->next = NULL;
}
}
void cleanup(void) {
while (head) {
Node* next = head->next;
free(head);
head = next;
}
endwin();
}
int main(void) {
init_game();
while (!game_over) {
input();
update();
draw();
usleep(100000);
}
// 게임 오버 메시지
mvprintw(HEIGHT / 2, WIDTH / 2 - 5, "GAME OVER!");
mvprintw(HEIGHT / 2 + 1, WIDTH / 2 - 6, "Score: %d", score);
refresh();
nodelay(stdscr, FALSE);
getch();
cleanup();
return 0;
}
컴파일¶
# macOS
gcc -o snake_ncurses snake_ncurses.c -lncurses
# Linux
gcc -o snake_ncurses snake_ncurses.c -lncurses
연습 문제¶
연습 1: 일시정지 기능¶
P 키를 누르면 게임이 일시정지되도록 구현하세요.
연습 2: 스페셜 아이템¶
가끔 나타나는 특별 아이템을 추가하세요: - 골든 사과: 30점 - 스피드 다운: 일시적으로 속도 감소 - 투명화: 잠시 자기 몸 통과 가능
연습 3: 2인 플레이¶
WASD와 방향키로 각각 조작하는 2인 모드를 구현하세요.
연습 4: AI 뱀¶
자동으로 음식을 찾아가는 AI 뱀을 추가하세요. - 힌트: BFS 또는 간단한 휴리스틱 사용
핵심 개념 정리¶
| 개념 | 설명 |
|---|---|
| ANSI Escape Codes | 터미널 화면 제어 (커서, 색상) |
| termios | 터미널 입출력 설정 |
| Raw 모드 | 버퍼링 없는 즉시 입력 |
| 게임 루프 | 입력 → 업데이트 → 렌더링 반복 |
| 프레임 레이트 | usleep으로 속도 조절 |
| ncurses | 터미널 UI 라이브러리 |
다음 단계¶
뱀 게임을 완성했다면 다음 프로젝트로 넘어가세요: - 프로젝트 11: 미니 쉘 - 간단한 명령어 쉘 구현