简单做了一个控制台的贪吃蛇,基本是想到什么就堆什么上去了,所以有些杂乱,属于粗制烂造了。
特别是蛇和空闲的位置用了双向循环链表,比较占空间。
Aconsole.h
这个文件主要用于声明类Aconsole,方便在控制台生成需要的图形,主要实现了以下的功能:
- 初始化控制台的单个字符大小,
改变窗口标题,隐藏光标 - 设置窗口大小
- 更改输入字符的前景色和后景色
- 移动光标到指定位置
- 以左上角为基准,绘制矩形或者空心矩形
- 获取键盘输入
除了写贪吃蛇,其他相关的也能使用。事实上我还想加个画圆的,但贪吃蛇里好像没必要用。
/*
这个文件用于在控制台生成指定图形, 以及读取键入
*/
#ifndef _ACONSOLE_
#define _ACONSOLE_
#include <windows.h>
class Aconsole{
private:
HANDLE handle;
void Block(int times, char* str, COORD coord);
public:
Aconsole(short fontSize = 15, LPCSTR title = " ");
void SetWinSize(int width, int height);
void Color(short fontcolor, short background = 0);
void SetCoord(short X, short Y);
void Rect(int width, int height, int i_height = 0, int i_width = 0, char ch = ' ');
int GetKey();
};
#endif
Aconsole.cpp
Aconsole类的具体定义
#include "Aconsole.h"
#include <stdio.h>
#include <conio.h>
// 初始设置控制台
Aconsole::Aconsole(short fontSize, LPCSTR title){
handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTitle(title); //设置控制台标题
// 改变字符大小
CONSOLE_FONT_INFOEX CurrentFontEx;
GetCurrentConsoleFontEx(handle, FALSE, &CurrentFontEx);
CurrentFontEx.cbSize = sizeof(CONSOLE_FONT_INFOEX);
CurrentFontEx.FontFamily = 0;
CurrentFontEx.FontWeight = 0;
CurrentFontEx.nFont = 0;
CurrentFontEx.dwFontSize = {fontSize,fontSize};
SetCurrentConsoleFontEx(handle, FALSE, &CurrentFontEx);
// 隐藏光标
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo); //获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(handle, &CursorInfo); //设置控制台光标状态
}
// 按次数逐行打印str
void Aconsole::Block(int times, char* str, COORD coord){
for (int i = 0; i < times; i++){
SetConsoleCursorPosition(handle, coord);
printf("%s", str);
coord.Y++;
}
}
// 设置窗口大小
void Aconsole::SetWinSize(int width, int height){
char chCmd[32];
sprintf(chCmd,"mode con cols=%d lines=%d",width,height);
system(chCmd);
}
// 设置字符颜色, fontcolor为字体颜色,background为背景色
void Aconsole::Color(short fontcolor, short background){
SetConsoleTextAttribute(handle, fontcolor + background*16);
}
// 光标移动到指定坐标(X,Y)
void Aconsole::SetCoord(short X, short Y){
COORD place = {X,Y};
SetConsoleCursorPosition(handle, place);
}
// 绘制长方形,i参数是内长方形参数,ch是绘制长方形的字符
void Aconsole::Rect(int width, int height, int i_width, int i_height, char ch){
CONSOLE_SCREEN_BUFFER_INFO para;
GetConsoleScreenBufferInfo(handle, ¶);
COORD place = para.dwCursorPosition; // 获取光标坐标
char row[width+1];
for (int i = 0; i < width; i++)
row[i] = ch;
row[width] = '\0';
// 上边
int temp = (height - i_height)/2;
Block(temp, row, place);
// 下边
place.Y += i_height + temp;
Block(height-i_height-temp, row, place);
temp = (width - i_width)/2;
for (int i = 0; i < temp; i++)
row[i] = ch;
row[temp] = '\0';
// 左边
place.Y -= i_height;
Block(i_height, row, place);
// 右边
place.X += i_width + temp;
if (2 * temp != width - i_width){
row[temp++] = ch;
row[temp] = '\0';
}
Block(i_height, row, place);
}
// 读取键入的键值, 无键入输出0
int Aconsole::GetKey(){
if (_kbhit()){
int ch;
fflush(stdin);
if((ch = _getch()) == 224)
ch += _getch();
// 某些键值会输出224和另一个数字,
// 只读另一个数字会和某些键冲突,所以直接两值相加了
return ch;
}
return 0;
}
那么接下来,我来讲讲我的构思。
- 按键只响应WASD键,wasd键,方向键,空格键,ESC键;
- 有开始菜单和暂停菜单,为节省点空间,两个是共用的同一个逻辑;
- 有前十的排名表;
- 地图底部有一个专门用于提示的空行;
- 贪吃蛇的主体游戏;
前面提到了双向循环链表,接下来是为什么会采用这种结构的原因。
为了提高生成食物的效率,(但实际上并没有提高,可能还加长了处理时间,毕竟需要遍历链表)非蛇占用的空间与蛇占用的空间分开用两个链表记录。
蛇主要有两个状态,一个是吃到食物的状态,这个时候蛇头插入,非蛇的表删除,蛇尾无操作;另一个是移动的状态,这时候除了上述的操作外,还会进行蛇尾的删除,并插入非蛇的表中。
总结下来,主要需要一种链表的操作方式:将指定元素从一个链表中删除并插入另一个链表中。
如果用顺序表,这个操作会需要更多的时间,因此用了链表。
进行操作的的元素是蛇头附近的块,以及蛇尾。这里主要有个问题,如何确定蛇头附近的块是在另一个链表的何处,如果遍历链表来找将会十分浪费时间。
我的解决方法是建立和地图每个块对应的二维的结构体的数组,并添一个坐标属性(毕竟顺序表比链表好查元素,而且这两个链表的元素总和是不变的,也不需要对数组更改大小)。这样,根据蛇头的坐标可以快速定位蛇下一步对应的元素的位置并进行修改。
也许,到这你会发现仅仅只有蛇的话,单链表足以应付了,因为只有蛇尾删除和蛇头插入罢了。但另一个链表,在蛇头移动时,却是需要链表中随机位置删除的。如果不用双向链表,对于前继的修改则又需要遍历。
greedysnake.h
GreedySnake类的声明
#ifndef GREEDY_SNAKE_H
#define GREEDY_SNAKE_H
#include "Aconsole.h"
enum KEY{NONE, UP, DOWN, RIGHT, LEFT, ENSURE, PAUSE}; //标识按键类型
enum MENU{Start, Rank, Quit}; // 菜单选项相关
enum STATUS{SPACE, SNAKE, FOOD}; // 图像块属性相关
enum COLOR{BLACK, SNAKEHEAD_C=2, SNAKE_C=10, FOOD_C=12};
struct Block{
STATUS status; // 单个块的状态,例如这个块是SNAKE那么它就是蛇身
short X; // 在图上的坐标
short Y;
Block* pre; // 链表用,指向前驱
Block* next; // 后继
};
class GreedySnake{
private:
int Height; // 地图的高度
int Width; // 地图的宽度
int Speed; // 蛇的速度
Aconsole Map;
/*两个循环链表的操作函数*/
// 将B1从原链表删除并插入到B2后面
void GetBlock(Block * B1, Block * B2);
public:
GreedySnake(int W = 20, int H = 15); // 构造函数
/* 图像相关*/
// 重新绘制地图
void InitMap();
// 绘制菜单, mode为选择
void DrawMenu(int mode, const char* list[]);
// 绘制排名表
void DrawRanks();
// 指定位置上色
void DrawBlock(int X, int Y, int color);
// 提示语打印
int Note(const char* note);
// 结尾动画
void SeeYou();
/*统计相关*/
// 读取记录
int * Read_Rank();
// 存储记录
void Save_Rank(int total);
/*功能相关*/
// 获取6种按键的其中一种
int GetKey();
// 创建菜单
int Menu(const char* list[]);
// 随机生成食物
Block * CreateFood(Block * SpaceHead, int & total);
// 蛇头的移动
void HeadMove(Block * target, Block * Head);
// 获取1~num的随机整数
int Randnum(int num);
// 游戏暂停
bool Pause(const char* list[], Block * Head, Block *Food);
// 游戏主体
void Game(const char* list[]);
};
#endif
greedysnake.cpp
GreedySnake类的定义
#include <stdio.h>
#include <cstdlib>
#include <ctime>
#include <conio.h>
#include <fstream>
#include "Aconsole.h"
#include "greedysnake.h"
//构造函数
GreedySnake::GreedySnake(int W, int H):Width(W), Height(H), Speed(100), Map(15, "贪吃蛇"){
Map.SetWinSize(Width+2,Height+3);
InitMap();
Note("欢迎游玩。");
// 判断rank.txt是否存在
std::fstream file;
file.open("rank.txt", std::ios::in);
if (!file){
std::ofstream fo("rank.txt");
fo << "0,0,0,0,0,0,0,0,0,0,";
fo.close();
}
else
file.close();
}
// 链表获得元素
void GreedySnake::GetBlock(Block * B1, Block * B2){
B1->next->pre = B1->pre;
B1->pre->next = B1->next;
B1->pre = B2;
B1->next = B2->next;
B2->next->pre = B1;
B2->next = B1;
};
// ***************************************************************
// 地图初始化,生成一个厚度为1的空心矩形
void GreedySnake::InitMap(){
Map.SetCoord(0,0);
Map.Rect(Width+2,Height+2, Width, Height, '#');
Map.SetCoord(1,1);
Map.Rect(Width, Height);
}
// 依据提供的三个字符串绘制菜单,并高亮选择
void GreedySnake::DrawMenu(int mode, const char* list[]){
// 绘制菜单外框
short temp = Width/2-5;
Map.SetCoord(temp, 5);
Map.Color(7);
Map.Rect(10, 9, 8, 7, '+');
Map.SetCoord(temp + 1, 6);
Map.Rect(8, 7);
// 绘制选项
temp += 3;
for (int i = 0; i < 3; i++){
if (i == mode){
Map.SetCoord(temp-1, 7+2*i);
Map.Color(0, 7);
printf(">%s", list[i]);
Map.Color(7);
}
else{
Map.SetCoord(temp, 7+2*i);
printf("%s", list[i]);
}
}
}
// 排名表绘制
void GreedySnake::DrawRanks(){
int *scores = Read_Rank();
// 绘制内容
Note("目前游玩的前十名。");
Map.SetCoord(Width/2 - 1,1);
printf("排名");
for (int i = 0; i < 10; i++){
Map.SetCoord(Width/10,2 + 2*i);
printf("%2d ",i+1);
for (int j = 0; j < 4*Width/5-6;j++)
printf("*");
printf("%3d",scores[i]);
Sleep(50);
}
getch();
InitMap();
}
// 指定位置上色
void GreedySnake::DrawBlock(int X, int Y, int color){
Map.SetCoord(X, Y);
Map.Color(color,color);
printf(" ");
}
// 输出提示语
int GreedySnake::Note(const char* note){
Map.SetCoord(0, Height+2);
Map.Color(0);
Map.Rect(Width+2, 1);
Map.SetCoord(0, Height+2);
Map.Color(7);
printf("%s", note);
return 10;
}
// ***************************************************************
// 获取按键
int GreedySnake::GetKey(){
int ch = Map.GetKey();
switch (ch){
case 296: // UpArrow键
case 87: // W键
case 119: // w键
return UP;
case 304: // DwArrow键
case 83: // S键
case 115: // s键
return DOWN;
case 32:
return ENSURE;
case 299: // LeftArrow键
case 65: // A键
case 97: // a键
return LEFT;
case 301: // RightArrow键
case 68: // D键
case 100: // d键
return RIGHT;
case 27: // ESC键
return PAUSE;
default:
return NONE;
}
}
// 菜单创建
int GreedySnake::Menu(const char* list[]){
// 初始化菜单
int mode = Start;
DrawMenu(mode, list);
// ch保存键入
int ch;
bool flag = true; // true为未完成选择,false表示完成选择
while (flag){
if (ch = GetKey()){ // 当前有键入时执行下述操作
switch (ch){
case DOWN:
if (++mode > 2)
mode -= 3;
break;
case UP:
if (--mode < 0)
mode += 3;
break;
case ENSURE:
flag = false;
}
DrawMenu(mode, list);
}
}
InitMap(); // 擦除菜单栏
return mode;
}
// 获得一个1~num的随机整数
int GreedySnake::Randnum(int num){
srand(time(0)+rand());
return rand()%num+1;
}
// 随机生成食物
Block * GreedySnake::CreateFood(Block * SpaceHead, int & total){
Block * temp = SpaceHead;
// 从空闲块的链表中随机选取一块
int X = Randnum(Height*Width - total-1);
for (int i = 0; i < X; i++)
temp = temp->next;
X = temp->X;
int Y = temp->Y;
temp->status = FOOD;
total++;
DrawBlock(X, Y, FOOD_C);
return temp; // 将选取的块返回
}
// 蛇头的移动
void GreedySnake::HeadMove(Block * target, Block * Head){
GetBlock(target, Head); //将目标放进蛇的链表
target->status = SNAKE;
DrawBlock(target->X, target->Y, SNAKEHEAD_C); // 绘制蛇头
DrawBlock(target->next->X, target->next->Y, SNAKE_C); // 绘制蛇身
}
// ***************************************************************
// 读取记录
int * GreedySnake::Read_Rank(){
std::ifstream file;
file.open("rank.txt");
char temp[4];
int * scores = new int[10];
for (int i = 0; i < 10; i++){
char ch = file.get();
int j = 0;
while (ch != ',' && !file.eof()){
temp[j++] = ch - '0';
ch = file.get();
}
switch (j){
case 3:
scores[i] = temp[0]*100 + temp[1]*10 + temp[2];break;
case 2:
scores[i] = temp[0]*10 + temp[1];break;
case 1:
scores[i] = temp[0];break;
}
}
file.close();
return scores;
}
// 存入记录
void GreedySnake::Save_Rank(int total){
int *scores = Read_Rank();
int index = 0;
while (index < 10 && scores[index] > total)
index++;
if (index == 10)
return;
else
for (int i = 9; i > index; i--)
scores[i] = scores[i-1];
scores[index] = total;
std::ofstream file;
file.open("rank.txt");
for (int i = 0; i < 10; i++)
file << scores[i] << ',';
file.close();
}
// 暂停游戏
bool GreedySnake::Pause(const char* list[], Block * Head, Block *Food){
int choice = Menu(list);
while (choice != Quit){
if (choice == Rank)
DrawRanks();
else if (choice == Start){
Note("欢迎回来,准备场地中。。。");
// 依据蛇链表重新绘制蛇
Block * temp = Head->next;
DrawBlock(temp->X, temp->Y, SNAKEHEAD_C);
while (temp->next != Head){
temp = temp->next;
DrawBlock(temp->X, temp->Y, SNAKE_C);
}
// 绘制食物
DrawBlock(Food->X, Food->Y, FOOD_C);
return false; // 游戏继续
}
choice = Menu(list);
}
return true; // 游戏结束
}
// 游戏主体
void GreedySnake::Game(const char* list[]){
Note("正在准备场地...");
// Space保存所有块的数据
Block Space[Width][Height];
// 生成空块的链表表头
Block* SpaceHead = new Block;
// 使用现有的数据,不进行new
// 特殊处理第一块和最后一块
Space[Width-1][Height-1] = {SPACE, (short)Width, (short)Height, &Space[Width-1][Height-2], SpaceHead};
Space[0][0] = {SPACE, 1, 1, SpaceHead, &Space[0][1]};
// 表头链接第一块和最后一块
SpaceHead->pre = &Space[Width-1][Height-1];
SpaceHead->next = &Space[0][0];
// 剩余块的链接
short X,Y;
for (int i = 1; i < Width * Height-1; i++){
X = i/Height + 1;
Y = i-i/Height*Height+1;
Space[X-1][Y-1] = {SPACE, X, Y,
&Space[(i-1)/Height][i-(i-1)/Height*Height-1], &Space[(i+1)/Height][i-(i+1)/Height*Height+1]};
}
// 蛇的空链表
Block* SnakeHead = new Block;
SnakeHead->pre = SnakeHead;
SnakeHead->next = SnakeHead;
// 随机生成蛇头, X,Y将记录蛇头位置
X = Randnum(Width);
Y = Randnum(Height);
GetBlock(&Space[X-1][Y-1], SnakeHead);
Space[X-1][Y-1].status = SNAKE;
DrawBlock(X, Y, 2);
// 生成食物
int total = 0; // 记录蛇长
Block * Food = CreateFood(SpaceHead, total);
int update, ch, count;
// 开局自主暂停
Note("按方向键或WASD开始。");
while (!(update = GetKey()));
if (update == ENSURE || update == PAUSE){ // 非方向键键入将随机生成方向
count = Note("不选一个好方向吗,那我来开个头。");
update = Randnum(4);
}
while (true){ // 主循环
if(ch = GetKey()){
if (ch == PAUSE){
if (Pause(list, SnakeHead, Food))
break;
Note("按方向键或WASD继续。");
ch = 0;
while (!ch || ch == ENSURE || ch == PAUSE){ // 等待输入正确的方向
if (ch == ENSURE)
Note("。。。空格?");
else if (ch == PAUSE)
Note("那我再等一会。");
ch = GetKey();
}
}
else if (ch == ENSURE)
ch = update;
if (ch != update && ch + update != 3 && ch + update != 7) // 检测输入是否合法
update = ch;
}
switch (update){ // 检测按键,更新下一步坐标
case LEFT:
X--;break;
case RIGHT:
X++;break;
case UP:
Y--;break;
case DOWN:
Y++;break;
}
// 状况判断
if (X < 1 || X > Width || Y < 1 || Y > Height){
DrawBlock(X, Y, SNAKEHEAD_C);
DrawBlock(SnakeHead->next->X, SnakeHead->next->Y, SNAKE_C);
count = Note("撞墙了。");
break;
}
else if (Space[X-1][Y-1].status == SNAKE){
DrawBlock(X, Y, SNAKEHEAD_C);
DrawBlock(SnakeHead->next->X, SnakeHead->next->Y, SNAKE_C);
count = Note("咬到自己了。");
break;
}
else if (Space[X-1][Y-1].status == FOOD){
HeadMove(&Space[X-1][Y-1], SnakeHead);
Food = CreateFood(SpaceHead, total);
count = Note("加油。");
if (total % 10)
printf("分数总计%d分", total-1);
else{
printf("速度提升, 当前%d级", total/10+1);
Speed -= 5;
}
}
else
{ // 蛇的正常移动
SnakeHead->pre->status = SPACE;
HeadMove(&Space[X-1][Y-1], SnakeHead);
DrawBlock(SnakeHead->pre->X, SnakeHead->pre->Y, BLACK);
GetBlock(SnakeHead->pre, SpaceHead);
}
// 虽然觉得不太可能,但还是得写一下
if (total == Width*Height){
Note("这下无人能及了。");
break;
}
// 提示自动消失
if (count == 0)
Note(" ");
else if (count >= 0)
count--;
//延时
Sleep(Speed);
}
delete SnakeHead;
delete SpaceHead;
printf("分数总计%d分", total-1);
Save_Rank(total-1);
while (!Map.GetKey());
}
// 结尾动画
void GreedySnake::SeeYou(){
Note("下次再见了, Bye Bye!");
short S_ch[2][11] = {
{3,2,1,0,1,2,3,2,1,0,-1},
{0,0,0,1,2,2,3,4,4,4,-1}
};
short E_ch[2][15] = {
{0,1,2,3,0,1,2,3,0,0,0,1,2,3,-1},
{0,0,0,0,2,2,2,2,1,3,4,4,4,4,-1},
};
short Y_ch[2][8] = {
{0,1,2,4,3,2,2,-1},
{0,1,2,0,1,3,4,-1}
};
short O_ch[2][11] = {
{1,2,3,3,3,2,1,0,0,0,-1},
{0,0,1,2,3,4,4,3,2,1,-1}
};
short U_ch[2][11] = {
{0,0,0,0,1,2,3,3,3,3,-1},
{0,1,2,3,4,4,3,2,1,0,-1}
};
short SeeYou_ch[6][2] = {
{6,4},{12,4},{18,4}, // S, E, E
{10,12},{16,12},{22,12} // Y, O, U
};
for (int i = 1; i <= 70; i++){
if (!(i%10)){
Map.Color(0,Randnum(15));
Map.SetCoord(SeeYou_ch[3][0]+Y_ch[0][i/10-1], SeeYou_ch[3][1]+Y_ch[1][i/10-1]);
printf(" ");
};
if(!(i%5)){
Map.Color(0,Randnum(15));
Map.SetCoord(SeeYou_ch[1][0]+E_ch[0][i/5-1], SeeYou_ch[1][1]+E_ch[1][i/5-1]);
printf(" ");
Map.SetCoord(SeeYou_ch[2][0]+E_ch[0][i/5-1], SeeYou_ch[2][1]+E_ch[1][i/5-1]);
printf(" ");
}
if(!(i%7)){
Map.Color(0,Randnum(15));
Map.SetCoord(SeeYou_ch[0][0]+S_ch[0][i/7-1], SeeYou_ch[0][1]+S_ch[1][i/7-1]);
printf(" ");
Map.SetCoord(SeeYou_ch[4][0]+O_ch[0][i/7-1], SeeYou_ch[4][1]+O_ch[1][i/7-1]);
printf(" ");
Map.SetCoord(SeeYou_ch[5][0]+U_ch[0][i/7-1], SeeYou_ch[5][1]+U_ch[1][i/7-1]);
printf(" ");
}
Sleep(10);
}
Sleep(1000);
}
main.cpp
主函数
#include <stdio.h>
#include <conio.h>
#include "greedysnake.h"
static const char* start[3] = {"开始", "排行", "退出"};
static const char* resume[3] = {"继续", start[1], start[2]};
static const char* restart[3] = {"重来", start[1], start[2]};
int main(){
GreedySnake snake{30, 20};
int choice = snake.Menu(start);
while (choice != Quit){
if (choice == Rank){
snake.DrawRanks();
choice = snake.Menu(start);
}
else if (choice == Start){
snake.Game(resume);
choice = snake.Menu(restart);
}
}
snake.InitMap();
snake.SeeYou();
return 0;
}
部分画面展示
我只做了退出的动画,有兴趣的还可以做个开屏的动画。
记得头文件名不要出错,编译的时候将所有文件一起编译。
g++ -fexec-charset=gbk -g *.cpp -o snake.exe