这是一个在windows控制台上运行的程序,游戏分为四个难度,其中简单难度和贪食蛇基本没有区别,只是可以穿越墙壁。另外三个难度加入了陷阱,吃到陷阱长度减3,下面直接贴上整份源代码,注释的比较详细了
#include <iostream>
#include <string>
#include <vector>
#include <deque>
#include <ctime>
#include <algorithm>
#include <windows.h>
#include <conio.h>
//#define _CRT_SECURE_NO_WARNINGS //visual studio无法使用sprinf,需要加入该行
using namespace std;
const int N = 30, M = 120;
const int ObjWidth = 2; //物品宽度,若部分电脑出线乱码,可以修改为1,并使用下面注释掉的图案
const char *const str[] = {" ", "■", "●", "□", "★", "×"};
//const char *const str[] = {" ", "#", "O", "o", "F", "X"};
char ScreenData[N][M]; //储存需要被显示的内容
int ScreenColor[N][M]; //储存需要显示的字体颜色
int ConsoleColor[2][N][M]; //储存修改之前的颜色
HANDLE hOutbuf[2]; //输出缓冲区
int bufid; //当前激活的缓冲区,通过每次异或1来进行0/1之间的来回切换
CONSOLE_CURSOR_INFO cur;
void ProgramInit() { //程序初始化
srand(time(nullptr));
SetConsoleTitleA("贪食蛇■●□★×"); //设置控制台窗口,顺便检测是否会乱码
for (int i = 0; i < N; ++i) {
for (int j = 0; j < M; ++j) {
ScreenColor[i][j] = 0x0f; //初始化为黑底白字
}
}
bufid = 0;
hOutbuf[0] = hOutbuf[1] = CreateConsoleScreenBuffer( //创建两个控制台缓冲区
GENERIC_READ | GENERIC_WRITE, //控制台缓冲安全与访问权限
FILE_SHARE_READ | FILE_SHARE_WRITE, //定义缓冲区可共享权限
nullptr, //安全属性默认为NULL
CONSOLE_TEXTMODE_BUFFER, //缓冲区类型,固定参数
nullptr
);
cur.bVisible = 0; cur.dwSize = 1; //用于隐藏光标
SetConsoleCursorInfo(hOutbuf[0], &cur);
SetConsoleCursorInfo(hOutbuf[1], &cur);
SetWindowLongPtrA( //禁用调整控制台大小,若可以手动扩大控制台大小,未知的原因会导致程序崩溃且进程不正常结束,需要任务管理器来杀死进程
GetConsoleWindow(),
GWL_STYLE,
GetWindowLongPtrA(GetConsoleWindow(),GWL_STYLE)
& ~WS_SIZEBOX & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX
);
}
void ShowConsole() { //在控制台中显示新的画面
DWORD bytes = 0; COORD coord;
bufid ^= 1; //再0和1之间切换,切换到背后的缓冲区
for (short i = 0; i < N; ++i) {
coord = {0, i};
WriteConsoleOutputCharacterA(hOutbuf[bufid], ScreenData[i], M, coord, &bytes); //更新一行
for (short j = 0; j < M; ++j) {
coord.X = j;
if (ConsoleColor[bufid][i][j] != ScreenColor[i][j]){ //若和之前颜色不同则需要更改颜色
FillConsoleOutputAttribute(hOutbuf[bufid], ScreenColor[i][j], 1, coord, &bytes); //设置颜色
ConsoleColor[bufid][i][j] = ScreenColor[i][j];
}
}
}
SetConsoleActiveScreenBuffer(hOutbuf[bufid]);
}
enum Obj { //表示地图上的物品
Empty = 0,
Wall = 1,
Head = 2,
Body = 3,
Bean = 4,
Trap = 5,
};
const int n = 20, m = 20;
const int sx = 1, sy = 1;
struct P { //点类
int x, y;
P (int a = 0, int b = 0) : x(a), y(b) {}
P operator + (const P &b) const { return P(x + b.x, y + b.y); }
bool operator < (const P &b) const { return x < b.x || x == b.x && y < b.y; }
};
P HeadPos; //蛇头位置
P BeanPos; //豆豆位置,用于快捷删除豆子
enum Direction { //表示方向的下标
Up = 0,
Right = 1,
Down = 2,
Left = 3
} dir, lastdir; //当前方向,上一步的方向
P dx[] = {P(-1, 0), P(0, 1), P(1, 0), P(0, -1)}; //表示四个方向的向量
Obj Map[25][25]; //储存游戏地图,下标范围是[1~n][1~m],[i][j]表示第i行第j列,最外圈为墙壁
deque<P>dq; //储存蛇身的队列,dq.size()表示长度,维护的位置队列实际上没有作用,但是可能会用于扩展功能
enum Difficulty { //表示游戏困难度
Easy = 0, //简单,适合手残党或者想获得很长长度的玩家
Medium = 1, //中等,适合大多数玩家,加入了3个陷阱
Hard = 2, //困难,适合大多数玩家,加入了7个陷阱,游戏节奏较快
Demon = 3, //恶魔,适合高玩,15个陷阱和超快的速度可能会导致你难以获得超过7的分数
} difficulty;
int MaxTrapCnt[] = {0, 3, 7, 15}; //最大陷阱数量
int TrapCnt; //当前陷阱数量,用于判断是否需要刷新陷阱
// 速度用int表示,速度实际上指的是时钟周期,即每过多久会走一步
int BaseSpeed[] = {450, 380, 310, 250}; //基础速度
int Acceleration[] = {10, 20, 25, 50}; //加速度
int MaxSpeed[] = {220, 180, 135, 100}; //最大速度
int MoveTime; //实际移动一次的时间间隔
int MaxScore; //储存最高分数,因为吃到陷阱会减分,最终得分应该为最高得分
int EmptyMoveCnt; //储存空移动次数,当空移动80次后刷新豆子位置
enum GAMESTATE {
End = 0,
Run = 1,
Pause = 2,
} GameState;
void UpdateMap() { //把地图内容更新到ScreenData数组,并根据内容赋值对应颜色
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
strcpy(ScreenData[sx + i] + sy + j * ObjWidth, str[Map[i][j]]);
for (int k = 0; k < ObjWidth; ++k) {
ScreenColor[sx + i][sy + j * ObjWidth + k] = 0x60;
switch (Map[i][j]) {
case Empty: {
break;
}
case Wall: {
break;
}
case Head: {
ScreenColor[sx + i][sy + j * ObjWidth + k] |= 0x01;
break;
}
case Body: {
ScreenColor[sx + i][sy + j * ObjWidth + k] |= 0x09;
break;
}
case Bean: {
ScreenColor[sx + i][sy + j * ObjWidth + k] |= 0x04;
break;
}
case Trap: {
break;
}
}
}
}
}
}
void UpdateUI() { //更新UI
vector<string>v_ui;
static char s[30];
switch (GameState) {
case End: {
sprintf(s, "游戏状态 : 结束");
break;
}
case Run: {
sprintf(s, "游戏状态 : 运行");
break;
}
case Pause: {
sprintf(s, "游戏状态 : 暂停");
break;
}
}
v_ui.emplace_back(s);
sprintf(s, "当前智商 = %3d", int(dq.size()) - 1);
v_ui.emplace_back(s);
sprintf(s, "最高智商 = %3d", MaxScore);
v_ui.emplace_back(s);
const int sx1 = sx + n + 2, sy1 = sy + 10;
for (unsigned i = 0; i < v_ui.size(); ++i) {
for (unsigned j = 0; j < v_ui[i].length(); ++j) {
ScreenColor[sx1 + i][sy1 + j] = 0x07;
ScreenData[sx1 + i][sy1 + j] = v_ui[i][j];
}
}
}
void UpdateScreen() { //更新屏幕显示
UpdateMap();
UpdateUI();
ShowConsole();
}
void GenerateBean() { //随机生成一个豆豆
vector<P>v;
for (int i = 2; i < n; ++i) {
for (int j = 2; j < m; ++j) {
if (Map[i][j] == Empty) {
v.emplace_back(i, j);
}
}
}
int i = rand() % v.size();
Map[v[i].x][v[i].y] = Bean;
BeanPos = P(v[i].x, v[i].y);
}
void GenerateTrap(int cnt = MaxTrapCnt[difficulty]) { //随机生成cnt个陷阱
vector<P>v;
for (int i = 2; i < n; ++i) {
for (int j = 2; j < m; ++j) {
if (Map[i][j] == Empty) {
v.emplace_back(i, j);
}
else if (Map[i][j] == Trap){
Map[i][j] = Empty;
}
}
}
random_shuffle(v.begin(), v.end());
for (int i = 0; i < cnt && i < v.size(); ++i) {
Map[v[i].x][v[i].y] = Trap;
}
}
void ShowHelp() { //显示游戏帮助
vector<string>v_help;
static char s[40];
v_help.emplace_back("游戏帮助:");
v_help.emplace_back("按p暂停,按esc退出");
v_help.emplace_back("按方向键或者wsad控制移动");
sprintf(s, "%s表示蛇头,%s表示蛇身", str[Head], str[Body]);
v_help.emplace_back(s);
sprintf(s, "速度随着长度增长而加快,不同难度速度上限不同");
v_help.emplace_back(s);
sprintf(s, "%s表示豆子,吃到一个豆子蛇身长度+1", str[Bean]);
v_help.emplace_back(s);
sprintf(s, "%s表示陷阱,吃到一个陷阱蛇身长度-3", str[Trap]);
v_help.emplace_back(s);
v_help.emplace_back("可以穿墙,但会导致蛇身长度-1");
v_help.emplace_back("80步没吃到豆豆,豆豆会自动刷新位置");
v_help.emplace_back("吃到2个陷阱后,会重新生成陷阱");
v_help.emplace_back("蛇头撞到蛇身,游戏结束");
v_help.emplace_back("作者 : Markyyz");
const int sx1 = sx + 5, sy1 = sy + m * ObjWidth + 5;
for (int i = 0; i < v_help.size(); ++i) {
sprintf(ScreenData[sx1 + i] + sy1, "%s", v_help[i].c_str());
for (int j = 0; j < v_help[i].length(); ++j) {
ScreenColor[sx1 + i][sy1 + j] = 0x06;
}
}
}
void Initial() { //游戏初始化
memset(ScreenData, ' ', sizeof ScreenData);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < M; ++j) {
ScreenColor[i][j] = 0x0f;
}
}
ShowHelp();
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
Map[i][j] = Empty;
}
}
for (int i = 1; i <= n; ++i) {
Map[i][1] = Map[i][m] = Wall;
}
for (int i = 1; i <= m; ++i) {
Map[1][i] = Map[n][i] = Wall;
}
MaxScore = 0;
EmptyMoveCnt = 0;
HeadPos = P(n / 2, 4); //固定起点
Map[HeadPos.x][HeadPos.y] = Head;
dq.clear();
dq.push_back(HeadPos);
dir = Right;
GenerateBean();
GenerateTrap();
TrapCnt = MaxTrapCnt[difficulty];
GameState = Run;
UpdateScreen();
}
void ChangeDir(int c) { //更新运动方向
Direction tdir = dir;
switch (c) {
case 'w':
case 72: { //up
tdir=Up;
break;
}
case 's':
case 80: { //down
tdir=Down;
break;
}
case 'a':
case 75: { //left
tdir=Left;
break;
}
case 'd':
case 77: { //right
tdir=Right;
break;
}
}
//只有三个方向是合法的,使用lastdir可以避免在一个时钟周期内连续多次输入从而完成原地掉头的现象
if ((tdir - lastdir + 4) % 4 != 2) {
dir = tdir;
}
}
int Move() { //移动函数,每过一个时钟周期会调用一次
P tp = HeadPos + dx[dir]; //根据方向计算下一步的位置
if (tp.x <= 1 || tp.x >= n || tp.y <= 1 || tp.y >= m) { //穿越边界,从另一侧出现
tp.x = 2 + (tp.x - 2 + (n - 2)) % (n - 2);
tp.y = 2 + (tp.y - 2 + (m - 2)) % (m - 2);
if (dq.size() > 1) { //穿越边界会减少一节身体,不会删除头部
P p = dq.front();
Map[p.x][p.y] = Empty;
dq.pop_front();
}
}
Obj ret = Map[tp.x][tp.y];
switch (Map[tp.x][tp.y]) {
case Bean: { //吃掉豆豆,只要移动头,尾巴不减少
Map[HeadPos.x][HeadPos.y] = Body;
EmptyMoveCnt = 0; //清除空移动次数
GenerateBean(); //再生成一个豆豆
break;
}
case Trap: { //撞到陷阱
for (int i = 0; i < 3; ++i) { //去掉3个尾巴,不足的话至少保留一个头
if (dq.size() <= 1) break;
P p = dq.front();
Map[p.x][p.y] = Empty;
dq.pop_front();
}
Map[HeadPos.x][HeadPos.y] = Body;
P p = dq.front();
Map[p.x][p.y] = Empty;
dq.pop_front();
--TrapCnt;
if (TrapCnt + 1 < MaxTrapCnt[difficulty]) { //若踩了两个陷阱,则需要重新生成所有陷阱
TrapCnt = MaxTrapCnt[difficulty];
GenerateTrap();
}
break;
}
case Empty: { //空移动,需要去除尾巴
Map[HeadPos.x][HeadPos.y] = Body;
P p = dq.front();
Map[p.x][p.y] = Empty;
dq.pop_front();
++EmptyMoveCnt;
if (EmptyMoveCnt >= 80) { //若空移动次数多余80,则需要变换豆豆的位置
EmptyMoveCnt = 0; //清除空移动次数
Map[BeanPos.x][BeanPos.y] = Empty; //原先豆豆的位置变为空气
GenerateBean();
}
break;
}
case Body: { //撞到蛇身,游戏结束
return -1;
}
default:{
break;
}
}
HeadPos = tp; //最后更新头部,因为所有情况头的变化相同
dq.push_back(HeadPos);
Map[HeadPos.x][HeadPos.y] = Head;
MaxScore = max(MaxScore, int(dq.size()) - 1); //移动完后更新显示内容
return int(ret);
}
int Input() { //检测键盘输入
if (kbhit()) { //非阻塞函数,检测有键盘按下
int key = getch(); //获取键盘信息
switch (key) {
case 'w':
case 72: //up
case 's':
case 80: //down
case 'a':
case 75: //left
case 'd':
case 77: { //right
ChangeDir(key); //更新方向
break;
}
case 'p': { //pause
GameState = Pause;
break;
}
case 27: { //esc
GameState = End;
return -1;
}
}
}
return 0;
}
void Game() { //游戏主函数
while (true) {
clock_t temp = clock();
lastdir = dir;
while (clock() - temp < MoveTime) {
if (Input() == -1) {
GameState = End;
return;
}
}
//一个周期结束后要更新游戏状态
bool flag = false; //暂停过
if (GameState == Pause) {
UpdateScreen();
while (GameState == Pause) {
if (kbhit()) {
switch (getch()) {
case 'p': {
GameState = Run;
UpdateScreen();
flag = true;
break;
}
case 27: {
GameState = End;
return;
}
}
}
}
}
if (flag) continue;
if (Move() == -1) { //游戏结束
GameState = End;
return;
}
UpdateScreen();
MoveTime = max(BaseSpeed[difficulty] - Acceleration[difficulty] * int(dq.size()), MaxSpeed[difficulty]);
}
}
void Start() {
Initial();
Game();
UpdateScreen();
}
void ShowMenu() { //显示游戏菜单
vector<string>v_menu;
v_menu.emplace_back("欢迎来到贪食蛇游戏");
v_menu.emplace_back("1 : easy");
v_menu.emplace_back("2 : medium");
v_menu.emplace_back("3 : hard");
v_menu.emplace_back("4 : demon");
for (int i = 0; i < v_menu.size(); ++i) {
for (int j = 0; j < v_menu[i].length(); ++j) {
ScreenColor[i][j] = 0x06;
ScreenData[i][j] = v_menu[i][j];
}
}
ShowConsole();
}
void CleanScreen() { //清除屏幕
memset(ScreenData, ' ', sizeof ScreenData);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < M; ++j) {
ScreenColor[i][j] = 0x0f;
}
}
}
bool Menu() { //菜单函数
CleanScreen();
ShowMenu();
FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));
bool flag = true;
while (flag) {
flag = false;
switch (getch()) {
case '1': {
difficulty = Easy;
break;
}
case '2': {
difficulty = Medium;
break;
}
case '3': {
difficulty = Hard;
break;
}
case '4': {
difficulty = Demon;
break;
}
case 27: { //esc
return false;
}
default: {
flag = true;
break;
}
}
}
Start();
sprintf(ScreenData[0], "你太菜了!你的智商只有 : %d", MaxScore);
UpdateScreen();
Sleep(800); //等待一些时间,避免游戏结束时不小心多按
sprintf(ScreenData[1], "按r重玩,按esc退出");
UpdateScreen();
FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)); //清除输入缓冲,避免之前多按了一下,在这里被输入,导致直接结束
while (true) {
switch (getch()) {
case 27: { //esc
return false;
}
case 'r':{
return true;
}
default:
break;
}
}
}
int main() {
ProgramInit();
while (Menu()) {}
}