65.9K
CodeProject 正在变化。 阅读更多。
Home

在学习 C# 扩展方法的同时,重温生命游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (8投票s)

2008年5月26日

CPOL

11分钟阅读

viewsIcon

56660

downloadIcon

607

对生命游戏进行有趣的变体,并使用扩展方法进行重构

引言

本文旨在提供一种有趣的生命游戏变体,同时学习 C# 3.0 (.NET 3.5) 中的扩展方法。利用生命游戏的细胞算法,我们生成各种分形图像。扩展方法用于重构一个已变得臃肿的基类。有关进一步介绍,请参阅下文的“关于”部分。

生命游戏自计算机发明以来,已被许多爱好者分析、编程和探索。再次听到生命游戏,你们中的许多人可能会打哈欠并关闭文章,因为你们已经看过太多次了。对于那些即将退出的人来说,扩展方法的使用可能会引起你们的兴趣。它们是否真的能提供多重继承的一个良好替代方案?

超出范围

时间限制是排除对此项目进行进一步增强的原因。例如,用户界面很简单,不具备获奖的外观和感觉。此外,代码的效率尚未经过充分测试,因此无法断定其性能与其他类似产品相当。此外,重构仅限于扩展方法。

关于生命游戏

如果您知道 1970 年出现的原始生命游戏,那么您可能会对这次复兴感到好奇。

生命游戏是由 John Conway 创建的细胞自动机。使用“游戏”一词具有欺骗性,因为它实际上是一个具有二维矩阵表示形式的模拟。用户可以通过更改二维正交网格内单元格的状态来“玩”这个“游戏”。

描述原始生命游戏的转换规则可以帮助读者理解此类模拟的基本前提。这些规则是

  • 单元格要么是死的,要么是活的。
  • 每个单元格(死或活)都根据其直接邻居的数量进行评估(原始模型最多八个)。
  • 然后,根据邻居的数量,每个单元格可能会诞生、存活或死亡。

下表显示了原始模型的决策矩阵

根据邻居数量的新状态
当前单元格状态 0 1 2 3 4 5 6 7 8

生命游戏的爱好者已经创建了许多细胞自动机的变体。本篇文章提供的变体很可能以前已经尝试过。如果您想声称自己是第一个,请举手,我可能会通过修改本文来认可您的工作。

关于原始生命游戏就说这么多。如果您想了解更多,网络上有很多资料。

这里是一个不错的起点,如果您想查看各种自动机的示例,可以查看此链接

Code Project 上其他关于生命游戏的作品

关于扩展方法

扩展方法只是向类添加方法的另一种方式。在我们深入示例代码之前,请考虑向类添加方法的其他技术

  • 直接修改基类(很快会导致类变得臃肿)
  • 继承(不应仅为添加方法而使用,很快继承层次结构将变得臃肿)
  • 部分类(您的基类也必须是部分的,并且需要相同的命名空间)
  • 多重继承(C# 中不可用)

以上每种技术都有明显的缺点,但讨论这些设计问题并非本文的宗旨。仅提及它们是为了引出扩展方法技术。

一个更显而易见的快速解决方案是创建一个包含所需方法的助手类。尽管方法需要一个参数来传递基类的实例。

class Base
{
}
static class Helper
{
    public static void NewMethod(Base obj)
    {
    }
}
//...
// Usage:
Base obj = new Base();
Helper.NewMethod(obj);

不幸的是,对于大型项目来说,助手类是一个糟糕的设计选择,主要是因为新方法的调用者必须打破整洁的面向对象语法。提到助手类是因为它们将设计引向扩展方法。

扩展方法是具有一个微小变化的助手类。用于传递基类实例的参数用“this”谓词声明。此更改有效地将我们的新方法添加到参数类的域。

class Base
{
}
static class Helper
{
    public static void NewMethod(this Base obj)
    {
    }
}
//...
// Usage:
Base obj = new Base();
obj.NewMethod();

有趣的是,程序员现在可以轻松地将他们的助手类转换为扩展方法。这一优势可以在不费多少力气的情况下带来设计改进。

简而言之,扩展方法的优点是它们可以在不更改基类的情况下轻松扩展类功能。扩展方法的一些缺点是它们仅适用于方法,不适用于属性;并且它们不能轻易地允许您添加private变量。请注意,如果您添加一个static private变量;除非您希望所有基类实例更新同一个private变量实例(还会存在线程问题)。

扩展方法的另一个优点是,如果static类使用命名空间,则可以有选择地隐藏附加方法。

class Base
{
}
namespace MyHelpers
{
    public static class HelperExtension
    {
        public static void NewMethod(this Base obj)
        {
        }
    }
}
/...
// For the extension method to be accessible, a using statement is required:
using MyHelpers;
// ...
Base obj = new Base();
obj.NewMethod();

如果您的代码上下文不需要扩展方法,请不要添加using语句。这有效地隐藏了扩展方法,并使对扩展方法使用位置的控制更好。

有关扩展方法的进一步阅读,请参阅以下文章

希望这个关于扩展方法的简短解释足以说明,因为本文的重点需要回到趣味性上。

生命游戏变体

第一个要提到的变体是细胞衰老的概念。当一个细胞诞生时,它会被赋予一个倒计时值来模拟年龄。每个单元格的倒计时值在每次游戏计算迭代后都会递减。当一个单元格的倒计时达到零时,它就会死亡。

第二个要提到的变体是细胞的扩展邻域。计算细胞邻居数量的方式取决于一个称为邻居距离 (ND) 的参数。可计数邻居的范围定义如下。为了说明,原始生命游戏基于邻居距离 2。

ND=1
x
x c x
x
ND=2
x x x
x c x
x x x
ND=3
x
x x x
x x c x x
x x x
x
ND=4
x x x
x x x x x
x x c x x
x x x x x
x x x
ND=5
x x x x x
x x x x x
x x c x x
x x x x x
x x x x x

如果生命模型具有 5 的邻居距离,则邻居的最大数量为 24。将此变体添加到生命游戏中会产生几种迷人的“生命形态”。

第三个变体是根据邻居数量和年龄为细胞着色。邻居数量决定色调,方法是将红色、绿色和蓝色根据整数的三个最低有效位进行组合。例如,如果邻居数量为 4 或 12,则细胞的色调将是蓝色。单元格的年龄用于确定单元格的不透明度。单元格在诞生时将完全不透明,然后随着年龄的增长逐渐变得透明。即将因衰老而死亡的单元格的可见性将与已经死亡的单元格相似,外观将是黑色的。

第三个变体仅仅是大多数其他爱好者所做的事情。允许模型更改决策矩阵的特性。这种灵活性使用户可以更改哪个邻居计数会触发单元格的死亡或诞生。

代码

生命形态模型被编码到一个类中,没有重要的方法实现。

    public class LifeModelBase {
        public LifeModelBase() {
            CellSpawnIndicators = new List<int>();
            CellDeathIndicators = new List<int>();
        }

        public List<int> CellSpawnIndicators { get; private set; }
        public List<int> CellDeathIndicators { get; private set; }

        private int _NeighbourDistance = 4;
        public int NeighbourDistance { get {return _NeighbourDistance;} 
				  set {_NeighbourDistance = value;} }

        private int _MaximumAge = int.MaxValue;
        public int MaximumAge { get {return _MaximumAge;} set {_MaximumAge = value;} }
    }

LifeModelBase类最初包含实现细胞状态转换的方法。然而,为了清理和重构代码,这些方法被移动到作为扩展方法的类中。这是一个设计上的改进,因为实现可以根据它们提供的扩展类型分离到不同的类中。您可以将其视为“关注点分离”,而无需大量接口、继承和依赖注入。现在足够了,这里是部分扩展方法的摘录。

namespace LifeSimulation.Extensions {
    static class ProcessLifeModelExtension {
        public static void Process(this LifeModelBase model, World World) {
            List<Point> dieList = new List<Point>();
            List<Point> spawnList = new List<Point>();
            for (int i = 0; i < World.Space.GetLength(0); i++) {
                for (int j = 0; j < World.Space.GetLength(1); j++) {
                    switch (GetCellOutcome(model, World, i, j)) {
                        case CellOutcome.Dies:
                            dieList.Add(new Point(i, j));
                            break;
                        case CellOutcome.Spawns:
                            spawnList.Add(new Point(i, j));
                            break;
                    }
                }
            }

            //Die
            foreach (Point point in dieList)
                World.Space[point.X, point.Y] = 0;
            //Spawn
            foreach (Point point in spawnList)
                World.Space[point.X, point.Y] = model.MaximumAge;

            //Age
            for (int i = 0; i < World.Space.GetLength(0); i++)
                for (int j = 0; j < World.Space.GetLength(1); j++)
                    if (World.Space[i, j] > 0)
                        World.Space[i, j]--;
        }

        public static int GetMaximumNeighbours(this LifeModelBase model) {
            return GetMaximumNeighbours(model.NeighbourDistance);
        }
    }
}

namespace LifeSimulation.Extensions {
    static class SerializeLifeModelExtension {
        public static string Serialize(this LifeModelBase model) {
            StringBuilder sb = new StringBuilder();
            sb.Append("ND=");
            sb.Append(model.NeighbourDistance.ToString());
            sb.Append(" SI=");
            bool first = true;
            foreach (int i in model.CellSpawnIndicators) {
                if (!first) sb.Append(",");
                sb.Append(i.ToString());
                first = false;
            }
            sb.Append(" DI=");
            first = true;
            foreach (int i in model.CellDeathIndicators) {
                if (!first) sb.Append(",");
                sb.Append(i.ToString());
                first = false;
            }
            sb.Append(" MA=");
            sb.Append(model.MaximumAge.ToString());

            return sb.ToString();
        }

        public static void Deserialize(this LifeModelBase model, string value) {
            try {
                string[] parts = SerialToParts(value);

                //NeighbourDistance
                model.NeighbourDistance = int.Parse(parts[0]);

                //SpawnIndicators
                model.CellSpawnIndicators.Clear();
                foreach (string part in parts[1].Split(',')) {
                    if (part.Length > 0)
                        model.CellSpawnIndicators.Add(int.Parse(part));
                }

                //CellDeathIndicators
                model.CellDeathIndicators.Clear();
                foreach (string part in parts[2].Split(',')) {
                    if (part.Length > 0)
                        model.CellDeathIndicators.Add(int.Parse(part));
                }

                //MaxEnergy
                model.MaximumAge = int.Parse(parts[3]);
            }
            catch (Exception ex) {
                MessageBox.Show(ex.Message);
            }
        }
    }
}

接下来的代码片段展示了如何调用扩展方法

using LifeSimulation.Extensions;

//...
private void paintTimer_Tick(object sender, EventArgs e) {
    if (!drawing) appState.LifeModel.Process(appState.World);
    pictureBox1.Invalidate();
}

//...

private void statusStripTimer_Tick(object sender, EventArgs e) {
    toolStripStatusLabel4.Text = appState.LifeModel.Serialize();
}

//...

private void editToolStripStatusLabel_Click(object sender, EventArgs e) {
    drawing = true;
    EditForm frm = new EditForm();
    appState.LifeModel.Deserialize(frm.EditModel(appState.LifeModel.Serialize()));
    PaintFavourites();
    drawing = false;
}

代码可以在许多其他方面得到改进 - 请继续并留下您的评论。目的是将重构限制在扩展方法上,从而有助于阐明该功能可以被利用的程度。扩展方法的一个最初似乎限制其使用方面是,那些static类中的大多数支持性private方法也必须包含基类的参数。事实证明,这并非限制,而是一个有用的设计特性。那些private方法仅在助手类的上下文内成为基类的扩展。最后一次查看代码将说明我的意思。

namespace LifeSimulation.Extensions {
    static class AppStateExtension {
        
        #region Extension methods

        public static void Spawn(this AppState appState, bool random) {
            if (random)
                appState.CreateRandomLife(10);
            else
                appState.CreateLifeAtCentre();
        }
                //...
        #endregion

        #region Private methods

        private static void CreateLifeAtCentre(this AppState appState) {
            appState.CreateLifeBlossom(
                appState.World.Space.GetLength(0) / 2,
                appState.World.Space.GetLength(1) / 2
            );
        }

        private static void CreateRandomLife(this AppState appState, int num) {
            for (int i = 0; i < num; i++)
                appState.CreateRandomLife();
        }
                //...
        #endregion
    }
}

关于扩展方法就说这么多,因为我们确实需要回到有趣的部分。

用户界面

如前所述,选择了一个简单的用户界面。主窗体大部分用于显示单元格。底部有一个简单的工具栏,包含信息和控件功能。

演示模式

有趣的部分来了。运行应用程序。选择一个缩放因子(这决定了每个单元格的大小以及可以在窗口中容纳多少个单元格)。然后最大化窗口。然后在选项下打开演示模式。现在坐下来观看表演。每半分钟将生成一个新的生命形态模型。有些生命形态会令人失望 - 它们在老化后消失,无法再生。检测到灭绝,演示会更早地启用另一个生命形态,因为您显然不想在剩下的半分钟里看着空白屏幕。

如果花足够的时间循环浏览其他生命形态,您很快就会看到一些辉煌的分形。有些生长迅速,有些生长缓慢,有些在几次生命爆发后灭绝。许多开始时非常有条理,随着反射到矩阵边界而逐渐失去条理,引入了混乱。您认为这是熵的一个很好的例证吗?

一些生命形态以彩色波的形式脉动,仿佛细胞的聚集体就像一个更高级物种的器官。与动植物的相似性很快就显现出来。大多数生命形态似乎是植物形的,但其他则显然是晶体形的。一些晶体生命形态看起来有点像《星际迷航》中的“博格”。另一种稀有现象是那些填充矩形区域并且不再进一步扩展的生命形态。这些矩形生命形态可以通过点击其边界附近来诱使它们进一步生长,然后矩形的新维度限制将缓慢地被吸收,就好像您给了它这样做一样。

继续并亲自发现这个微观世界。尝试随机选择(使用“New”功能)。或者,如果您认为自己能够承担上帝的责任,请编辑您的生命形态的“DNA”,看看是否能够创造出能够激发长时间讨论的生命。

案例研究

原始生命游戏

使用以下模型

ND=2 SI=3 DI=0,1,4,5,6,7,8 MA=99999

但我们不妨碍您欣赏图片。

博格案例

为了展示晶体类比,请尝试以下模型

ND=2 SI=2 DI=0,4,5,6,7,8,9,10,11 MA=50

Life19.png

或者您认为这类似于蠕虫或电子电路板?

教堂窗户案例

现在开始

ND=4 SI=1 DI=10,11,12,13,14,15,16,17,18,19,20 MA=200

并获得这种教堂窗户效果...

Life41.png

现在,当教堂窗户还活着时,将模型更改为

ND=4 SI=10,11,12,13,14,15,16,17,18,19,20 DI=0 MA=100

并观看整个场景爆炸...

Life42.png

然后几秒钟后,它似乎稳定在一个蓝色的主题中...

Life43.png

但只要有足够的时间,它就会再次爆发...

Life44.png

超新星

ND=4 SI=6,7,8 DI=5,9,10,11,12,13,14,15,16,17,18,19,20 MA=70

Life45.png

但静态图像无法充分展示这一点。试试看!

植物类比

要获得植物效果,请使用模型

ND=4 SI=6 DI=5 MA=70

Life1.png

但让我们不要通过预告所有案例来破坏您的乐趣。继续亲自发现它们。

结论

作者的絮叨暗示了这类动画图像能给观众带来的乐趣。不幸的是,本文包含的所有图像都是static的,没有应有的吸引力。

生命游戏决策因素的大多数变体产生的生命形态生成速度太快,无法引起我们的兴趣。正如您在其他文章中读到的那样,找到一个独特而迷人的公式是罕见的。然而,衰老机制的引入使我们能够重新考虑许多先前被我们拒绝的组合。

该软件允许您保留一份您最喜欢的生命形态列表。用户也可以随时编辑决策因素。死亡指标的哪些变化会导致癌症生命形态慢慢消失于虚无?

对于那些不想编译源代码的人,作者已在 Coded Silicon 网站上通过 ClickOnce 发布了此软件。但这需要 .NET 3.5。您可以在此处找到。

历史

  • 2008 年 5 月 26 日:初始版本
  • 2008 年 5 月 27 日:更新以防止空引用错误
© . All rights reserved.