GO系列: go语言实现康威生命小游戏

336 阅读5分钟

目录

1.游戏规则

2.实现思路

3.具体实现

4.总结

游戏规则

游戏在一个类似于围棋棋盘一样的,可以无限延伸的二维方格网中进行。例如,设想每个方格中都可放置一个生命细胞,生命细胞只有两种状态:“生”或“死”。用黑色的方格表示该细胞为“生”, 空格表示该细胞为“死” 。游戏开始时, 每个细胞可以随机地(或给定地)被设定为“生”或“死”之一的某个状态, 然后,再根据某种规则(生存定律)计算下一代每个细胞的状态:

我们可以规定如下的‘生存定律’:

1. 每个细胞的状态由该细胞及周围八个细胞上一次的状态所决定;

  1. 如果一个细胞周围有3个细胞为生,则该细胞为生,即该细胞若原先为死,则转为生,若原先为生,则保持不变;

  2. 如果一个细胞周围有2个细胞为生,则该细胞的生死状态保持不变;

  3. 在其它情况下,该细胞为死,即该细胞若原先为生,则转为死,若原先为死,则保持不变

实现思路

  1. 用一个二维数组来表示细胞的世界

  2. 初始化这个世界,并散播随机的初始细胞

  3. 需要有一个函数来打印这个二维数组,即展示这个细胞世界

  4. 计算细胞在下一个时代的存活情况

  5. 判断每一个细胞在下一个时代的存活情况,并保存.

  6. 不断迭代

具体实现

1. 初始化世界

定义世界的大小和基本type

type Universe [][]bool

func (u Universe) Getx() int { return len(u) } //获取地图height
func (u Universe) Gety() int { return len(u[0]) }  //获取地图width

var (
		width        = 30
		height       = 30
		WordStepNums = 0
		LiveNums     = 0
		WordStatus   = 0
		prevprevWord Universe
		)

定义一个函数叫做 Newuniverse( w , h ),初始化二维数组

func NewUniverse(w int, h int) Universe {
	fmt.Printf('33[2J33[1;1H') //终端清屏
	table := make(Universe, h)
	for i := range table {
		table[i] = make([]bool, w)
	}
	return table
}

定义一个seed( ) 函数,初始化地图

其中的 r%4 的作用是用来设置大约25%的空间默认存在细胞

通过调整数字的大小可以修改初始化世界时,初始细胞的数量

// 激活初始细胞
func (u Universe) seed() {
	nums := 0
	for j := range u {
		for i := 0; i < len(u[j]); i++ {
			r := rand.Intn(100)
			if r%4 == 0 {
				nums++
				u[j][i] = true
			}
		}
	}
	fmt.Printf('成功激活初始细胞:%v个', nums)
}

定义一个show( )函数,用于打印地图

// 打印世界
func (u Universe) Show() {
	for _, cellgroup := range u {
		for _, cell := range cellgroup {
			var str string
			if cell {
				str = '■'
			} else {
				str = '.'
			}
			fmt.Printf('[%v]', str)
		}
	}
}

在main.go调用

  prevprevWord = NewUniverse(width, height)
	universe1 := NewUniverse(width, height) //创建
	universe1.seed()                        //初始化
	universe1.Show()                        //新的世界诞生了!!!
	fmt.Printf('新的世界诞生了!!!
')

效果预览

2.细胞迭代

首先需要有一个 Alive( x , y ) 函数来判断细胞是否存活

实现Alive方法最困难的就是处理越界情况。例如,我们如何判断位于(-1,-1)的细胞存活还是死亡呢?或者,我们如何在一个80×15的网格上,判断位于(80,15)的细胞存活还是死亡呢?

为了解决这个问题,我们需要为世界实现回绕。这样一来,与(0.0)相邻的上方将不再是(0,-1),而是(0,14),这一点可以通过将height与y相加得出。如果y超讨了网格的height,就需要用到之前计算闰年时介绍过的取模运算符(%),然后通过对y取模height来得出相应的余数。这一方法也适用于x和 width。

// 判断细胞是否存活
func (u Universe) Alive(x, y int) bool {
	ax := x
	ay := y
	if x < 0 {
		ax = ax + u.Getx()
	}
	if y < 0 {
		ay = ay + u.Gety()
	}
	if x > u.Getx() {
		ax = ax % u.Getx()
	}
	if y > u.Gety() {
		ay = ay % u.Gety()
	}
	// fmt.Printf('[%v,%v]', ax, ay)
	if u[ax][ay] == true {
		return true
	} else {
		return false
	}
}

Alive函数只能确定当前时代的细胞是否存活,想要知道下一个时代的当前细胞是消失还是继续存在,需要创建一个Neighbors( x , y )函数去计算目标细胞周围有多少存活的细胞,然后再进行下一步判断

// 计算邻近存活细胞数量
func (u Universe) Neighbors(x, y int) int {
	AroudAliveCellNums := 0
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			ax := x - 1 + i
			ay := y - 1 + j
			if u.Alive(ax, ay) == true {
				AroudAliveCellNums++
			}

		}
	}
	//除去自己
	if u[x][y] == true {
		AroudAliveCellNums--
	}
	return AroudAliveCellNums
}

现在我们可以知道地图上每一个点周围有多少存活的细胞数量了! 然后我们利用 Next( ) 函数进行进一步的判断,

// 判断下一世代是否存活
func (u Universe) Next(x, y int) bool {
	AroudAliveCellNums := u.Neighbors(x, y)
	// 当前存活,小于2,大于3,死亡
	if u[x][y] == true && AroudAliveCellNums < 2 || AroudAliveCellNums > 3 {
		return false
		// 当前死亡,等于3,存活
	} else if u[x][y] == false && AroudAliveCellNums == 3 {
		return true
		// 当前存活,∈[2,3],存活
	} else if u[x][y] == true && AroudAliveCellNums >= 2 && AroudAliveCellNums <= 3 {
		return true
		//当前死亡,不等于3,存活
	} else {
		return false
	}
}

好了! 现在我们可以得到地图上每个点在下一个时代是否会存在细胞

接下来,我们可以不断用下一世代的二维数组来替换当前时代的二维数组实现细胞的迭代

这里有一个需要注意的问题,那就是统计邻近细胞必须基于世界先前的状态。如果程序在执行统计的同时直接修改世界,那么这样的修改势必会对邻近细胞的统计结果产生影响。

解决这个问题的一个简单办法就是创建两个同等大小的世界,然后在读取世界A的时候对世界B进行设置 在这里 我们定义一个Step( a , b ) 函数去实现更新世界,入参里的 a b 为两个不同的universe

扩展: 你能够使用原地算法在不创建新数组的情况下实现迭代吗?

// 更新下一世代
func Step(cancel context.CancelFunc, WordStepNums int, prevprevWord Universe, a, b Universe) (Universe, int, Universe, int) {
	AliveNums := 0
	SameAliveNums := 0  //与上个时代情况相同的细胞数量
	PrevSameAliveNums := 0  //与上上个时代情况相同的细胞数量
	wordStatus := 0
	fmt.Printf('33[2J33[1;1H')
	for i := 0; i < a.Getx()-1; i++ {
		for j := 0; j < a.Gety()-1; j++ {
			NextAlive := a.Next(i, j)
			if NextAlive {
				AliveNums++
			}
			if NextAlive == a[i][j] && NextAlive == true {
				SameAliveNums++
			}
			if NextAlive == prevprevWord[i][j] && NextAlive == true {
				PrevSameAliveNums++
			}
			b[i][j] = NextAlive
		}
	}
	prevprevWord = a //记录上上次的世界

	// 没有细胞存活时,结束
	if AliveNums == 0 {
		wordStatus = 1
		cancel()
	} else if AliveNums == SameAliveNums || AliveNums == PrevSameAliveNums {
		wordStatus = 2
		cancel()
	}
	a, b = b, a
	return a, AliveNums, prevprevWord, wordStatus

}

main( )函数完整代码

func main() {
	var (
		width        = 10
		height       = 10
		WordStepNums = 0
		LiveNums     = 0
		WordStatus   = 0
		prevprevWord Universe
	)
	prevprevWord = NewUniverse(width, height)
	universe1 := NewUniverse(width, height) //创建
	universe1.seed()                        //初始化
	universe1.Show()                        //新的世界诞生了!!!
	fmt.Printf('新的世界诞生了!!!
')
	time.Sleep(1000 * time.Millisecond)
	fmt.Printf('即将开始新的进化!!
')
	time.Sleep(1000 * time.Millisecond)
	bg := context.Background()
	ctx, cancel := context.WithCancel(bg)

	for {
		select {
		case <-ctx.Done():
			switch WordStatus {
			case 1:
				fmt.Printf('世界终止了进化,您进化到了第 %v 代,黯淡的宇宙里再也不会有生命的气息', WordStepNums)
			case 2:
				fmt.Printf('世界终止了进化,您进化到了第 %v 代,世界没有了更多的可能,只剩下无尽的重复', WordStepNums)
			}

			return
		default:
			WordStepNums++
			time.Sleep(100 * time.Millisecond)
			universe1, LiveNums, prevprevWord, WordStatus = Step(cancel, WordStepNums, prevprevWord, universe1, NewUniverse(width, height)) //下一世代
			universe1.Show()
			fmt.Printf('世界进化到了第 %v 代,细胞剩余:%v ', WordStepNums, LiveNums)
		}

	}
}

最终效果展示

总结

许久没更新,本来打算更新python,但是无意间发现了go这门语言,沉淀了两个周,发现了这款小游戏,水个文章,顺便展示一下学习成果.