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
類型用於持有二維細胞網格,並通過布爾類型的值 true
和 false
分別表示細胞的存活和死亡:
type Universe [][]bool
通過使用切片而不是數組來表示世界,可以讓函數和方法更容易地共享和修改世界。
在此之後,我們還要編寫 NewUniverse
函數,它使用 make
分配並返回一個 height
行 width
列的 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)
,這一點可以通過將 height
與 y
相加得出。如果 y
超過了網格的 height
,就需要用到之前計算閏年時介紹過的取模運算符(%
),然後通過對 y
取模 height
來得出相應的餘數。這一方法也適用於 x
和 width
。
統計相鄰細胞#
請編寫一個方法,統計給定細胞鄰近的存活細胞數量,然後返回 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