DartでLifegame

表題通り、Dartでライフゲームを実装してみた。

環境

Dart Editor version 0.5.3_r22223
Dart SDK version 0.5.3.0_r22223

ソース

lifegame.html

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <title>Lifegame</title>
    <link rel="stylesheet" href="lifegame.css">
  </head>
  <body>
    <h1>Lifegame</h1>
    
    <canvas id="world" useMutation="1"></canvas>
    <div>
      <button id="btn_start">Start</button>
      <button id="btn_stop">Stop</button>
      <button id="btn_restart">Restart</button>
    </div>

    <script type="application/dart" src="lifegame.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>

lifegame.css

body {
  background-color: #F8F8F8;
  font-family: 'Open Sans', sans-serif;
  font-size: 14px;
  font-weight: normal;
  line-height: 1.2em;
  margin: 15px;
}

h1, p {
  color: #333;
}

#world {
  background-color:white;
}

lifegame.dart

import 'dart:html';
import 'dart:math';
import 'dart:async';

void main() {
  var model = new LifeWorldModel("#world", 35, 35);
  var pixell = new LifeWorldCellBase(10);
  var view = new LifeWorldView(model, pixell);
  view.setupButton("#btn_start", "#btn_stop", "#btn_restart");
}

class Color {
  final int _r, _g, _b;
  
  get r => this._r;
  get g => this._g;
  get b => this._b;
  
  Color(this._r, this._g, this._b);
  
  String toString() => 
      "rgb($r,$g,$b)";
}

class LifeCell {
  final int _x, _y;
  
  get x => this._x;
  get y => this._y;
  
  bool _live;
  get live => this._live;
  set live(value) {
    this._live = value;
  }
  
  LifeCell(this._x, this._y, [bool live = false]) {
    this._live = live;
  }
  
  String toString() =>
      this.live ? "■" : "□";
}

class LifeWorldModel {
  List<LifeCell> cells = new List<LifeCell>();
  
  final String name;
  final int _width, _height;
  
  get width => this._width;
  get height => this._height;
  
  bool _closed = true;
  get closed => this._closed;
  set closed(value) {
    this._closed = value;
  }
  
  LifeWorldModel(this.name, this._width, this._height) {
    for(var x=0; x<this._width; x++) {
      for(var y=0; y<this._height; y++) {
        var cell = new LifeCell(x, y);
        this.cells.add(cell);
      }
    }
    
    this.randomInit();
  }
  
  List<LifeCell> getNeighbors(int x, int y) {
    if(this.closed) {
      return this.getNeighborsClosed(x, y);
    }
    else {
      return this.getNeighborsOpened(x, y);
    }
  }
  
  /**
   * 端が閉じているモデル。
   * */
  List<LifeCell> getNeighborsClosed(int x, int y) {
    if(x == 0 || y == 0 || x == this.width - 1 || y == this.height - 1) {
      return this.cells.where(
          (LifeCell c) {
            bool xJudge;
            bool yJudge;
            if ((c.x == x && c.y == y) == false) {
              if (x == 0) {
                xJudge = (width - 1 <= c.x || c.x <= x + 1);
              }
              else if(x == width - 1) {
                xJudge = (x - 1 <= c.x || c.x <= 0);
              }
              else {
                xJudge = (x - 1 <= c.x && c.x <= x + 1);
              }
              if (y == 0) {
                yJudge = (height - 1 <= c.y || c.y <= y + 1);
              }
              else if(y == height - 1) {
                yJudge = (y - 1 <= c.y || c.y <= 0);
              }
              else {
                yJudge = (y - 1 <= c.y && c.y <= y + 1);
              }
              return ( xJudge && yJudge );
            }
            else {
              return false;
            }
          }
      ).toList();
    }
    else {
      return this.getNeighborsOpened(x, y);
    }
  }
  
  /**
   * 端が閉じていないモデル。
   * */
  List<LifeCell> getNeighborsOpened(int x, int y) =>
    this.cells.where(
        (LifeCell c) => (c.x == x && c.y == y) == false
                        && (x - 1 <= c.x && c.x <= x + 1)
                        && (y - 1 <= c.y && c.y <= y + 1)
        ).toList();
  
  /**
   * ランダムにセルを生成。
   * */
  void randomInit([int seed = null]) {
    Random r;
    if(seed == null) {
      r = new Random(seed);
    }
    else {
      r = new Random();
    }
    var max = this.cells.length;
    for(var i=0; i<max; i++) {
      this.cells[i].live = (r.nextInt(100) % 2 == 1) ? true : false;
    }
  }
  
  /**
   * 世代を進めて、残りライフを返す。
   * */
  int step() {
    int max = this.cells.length;
    List<LifeCell> newCells = [];
    int lifeCount = 0;
    
    this.cells.forEach( (LifeCell thiscell) {
      List<LifeCell> neighbors = this.getNeighbors(thiscell.x, thiscell.y);
      int liveCount = 0;
      int deafCount = 0;
      bool live = false;
      if(neighbors.length > 0) {
        neighbors.forEach((c) {
          liveCount += c.live ? 1 : 0;
          deafCount += c.live ? 0 : 1;
        });
        
        if(thiscell.live) {
          if(liveCount == 2 || liveCount == 3) {
            live = true;
          }
          else if(liveCount <= 1) {
            live = false;
          }
          else if(liveCount >= 4) {
            live = false;
          }
        }
        else {
          if(liveCount == 3) {
            live = true;
          }
        }
      }
      
      newCells.add(new LifeCell(thiscell.x, thiscell.y, live));
      if(live) lifeCount++;
      
    } );
    this.cells = newCells;
    return lifeCount;
  }
  
  String toString() {
    for(int y=0; y<this._height; y++) {
      StringBuffer sb = new StringBuffer();
      var cells = this.cells.where((c) => c.y == y)
                  .forEach((LifeCell c) => sb.write(c));
      if(sb.length > 0) print(sb);
    }
  }
}

class LifeWorldCellBase {
  final int cellPixell;
  LifeWorldCellBase(this.cellPixell);
}

class LifeWorldView {
  final LifeWorldModel world;
  final LifeWorldCellBase cellBase;
  
  CanvasElement canvas;
  CanvasRenderingContext2D graphics;
  
  Color deadCellBgColor = new Color(255, 255, 255);
  Color deadCellBorderColor = new Color(192, 192, 192);
  Color liveCellBgColor = new Color(0, 0, 0);
  Color liveCellBorderColor = new Color(0, 0, 0);
  
  LifeWorldView(this.world, this.cellBase) {
    this.canvas = document.query(this.world.name);
    this.canvas.width = this.world.width * this.cellBase.cellPixell;
    this.canvas.height = this.world.height * this.cellBase.cellPixell;
    this.graphics = this.canvas.getContext("2d");
  }
  
  List<Color> getLiveCellColor() => 
      [this.liveCellBgColor, this.liveCellBorderColor];
  
  List<Color> getDeadCellColor() =>
      [this.deadCellBgColor, this.deadCellBorderColor];
  
  void createCell(int x, int y, Color bgColor, Color borderColor) {
    int basex = x * this.cellBase.cellPixell;
    int basey = y * this.cellBase.cellPixell;
    int w = this.cellBase.cellPixell;
    
    this.graphics.fillStyle = bgColor.toString();
    this.graphics.fillRect(basex, basey, w, w);
    
    this.graphics.beginPath();
    this.graphics.moveTo(basex, basey);
    this.graphics.lineTo(w + basex, basey);
    this.graphics.lineTo(w + basex, w + basey);
    this.graphics.lineTo(basex, w + basey);
    this.graphics.lineTo(basex, basey);
    this.graphics.strokeStyle = borderColor.toString();
    this.graphics.lineWidth = 1;
    this.graphics.stroke();
  }
  
  void createDefaultCells(int width, int height) {
    var deadCellColor = this.getDeadCellColor();
    Color bgColor = deadCellColor[0];
    Color borderColor = deadCellColor[1];
    for(var i=0; i<width; i++){
      for(var j=0; j<height; j++){
        this.createCell(i, j, bgColor, borderColor);
      }
    }
  }
  
  void show() {
    var max = this.world.cells.length;
    var deadColor = this.getDeadCellColor();
    var liveColor = this.getLiveCellColor();
    for(var i=0; i<max; i++){
      var color;
      var cell = this.world.cells[i];
      if(cell.live){
        color = liveColor;
      }
      else{
        color = deadColor;
      }
      this.createCell(cell.x, cell.y, color[0], color[1]);
    }
  }
  
  int step() {
    return this.world.step();
  }
  
  bool _workable = false;
  get workable => this._workable;
  set workable(value) {
    this._workable = value;
  }
  
  Timer timer;
  void start() {
    if(this.workable) {
      timer = new Timer(const Duration(milliseconds: 100), (){
        this.show();
        int lifeCount = this.step();
        if(lifeCount > 0) {
          this.start();
        }
      });
    }
  }
  
  void stop(){
    this.workable = false;
    //if(timer != null) timer.cancel();
  }
  
  void setupButton(String startButtonName, String stopButtonName, String resetButtonName){
    ButtonElement btnStart = document.query(startButtonName);
    ButtonElement btnStop = document.query(stopButtonName);
    ButtonElement btnReset = document.query(resetButtonName);
    
    // イベント設定
    btnStart.onClick.listen((e){
      this.workable = true;
      this.start();
      btnStart.attributes["disabled"] = "true";
      btnStop.attributes.containsKey("disabled");
      btnStop.attributes.remove("disabled");
    });
    btnStop.onClick.listen((e){
      this.workable = false;
      this.stop();
      btnStart.attributes.containsKey("disabled");
      btnStart.attributes.remove("disabled");
      btnStop.attributes["disabled"] = "true";
    });
    btnReset.onClick.listen((e) {
      this.world.randomInit();
      this.createDefaultCells(this.world.width, this.world.height);
      btnStart.click();
    });
    
    btnStop.attributes["disabled"] = "true";
  }
}

スーパーpre記法がDartには対応してなかったみたいなので、Javaにしてみたけど割と大丈夫そう。

結論

やっぱりDartとして動かすときびきびしていて、JavaScriptに変換して動かすと多少もっさりしている。
文法自体はストレスが少なくていいけど、ドキュメントが(発展途中ということもあって)少なめなのがよろしくない。
とはいえ、DartEditorでクラスメソッドの定義なんかも追いやすいので、そんなに困らないかも。普通にAPIリファレンス追ってもいいし(http://api.dartlang.org/docs/releases/latest/)。