简介

《生命游戏》是英国数学家J.H.康威于1970年构思的模拟,由马丁·加德纳在他的《科学美国人》专栏中推广。该游戏使用二维细胞网格对细菌的生命周期进行模型。根据最初的模式,游戏使用一套简单的规则模拟后代细胞的出生和死亡。在此任务中,您将实现 J.H.康威 模拟的简化版本和观看细菌随着时间而增长的基本用户界面。

模拟从网格上的细胞初始模式开始,并根据以下规则计算连续几代细胞:

  • 一个零或一个邻居的位置将在下一代空。如果一个细胞在那里,它就会死亡。
  • 有两个邻居的位置是稳定的。如果它有一个细胞,它仍然包含一个细胞。如果是空的,还是空的。
  • 一个有三个邻居的位置将包含下一代的细胞。如果它以前无人居住,一个新的细胞诞生了。如果它目前包含一个细胞,细胞仍然存在。
  • 在下一代中,一个有四个或更多邻居的位置将空无一人。如果那个地方有一个牢房,它就会因人满为患而死亡。

功能模块

用户交互模块

您的生命游戏计划应首先提示用户提供文件名称,并使用该文件的内容来设置细菌群落网格的初始状态。然后,它应该询问模拟是否应该环绕网格。然后,该计划将允许用户通过几代人的增长推进殖民地。用户可以键入 t 以”勾选”一代的细菌模拟,或开始一个动画循环,将模拟向前滴答作响几代人,每 50 毫秒一次:或q退出

选择初始游戏网格

  • 供用户选择初始游戏网格
  • fileExists(filePath) -Checks if a file with the corresponding fileName exists. Returns a bool.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void input_File(string filePath, ifstream& fin) {
    while(true) {
    string filename = getLine("Grid input file name? ");
    filePath = "res/" + filename + ".txt";
    if(!fileExists(filePath)) {
    cout << "Can't locate the file, try again.\n" <<endl;
    }else {
    break;
    }
    }
    }

选择游戏模式(wrap)

模式1:wrap around the grid

游戏网格无边界,显示的不足8个neighbor边界网格将以其他边界网格替代

模式2: no wrap around the grid

游戏网格存在边界,不足8个neighbor的边界网格不作填充

游戏’ui‘

  • animate 指定动画次数,自动每50ms显示下一代细胞繁衍情况
  • tick 手动点击显示下一代细胞繁衍情况
  • quit 停止游戏 退出

模拟算法模块

存储
1
#include"grid.h"
  • 采用grid存储而非二维vector因为grid更利于操作,例如g.inBounds(r,c)可判断(r,c)位置是否越界
  • grid需要严格设置边界为初始化网格的长宽,因为初始化文件中还有其他杂项(注释等)
    g.resize(r,c)可重置网格维度
模式

思路: 遍历网格的每一个单元格,计算其neighbor数,从而设置该单元格的显示内容(细胞的生命状态)
难点: 不同模式不同的单元格neighbor数的计算方式
解决方案:

对于指定位置遍历其相邻8个网格的方法 –相对位置
与中心网格相邻的网格(’边界‘网格)与中心网格的横纵相对位置差值均在-1-+1中 (0为本身的情况需排除)

模式1:wrap around the grid

游戏网格无边界,显示的不足8个neighbor边界网格将以其他边界网格替代
无边界就是边界外部的网格对应到网格内的网格,%操作即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int neighbourNum_Wrap(const Grid<char>& g, int r, int c) {
int neighbour = 0;

// count neighbours around each cell's 8 directioins
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
if (g[(r+i+g.numRows()) % g.numRows()][(c+j+g.numCols()) % g.numCols()] == 'X') {
neighbour++;
}
}
}

// do not count itself if cell exists
if (g[r][c] == 'X') {
neighbour--;
}

return neighbour;
}

模式2: no wrap around the grid

游戏网格存在边界,不足8个neighbor的边界网格不作填充
因为存在边界,因此边界外部的网格需要舍弃,添加inBounds判断即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int neighbourNum_NonWrap(const Grid<char>& g, int r, int c) {
int neighbour = 0;

// count neighbours around each cell's 8 directioins
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
if (g.inBounds(r + i, c + j) && g[r + i][c + j] == 'X') {
neighbour++;
}
}
}

// do not count itself if cell exists
if (g[r][c] == 'X') {
neighbour--;
}

return neighbour;
}

知识点汇总

File Pointer 文件指针

文件位置指针是一个整数值,用于标注文件当前读写位置。

文件指针以字节为单位,文件第一个字节位置号为0,长度为N字节的文件有效读写范围为0~N-1。指针位置在此之外进行读/写操作,则失败;

istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。

这些成员函数包括关于 istream 的 seekg(”seek get”)和关于 ostream 的 seekp(”seek put”)。

seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),
也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

下面是关于定位 “get” 文件位置指针的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13

// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );

// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );

// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );

// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

I/O流

cin.fail() –文件流操作失败 读取的类型不匹配等 返回类型为bool
!cin –相当于cin.fail() 返回类型为bool
cin.peek() –返回文件指针当前指向的字符 返回类型为char字符

连续输入时操作失败处理方法
cin.rdstate() 查看错误错误标识符
cin.clear() 清除错误标识符(错误状态)
cin.sync() 清除输入错误时存入缓冲区的字符

cin.ignore(999, ‘\n’) 清除缓冲区前999个字符,第一个参数足够大可用于清除换行符之前的所有缓冲区字符,消除上一次输出对本次输入的影响