获得生命

2021-03-01 04:14:50

当我还是个孩子的时候,我写了一个BASIC程序来探索我的8088上Conway的《生命游戏》。它非常缓慢,所以我开始重新编写程序集。但是我在Life上失败了:我无奈地放弃了该项目,因为我一直冻结自己的系统。

也一样。我相信,我唯一想到的优化是纠缠不清,并专注于活细胞或活细胞周围。因此,当我后来了解到比尔·戈斯珀(Bill Gosper)的Hashlife算法时,我感到惊讶,该算法奇迹般地模拟了线性时间内的指数世代。世界上所有乱七八糟的事情都无法接近。

我热切地研究了我能找到的任何解释。不幸的是,尽管Hashlife的成分是孤立的基本成分(四叉树;记忆;实习),但混合物的某些方面让我感到困惑。我希望有一天能全力以赴,并在“快车道”中运行“人生”。

我终于实现了这一毕生的野心。也许我是出于愤怒而采取的行动,就像我不时地看到一则有关编写《生命游戏》的文章,只是发现潜伏在其中的幼稚方法。严重地?!谁在乎elegantsource,或者算法是否中等,谁在乎呢? [我夸大了;实际上,Ienjoy在APL的一行中看到了生命游戏,并且在生命游戏本身等方面都带有荣誉感。]

在某些时候,更容易放弃抱怨并反击。然后,这里还有另一篇关于《生活游戏》编程的文章,但其中少数人具有Hashlife的特征。包括电池:我们遍历了上面的演示的整个资源。

{-#语言CPP#-}#ifdef __HASTE__ {-#语言PackageImports#-}#endif {-#语言LambdaCase,TupleSections,RecordWildCards#-} {-#语言FlexibleContexts#-}#ifdef __HASTE__ import" mtl& #34; Control.Monad.State.Strict import Haste.DOM import Haste.Events import Haste.Graphics.Canvas import Data.IORef import Text.Read(readMaybe)#else import Control.Monad.State.Strict#endif import Data.List(找到)导入Data.Map(Map,(!))将合格的Data.Map导入为M

生命发生在无限的细胞网格上,每个细胞都死了或还活着。一个细胞的邻居是围绕它的八个细胞(摩尔区)。然后:

我们用0表示死单元,用1表示活单元。以下函数汇总了规则:

nextLife :: Int->整数-> Int nextLife 0 3 = 1 nextLife 1 2 = 1 nextLife 1 3 = 1 nextLife _ _ = 0

奇怪的是,这与说这两个数字的总和或乘积为3时一个单元确切地还活着是一样的。

四叉树是Hashlife的基础。在我们的渲染图的最低层,我们用一点表示单个单元格。

在以上级别,我们将单元组织为2x2块。我们用一个整数表示一个块的4位。我们提供在2x2块中的Int和the4单元之间转换的函数:

数据ZNode = ZNode Int Int Int Int派生(Show,Eq,Ord)pack4 :: Int->整数->整数->整数->整数pack4 a b c d = a + 2 * b + 4 * c + 8 * d unpack4 ::整数-> ZNode unpack4 n = ZNode(f 1)(f 2)(f 4)(f 8)其中f k = div n k`mod`2

我们以Z顺序记录2x2块的4个像元。如果我们想象一个指南针,那么顺序是NW,NE,SW,SE,这与笔写字母Z时笔的行进顺序相同。我们使用屏幕坐标。

在上面的下一个级别,我们将2x2块本身组织为2x2superblocks。这些被表示为ZNode。每个数字代表Z顺序的2x2单元格块。

考虑一个带标记的单元格的4x4块,其中每个单元格的状态都用一个位表示:

我们用ZNode a b c d表示以上内容,其中a是二进制表示a3 a2 a1 a0的Int,对于其他字段也类似。

给定一个4x4的单元块,以下代码返回下一代2x2的中心单元块。规则暗示4x4网格之外的任何像元都不会影响此计算。他们还暗示,这个2x2中心块是我们可以可靠地计算的下一代的唯一部分。

base :: Int->整数->整数->整数-> Int base abcd = pack4 nw其他地方,其中ZNode a0 a1 a2 a3 = unpack4 a ZNode b0 b1 b2 b3 = unpack4 b ZNode c0 c1 c2 c3 = unpack4 c ZNode d0 d1 d2 d3 = unpack4 d nw = nextLife a3 $ sum [a0 ,a1,b0,a2,b2,c0,c1,d0] ne = nextLife b2 $ sum [a1,b0,b1,a3,b3,c1,d0,d1] sw = nextLife c1 $ sum [a2,a3,b2 ,c0,d0,c2,c3,d2] se = nextLife d0 $ sum [a3,b2,b3,c1,d1,c3,d2,d3]

我们设计了一种将ZNode表示为Int的方法,以便将8x8的像元块表示为ZNode,其字段现在指的是4x4像元。我们可以进一步:通过归纳,对于任何n,我们都可以用ZNode表示任何2 n x 2 n单元格块,我们将其解释为2 n-1 x 2 n-1单元格的2x2块。

我们有时将此称为n级。它不存储在ZNode本身中,因此采用ZNode的函数通常需要另一个包含该级别的参数。

我们只存储一个ZNode的副本(请考虑哈希约束;字符串实习;最大共享; flyweight模式)。生活模式通常是重复的,因此少量的ZNode有时足以表示许多单元。

尽管我们将使用有序树而不是哈希将ZNode映射到Ints,但这就是Hashlife的名称。

我们维护ZNodes到Ints的Data.Map。遇到ZNode时,我们首先检查该地图:如果已经存在,则重用其对应的Int,否则我们为其分配一个未使用的Int并将其添加到地图中。整数从16开始,以避免与代表2x2单元块的数字发生冲突。

另外,每个ZNode都与一个Int配对,该Int代表其Futurecenter;见下文。

空单元格是一种特殊情况。 Int 0表示任何级别的空正方形。当我们要在大型空间中嵌入给定模式时,这会派上用场。

数据Mem = Mem(Map Int(ZNode,Int))(Map ZNode Int)派生Show initMem :: Mem initMem = Mem mempty mempty实习生:: ZNode->整数->状态Mem Int实习生z future = do mem m idxs<-让let next = M。大小idxs + 16 put $ Mem(M.插入next(z,future)m)$ M。插入z next idxs纯净的next调用:: Int->状态Mem(ZNode,Int)调用0 =纯(ZNode 0 0 0 0,0)调用k =(\(Mem m _)-> m!k)< $>得到

这是一本可能会出现在儿童书籍中的难题。在4x4网格中,您可以看到多少2x2框?

+-+-+ +-+-+ +-+-+ | a0 | a1 | | a1 | b0 | | b0 | b1 | +-+-+ +-+-+ +-+ | + a2 | a3 | | a3 | b2 | | b2 | b3 | +-+-+ +-+-+ +-+-++-+-+ +-+-+ +-+-+ | a2 | a3 | | a3 | b2 | | b2 | b3 | +-+-+ +-+-+ +-+-+ | c0 | c1 | | c1 | d0 | | d0 | d1 | +-+-+ +-+-+ +-+-++-+-+ +-+-+ +-+-+ | c0 | c1 | | c1 | d0 | | d0 | d1 | +-+-+ +-+-+ +-+ | + c2 | c3 | | c2 | d0 | | d2 | d3 | +-+-+ +-+-+ +-+-+

如果我们将每个2x2框替换为以同一点为中心的单个1x1框,则它们不再重叠,而是完美平铺以制作3x3网格:

同样,如果我们将每个2x2框替换为以同一点为中心的单个1x1框,则它们将完美平铺:

总而言之,我们从4x4网格开始,到中间的2x2网格结束。 Hashlife将这一过程与时间旅行结合在一起。

我们的基本函数采用4x4单元格,并将中心的2x2单元格返回到下一代。

假设我们得到了8x8的单元格。我们将它们视为2x2单元格的4x4网格,并按照上述过程进行操作,只要我们希望将2x2框(测量4x4单元格)替换为1x1框(测量2x2单元格),就调用基本函数。 3x3网格,并推进另一代技术以达到2x2网格。

总而言之,给定8x8单元,重复调用base可以揭示未来2代的中心4x4cell。

通过归纳,我们可以对2的高次幂执行相同的操作。例如,给定256x256单元,一堆递归使我们未来的核心128x128单元精确地达到了64代。

我们拥有完美的记忆条件。因此,我们第一次计算未来的中心细胞,然后每次都查找它们。这就是为什么我们将每个ZNode与一个Int相关联的原因;后者指的是未来的中心细胞。

我们拆分了一个将3x3网格缩小为2x2网格的功能,因为稍后它将有更多用途。 reduce3x3函数将函数f应用于3x3网格中的每个2x2框,然后将g应用于其结果。

gosper :: Int->整数->整数->整数->整数->状态Mem Int闲聊0 abcd =纯$基本abcd闲聊nabcd = do(ZNode _ a1 a2 a3,x0)<-召回a(ZNode b0 _ b2 b3,x2)<-召回b(ZNode c0 c1 _ c3, x6)<-调用c(ZNode d0 d1 d2 _,x8)<-调用d x1<-rec a1 b0 a3 b2 x3<-rec a2 a3 c0 c1 x4<-rec a3 b2 c1 d0 x5< ; -REC b2 b3 d0 d1 x7<-rec c1 d0 c3 d2 reduce3x3 rec(备忘n)x0 x1 x2 x3 x4 x5 x6 x7 x8 x8其中rec abcd =备忘nabcd> == fmap snd。召回reduce3x3 fg x0 x1 x2 x3 x4 x5 x6 x7 x8 = do y0 2 ^ k> = n)[0 ..])-1 sz = loggish $ max (ymax-ymin)(xmax-xmin)+ 1 enc _(ox,oy)|牛xmax || oy> ymax =纯0 enc n(ox,oy)= mapM go zorder> ==(\ [a,b,c,d]->备忘录n a b c d)其中go(dx,dy)| n == 0 =纯$ fromEnum $ elem(ox + dx,oy + dy)ps |否则= enc(n-1)(ox + dx * p,oy + dy * p)p = 2 ^ n

我们想与此函数取反,但首先要解决一个烦恼。对于最低级别的四叉树节点,令人讨厌的是我们将数字分成4位而不是在地图上查找。我们添加了一个函数来隐藏它:

访问:: Int-> Zem的状态Mem访问k | k < 16 =纯$ unpack4 k |否则= fst< $>召回k

列出给定模式的活动单元的坐标现在是另一种直接递归:

点数::生活-> [(Int,Int)]点Life {..} = evalState(coords lifeSize lifeOrigin lifeIndex)lifeMemory其中coords :: Int-> (Int,Int)->整数->状态Mem [(Int,Int)]坐标_ 0 =纯[]坐标n(x,y)k =做ZNode a b c d<-访问k concat< $> zipWithM go [a,b,c,d] zorder其中go p(dx,dy)| n == 0 =纯$如果p == 0,则[]否则[(x + dx,y + dy)] |否则=坐标(n-1)(x + e * dx,y + e * dy)p e = 2 ^ n

让我们预测未来!下面的函数采用2 n + 1 x 2 n + 1模式,并返回其2 n x 2 n中心,即未来的2 n-1代,我们只提取了存储的未来中心索引并将其重新打包:

scry ::生活-> Life scry Life {..} =生命n(ox + p,oy + p)x lifeMemory其中(ox,oy)= lifeOrigin n = lifeSize-1 p = 2 ^ n x = snd $ evalState(回收lifeIndex)lifeMemory

返回单点[(1,2)]。 r-pentomino的大小为4x4正方形,因此scry将中间的2x2正方形1代返回到未来,结果是正确的,但令人难以置信。

为了进一步穿越时空,我们在调用scry之前反复用空白方块填充图案。在这里,我们发现0代表任何大小的空正方形都很方便。为了代码重用,为了使模式的尺寸加倍,我们用空正方形将其围绕起来以形成3x3网格,然后以中间的方式减少,该函数采用4x4网格并丢弃除中心2x2网格以外的所有内容。

垫::生活->救生垫Life {..} = Life {lifeSize = n,lifeOrigin =(ox-p,oy-p),lifeIndex = i' ,lifeMemory = st}其中(ox,oy)= lifeOrigin p = 2 ^ lifeSize n = lifeSize + 1 i = lifeIndex(i',st)= runState(reduce3x3(Middle n)(memo n)0 0 0 0 i 0 0 0 0)lifeMemory中间:: Int->整数->整数->整数->整数->状态Mem Int中间nabcd =做ZNode _ _ _ a3<-访问ZNode _ _ b2 _<-访问b ZNode _ c1 _ _ _<-访问c ZNode d0 _ _ _ _<-访问d备忘录(n -1)a3 b2 c1 d0

让我们绘制中心的64x64单元格,而不是返回点列表:

#ifndef __HASTE__ main :: IO()main = putStr $ unlines $ [[[如果elem(c,r)ps则'#'其他' | c<-[[-32 .. 32]] | r<-[--32 .. 32]]其中ps =点$ scry $迭代垫(使rPentomino充满活力)! 10#endif

我们应该看到什么?好吧,我们从4x4模式开始,然后填充10次,得到2 12 x 2 12模式。 Hashlife应该将Central2 11 x 2 11个单元格2 10 = 1024代返回到将来,我们将显示Central 64x64单元格:

#########################################################################################################################中####### ######################################## ##

这在我的笔记本电脑上花了不到四分之一秒。显然,我们每代花费不到250微秒。

如果将10提升到20,我们发现运行时间几乎没有变化,因此我们以某种方式击败了250纳秒的一代。将其增加一些,看来我们可以在不到CPU时钟周期的情况下计算每一代!当然,不现实的情况已经稳定了,我们只是在回收已记录的答案。

Hashlife非常适合探索遥远的未来,但是如果我们想慢慢来,并观察一次一代人的变化,该怎么办?

没问题。除了在每个简化步骤中将2代幂的能量传递到未来的步骤中,我们仅需传递0代能量,除了基本情况与之前相同外,即它提前计算了1代能量。

更一般而言,要对任意k一次前进2 k世代,我们递减0代,直到我们减小到期望的水平,然后切换到原始的Hashlife算法。

我们从更通用的gosper版本开始,就像reduce3x3的老兄一样:该函数将2x2窗口滑动到4x4网格上,并对每个函数应用函数f,然后将g应用于3x3结果。

我们可以重写gosper来使用此功能,但这将导致更多查找,并且我们希望上面的代码独立存在。

reduce4x4 fgabcd =做ZNode a0 a1 a2 a3<-访问ZNode b0 b1 b2 b3<-访问b ZNode c0 c1 c2 c3<-访问c ZNode d0 d1 d2 d3<-访问d x0<-f a0 a1 a2 a3 x1<-f a1 b0 a3 b2 x2<-f b0 b1 b2 b3 x3<-f a2 a3 c0 c1 x4<-f a3 b2 c1 d0 x5<-f b2 b3 d0 d1 x6< ;-f c0 c1 c2 c3 x7<-f c1 d0 c3 d2 x8<-f d0 d1 d2 d3 g x0 x1 x2 x3 x4 x5 x6 x7 x8

我们从递归地减少中间开始,因为这个函数提取没有时间旅行的中心单元。达到k级后,我们向上看将来的中心。

宝贝:: Int->生活-> Life baby k Life {..} = Life {lifeSize = sz,lifeOrigin =(ox + p,oy + p),lifeIndex = i' ,(lifeMemory = st}其中(ox,oy)= lifeOrigin sz = lifeSize-1 p = 2 ^ sz go _ 0 0 0 0 =纯0 go n a b c d | n< = k =备忘录(n + 1)a b c d> = fmap snd。召回否则= reduce4x4(中间n)(reduce3x3(go(n-1))(备忘n))abcd(i',st)= runState(访问lifeIndex>>>> = \(ZNode abcd)-> go sz abcd)lifeMemory

图案的边缘上可能有活细胞,因此在执行步骤之前,请用足够的空细胞填充它,以确保还原后不会丢失活细胞。至少,这需要将两个尺寸都增加三倍。

之后,多余的空间可能被证明是不必要的。在交互式设置中,在前进了几代之后,将分别显示图案,因此为了避免失控的填充,我们会在可能的情况下缩小图案。我们在4x4网格中寻找a2x2框,以使框外的所有单元格都失效。如果一个人存在,那么我们将丢弃该框以外的所有内容并递归。

收缩::生活->寿命缩减寿命{..} =非货币($)$ runState(go lifeSize lifeOrigin lifeIndex)lifeMemory其中fabcd = pure $ ZNode abcd zsum(ZNode abcd)= a + b + c + d go 0 dk = pure $ Life 0 dk go n(dx,dy)k =做ZNode abcd<-访问k reduce4x4 fgabcd其中g x0 x1 x2 x3 x4 x5 x6 x7 x8 = let tot = sum $ zsum< $> [x0,x2,x6,x8] xs = [x0,x1,x2,x3,x4,x5,x6,x7,x8] xds = zip xs [0 ..]以防万一((tot ==)。 。fst)Just的xds(ZNode abcd,i)-> let(y,x)= divMod i 3 in go(n-1)(dx + x * 2 ^(n-1),dy + y * 2 ^(n-1))=<<备忘录(n-1)a b c d无-> pure $ Life n(dx,dy)k run :: Int->生活->生命运行lf @ Life {..} =缩小$宝贝k $重复填充lf! n其中n =最大值2 $ k +1-lifeSize

我们的显示仅限于一个视口,因此我们改进了points函数以修剪给定矩形之外的单元格。我们还使用差异列表来避免代价高昂的串联。

-| 假设x0 y0为偶数,x1 y1为奇数,x0< x1,y0 < y1。 作物::(Int,Int)-> (Int,Int)-> 生活-> [(Int,Int)]作物(x0,y0)(x1,y1)Life {..} = evalState(go lifeSize lifeOrigin lifeIndex)lifeMemory []其中go _ _ 0 =纯id go n(x,y)k | x> x1 || & y1 || x + 2 * e< = x0 || y + 2 * e< = y0 =纯ID | 否则= ZNode a b c d<-访问k文件夹(。)id< $>。 zipWithM f [a,b,c,d] zorder其中f p(dx,dy)| n == 0 =纯$如果p == 0,则返回id,否则((x + dx,y + dy):)| 否则= go(n-1)(x + e * dx,y + e * dy)p e = 2 ^ n 最后,我们在此页面顶部为交互式演示拼凑了一个临时用户界面: #ifdef __HASTE__ main :: IO()main = withElems [" canvas" ,&#34 ......