banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

Golang实现康威生命游戏

title: Golang 实现康威生命游戏
date: 2022-05-23 17:15:00
toc: false
index_img: http://api.btstu.cn/sjbz/?lx=m_dongman&cid=2
category:

  • Go
    tags:
  • 读取
  • 创建
  • Golang
  • 遍历
  • 计算
  • 解决
  • 模拟
  • 输出
  • 统计
  • 打印
  • 游戏

实验:切片人生#

本次实验将要构建一个名为 “康威生命游戏”(Conway's Game of Life)的模拟器,并使用它模拟人类的繁衍过程。因为模拟需要在一个布满细胞的二维网格上进行,所以这次实验将聚焦于切片。

网格中的每个细胞在水平、垂直和对角线方向上总共有 8 个相邻细胞。在每一世代,单个细胞的生死存亡将取决于相邻细胞的存活数量。

开天辟地#

在初次实现生命游戏时,我们需要将世界限制在固定的大小之内。具体来说,我们需要决定网格的尺寸并定义相应的常量:

const (
  width  = 80
  height = 15
)

接着还需要定义 Universe 类型用于持有二维细胞网格,并通过布尔类型的值 truefalse 分别表示细胞的存活和死亡:

type Universe [][]bool

通过使用切片而不是数组来表示世界,可以让函数和方法更容易地共享和修改世界。

在此之后,我们还要编写 NewUniverse 函数,它使用 make 分配并返回一个 heightwidth 列的 Universe

func NewUniverse() Universe

因为新分配切片的各个元素将被设置为默认的零值 false,所以世界在刚开始的时候将不存在任何存活细胞。

观察世界#

请为 Universe 编写一个方法,它能够用 fmt 包中的函数将世界目前的状态打印至屏幕,其中存活的细胞用星号表示,而死亡的细胞则用空格表示。此外,它还需要在每次打印完一行细胞之后,将光标移动至新的输出行:

func (u Universe) Show()

请编写一个 main 函数,它会调用 NewUniverse 函数创造出新世界,然后调用 Show 函数把这个世界打印出来。在继续进行实验之前,请先确保你的程序能够正常运行,即使整个世界目前还没有存活细胞。

激活细胞#

请编写一个 Seed 方法,它可以随机激活世界中大约 25% 的细胞(将对应切片元素的值设置为 true):

func (u Universe) Seed()

在实现这个方法的时候,别忘了导入 math/rand 包以使用 Intn 函数。在此之后,请修改 main 函数并使用 Seed 方法对世界进行激活,然后使用 Show 函数将激活后的世界打印出来。

适者生存#

以下是康威生命游戏的具体规则:

  • 当一个存活细胞邻近的存活细胞少于 2 个时,该细胞死亡。
  • 当一个存活细胞邻近有 2 个或 3 个存活细胞时,该细胞将延续至下一世代。
  • 当一个存活细胞邻近有多余 3 个存活细胞时,该细胞死亡。
  • 当一个死亡细胞邻近正好有 3 个存活细胞时,该细胞存活。

为了实现这些规则,我们需要将它们分解成以下 3 个步骤,并将每个步骤实现为相应的方法:

  • 判断细胞是否存活的方法
  • 统计邻近存活细胞数量的能力
  • 判断细胞在下一世代存活或死亡的逻辑

存活还是死亡#

判断细胞是否存活可以通过检查 Universe 切片中对应元素的布尔值来实现,只要该值为 true,那么细胞就是存活的。

请为 Universe 类型编写一个带有以下签名的 Alive 方法:

func (u Universe) Alive(x, y int) bool

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

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

统计相邻细胞#

请编写一个方法,统计给定细胞邻近的存活细胞数量,然后返回 0~8

func (u Universe) Neighbors(x, y int) int

为了使世界实现回绕,请使用 Alive 方法而不是直接访问世界数据。

另外需要注意的是,在统计相邻细胞的时候别把给定的细胞也统计进去了。

游戏逻辑#

在实现了统计邻近存活细胞数量的方法之后,我们就可以正式在 Next 方法里面实现本节开头列出的游戏规则了:

func (u Universe) Next(x, y int) bool

这个方法不会直接修改世界,而会返回一个布尔值,并以此来表示给定细胞在下一世代存活或死亡。

平行世界#

为了完成模拟操作,程序需要遍历世界中的每个细胞,并使用 Next 判断它们在下一世代中的状态。

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

解决这个问题的一个简单办法就是创建两个同等大小的世界,然后在读取世界 A 时候对世界 B 进行设置。请编写函数 Step 以执行该操作:

func Step(a, b Universe)

当世界 B 被更新到了下一世代之后,程序就可以交换这两个世界,然后继续下一次更新:

a, b = b, a

在展示新时代的细胞之前,程序需要使用特殊的 ANSI 转义序列 "\x0c" 来清空屏幕。在此之后,程序就可以打印出整个世界,并使用 time 包中的 Sleep 函数来减缓世代更迭的速度。

注意:在 Go Playground 以外的地方,你需要使用其他机制才能清空屏幕,例如,在 macOS 上就需要打印 "\033[H" 而不是 "\x0c"

现在,你应该已经有了编写并且在 Go Playground 上运行完整的康威生命游戏所需的全部组件。

实现#

截止到开天辟地的代码#

package main

import "fmt"

const (
	width  = 25
	height = 15
)

type Universe [][]bool

func NewUniverse() Universe {
	u := make(Universe, height)
	for i := range u {
		u[i] = make([]bool, width)
	}
	return u
}

func main() {
	u := NewUniverse()
	for _, i := range u {
		fmt.Println(i)
	}
}

运行结果:

[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]
[false false false false false false false false false false false false false false false false false false false false false false false false false]

Program exited.

完整代码#

package main

import (
	"fmt"
	"math/rand"
	"time"
)

const (
	width  = 80
	height = 15
)

type Universe [][]bool

func NewUniverse() Universe {
	u := make(Universe, height)
	for i := range u {
		u[i] = make([]bool, width)
	}
	return u
}

func (u Universe) Set(x, y int, b bool) {
	u[y][x] = b
}

func (u Universe) Seed() {
	for i := 0; i < (width * height / 4); i++ {
		u.Set(rand.Intn(width), rand.Intn(height), true)
	}
}

func (u Universe) Alive(x, y int) bool {
	x = (x + width) % width
	y = (y + height) % height
	return u[y][x]
}

func (u Universe) Neighbors(x, y int) int {
	n := 0
	for v := -1; v <= 1; v++ {
		for h := -1; h <= 1; h++ {
			if !(v == 0 && h == 0) && u.Alive(x+h, y+v) {
				n++
			}
		}
	}
	return n
}

func (u Universe) Next(x, y int) bool {
	n := u.Neighbors(x, y)
	return n == 3 || n == 2 && u.Alive(x, y)
}

func (u Universe) String() string {
	var b byte
	buf := make([]byte, 0, (width+1)*height)

	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			b = ' '
			if u[y][x] {
				b = '*'
			}
			buf = append(buf, b)
		}
		buf = append(buf, '\n')
	}

	return string(buf)
}

func (u Universe) Show() {
	fmt.Print("\x0c", u.String())
}

func Step(a, b Universe) {
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			b.Set(x, y, a.Next(x, y))
		}
	}
}

func main() {
	a, b := NewUniverse(), NewUniverse()
	a.Seed()
	for i := 0; i < 300; i++ {
		Step(a, b)
		a.Show()
		time.Sleep(time.Second / 30)
		a, b = b, a
	}
}

请前往 Go Playground 查看:Go Playground - The Go Programming Language

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。