C语言实现贪吃蛇(洗牌算法 && 循环数组 && 二维坐标与一维坐标的转化)

目录

一:实现环境

二:程序优点

1、洗牌算法:

2、循环数组:

3、二维坐标与一维坐标的转化:

三:准备过程(开始之前务必熟悉)

四:结构体的定义

五:主要函数的实现

1、移动蛇的函数:

2、随机产生食物函数:

3、边界,障碍物,食物,头撞到身体的判断

六:运行截图

七:总结

八:源码地址



一:实现环境

Sublime Text 3,TC 2.0


二:程序优点

1、洗牌算法:

之前在网上看过很多贪吃蛇的博客,代码。产生随机食物的方法大多都是利用随机函数产生,然后用循环判断食物是否落到边界,蛇身等,如果是,则再调用随机函数产生食物。说实话这样的方法逻辑是很简单,但是,贪吃蛇是一个实时性要求较高的小游戏,如果随机产生的食物一直落到边界,那就一直调用随机函数吗?而且这样做也增加了时间复杂度。看一段代码(网上找的)。

unsigned char generate_food(void)
{
    unsigned char food_,fx,fy;
    int in_snake=0,i;
    srand((unsigned int)time(NULL));
    do {
        food_ = rand() % 255;
        tran(food_, &fx, &fy);
        for (i = 0; i < len; i++)
            if (food_ == snake[i])
                in_snake = 1;
    } while (fx == 0 || fx == 16 || fy == 0 || fy == 16 || in_snake);
    return food_;
}

一个do-while循环,大大提高了时间复杂度。所以,我的方法是利用洗牌算法完成。利用洗牌算法, 产生的随机食物一定不会出现在边界和蛇身上。

2、循环数组:

之前考虑过用链表实现,但是,使用链表的方式效率不是很好。因为,贪吃蛇的移动需要找蛇尾,蛇头,使用链表的话,就需要每次从链表的表头查找,移动一下,就找一次,不觉得很麻烦吗?但是,利用循环数组,就解决了这样的问题,利用下标运算,可以任意定位到蛇身,蛇尾,蛇头的每一个点,提高了时间复杂度。 

3、二维坐标与一维坐标的转化:

二维数组的本质就是一维数组。一维数组操作简单,当然首选的就是一维数组。由于屏幕的坐标是二维的,所以,两者之间的转化是一个很重要的过程。并且,定义一个和屏幕一样大的数组,把蛇的身体,障碍物,食物,边界的坐标都转化为一维数组的下标存进去,写一个专门的check函数,在函数内部进行各种判断,程序的逻辑也就变得简单明了。 

定义一个大的数组,采用的是空间换时间思想。对于一个算法来说,空间复杂度和时间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。有时我们可以用空间来换取时间以达到目的。


三:准备过程(开始之前务必熟悉)

  1. void gotoxy(int x, int y)函数:实现光标的移动。注意,TC中第一个参数为列坐标,第二个参数为行坐标。

  2. int bioskey (int cmd)函数:bioskey()完成直接键盘操作,cmd的值决定执行什么操作。 
    cmd = 0: 
    当cmd是0,bioskey()返回下一个在键盘键入的值(它将等待到按下一个键)。它返回一个16位的二进制数,包括两个不同的值。当按下一个普通键时,它的低8位数存放该字符的ASCII码;对于特殊键(如方向键、F1~F12等等),低8位为0,高8位字节存放该键的扫描码。 
    cmd = 1: 
    当cmd是1,bioskey()查询是否按下一个键,若按下一个键则返回非零值,否则返回0。 
    cmd = 2: 
    当cmd是2,bioskey()返回Shift、Ctrl、Alt、ScrollLock、NumLock、CapsLock、Insert键的状态。各键状态存放在返回值的低8位字节中。
  3. 循环数组:后一个下标 = (前一个下标 + 1) % 最大长度。
  4. 洗牌算法:可自行实现一段代码来体会。例:有数组array[10] = {2,5,8,7,1,10,34,23,16,11},每次从其中随机取一个数,连续取十次,实现每次取出的数都不相同。(思路: 把第一次从前10个数中取出的数和最后一个交换,把第二次从前9个数中取出的数和倒数第二个数交换······)。
  5.  一维坐标与二维坐标之间的转化。

四:结构体的定义

typedef struct DELTA_MOVE {
	int deltaCol;
	int deltaRow;
} DELTA_MOVE;
const DELTA_MOVE deltaMove[4] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};

 存储移动方向上,下,左,右的增量。


typedef struct SNAKE_BODY {
	int bodyCol;
	int bodyRow;
} SNAKE_BODY;

存储蛇身体的坐标。


typedef struct FOOD_POINT {
	int foodCol;
	int foodRow;
} FOOD_POINT;

存储食物的坐标。


typedef struct SNAKE {
	int head;
	int len;
	int curLen;
	int maxLen;
	int direct;
	SNAKE_BODY snakeBody[MAX_SANKE_LEN];
} SNAKE;

存储蛇的各项信息。curLen,len两个属性为以后蛇的移动,增长提供了非常大的便利。


五:主要函数的实现

1、移动蛇的函数:

/*移动蛇*/
/*
使用循环数组来存储蛇身体每个点的行,列坐标
初始显示一个蛇头,慢慢增多,知道等于设定的蛇的长度
蛇移动的过程:
	1、先在蛇头的位置输出一个蛇的身体
	2、再在之前蛇头的下一个位置输出一个蛇头
	3、消去尾部
*/
void moveSnake(SNAKE *snake, int directionIndex) {
	int oldSnakeHeadPoint;
	int removeOphiurid;
	SNAKE_BODY *snakeBody = snake->snakeBody;

	if(snake->curLen > 1) {
		gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
		putchar('*');
	}

	oldSnakeHeadPoint = snake->head;
	snake->head = (snake->head + 1) % snake->maxLen;
	changeSnakeHeadPoint(&snakeBody[snake->head], snakeBody[oldSnakeHeadPoint], directionIndex);
	gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
	putchar(showSnakeHead[directionIndex]);

	if(snake->curLen >= snake->len) {
		removeOphiurid = (snake->head - snake->len + snake->maxLen) % snake->maxLen;
		gotoxy(snakeBody[removeOphiurid].bodyCol, snakeBody[removeOphiurid].bodyRow);
		putchar(32);
		/*把上一次蛇尾在barrierArray中的数据清零*/
		barrierArray[(snakeBody[removeOphiurid].bodyRow - 1) * 65 + snakeBody[removeOphiurid].bodyCol - 1] = 0;	
	} else {
		(snake->curLen)++;
	}

	gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
}

主要思路:当蛇的当前长度curLen等于1时,只输出一个蛇头。

当蛇的当前长度curLen大于1,小于蛇的长度len时,先在蛇头的位置输出一个蛇身,再找到下一个蛇头的位置输出蛇头,直到等于蛇的长度len(刚开始运行阶段)。

当蛇的当前长度curLen大于等于蛇的长度len时,先在蛇头的位置输出一个蛇身,再找到下一个蛇头的位置输出蛇头,最后找到蛇尾,输出空格即可(这就是移动)。

实现蛇的移动,循环数组是关键。利用循环数组, 可以实现蛇身上任意一个点的快速定位。

C语言实现贪吃蛇(洗牌算法 && 循环数组 && 二维坐标与一维坐标的转化)_第1张图片

蛇的最大长度就为循环数组的长度,循环数组里存着每个蛇身的坐标。


2、随机产生食物函数:

/*产生随机食物过程:
	1、定义一个能存储全屏幕坐标的一维数组,大小为80*25,初始化为0-1999
	2、根据蛇的当前长度,利用蛇头找到蛇的每个身体的坐标,把蛇身体坐标转换为一维数组的下标,此下标元素赋值为-1
	3、定义一个头指针,一个尾指针,分别从下标为0和1999相向遍历,若head对应的元素为-1,停下。若tail对应的元素不为-1,则停下
	4、交换head和tail对应的元素,就这样一直找,知道把所有的-1移到数组的最后
	5、在数组前面不为-1的元素里面,利用洗牌算法,取出一个随机数作为下标,把对应的元素再转换为坐标输出到屏幕上,即产生的食物*/
/*显示的区域:63列,23行*/
void makeFood(SNAKE snake, int foodCount, FOOD_POINT *foodPoint) {
	short screen[1449];
	int head = 0;
	int tail = 1448;
	int i;
	int y;
	int x;
	int index;
	int randIndex;
	int count = 1449;
	int foodCol;
	int foodRow;

	for(i = 0; i < 1449; i++) {
		screen[i] = i;
	}
	for(i = 0; i < snake.curLen; i++) {
		index = (snake.head - i + snake.maxLen) % snake.maxLen;
		y = snake.snakeBody[index].bodyCol;
		x = snake.snakeBody[index].bodyRow;
		screen[(x - 2) * 63 + y - 2] = -1;
		count--;
	}

	while(head <= tail) {
		while(screen[head] != -1) {
			head++;
		}
		while(screen[tail] == -1) {
			tail--;
		}
		if(head <= tail) {
			swap(&screen[head], &screen[tail]);
		}
	}

	srand(time(0));
	for(i = 0; i < foodCount; i++) {
		randIndex = rand() % ((count--) + 1);
		foodPoint[i].foodCol = screen[randIndex] % 63 + 2;
		foodPoint[i].foodRow = screen[randIndex] / 63 + 2;
		gotoxy(foodPoint[i].foodCol, foodPoint[i].foodRow);
		putchar('O');
		/*食物的二维坐标转换为数组的一维下标存起来,为4*/
		barrierArray[(foodPoint[i].foodRow - 1) * 65 + foodPoint[i].foodCol - 1] = 4;
		swap(&screen[randIndex], &screen[count]);
	}
}

主要思路:洗牌算法。

准备过程:这多次用到一维数组与二维数组下标转换的关系。

1、例显示区域为80*25,则先准备一个含有2000个元素的数组,赋值为0-1999。

2、把蛇的当前的蛇身坐标转换为一维数组的下标,并更改对应数组元素为-1.

3、定义一个头指针,一个尾指针,分别从下标为0和1999相向遍历,若head对应的元素为-1,停下。若tail对应的元素不为-1,则停下。交换head和tail对应的元素,就这样一直找,知道把所有的-1移到数组的最后。

4、准备工作已经做好,开始洗牌算法。在数组前面不为-1的元素里面,利用洗牌算法,取出一个随机数作为下标,把对应的元素再转换为坐标输出到屏幕上,即产生的食物。


3、边界,障碍物,食物,头撞到身体的判断

void checkBarrier(SNAKE *snake, int *finished, int *foodCount, FOOD_POINT *foodPoint) {
	int col = snake->snakeBody[snake->head].bodyCol;
	int row = snake->snakeBody[snake->head].bodyRow;
	int snakeHeadElement = barrierArray[(row - 1) * 65 + col - 1];
	int i;

	if(snakeHeadElement == 1 || snakeHeadElement == 2 || snakeHeadElement == 3) {
		*finished = 1;
	}

	if(snakeHeadElement == 4) {
		(snake->len)++;
		--(*foodCount);
	}

	for(i = 0; i < *foodCount; i++) {
		if((foodPoint[i].foodRow - 1) * 65 + foodPoint[i].foodCol - 1 == 3) {
			makeFood(*snake, *foodCount, foodPoint);
		}
	}
}

主要思路:把边界,障碍物,蛇身,食物的二维坐标转换为数组的一维下标存起来,分别对对应的元素赋值为1, 2, 3, 4。把蛇头的坐标转换为数组下标,比较对应的元素是不是1,2,3,若是,则游戏结束;若为4,则表明吃到食物, 蛇变长。若食物落到障碍物上,则重新随机产生食物。


六:运行截图

C语言实现贪吃蛇(洗牌算法 && 循环数组 && 二维坐标与一维坐标的转化)_第2张图片

C语言实现贪吃蛇(洗牌算法 && 循环数组 && 二维坐标与一维坐标的转化)_第3张图片


七:总结

这个贪吃蛇在TC上可完美运行,控制上也较简单。 贪吃蛇是一个即时性要求高的程序,所以这里采用了空间换时间的方法(定义了两个大数组),例如洗牌算法那里时间复杂度最终为O(n)。精妙之处在于循环数组的运用和洗牌算法。

不足之处是每次随机出现食物只能出现一个固定的值,我觉得每次给食物的数量加一是个不错的办法,即随机产生食物坐标的时候多产生一个即可,设置一个食物增加的增量。

发挥想象,还可以再增加一个蛇, 加入wasd按键控制,实现双人对战。即再定义一个蛇的结构体变量,函数调用多传一个参数等等,把细节考虑到位即可。

这个贪吃蛇程序仅仅为数据结构练手的项目,通过这个练习,明显的感觉到在编程时,对函数的形参与实参的调用,指针的运用等等有了更加深刻的认识。

程序=数据结构+算法。选取一个好的算法在特定的地方使用,会使自己的程序在效率上大大提升。

在编程时,一定一定要组织好代码的大体框架,逻辑一定要清晰。否则,当程序慢慢变得复杂起来,出现问题时,逻辑不清晰,调试就很难很难,这时候不妨删掉以前的代码,重新组织编写。 


八:源码地址

单人版:

https://github.com/yangchaoy259189888/Gluttonous-Snake/

双人版(做了一点点优化):

https://github.com/yangchaoy259189888/Gluttonous-Snake-Double-against/

你可能感兴趣的:(项目练习)