ポートフォリオサイト公開中

人工生命を作ろうではないか

この記事はtomowarkar ひとりAdvent Calendar 2019の7日目の記事です。


はじめに

この記事ではGoを使って人工生命を作っていきます。(👇こんな感じのもの👇)

人工生命とはいっても、生命のプロセスを簡易的なモデルとして表したライフゲームというゲームなのでその動きを眺めながら楽しむ感じとなります。

また今回の記事はこちらの記事(Goを使ってCLIにフラッシュ画を描画する)の延長として書いているので、コマンドラインにテキストを描画する方法などはこちらをご覧ください。

ライフゲームとは

Wikipedia先生の方がぼくの100倍は賢いので丸投げしてしまいましょう。

ライフゲーム (Conway’s Game of Life) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。生物集団においては、過疎でも過密でも個体の生存に適さないという個体群生態学的な側面を背景に持つ。セル・オートマトンのもっともよく知られた例でもある。 ライフゲーム Wikipedia

このゲームをGoで書いていこうというわけです。


コード全文

長いの折りたたんでます。

package main

import (
	"math/rand"
	"time"

	"github.com/nsf/termbox-go"
)

const (
	coldef = termbox.ColorDefault
	death  = 0
	alive  = 1
)

// Earth ...
type Earth struct {
	width  int
	height int
	field  [][]int
}

// InitEarth ...
func (e *Earth) InitEarth(h, w int) {
	e.width = w
	e.height = h
	e.field = make([][]int, h)
	for i := 0; i < h; i++ {
		e.field[i] = make([]int, w)
	}
}

// RandEarth ...
func (e *Earth) RandEarth() {
	for j := 0; j < e.height; j++ {
		for i := 0; i < e.width; i++ {
			e.field[j][i] = rand.Intn(2)
		}
	}
}

// DrawField ...
func (e Earth) DrawField() {
	for j := 0; j < e.height; j++ {
		for i := 0; i < e.width; i++ {
			if e.field[j][i] == death {
				termbox.SetCell(i*2, j, '⚪', coldef, coldef)
			} else {
				termbox.SetCell(i*2, j, '🔴', coldef, coldef)
			}
		}
	}
	termbox.Flush()
}

// Digenesis ...
func (e Earth) Digenesis() Earth {
	var child Earth
	child.InitEarth(e.height, e.width)

	for j := 0; j < e.height; j++ {
		for i := 0; i < e.width; i++ {
			sibs := countSibs(j, i, e)
			if e.field[j][i] == death {
				// 誕生: 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
				if sibs == 3 {
					child.field[j][i] = alive
				}
			} else {
				// 生存: 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
				// 過疎: 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
				// 過密: 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
				if sibs == 2 || sibs == 3 {
					child.field[j][i] = alive
				} else {
					child.field[j][i] = death
				}
			}
		}
	}
	return child
}

// SetDress ...
func (e Earth) SetDress() Earth {
	var ee Earth
	ee.InitEarth(e.height, e.width)
	ee.width += 2
	ee.height += 2
	for i, v := range e.field {
		ee.field[i] = append(make([]int, 1), append(v, 0)...)
	}
	ee.field = append([][]int{make([]int, ee.width)}, ee.field...)
	ee.field = append(ee.field, make([]int, ee.width))
	return ee
}

func countSibs(y, x int, e Earth) int {
	var sibs int
	dressed := e.SetDress()

	sibs += dressed.field[y][x]     // ↖︎
	sibs += dressed.field[y][x+1]   // ↑
	sibs += dressed.field[y][x+2]   // ↗︎
	sibs += dressed.field[y+1][x]   // ←
	sibs += dressed.field[y+1][x+2] // →
	sibs += dressed.field[y+2][x]   // ↙︎
	sibs += dressed.field[y+2][x+1] // ↓
	sibs += dressed.field[y+2][x+2] // ↘︎
	return sibs
}

//key events
func keyEventLoop(kch chan termbox.Key) {
	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			kch <- ev.Key
		default:
		}
	}
}

//timer event
func timerLoop(tch chan bool, span int) {
	for {
		tch <- true
		time.Sleep(time.Duration(span) * time.Millisecond)
	}
}

func mainLoop(et Earth, tch chan bool, kch chan termbox.Key) {
	for {
		select {
		case key := <-kch: //key event
			switch key {
			case termbox.KeyEsc, termbox.KeyCtrlC: //end event
				return
			case termbox.KeySpace:
				et.RandEarth()
			}
		case <-tch: //time event
			et = et.Digenesis()
			et.DrawField()
			break
		default:
		}
	}
}

func main() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	rand.Seed(time.Now().UnixNano())

	var earth Earth
	earth.InitEarth(25, 25)
	earth.RandEarth()

	kch := make(chan termbox.Key)
	tch := make(chan bool)
	go keyEventLoop(kch)
	go timerLoop(tch, 100)

	mainLoop(earth, tch, kch)
}

コード全文はGithub Gistにも載せているのでよかったらこちらもご覧ください。


コード解説

const (
    death  = 0
    alive  = 1
)

// Digenesis ...
func (e Earth) Digenesis() Earth {
    var child Earth
    child.InitEarth(e.height, e.width)

    for j := 0; j < e.height; j++ {
        for i := 0; i < e.width; i++ {
            sibs := countSibs(j, i, e)
            if e.field[j][i] == death {
                if sibs == 3 {
                    child.field[j][i] = alive
                }
            } else {
                if sibs == 2 || sibs == 3 {
                    child.field[j][i] = alive
                } else {
                    child.field[j][i] = death
                }
            }
        }
    }
    return child
}

そのセルの状態(生存or死亡)と隣接するセルの数に応じて次の世代の状態を分岐させます。

  • 誕生: 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生。
  • 生存: 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存。
  • 過疎: 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅。
  • 過密: 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅。

この際、隣接するセルの状態をカウントするのをどうしようか迷ったのですが、フィールドを囲うように死亡ステータスを持ったセルを追加してカウントしました。

(pythonなどみたくarray[-1]みたいなのが使えないため。)

// SetDress ...
func (e Earth) SetDress() Earth {
    var ee Earth
    ee.InitEarth(e.height, e.width)
    ee.width += 2
    ee.height += 2
    for i, v := range e.field {
        ee.field[i] = append(make([]int, 1), append(v, 0)...)
    }
    ee.field = append([][]int{make([]int, ee.width)}, ee.field...)
    ee.field = append(ee.field, make([]int, ee.width))
    return ee
}

こちらがフィールドを囲うように死亡ステータス(0)を持ったセルを追加

この辺りをしっかり書かないといけないのでコード力がつきますね。

func countSibs(y, x int, e Earth) int {
    var sibs int
    dressed := e.SetDress()

    sibs += dressed.field[y][x]     // ↖︎
    sibs += dressed.field[y][x+1]   // ↑
    sibs += dressed.field[y][x+2]   // ↗︎
    sibs += dressed.field[y+1][x]   // ←
    sibs += dressed.field[y+1][x+2] // →
    sibs += dressed.field[y+2][x]   // ↙︎
    sibs += dressed.field[y+2][x+1] // ↓
    sibs += dressed.field[y+2][x+2] // ↘︎
    return sibs
}

そして周りの状態をカウント。


出現するパターン(一例)

固定物体

以下のような物体はここから動かず固定物体と呼ばれています。

image.png

振動子

  • 銀河
  • パルサー
  • 8角形

の三つは同じパターンをループして繰り返すため、振動子と呼ばれています。

var galaxy = [][]int{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}

var earth Earth
earth.InitEarth(15, 15)
earth.field = galaxy

image.png

var pulser = [][]int{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}

var earth Earth
earth.InitEarth(15, 15)
earth.field = pulser

image.png

var octagon = [][]int{
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 0, 1, 0, 0},
{0, 1, 0, 0, 0, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{0, 1, 0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 1, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
}
var earth Earth
earth.InitEarth(8, 8)
earth.field = octagon

image.png


おわりに

隣接するセルの状態確認以外はサクサク書け、みていて楽しいものができました。

隣接するセルのカウントもpythonなどでは、良くも悪くもなあなあに実装している部分なのでコード力がついて(る気がする)のでGoらしくていいですね。

以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です