C語言數據結構(7)貪吃蛇項目2.貪吃蛇項目實現

8. 核心邏輯實現分析

8.1 游戲主邏輯

程序開始就設置程序支持本地模式,然后進入程序的主邏輯。

主邏輯分為3個過程:

? 游戲開始(GameStart)完成游戲的初始化。

? 游戲運行(GameRun)完成游戲運行邏輯的實現。

? 游戲運行(GameEnd)完成游戲結束的說明、資源的釋放。

#include <locale.h>
void test()
{int ch = 0;srand((unsigned int)time(NULL));do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("再來?局嗎?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y');SetPos(0, 27);
}int main()
{//修改當前地區為本地模式,為了支持中文寬字符的打印setlocale(LC_ALL, "");//測試邏輯test();return 0;
}

8.2 游戲開始

這個模塊完成游戲的初始化任務。

? 控制臺窗口大小的設置

? 控制臺窗口名稱的設置

? 鼠標光標的隱藏

? 打印歡迎界面

? 創建地圖

? 初始化蛇

? 創建第一個食物

void GameStart(pSnake ps)
{//設置控制臺窗?的??,30?,100列//mode 為DOS命令system("mode con cols=100 lines=30");//設置cmd窗?名稱system("title 貪吃蛇"); //獲取標準輸出的句柄(?來標識不同設備的數值)HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//影藏光標操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//獲取控制臺光標信息         CursorInfo.bVisible = false; //隱藏控制臺光標SetConsoleCursorInfo(hOutput, &CursorInfo);//設置控制臺光標狀態//打印歡迎界?WelcomeToGame();//打印地圖CreateMap();//初始化蛇InitSnake(ps);//創造第?個?物CreateFood(ps);
}

8.2.1 打印歡迎界面

在游戲正式開始之前,做一些功能提醒。

void WelcomeToGame()
{SetPos(40, 15);printf("歡迎來到貪吃蛇?游戲");SetPos(40, 25);    //讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");SetPos(25, 12);printf("? ↑ . ↓ . ← . → 分別控制蛇的移動, F3為加速,F4為減速\n");SetPos(25, 13);printf("加速將能得到更?的分數。\n");SetPos(40, 25);//讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");
}

8.2.2 創建地圖

創建地圖就是將墻打印出來,因為是寬字符打印,所有使用wprintf函數,打印格式串前使用L

打印地圖的關鍵是要算好坐標,才能在想要的位置打印墻體。

墻體打印的寬字符:

#define WALL L'□'

易錯點:就是坐標的運算。

? 上:(0,0)到(56,0)

? 下:(0,26)到(56,26)

? 左:(0,1)到(0,25)

? 右:(56,1)到(56,25)

創建地圖函數CreateMap

void CreateMap()
{int i = 0;//上(0,0)-(56, 0)SetPos(0, 0);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//左//x是0,y從1開始增?for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}//x是56,y從1開始增?for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}

8.2.3 初始化蛇身

蛇最開始長度為5節,每節對應鏈表的一個節點,蛇身的每一個節點都有自己的坐標。

創建5個節點,然后將每個節點存放在鏈表中進行管理。創建完蛇身后,將蛇的每一節打印在屏幕上。

? 規定蛇的初始位置從(24,5)開始。

再設置當前游戲的狀態,蛇移動的速度,默認的方向,初始成績,蛇的狀態,每個食物的分數。

? 游戲狀態時OK

? 蛇的移動速度:200ms

? 蛇的默認方向:RIGHT

? 初始成績:0

? 每個食物的分數:10

蛇身打印的寬字符:

#define BODY L'●'

初始化蛇身函數:InitSnake

//初始化蛇
void InitSnake(pSnake ps)
{//創建5個蛇身的結點pSnakeNode cur = NULL;int i = 0;//創建蛇身結點,并初始化坐標//頭插法for (i = 0; i < 5; i++){//創建蛇身結點cur = (pSnakeNode)malloc(sizeof(SnakeNode));                                                                 //一次循環開始,cur先賦值指向下一塊空間if (cur == NULL){perror("InitSnake():malloc()");return;}//設置坐標——從左向右創建cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//頭插法——一開始以右為蛇頭,鏈表從右向左,右邊新創建的是頭if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;//給新頭的next賦值——舊頭地址賦給新next,讓新頭指向舊身ps->pSnake = cur;//把新創建的作為蛇頭                                                                    //一次循環結束,cur和pSnake指向同一}}//打印蛇身(遍歷)cur = ps->pSnake;//繼續使用遍歷變量while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//初始化貪吃蛇的數據ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;//Windows中Sleep函數的單位是:ms(毫秒)ps->status = OK;
}

8.2.4 創建第一個食物

? 先隨機生成食物的坐標。

????????? x坐標必須是2的倍數。

????????? 食物的坐標不能和蛇身每個節點的坐標重復。

? 創建食物節點,打印食物。

食物打印的寬字符:

#define FOOD L'★'

創建食物的函數:CreateFood

//創建食物
void CreateFood(pSnake ps)
{int x = 0;int y = 0;again://x為奇數執行一次以上的循環//產生的x坐標應該是2的倍數,這樣才可能和蛇頭坐標對齊。do{x = rand() % 53 + 2;                                                                                 //1.隨機坐標 //y = rand() % 24 + 1;  y = rand() % 25 + 1;                                 } while (x % 2 != 0);//生成指定范圍內的隨機數:x(2-54且2的倍數),y(1-25)                             //2./在墻內//坐標和蛇的身體的每個節點的做坐標比較(遍歷)pSnakeNode cur = ps->pSnake;    //獲取指向蛇頭的指針//食物不能和蛇身沖突while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}                                                                                                        //3.不在蛇身上//創建食物——類型:蛇身結點類型pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//創建食物if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);}

8.3 游戲運行

游戲運行期間,右側打印幫助信息,提示玩家

根據游戲狀態檢查游戲是否繼續,如果是狀態是OK,游戲繼續,否則游戲結束。

如果游戲繼續,就是檢測按鍵情況,確定蛇下一步的方向,或者是否加速減速,是否暫停或者退出游戲。

確定了蛇的方向和速度,蛇就可以移動了。

//游戲運行
void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//當前的分數情況SetPos(62, 10);printf("總分:%5d\n", ps->Score);SetPos(62, 11);printf("食物的分值:%02d\n", ps->FoodWeight);//沒有02則由10變8會顯示由10變80 //檢測按鍵//上、下、左、右、ESC、空格、F3、F4if (KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上鍵并且當前方向不是朝下走{ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT){ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT){ps->dir = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戲要暫定pause();//暫定和回復暫定}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//蛇每次?定之間要休眠的時間,時間短,蛇移動速度就快//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);//正常狀態下是死循環,一直執行
}

8.3.1 KEY_PRESS

檢測按鍵狀態,我們封裝了一個宏

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

8.3.2 PrintHelpInfo

void PrintHelpInfo()
{//打印提?信息SetPos(64, 15);printf("不能穿墻,不能咬到??\n");SetPos(64, 16);printf("?↑.↓.←.→分別控制蛇的移動.");SetPos(64, 17);printf("F1 為加速,F2 為減速\n");SetPos(64, 18);printf("ESC :退出游戲.space:暫停游戲.");SetPos(64, 20);printf("?特就業課@版權");
}

8.3.3 蛇身移動SnakeMove(重難點

先創建下一個節點,根據移動方向和蛇頭的坐標,蛇移動到下一個位置的坐標。

確定了下一個位置后,看下一個位置是否是食物(NextIsFood),是食物就做吃食物處理 (EatFood),如果不是食物則做前進一步的處理(NoFood)。

蛇身移動后,判斷此次移動是否會造成撞墻(KillByWall)或者撞上自己蛇身(KillBySelf),從而影響游戲的狀態。

//蛇的移動
void SnakeMove(pSnake ps)
{//創建下?個節點pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SnakeMove()::malloc()");return;}pNext->next = NULL;//根據按鍵·反饋蛇的下一個結點的坐標//確定下?個節點的坐標,下?個節點的坐標根據,蛇頭的坐標和?向確定switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;//——縱減一break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;//——橫減二pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//如果下一個坐標處是食物if (NextIsFood(ps, pNext)){//是食物就吃掉EatFood(ps, pNext);}else{//不是食物就正常一步NotEatFood(ps, pNext);}//檢測撞墻KillByWall(ps);//檢測撞到自己KillBySelf(ps);
}
8.3.3.1 NextIsFood
//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
int NextIsFood(pSnakeNode psn, pSnake ps)
{return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
8.3.3.2 EatFood
//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
void EatFood(pSnakeNode psn, pSnake ps)
{//頭插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;//釋放?物節點free(ps->_pFood);//創建新的?物CreateFood(ps);
}

8.3.3.3 NoFood

將下一個節點頭插入蛇的身體,并將之前蛇身最后一個節點打印為空格,釋放掉蛇身的最后一個節點。

易錯點:這里最容易錯誤的是,釋放掉最后一個結點之后,還需要將指向在最后一個結點的指針改為NULL,保證蛇尾打印可以正常結束,不會越界訪問。

//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
//不是食物就正常進一步(1.頭插;2.打印新蛇;3.留白舊尾;4.釋放舊尾)
void NotEatFood(pSnake ps, pSnakeNode pNext)
{//頭插法pNext->next = ps->pSnake;ps->pSnake = pNext;//打印新蛇,留白蛇尾,釋放尾結點——先找到尾結點的前驅結點pSnakeNode cur = ps->pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//將尾節點的位置打印成空白字符——注意是兩個空格字符SetPos(cur->next->x, cur->next->y);printf("  ");//釋放結點free(cur->next);cur->next = NULL;//易錯
}

8.3.3.4 KillByWall

判斷蛇頭的坐標是否和墻的坐標沖突

//pSnake ps 維護蛇的指針
//檢測是否撞墻
void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}//int KillByWall(pSnake ps)
//{
//	if (ps->pSnake->x == 0 ||
//		ps->pSnake->x == 56 ||
//		ps->pSnake->y == 0 ||
//		ps->pSnake->y == 26)
//	{
//		ps->status = KILL_BY_WALL;
//      return 1;
//	}
//  return 0;
//}

8.3.3.5 KillBySelf

判斷蛇頭的坐標是否和蛇身體的坐標沖突

1 //pSnake ps 維護蛇的指針
//檢測是否撞自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->pSnake->next;//從第二個節點開始while (cur){if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}//int KillBySelf(pSnake ps)
//{
//	pSnakeNode cur = ps->pSnake->next;//從第二個節點開始
//	while (cur)
//	{
//		if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
//		{
//			ps->status = KILL_BY_SELF;
//			return 1;
//		}
//		cur = cur->next;
//	}
//  return 0;
//}

這兩個函數建議寫成返回值void的形式——足以實現預定的功能,通過修改貪吃蛇結構體的狀態參數。

寫成int返回值類型,沒必要接收返回值,而且和其他大部分的函數接口沒有保持統一性——其他大部分都是void的返回值類型。

保持接口的一致性有利于對這些函數的使用,不用去記哪些函數應該怎么用,都是一樣的用法。

8.4 游戲結束

游戲狀態不再是OK(游戲繼續)的時候,要告知游戲結束的原因,并且釋放蛇身節點。

//結束判定
void GameEnd(pSnake ps)	
{SetPos(15, 12);switch (ps->status){case ESC:printf("主動退出游戲,正常退出\n");break;case KILL_BY_WALL:printf("很遺憾,您撞墻了,游戲結束\n");break;case KILL_BY_SELF:printf("很遺憾,您咬到自己了,游戲結束\n");break;}//釋放貪吃蛇的鏈表資源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;//釋放蛇?的節點while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}

9. 參考代碼

完整代碼實現,分3個文件實現

test.c

test.c——貪吃蛇游戲的測試。

#include "Snake.h"
#include <locale.h>//大BUG:搞了一個小時沒搞懂
//誘因:輸入Y / y程序直接結束?
//因為只有兩個“按任意鍵繼續……”,按完兩下空格鍵,但是進入游戲界面還要按一下空格鍵蛇才能動(而且只有按空格鍵才行,不像之前兩次任意鍵)
//所以導致需要先清理游戲一開始的空格字符才能重開循環
//getchar();//清理緩沖區的換行符\n
//ch = getchar();
//getchar();//清理緩沖區的換行符\n
//發現兩個任意鍵中其中任何一個使用了空格鍵,在游戲一開始都需要多按一下空格鍵(唯一可行鍵)void test()
{//創建變量,接收用戶輸入的“進行/退出”選項int ch = 0;            //getchar()的返回值是int類型,但是用char ch來接收也行;但是getchar()接收失敗返回的EOF是int類型,故此最好使用int類型的變量來接收返回值//srand((unsigned int)time(NULL));加不加這一句,取決于用戶希望每次運行游戲是希望同樣的進程(食物出現的位置、順序都和上一次玩的時候一模一樣),還是不同的進程。do{//用貪吃蛇類型,創建貪吃蛇Snake snake = { 0 };GameStart(&snake);//游戲開始前的初始化——需要改變局部變量的值——傳址調用//getchar();為了測試前面代碼功能的時候,程序執行到這里能夠停下來GameRun(&snake);//玩游戲的過程(開始——進行——結束判定)GameEnd(&snake);//善后的工作(資源釋放……)SetPos(20, 15);printf("再來一局嗎?(Y/N):");ch = getchar();//返回值為int,讀取成功沒事,讀取失敗返回EOF(-1),所以最好將ch設置為int類型getchar();//清理緩沖區的換行符\n——如果沒有第二個getchar(),如果后續代碼再次調用 getchar(),它會直接讀到 \n,而不是等待用戶新輸入。} while (ch == 'Y' || ch == 'y');//SetPos(0, 27);
}int main()
{//修改當前地區為本地模式,為了支持中文寬字符的打印setlocale(LC_ALL, "");//測試邏輯test();//程序退出的提示語,從(0,27)開始打印出來(不加這句代碼,退出語的打印會破壞游戲界面)SetPos(0, 27);return 0;
}

snake.h

snake.h——貪吃蛇游戲中類型的聲明、函數的聲明。

#pragma once#define _CRT_SECURE_NO_WARNINGS 1#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'//蛇默認的起始坐標
#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//游戲狀態
enum GAME_STATUS
{OK = 1,                       //正常運行ESC,                          //退出KILL_BY_WALL,                 //撞墻死KILL_BY_SELF                  //撞蛇死
};//方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};//定義·蛇身結點類型
typedef struct SnakeNode
{int x;int y;                        //每個蛇結點在控制臺中都有一個地址(坐標),是連續的。struct SnakeNode* next;       //每個蛇結點在內存(堆)中都有一個地址,  不是連續的。
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;//貪吃蛇——維護整個游戲的狀態
typedef struct Snake
{pSnakeNode pSnake;    //維護整條蛇的指針(鏈表——全結點(利用頭結點維護整個鏈表))pSnakeNode pFood;     //指向食物的指針——和蛇身節點是同一個類型,因為它是未來的蛇身結點,而且均是依靠(x,y)坐標來維護int Score;            //當前累積的分數int FoodWeight;       //一個食物的分數int SleepTime;        //蛇休眠的時間,休眠的時間越短,蛇的速度越快,休眠的時間越長,蛇的速度越慢enum GAME_STATUS status;//游戲當前的狀態(進行 / 暫停 / 結束),暫停可以歸類為正常運行enum DIRECTION dir;   //蛇當前走的方向(當前向上就不能立刻向下,當前向左就不能立刻向右)//...
}Snake, * pSnake;        //壞處就是容易造成混亂——pSnake* phead到底是—級指針還是二級指針很混亂//定位控制臺的光標位置
void SetPos(int x, int y);//游戲開始前的準備
void GameStart(pSnake ps);//打印歡迎界面
void WelcomeToGame();//繪制地圖
void CreateMap();//初始化貪吃蛇
void InitSnake(pSnake ps);//創建食物
void CreateFood(pSnake ps);//游戲運行的整個邏輯
void GameRun(pSnake ps);//打印幫助信息
void PrintHelpInfo();//蛇移動的函數- 每次走一步
void SnakeMove(pSnake ps);//判斷蛇頭的下一步要走的位置處是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);//下一步要走的位置處就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);//下一步要走的位置處不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);//檢測是否撞墻
void KillByWall(pSnake ps);//檢測是否撞自己
void KillBySelf(pSnake ps);//游戲結束——資源釋放
void GameEnd(pSnake ps);

snake.c

snake.c——貪吃蛇游戲的相關函數的實現。

//getchar();為了測試前面代碼功能的時候,程序執行到這里能夠停下來

#include "snake.h"//定位控制臺的光標位置
void SetPos(int x, int y)
{//獲得設備句柄HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);//根據句柄設置光標的位置COORD pos = { x, y };SetConsoleCursorPosition(hanlde, pos);
}//打印歡迎信息
void WelcomeToGame()
{//歡迎信息SetPos(38, 10);printf("歡迎來到貪吃蛇小游戲\n");SetPos(40, 20);    //讓按任意鍵繼續的出現的位置好看點system("pause");    system("cls");//功能介紹信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 來控制蛇的移動,F3是加速,F4是減速");SetPos(15, 11);printf("加速能得到更高的分數");SetPos(38, 20);    //讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");
}//打印地圖
void CreateMap()
{//定位一次,打印一排坐標int i = 0;//上排墻壁:(0,0)-(56,0)SetPos(0, 0);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下排墻壁:(0,26)-(56,26)SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//定位一次,打印一個坐標//左排墻壁:x是0,y從1開始增長for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右排墻壁:x是56,y從1開始增長for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//初始化蛇
void InitSnake(pSnake ps)
{//創建5個蛇身的結點,并初始化坐標//頭插法pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 5; i++){//創建蛇身結點cur = (pSnakeNode)malloc(sizeof(SnakeNode));        //一次循環開始,cur先賦值指向下一塊空間if (cur == NULL){perror("InitSnake():malloc()");return;}//設置每個結點的坐標——從左向右創建cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//頭插法——一開始以右為蛇頭,鏈表從右向左,右邊新創建的是頭if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;//給新頭的next賦值——舊頭地址賦給新next,讓新頭指向舊身ps->pSnake = cur;//把新創建的作為蛇頭          //一次循環結束,cur和pSnake指向同一}}//打印蛇身(遍歷)cur = ps->pSnake;//繼續使用遍歷變量while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//貪吃蛇的其他信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;//Windows中Sleep函數的單位是:ms(毫秒)ps->status = OK;//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//創建食物
void CreateFood(pSnake ps)
{int x = 0;int y = 0;again://產生的x坐標應該是2的倍數,這樣才可能和蛇頭坐標對齊。//x為奇數執行一次以上的循環do{//1.隨機坐標//2.在墻內——生成指定范圍內的隨機數x = rand() % 53 + 2;                  //x(2-54且2的倍數) ,則先生成0-52的隨機數          //y = rand() % 24 + 1;                //y(1-25),則先生成0-24的隨機數  y = rand() % 25 + 1;                 } while (x % 2 != 0);         //坐標和蛇的身體的每個節點的做坐標比較(遍歷)pSnakeNode cur = ps->pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}                                             //3.不在蛇身上//創建食物,直接復用“蛇身結點類型”pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;    //把食物的地址,給到貪吃蛇結構體內部對應的指針,進行維護SetPos(x, y);wprintf(L"%lc", FOOD);//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//游戲初始化
void GameStart(pSnake ps)
{//設置控制臺的信息,窗口大小,窗口名system("mode con cols=100 lines=31");    //貪吃蛇游戲框58列、27行system("title 貪吃蛇");//獲取標準輸出的句柄(用來標識不同設備的數值)HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//隱藏光標CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo);    //獲取控制臺光標信息CursorInfo.bVisible = false;                  //隱藏控制臺光標SetConsoleCursorInfo(handle, &CursorInfo);    //設置控制臺光標狀態//打印歡迎界面WelcomeToGame();//繪制地圖CreateMap();//初始化蛇——需要對參數進行操作,所以:1.需要參數;2.傳址調用InitSnake(ps);//創建食物——需要對參數進行操作,所以:1.需要參數;2.傳址調用CreateFood(ps);
}//打印幫助信息
void PrintHelpInfo()
{SetPos(62, 15);printf("1.不能穿墻,不能咬到自己");SetPos(62, 16);printf("2.用 ↑.↓.←.→ 來控制蛇的移動");SetPos(62, 17);printf("3.F3是加速,F4是減速");SetPos(64, 18);printf("4.ESC :退出游戲.space:暫停游戲.");SetPos(62, 20);printf("版權@比特就業課");
}//暫停狀態
void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}//判定行進方向的下一個坐標是否是食物
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
int NextIsFood(pSnake ps, pSnakeNode pNext)
{if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)return 1;//下一個坐標處是食物elsereturn 0;
}//是食物就吃掉        (1.頭插;2.打印新蛇;3.釋放食物;4.創建食物)
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
void EatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;//新頭的next指向舊頭ps->pSnake = pNext;//新頭賦值給舊頭//打印蛇(遍歷)pSnakeNode cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Score += ps->FoodWeight;//釋放舊的食物——因為“移動下一步”這個函數中創建了“下一格結點”——其坐標恰好與食物結點坐標重合,將其頭插入貪吃蛇鏈表//而在食物創建函數中也創建了一個結點,該結點需要被釋放free(ps->pFood);//新建食物CreateFood(ps);
}//不是食物就正常進一步——頭插+尾刪
//1.頭插;2.打印新蛇;3.留白舊尾;4.釋放舊尾
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
void NotEatFood(pSnake ps, pSnakeNode pNext)
{//頭插法//1.改變新結點的指針域指向//2.改變維護鏈表的指針的指向pNext->next = ps->pSnake;ps->pSnake = pNext;//打印新蛇,留白蛇尾+釋放尾結點——先找到尾結點的前驅結點pSnakeNode cur = ps->pSnake;while (cur->next->next)        //確實是沒有打印最后一個結點——即上一步的倒數第2個結點,相當于直接復用了上一步的打印結果{SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//將尾節點的位置打印成空白字符——注意是兩個空格字符——不覆蓋打印,會直接復用上一步的打印結果SetPos(cur->next->x, cur->next->y);printf("  ");free(cur->next);         //注意找到前驅之后,先將該結點坐標打印空白字符,再釋放;否則先釋放就找不到那個結點坐標了 cur->next = NULL;        //這一句容易漏寫,易錯
}//檢測是否撞墻
void KillByWall(pSnake ps)
{//走完之后,蛇頭和墻的坐標重合if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}//檢測是否撞自己
//pSnake ps 維護蛇的指針
void KillBySelf(pSnake ps)
{//從第二個節點開始——由于不接收上立刻下、左立刻右的指令,理論上從第3個結點開始也可以pSnakeNode cur = ps->pSnake->next;while (cur){//走完之后,蛇身的某一點和蛇頭重合if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}//蛇的移動
//pSnake ps 維護蛇的指針
void SnakeMove(pSnake ps)
{//創建下一結點pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SnakeMove()::malloc()");return;}pNext->next = NULL;//確定下?個節點的坐標,下?個節點的坐標根據——蛇頭的坐標和方向確定switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;//——縱減一break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;//——橫減二pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//移動到的下一個結點有4種情況:1.啥也沒有;2.食物;3.墻壁;4.蛇身//下一個坐標處是否是食物if (NextIsFood(ps, pNext)){//是食物就吃掉EatFood(ps, pNext);}else{//不是食物就正常一步NotEatFood(ps, pNext);}//走完之后再檢測//檢測撞墻——只需要改變狀態,不需要返回值KillByWall(ps);//檢測撞到自己——只需要改變狀態,不需要返回值KillBySelf(ps);//走一步函數結束,休眠一下,立刻進行下一次循環的條件判定——貪吃蛇的現態
}//游戲運行
//基本邏輯:1.睡眠一下;2.走一步 或者 1.走一步;2.睡眠一下
//過程中還要:打印幫助信息、檢測按鍵
void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//打印當前的分數情況SetPos(62, 10);printf("總分:%5d\n", ps->Score);SetPos(62, 11);printf("食物的分值:%02d\n", ps->FoodWeight);//沒有02則由10變8會顯示由10變80 //檢測按鍵//上、下、左、右、ESC、空格、F3、F4if (KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上鍵并且當前方向不是朝下走{ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT){ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT){ps->dir = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戲要暫定pause();//暫定和回復暫定}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);//正常狀態下是死循環,一直執行
}//結束判定
void GameEnd(pSnake ps)	
{SetPos(15, 12);switch (ps->status){case ESC:printf("主動退出游戲,正常退出\n");break;case KILL_BY_WALL:printf("很遺憾,撞墻了,游戲結束\n");break;case KILL_BY_SELF:printf("很遺憾,咬到自己了,游戲結束\n");break;}//釋放貪吃蛇的鏈表資源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;//釋放本結點之前,先保存下一結點;否則找不到cur = cur->next;free(del);}free(ps->pFood);ps = NULL;    //就不用再ps->pSnake=NULL、ps->pFood=NULL
}

參考:漢字字符集編碼查詢;中文字符集編碼:GB2312、BIG5、GBK、GB18030、Unicode

10.?擴展


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91971.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91971.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91971.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

知識蒸餾 - 最小化KL散度與最小化交叉熵是完全等價的

知識蒸餾 - 最小化KL散度與最小化交叉熵是完全等價的 flyfish KL散度與交叉熵的數學關系 對于兩個概率分布 PPP&#xff08;真實分布&#xff09;和 QQQ&#xff08;模型預測分布&#xff09;&#xff0c;KL散度的定義是&#xff1a; DKL(P∥Q)∑xP(x)log?(P(x)Q(x)) D_{KL}(P…

設計心得——網絡包的處理

一、介紹 在程序的開發中&#xff0c;網絡開發是一個重要的應用場景。畢竟這些年IT行業之所以火&#xff0c;主要還是互聯網&#xff08;移動互聯網&#xff09;帶來的。網絡開發&#xff0c;有各種平臺、框架以及系統和庫提供的API&#xff0c;如果說網絡開發是一個特別復雜和…

sqli-labs通關筆記-第30關GET字符注入(WAF繞過 雙引號閉合 手工注入+腳本注入兩種方法)

目錄 一、源碼分析 1、index.php代碼審計 2、login.php代碼審計 3、java_implimentation函數 4、whitelist函數 5、SQL安全性分析 二、滲透實戰 1、進入靶場 2、WAF探測 &#xff08;1&#xff09;觸發WAF &#xff08;2&#xff09;繞過WAF 3、手工注入 &#xf…

【openlayers框架學習】九:openlayers中的交互類(select和draw)

文章目錄openlayers進階28 openlayers中的事件29 openlayers中select交互類的使用30 openlayers中select常見的配置選項31 openlayers中繪制交互類&#xff08;Draw&#xff09;openlayers進階 28 openlayers中的事件 常用進行事件交互的對象&#xff1a;map\view\source29 o…

Java企業級應用性能優化實戰

在企業級Java應用開發中,性能優化是確保系統穩定運行的關鍵因素。本文將從多個維度深入分析Java應用性能瓶頸,并提供實戰優化方案。 ?? 性能優化核心領域 1. 對象操作性能優化 在企業應用中,對象拷貝是一個高頻操作,特別是在分層架構中的DO、DTO、VO轉換。選擇合適的拷…

LLM Prompt與開源模型資源(3)如何寫一個好的 Prompt

學習材料&#xff1a;https://www.hiascend.com/developer/courses/detail/1935520434893606913 &#xff08;3.5&#xff09;學習時長&#xff1a; 預計 60 分鐘學習目的&#xff1a; 了解提示工程的定義與作用熟悉提示工程的關鍵技術相關概念掌握基于昇騰適配的大模型提示工程…

日志管理工具 ——ELK Stack

一、ELK Stack 概述1.1 核心組件ELK Stack&#xff08;現更名為 Elastic Stack&#xff09;是一套開源的日志收集、存儲、分析和可視化平臺&#xff0c;由三個核心組件構成&#xff1a;Elasticsearch&#xff1a;分布式搜索引擎&#xff0c;負責日志數據的存儲、索引和快速查詢…

SpringAI:AI工程應用框架新選擇

Spring AI 是一個用于 AI 工程的應用框架 Spring AI 是一個用于 AI 工程的應用框架。其目標是將可移植性和模塊化設計等 Spring 生態系統設計原則應用于 AI 領域,并推廣使用 POJO 作為應用程序的構建塊到 AI 領域。 Spring AI 的核心是解決 AI 集成的基本挑戰:將企業數據和…

Kettle 開源ETL數據遷移工具從入門到實戰

ETL&#xff08;Extract, Transform, Load&#xff09;工具是用于數據抽取、轉換和加載的軟件工具&#xff0c;用于支持數據倉庫和數據集成過程。Kettle作為傳統的ETL工具是純 java 開發的開源的 ETL工具&#xff0c;用于數據庫間的數據遷移 。可以在 Linux、windows、unix 中運…

Maven中的bom和父依賴

maven最全避坑指南寫完后&#xff0c;發現自己對于bom和父pom的理解還是不夠深入&#xff0c;特此轉載DeepSeek的回答&#xff0c;和大家一起學習了。 在 Maven 的依賴管理中&#xff0c;父 POM (Parent POM) 和 BOM (Bill of Materials) 都是用于實現集中化管理和控制的核心機…

Python 操作 Word 文檔:主流庫對比與選擇指南

在辦公自動化、報告生成、數據處理等領域&#xff0c;利用 Python 程序化地創建、讀取或修改 Microsoft Word 文檔 (.docx 格式) 是一項非常實用的技能。Python 生態中有多個優秀的庫可以完成這項任務&#xff0c;但它們各有側重和優缺點。選擇哪一個“最好用”&#xff0c;關鍵…

怎么修改論文格式呢?提供一份論文格式模板

注!!!本文內容是作者自己整理的一份模板,僅供參考,各位如何修改,還需要看學校的要求。 一、參考文獻 1、有一定數量的近幾年參考文獻、不宜過多中文文獻 英文期刊模板 [1] Taesoo K, Sooyoung K, Kyunghan L, et al. Special issue on 6G and satellite communication…

MVC 發布

MVC 發布 引言 MVC(Model-View-Controller)模式是一種廣泛應用于軟件開發的架構模式。它將應用程序分為三個主要部分:模型(Model)、視圖(View)和控制器(Controller)。這種模式不僅提高了代碼的可維護性和可擴展性,而且使得開發者可以更加專注于各個組件的開發。本文…

arkui 動畫曲線

參考文檔 https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-curve#curvesinterpolatingspring10 可視化工具網站 https://easingwizard.com/ https://www.desmos.com/calculator/k01p40v0ct?langzh-CN 基本介紹 import { curves } from kit.A…

大語言模型(LLM)技術架構與工程實踐:從原理到部署

在自然語言處理領域,大語言模型(LLM)已成為顛覆性技術。從 GPT 系列到 LLaMA、ChatGLM,這些參數規模動輒百億甚至萬億的模型,不僅實現了流暢的自然語言交互,更在代碼生成、邏輯推理等復雜任務中展現出驚人能力。本文將從技術底層拆解 LLM 的核心架構,分析訓練與推理的關…

python后端之DRF框架(上篇)

一、DRF框架介紹 1、web應用開發模式 1.1、前后端不分離1.2、前后端分離2、RESTful介紹 RESTful是目前最流行的API設計風格 &#xff0c; REST 指的是一組架構約束條件和原則。滿足這些約束條件和原則的應用程序或設計就是 RESTful。 1、每一個URI代表1種資源&#xff1b; 2、客…

信創數據庫-DM(達夢)數據庫安裝教程

官方安裝文檔在這&#xff1a;安裝前準備 | 達夢技術文檔 本文也是基于這個來寫的&#xff0c;微調了一下。 1&#xff0c;下載安裝包 體驗版直接到官方下載即可&#xff1a;產品下載 | 達夢在線服務平臺 如果是有需要商業版等&#xff0c;需要聯系客服申請。 安裝包要選擇CPU…

docker常用命令集(6)

接前一篇文章&#xff1a;docker常用命令集&#xff08;5&#xff09; 本文內容參考&#xff1a; Docker login/logout 命令 | 菜鳥教程 Docker命令_docker login-CSDN博客 特此致謝&#xff01; 9. docker login 簡介 docker login命令用于登錄到docker注冊表&#xff08…

[LINUX操作系統]shell腳本之循環

1.編寫腳本for1.sh,使用for循環創建20賬戶&#xff0c;賬戶名前綴由用戶從鍵盤輸入&#xff0c;賬戶初始密碼由用戶輸入&#xff0c;例如:test1、test2、test3......[rootmaster ~]# vim for1.sh #!/bin/bashread -p "請輸入賬戶名稱前綴&#xff1a;" prefixread -p…

空間設計:不是餐廳的裝飾游戲

餐廳空間設計&#xff0c;是通過布局規劃與環境營造&#xff0c;將功能需求、品牌調性與顧客體驗融合的系統性工程 —— 它不僅決定顧客「坐得舒不舒服」&#xff0c;更影響「愿不愿意再來」「會不會主動分享」的消費決策。體驗感知的第一觸點&#xff1a;顧客進門 3 秒內&…