本系列收官之作,挑战经典中的经典——贪吃蛇。蛇由一串坐标组成,吃到食物变长,撞墙或咬到自己就结束。控制台版本采用"每按一次键走一格"的回合制,纯 stdio 实现,任何平台都能编译运行。

玩法介绍

游戏区是 20×12 的矩形,@ 是蛇头,# 是蛇身,* 是食物。按 W/A/S/D 转向(不能 180° 掉头),按一次蛇走一格;吃到食物蛇身变长一格、分数 +1。撞到边界或自己的身体,游戏结束。

第 1 步:用坐标数组表示蛇

蛇是一条"会移动的线",最直观的表示是坐标数组:snake[0] 是蛇头,后面依次是蛇身。初始 3 节、头朝右,方向 dir 与食物位置 food 各用变量记录。

#define W 20   /* 游戏区宽度 */
#define H 12   /* 游戏区高度 */

int snake[W * H][2];   /* 蛇身坐标, snake[0] 是蛇头 */
int len;               /* 当前蛇长 */
int dir;               /* 0上 1下 2左 3右 */
int food[2];           /* 食物坐标 */

第 2 步:绘制游戏区

双层循环逐格判断该画什么:食物画 *,蛇身画 #,蛇头画 @,其余留空格。开头的 \033[2J\033[H 是清屏转义序列,让画面固定在一个位置。

printf("\033[2J\033[H");   /* 清屏 */
for (y = 0; y < H; y++) {
    for (x = 0; x < W; x++) {
        char ch = ' ';
        if (x == food[0] && y == food[1]) ch = '*';
        for (i = 0; i < len; i++)
            if (snake[i][0] == x && snake[i][1] == y) {
                ch = (i == 0) ? '@' : '#';
                break;
            }
        putchar(ch);
    }
    putchar('\n');
}

第 3 步:读取按键与转向

getchar() 读一个字符,再吃掉回车。转向时检查"是否 180° 掉头"——蛇正在向右(dir == 3)时不能直接按 A 往左,否则蛇头会立刻撞进自己的身体。

cmd = getchar();
while (getchar() != '\n');   /* 吃掉回车 */

if (cmd == 'w' || cmd == 'W') { if (dir != 1) dir = 0; }
else if (cmd == 's' || cmd == 'S') { if (dir != 0) dir = 1; }
else if (cmd == 'a' || cmd == 'A') { if (dir != 3) dir = 2; }
else if (cmd == 'd' || cmd == 'D') { if (dir != 2) dir = 3; }
else if (cmd == 'q' || cmd == 'Q') break;

第 4 步:移动、吃食物、变长

移动的秘诀是"后一节顶替前一节":从尾巴开始,每一节挪到前面一节的位置,最后蛇头走到新坐标,整条蛇像传送带一样前进。撞墙或撞到自己返回 0。吃到食物时让蛇尾原地不动,蛇就凭空多了一节。

int tx = snake[len - 1][0], ty = snake[len - 1][1];  /* 记住蛇尾 */

for (i = len - 1; i > 0; i--) {      /* 身体前移 */
    snake[i][0] = snake[i - 1][0];
    snake[i][1] = snake[i - 1][1];
}
snake[0][0] = nx;
snake[0][1] = ny;

if (nx == food[0] && ny == food[1]) { /* 吃到食物 */
    snake[len][0] = tx; snake[len][1] = ty;  /* 尾巴不动 = 变长 */
    len++;
    place_food();
}

第 5 步:完整代码与编译运行

place_food()(在空白处随机放食物)、绘制、转向、移动拼进主循环,就是完整的回合制贪吃蛇:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define W 20   /* 游戏区宽度 */
#define H 12   /* 游戏区高度 */

int snake[W * H][2];   /* 蛇身坐标, snake[0] 是蛇头 */
int len;
int dir;               /* 0上 1下 2左 3右 */
int food[2];

void place_food(void) {
    int x, y, i, ok;
    while (1) {
        x = rand() % W;
        y = rand() % H;
        ok = 1;
        for (i = 0; i < len; i++)
            if (snake[i][0] == x && snake[i][1] == y) { ok = 0; break; }
        if (ok) { food[0] = x; food[1] = y; return; }
    }
}

void print_map(void) {
    int x, y, i;
    printf("\033[2J\033[H");   /* 清屏 */
    for (y = 0; y < H; y++) {
        for (x = 0; x < W; x++) {
            char ch = ' ';
            if (x == food[0] && y == food[1]) ch = '*';
            for (i = 0; i < len; i++)
                if (snake[i][0] == x && snake[i][1] == y) {
                    ch = (i == 0) ? '@' : '#';
                    break;
                }
            putchar(ch);
        }
        putchar('\n');
    }
    printf("长度: %d  分数: %d\n", len, len - 3);
}

int step(void) {
    int nx = snake[0][0], ny = snake[0][1], i;
    int tx = snake[len - 1][0], ty = snake[len - 1][1];

    if (dir == 0) ny--;
    else if (dir == 1) ny++;
    else if (dir == 2) nx--;
    else nx++;

    if (nx < 0 || nx >= W || ny < 0 || ny >= H) return 0;
    for (i = 0; i < len; i++)
        if (snake[i][0] == nx && snake[i][1] == ny) return 0;

    for (i = len - 1; i > 0; i--) {
        snake[i][0] = snake[i - 1][0];
        snake[i][1] = snake[i - 1][1];
    }
    snake[0][0] = nx;
    snake[0][1] = ny;

    if (nx == food[0] && ny == food[1]) {
        snake[len][0] = tx;
        snake[len][1] = ty;
        len++;
        place_food();
    }
    return 1;
}

int main(void) {
    char cmd;
    srand((unsigned)time(NULL));

    len = 3;
    snake[0][0] = 6;  snake[0][1] = H / 2;
    snake[1][0] = 5;  snake[1][1] = H / 2;
    snake[2][0] = 4;  snake[2][1] = H / 2;
    dir = 3;
    place_food();

    printf("贪吃蛇: W/A/S/D 转向, Q 退出。每按一次键, 蛇走一格。\n");
    while (1) {
        print_map();
        printf("输入方向: ");
        cmd = getchar();
        while (getchar() != '\n');

        if (cmd == 'w' || cmd == 'W') { if (dir != 1) dir = 0; }
        else if (cmd == 's' || cmd == 'S') { if (dir != 0) dir = 1; }
        else if (cmd == 'a' || cmd == 'A') { if (dir != 3) dir = 2; }
        else if (cmd == 'd' || cmd == 'D') { if (dir != 2) dir = 3; }
        else if (cmd == 'q' || cmd == 'Q') break;

        if (!step()) {
            print_map();
            printf("游戏结束! 你一共吃了 %d 个食物。\n", len - 3);
            break;
        }
    }
    return 0;
}

编译运行:

gcc -o snake snake.c
./snake
💡 改进方向:让蛇自动前进——用 kbhit()(Windows)或 ncurses(Linux)实现实时控制;吃到食物后加速;增加"穿墙模式"和障碍物;把最高分写进文件做排行榜;把移动与"游戏帧"绑定,做成真正的实时游戏循环。