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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。