关于复杂自适应系统的一切曾经是学生时代的最爱,其中一个很有代表性的原型就是这种叫做“生命游戏”的元胞自动机。后来由于种种原因,并没有把这个兴趣深入延续下去。最近在看一本关于HTML5的书,读到关于canvas的一章,突然有点手痒,花了一个下午的时间,用Javascript写了一个简单的生命游戏。

记得在学校的时候,也用ASP和Javascript来写过一个:ASP用来动态生成table,Javascript用来控制其中的td的背景色,在IE4上运行。相比现在的HTML5,那实在是一种繁琐和简陋的尝试。当时,由于对这些简单的数学程序背后隐藏的复杂世界的着迷,还用VB6开发过一些可以在网页上运行的ActiveX控件,可以通过调整方程的参数生成一些分形图画。其中一部分控件用的是当时微软大力鼓吹的ActiveDocument技术(即OLE的Web翻版),现在已经没有多少人记得了。如今,HTML5终于横空出世,并迅速普及,那么再过10年,是否还会有人记得现在的Flash、Sliverlight和Ajax呢?

在近期的展会上,基于HTML5的PACS/RIS已经横空出世。在iPad上达到医用质量的图像显示,至少软件层面的障碍已经清除。也许某一天,HTML5 API真的可以成为一个新的,更有生命力的行业标准,以及一个一统Web前端应用领域的开发平台。而在此过程中蕴藏的种种机会,正等待着大家去挖掘。

下面是生命游戏的完整源码,感兴趣的朋友可以复制粘贴到本地一个html文件中,用IE9以上版本打开。我在自己的机器(Intel i3 M350)上发现用IE9运行时确实比较慢,每一代之间都有明显的短暂停顿,CPU占用率在25%以上。但完全相同的代码,在Opera10上运行就快得多,几乎感觉不到停顿,CPU占用率在25%以下。我在网上看到国外某大学学生的作品,在IE9上跑速度比我的快很多,估计做了一些算法上的优化。据说IE10对Javascript引擎做了很大的性能提升,我原本不是个热衷于升级软件的人,但现在却莫名地冲动。

<!DOCTYPE html>  
<html>  
    <head>  
        <meta charset="utf-8"/>  
        <title>Conway's Game of Life</title>  
        <style type="text/css" mce_bogus="1">  
            table {margin:auto;}  
                </style>  
    </head>  
    <body>  
        <table>  
            <tr align="center"><td colspan="2"><h3>Conway's Game of Life</h3><hr height="1px"/></td></tr>  
            <tr>  
                <td align="left">Generation: <span id="spanGen">0</span></td>  
                <td align="right">  
                    <button id="btnStart" onclick="startGame();return true;">Start</button>  
                    <button id="btnPause" onclick="pauseGame();return true;" disabled="disabled">Pause</button>  
                    <button id="btnReset" onclick="resetGame();return true;" disabled="disabled">Reset</button>  
                    <button id="btnRandom" onclick="randomGame();return true;">Random</button>  
                    <button id="btnHelp" onclick="help();return true;">What's this?</button>  
                </td>  
            </tr>  
            <tr align="center">  
                <td colspan="2"><canvas id="space" width="500" height="500" style="border: 1px solid; background-color: #eeeeee;" /></td>  
            </tr>  
            <tr align="center">  
                <td colspan="2">&copy; May, 2011, lifegame@263.net</td>  
            </tr>  
        </table>  
    </body>  
    <script type="text/javascript">  
        var canvas = document.getElementById('space');  
        var context = canvas.getContext('2d');  
        var timerInterval = 10; // ms  
        var cellWidth = 10; // please ensure: cellWidth > 2  
        var cellXLen = 50;  // please ensure: cellWidth * cellXLen = 500  
        var cellYLen = 50;  // please ensure: cellWidth * cellYLen = 500  
        var cells = [];  
        var running = 0;  
        var generation = 0;  
          
        function drawCell(x,y,state){  
              var cx = x * cellWidth;  
              var cy = y * cellWidth;  
                if(state && state==1){  
                    context.fillStyle = "Gold";  
                    context.fillRect(cx, cy, cellWidth, cellWidth);  
                    context.strokeStyle = "DarkGoldenRod";  
                    context.strokeRect(cx+1, cy+1, cellWidth-2, cellWidth-2);  
                }  
                else{  
                    context.clearRect(cx, cy, cellWidth, cellWidth);  
                }  
        }  
          
        function drawPatterns(){  
            function setCell(x,y){  
                cells[[x,y]] = 1;  
              drawCell(x, y, 1);  
            }  
            function drawGliderPattern(){  
                setCell(1,0);  
                setCell(2,1);  
                setCell(2,2);  
                setCell(1,2);  
                setCell(0,2);  
            }  
            drawGliderPattern();  
        }  
          
        function applyRule(x,y){  
                var neighbours = [];  
                var neighbourCount = 0;  
                var currentState = cells[[x,y]];  
                var nextState = 0;  
                neighbours[0] = cells[[(x-1+cellXLen)%cellXLen, (y-1+cellYLen)%cellYLen]];  
                neighbours[1] = cells[[(x-1+cellXLen)%cellXLen, (y+1+cellYLen)%cellYLen]];  
                neighbours[2] = cells[[(x+cellXLen)%cellXLen, (y+1+cellYLen)%cellYLen]];  
                neighbours[3] = cells[[(x+cellXLen)%cellXLen, (y-1+cellYLen)%cellYLen]];  
                neighbours[4] = cells[[(x+1+cellXLen)%cellXLen, (y+1+cellYLen)%cellYLen]];  
                neighbours[5] = cells[[(x+1+cellXLen)%cellXLen, (y-1+cellYLen)%cellYLen]];  
                neighbours[6] = cells[[(x+1+cellXLen)%cellXLen, (y+cellYLen)%cellYLen]];  
                neighbours[7] = cells[[(x-1+cellXLen)%cellXLen, (y+cellYLen)%cellYLen]];  
                for(i=0;i<8;i++){  
                    state = neighbours[i];  
                    if(state && state==1) neighbourCount++;  
                }  
                if(currentState && currentState==1){  
                    if(neighbourCount<2 || neighbourCount>3) return 0;  
                    else return 1;  
                }  
                else{  
                    if(neighbourCount==3) return 1;  
                    else return 0;  
                }  
        }  
          
        function loadGame(){  
            canvas.onmousedown = function(e){  
                if(running==1) return;  
                if(e.offsetX) {  
                              x = e.offsetX;  
                              y = e.offsetY;  
                        }  
                        else if(e.layerX) {  
                          x = e.layerX;  
                          y = e.layerY;  
                        }  
                x = Math.floor(x / cellWidth);  
                y = Math.floor(y / cellWidth);  
                state = cells[[x,y]];  
                if(state && state==1)  
                {  
                    cells[[x,y]] = 0;  
                    drawCell(x, y, 0);  
                }  
                else  
                {  
                    cells[[x,y]] = 1;  
                    drawCell(x, y, 1);  
              }  
            }  
            drawPatterns();  
        }  
          
        function startGame(){  
            function runGame(){  
                var nextgen = [];  
                for(x=0;x<cellXLen;x++){  
                    for(y=0;y<cellYLen;y++){  
                        nextgen[[x,y]] = applyRule(x,y);  
                    }  
                }  
                for(x=0;x<cellXLen;x++){  
                    for(y=0;y<cellYLen;y++){  
                        cells[[x,y]] = nextgen[[x,y]];  
                    }  
                }  
                for(x=0;x<cellXLen;x++){  
                    for(y=0;y<cellYLen;y++){  
                        drawCell(x, y, cells[[x,y]]);  
                    }  
                }  
                generation++;  
                spanGen.innerHTML = generation;  
              if(running==1) setTimeout(runGame, timerInterval);      
            }  
            btnStart.disabled = true;  
            btnPause.disabled = false;  
            btnReset.disabled = true;  
            btnRandom.disabled = true;  
            running = 1;  
            runGame();  
        }  
          
        function pauseGame(){  
            running = 0;  
            btnStart.disabled = false;  
            btnPause.disabled = true;  
            btnReset.disabled = false;  
            btnRandom.disabled = false;  
        }  
          
        function resetGame(){  
            for(x=0;x<cellXLen;x++){  
                    for(y=0;y<cellYLen;y++){  
                        cells[[x,y]] = 0;  
                        drawCell(x,y,0);  
                    }  
                }  
            drawPatterns();  
            generation = 0;  
            spanGen.innerHTML = generation;  
        }  
          
        function randomGame(){  
            for(x=0;x<cellXLen;x++){  
                    for(y=0;y<cellYLen;y++){  
                        s = (Math.random()>=0.8) ? 1 : 0;  
                        cells[[x,y]] = s;  
                        drawCell(x,y,s);  
                    }  
                }  
            generation = 0;  
            spanGen.innerHTML = generation;  
        }  
          
        function help(){  
            window.open("http://en.wikipedia.org/wiki/Conway's_Game_of_Life");  
        }  
              
        window.addEventListener("load", loadGame, true);  
 </script>  
<html>  
下面是程序在Opera10上运行的截图,细心的读者会发现,在暂停状态下还可以用鼠标来种植新细胞,您可以动手创造一些有着有趣演化规律的细胞群。