跳到主要内容

贪吃蛇

· 阅读需 4 分钟
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cstdlib>
#include <deque>
#include <iostream>
#include <random>
#include <thread>

#ifdef _WIN32
#include <conio.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>
#endif

using namespace std;

struct Point {
int x;
int y;

bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};

enum class Direction {
Up,
Down,
Left,
Right
};

constexpr int BOARD_WIDTH = 30;
constexpr int BOARD_HEIGHT = 18;

deque<Point> snake;
Point food;

Direction direction = Direction::Right;

bool gameOver = false;
bool paused = false;
int score = 0;

#ifdef _WIN32

void setupTerminal() {
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(console, &cursorInfo);

cursorInfo.bVisible = FALSE;
SetConsoleCursorInfo(console, &cursorInfo);
}

void restoreTerminal() {
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(console, &cursorInfo);

cursorInfo.bVisible = TRUE;
SetConsoleCursorInfo(console, &cursorInfo);
}

bool keyPressed() {
return _kbhit() != 0;
}

char readKey() {
int key = _getch();

// Windows 方向键会返回 0 或 224,
// 再读取一次才能得到真正的方向码。
if (key == 0 || key == 224) {
key = _getch();

switch (key) {
case 72:
return 'U'; // 上
case 80:
return 'D'; // 下
case 75:
return 'L'; // 左
case 77:
return 'R'; // 右
default:
return '\0';
}
}

return static_cast<char>(key);
}

#else

termios originalTerminal;

void setupTerminal() {
tcgetattr(STDIN_FILENO, &originalTerminal);

termios rawTerminal = originalTerminal;

// 关闭规范模式和回显,实现实时读取按键
rawTerminal.c_lflag &= ~(ICANON | ECHO);
rawTerminal.c_cc[VMIN] = 0;
rawTerminal.c_cc[VTIME] = 0;

tcsetattr(STDIN_FILENO, TCSANOW, &rawTerminal);

int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

// 隐藏光标
cout << "\033[?25l";
}

void restoreTerminal() {
tcsetattr(STDIN_FILENO, TCSANOW, &originalTerminal);

// 显示光标
cout << "\033[?25h";
}

bool keyPressed() {
fd_set inputSet;
FD_ZERO(&inputSet);
FD_SET(STDIN_FILENO, &inputSet);

timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 0;

return select(STDIN_FILENO + 1, &inputSet, nullptr, nullptr, &timeout) > 0;
}

char readKey() {
char first = '\0';

if (read(STDIN_FILENO, &first, 1) != 1) {
return '\0';
}

// Linux/macOS 方向键通常会产生:
// ESC [ A:上
// ESC [ B:下
// ESC [ C:右
// ESC [ D:左
if (first == '\033') {
char sequence[2];

if (read(STDIN_FILENO, sequence, 2) == 2 &&
sequence[0] == '[') {
switch (sequence[1]) {
case 'A':
return 'U';
case 'B':
return 'D';
case 'C':
return 'R';
case 'D':
return 'L';
default:
return '\0';
}
}

return '\0';
}

return first;
}

#endif

void clearScreen() {
cout << "\033[2J\033[H";
}

bool isSnakeBody(const Point& point) {
return find(snake.begin(), snake.end(), point) != snake.end();
}

void createFood() {
random_device randomDevice;
mt19937 generator(randomDevice());

uniform_int_distribution<int> xDistribution(1, BOARD_WIDTH - 2);
uniform_int_distribution<int> yDistribution(1, BOARD_HEIGHT - 2);

do {
food = {
xDistribution(generator),
yDistribution(generator)
};
} while (isSnakeBody(food));
}

void initializeGame() {
snake.clear();

int startX = BOARD_WIDTH / 2;
int startY = BOARD_HEIGHT / 2;

snake.push_back({startX, startY});
snake.push_back({startX - 1, startY});
snake.push_back({startX - 2, startY});

direction = Direction::Right;
score = 0;
gameOver = false;
paused = false;

createFood();
}

void drawGame() {
clearScreen();

cout << "C++ 贪吃蛇 分数: " << score << '\n';
cout << "操作: 方向键移动,P 暂停,Q 退出\n\n";

for (int y = 0; y < BOARD_HEIGHT; ++y) {
for (int x = 0; x < BOARD_WIDTH; ++x) {
Point current{x, y};

if (x == 0 || x == BOARD_WIDTH - 1 ||
y == 0 || y == BOARD_HEIGHT - 1) {
cout << '#';
} else if (current == snake.front()) {
cout << '@';
} else if (current == food) {
cout << '*';
} else if (isSnakeBody(current)) {
cout << 'o';
} else {
cout << ' ';
}
}

cout << '\n';
}

if (paused) {
cout << "\n游戏已暂停,按 P 继续。\n";
}

cout.flush();
}

void handleInput() {
if (!keyPressed()) {
return;
}

char key = readKey();

if (key == 'q' || key == 'Q') {
gameOver = true;
return;
}

if (key == 'p' || key == 'P') {
paused = !paused;
return;
}

if (paused) {
return;
}

switch (key) {
case 'U':
if (direction != Direction::Down) {
direction = Direction::Up;
}
break;

case 'D':
if (direction != Direction::Up) {
direction = Direction::Down;
}
break;

case 'L':
if (direction != Direction::Right) {
direction = Direction::Left;
}
break;

case 'R':
if (direction != Direction::Left) {
direction = Direction::Right;
}
break;

default:
break;
}
}

void updateGame() {
if (paused) {
return;
}

Point head = snake.front();
Point newHead = head;

switch (direction) {
case Direction::Up:
--newHead.y;
break;

case Direction::Down:
++newHead.y;
break;

case Direction::Left:
--newHead.x;
break;

case Direction::Right:
++newHead.x;
break;
}

bool hitsWall =
newHead.x <= 0 ||
newHead.x >= BOARD_WIDTH - 1 ||
newHead.y <= 0 ||
newHead.y >= BOARD_HEIGHT - 1;

bool eatsFood = newHead == food;

// 没有吃到食物时,蛇尾会移动,
// 因此新头部可以进入原来的尾部位置。
bool hitsBody = false;

size_t bodyLimit = eatsFood
? snake.size()
: snake.size() - 1;

for (size_t i = 0; i < bodyLimit; ++i) {
if (snake[i] == newHead) {
hitsBody = true;
break;
}
}

if (hitsWall || hitsBody) {
gameOver = true;
return;
}

snake.push_front(newHead);

if (eatsFood) {
score += 10;
createFood();
} else {
snake.pop_back();
}
}

int main() {
setupTerminal();

initializeGame();

while (!gameOver) {
drawGame();
handleInput();
updateGame();

this_thread::sleep_for(chrono::milliseconds(120));
}

restoreTerminal();
clearScreen();

cout << "游戏结束!\n";
cout << "最终得分: " << score << '\n';

return 0;
}