一、项目介绍
这是我曾经在某个科普书上看到过的内容。在那本书当中有一个关于世界本源的讨论,即我们目前发现的物理定律,是否可能是更基础的模块的宏观呈现?因此,那本书提到了这个游戏,即设定一个简单的世界法则,以及对应的初始条件,然后观察它的演化,看看是否能呈现出某种更复杂的东西。
这个世界的法则十分简单。所有物质分为活细胞和死细胞,分别表示为绿色以及灰黑色。如果上一刻,一个活细胞周围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()用于关闭绘图区域。
四、完整源码
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程