還好,第一版的程式沒有難產。這版的目的很簡單,就是把題目實作出來,同時我會盡量套用物件導向的理念去設計程式的結構,而不是只把結果算出來而已。其實我一直覺的,這類生命模擬的程式,是非常適合用OOPL來實作的範例,大概OOPL所有強調的特性 (封裝、繼承、多型、動態聯結... 等等) 都用的到,算是完美的應用範例題吧!
不過很奇怪的,我特地 GOOGLE 了一下,不知 OOPL 高手都不屑寫這種範例還是怎樣,找到的範例程式,不管用什麼語言 (C/C++/Java/C#都有) 寫的,清一色都很沒有物件導向的 fu ... 好吧,只好自己來寫一個。
第一步,一定是先看看你的程式,分析出需要那些類別/物件,及它們之間的關係。比較正規的作法就是 UML 的 UseCase 了。不過這範例其實不大,我就直接跳到 Class Diagram 了 (因為VS2008剛好有現成的...)... 主要的類別有兩個: World (世界) 及 Cell (細胞)。
World 就是給 Cell 生活的空間,我們只訂義一個有限大小的二維空間,就一個 M x N 的棋盤這樣。而 Cell 則是一個細胞,描述單一一個細胞本身,在各種不同的條件下會有什麼反應。先貼一下 class diagram:
圖1. class diagram (World & Cell)
老實說,這張圖還蠻乏善可陳的,World對外公開的介面,大概包含了幾個主要功能,就是取得指定座標的 Cell (GetCell), 及把目前的整個 World 狀態印出來 (ShowMaps) 的 method 而已。而 Cell 的公開介面,不外乎是它目前是活著還是死的,還有它的建構式,及呼叫後會把狀態轉移到下一次狀態的 method。
其它都是 World / Cell 互相溝通用,或是 Init 用的 Method / Prop, 就不多作介紹。先來看看主程式,扮演上帝的你,如何讓這堆單細胞生物,在你的世界裡活起來:
static void Main(string[] args) { int worldSizeX = 30; int worldSizeY = 30; int maxGenerationCount = 100; World realworld = new World(worldSizeX, worldSizeY); for (int generation = 1; generation <= maxGenerationCount; generation++) { realworld.ShowMaps(string.Format("Generation: {0}", generation)); Thread.Sleep(1000); for (int positionX = 0; positionX < worldSizeX; positionX++) { for (int positionY = 0; positionY < worldSizeY; positionY++) { // do day pass Cell cell = realworld.GetCell(positionX, positionY) as Cell; cell.OnNextStateChange(); } } } }
主程式我還沒把不相干的動作刪掉,也才廿一行... line 1 ~ 5 只是初始值,line 6 建立整個世界,之後就每跑完一個世代 (generation) 就休息一秒鍾,繼續下一次進化。這樣隨著時間的過去,畫面上會一直更新整個世界的狀態... 直到只定的次數到了為止。
class World 的部份就沒什麼特別的,就只是把一個二維陣列包裝一下而已。直接貼 Code 就混過去吧 XD,一樣沒有刪掉程式碼,原 CODE 照貼:
public class World { private int SizeX = 0; private int SizeY = 0; private Cell[,] _map; public World(int maxPosX, int maxPosY) { this._map = new Cell[maxPosX, maxPosY]; this.SizeX = maxPosX; this.SizeY = maxPosY; for (int posX = 0; posX < maxPosX; posX++) { for (int posY = 0; posY < maxPosY; posY++) { this._map[posX, posY] = new Cell(this, posX, posY); } } } internal void PutOn(Cell item, int posX, int posY) { if (this._map[posX, posY] == null) { this._map[posX, posY] = item; item.PosX = posX; item.PosY = posY; } else { throw new ArgumentException(); } } public Cell GetCell(int posX, int posY) { if (posX >= this.SizeX) return null; if (posY >= this.SizeY) return null; if (posX < 0) return null; if (posY < 0) return null; return this._map[posX, posY]; } public void ShowMaps(string title) { Console.Title = title; Console.SetWindowSize(this.SizeX * 2, this.SizeY); Console.SetCursorPosition(0, 0); Console.Clear(); for (int y = 0; y < this.SizeY; y++) { for (int x = 0; x < this.SizeX; x++) { Cell item = this.GetCell(x, y); Console.SetCursorPosition(x * 2, y); Console.Write(item.IsAlive? "●":"○"); } } } }
接下來是封裝每個細胞本身跟環境互動的影響,把上一篇講的規則對應成程式碼的樣子。先來看看 CODE:
public class Cell //: Life { protected World CurrentWorld { get; private set; } internal int PosX = 0; internal int PosY = 0; private const double InitAliveProbability = 0.2D; private static Random _rnd = new Random(); public Cell(World world, int posX, int posY) //: base(world, posX, posY) { this.CurrentWorld = world; // setup world this.PosX = posY; this.PosY = posY; this.CurrentWorld.PutOn(this, posX, posY); this.IsAlive = (_rnd.NextDouble() < InitAliveProbability); } public bool IsAlive { get; private set; } protected IEnumerable<Cell> FindNeighbors() { foreach (Cell item in new Cell[] { this.CurrentWorld.GetCell(this.PosX -1, this.PosY-1), this.CurrentWorld.GetCell(this.PosX, this.PosY-1), this.CurrentWorld.GetCell(this.PosX+1, this.PosY-1), this.CurrentWorld.GetCell(this.PosX-1, this.PosY), this.CurrentWorld.GetCell(this.PosX+1, this.PosY), this.CurrentWorld.GetCell(this.PosX-1, this.PosY+1), this.CurrentWorld.GetCell(this.PosX, this.PosY+1), this.CurrentWorld.GetCell(this.PosX+1, this.PosY+1)}) { if (item != null) yield return item; } yield break; } public void OnNextStateChange() { int livesCount = 0; foreach (Cell item in this.FindNeighbors()) { if (item.IsAlive == true) livesCount++; } if (this.IsAlive == true && livesCount <1) { //孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。 this.IsAlive = false; } else if (this.IsAlive == true && livesCount >= 4) { //擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。 this.IsAlive = false; } else if (this.IsAlive == true && (livesCount == 2 || livesCount == 3)) { //穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。 //this.IsAlive = true; } else if (this.IsAlive == false && livesCount == 3) { //復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。 this.IsAlive = true; } else { // ToDo: 未定義的狀態? assert } } }
這裡開始應用到 OOPL 第一個特性: 封裝。從程式碼可以看到,主要的邏輯都被包在裡面了,就 Game Of Life 裡提到的四條規則。
程式這樣寫起來,比那些作業的標準答案看起來舒服多了吧? 雖然行數多了一些,不過看起來比較有 OO 的樣子了。當然只是看起來爽是沒用的,這樣的架構,到目前為只除了邏輯清楚一點之外,還看不到其它很明顯的好處。不過當這個規責稍微複雜一點,OOPL的優點就會被突顯出來了。
下回,把題目做點變化,再來看看程式該如何調整… ((待續))
--
附件: 範例程式碼