一、项目介绍
这是我曾经在某个科普书上看到过的内容。在那本书当中有一个关于世界本源的讨论,即我们目前发现的物理定律,是否可能是更基础的模块的宏观呈现?因此,那本书提到了这个游戏,即设定一个简单的世界法则,以及对应的初始条件,然后观察它的演化,看看是否能呈现出某种更复杂的东西。
这个世界的法则十分简单。所有物质分为活细胞和死细胞,分别表示为绿色以及灰黑色。如果上一刻,一个活细胞周围8格的活细胞多余3个或少于2个,那么它就会死去;如果一个死细胞周围恰好有3个活细胞,那么就会在此诞生一个新的活细胞。设定了这样的法则之后,我们就可以观察这个世界的演化了。
编译环境:visual c++ 6.0
第三方库:Easyx2022 注意需要提前安装easyX,如没有基础可以先了解easyX图形编程
二、运行截图
三、源码解析
程序的整体逻辑十分简单。
初始化;
然后开始循环;
如果按下1-9,调节游戏速度;
如果按下s,产生一个方形分布世界,重新开始;
如果按下r,产生一个随机分布世界,重新开始;
进行一回合的演化,绘制图像;
睡眠一定时间,继续循环;
游戏结束。
变量和函数也不多:
// 定义全局变量 __int8 world[102][102] = {0}; // 定义二维世界 IMAGE imgLive, imgEmpty; // 定义活细胞和无细胞区域的图案 // 函数声明 void Init(); // 初始化 void SquareWorld(); // 创建一个细胞以方形分布的世界 void RandWorld(); // 创建一个细胞随机分布的世界 void PaintWorld(); // 绘制世界 void Evolution(); // 进化
接下来我们看每个函数的实行:
// 初始化 void Init() { // 创建绘图窗口 initgraph(640,480); // 设置随机种子 srand((unsigned)time(NULL)); // 调整世界图案的大小 Resize(&imgLive, 4, 4); Resize(&imgEmpty, 4, 4); // 绘制有生命世界的图案 SetWorkingImage(&imgLive); setcolor(GREEN); setfillstyle(GREEN); fillellipse(0, 0, 3, 3); // 绘制无生命世界的图案 SetWorkingImage(&imgEmpty); setcolor(DARKGRAY); rectangle(1, 1, 2, 2); // 恢复对默认窗口的绘图 SetWorkingImage(NULL); // 输出简单说明 setfont(24, 0, "黑体"); outtextxy(254, 18, "生 命 游 戏"); RECT r = {440, 60, 620, 460}; setfont(12, 0, "宋体"); drawtext("生命游戏简介:\n, &r, DT_WORDBREAK); // 产生默认的细胞以方形分布的世界 SquareWorld(); }
initgraph用于初始化绘图窗口。在头文件Easyx当中,后面的函数如果没有说明,默认也是在Easyx当中的。
Resize用于调整指定绘图设备的尺寸,避免绘制图像过大。
然后绘制有生命的图案和无生命的图案,其原理类似:
SetWorkingImage用于设定当前的绘图设备,将数据写入对应地址位置。
setcolor用于设置绘图前景色,setfillstyle用于设置当前设备填充样式。
fillellipse这个函数用于画有边框的填充椭圆。
接下来输出说明:
setfont设置字体
outtextxy用于在特定位置输出文字
RECT r用于为drawtext设定一个指定矩形区域的指针
drawtext这个函数用于在指定区域内以指定格式输出字符串。
然后是两个创建世界的函数。
// 创建一个细胞以方形分布的世界 void SquareWorld() { memset(world, 0, 102 * 102 * sizeof(__int8)); for(int x = 1; x <= 100; x++) world[x][1] = world[x][100] = 1; for(int y = 1; y <= 100; y++) world[1][y] = world[100][y] = 1; } // 创建一个细胞随机分布的世界 void RandWorld() { for(int x = 1; x <= 100; x++) for(int y = 1; y <= 100; y++) world[x][y] = rand() % 2; }
利用二层循环遍历world[x][y]当中每个数据,将其调整到想要的值。随机世界将每个细胞的状态设置为随机数除以2的余数,保证每次产生的都是新的世界。
然后是绘制世界图像。
// 绘制世界 void PaintWorld() { for(int x = 1; x <= 100; x++) for(int y = 1; y <= 100; y++) putimage(16 + x * 4, 56 + y * 4, world[x][y] ? &imgLive : &imgEmpty); }
利用双层循环,将每个点的状态所对应图像打印出来。
最后是世界的演化函数,从世界的当前状态推出世界下一刻。
// 进化 void Evolution() { __int8 tmp[102][102] = {0}; // 临时数组 int sum; for(int x = 1; x <= 100; x++) { for(int y = 1; y <= 100; y++) { // 计算周围活着的生命数量 sum = world[x+1][y] + world[x+1][y-1] + world[x][y-1] + world[x-1][y-1] + world[x-1][y] + world[x-1][y+1] + world[x][y+1] + world[x+1][y+1]; // 计算当前位置的生命状态 switch(sum) { case 3: tmp[x][y] = 1; break; case 2: tmp[x][y] = world[x][y]; break; default: tmp[x][y] = 0; break; } } } // 将临时数组恢复为世界 memcpy(world, tmp, 102 * 102 * sizeof(__int8)); }
我们只需要定义一个临时数组,根据当前的世界状态推断就可以了。计算某个坐标周围8格活着的生命数量,得出该点的状态。注意数组的边界。
之后用memcpy将临时数据输入world变量当中。
memcpy是<string.h>当中的内存拷贝函数,可用于复制任何种类的数据。
// 主函数 int main() { Init(); int Speed = 500; // 游戏速度(毫秒) while(true) { if (kbhit() || Speed == 900) { char c = getch(); if (c == ' ' && Speed != 900) c = getch(); if (c >= '0' && c <= '9') Speed = ('9' - c) * 100; switch(c) { case 's': case 'S': SquareWorld(); // 产生默认的细胞以方形分布的世界 break; case 'r': case 'R': RandWorld(); // 产生默认的细胞随机分布的世界 break; case VK_ESCAPE: goto END; } } Evolution(); // 进化 PaintWorld(); // 绘制世界 if (Speed != 900) // 速度为 900 时,为按任意键单步执行 Sleep(Speed); } END: closegraph(); return 0; }
最后放一个主函数。
函数 kbhit()函数的作用是检查控制台窗口的按键是否被按下。
closegraph()用于关闭绘图区域。
四、完整源码
本文固定URL:https://www.dotcpp.com/course/1231
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程