githubのcalendarでライフゲーム

先日、宅配便が届くまで暇だったので、github calendarで動くライフゲームをchrome extensionsで動作するように書きました。


# github calendar

![image.png](https://qiita-image-store.s3.amazonaws.com/0/110426/41192fb8-7429-968f-2e72-ef4c88e1d287.png)

githubのユーザーページにある 「** contributions in the last year」 と表記されているカレンダーです。
このスカスカなcalendarでも数十世代分は動いてくれます。

![cells.gif](https://qiita-image-store.s3.amazonaws.com/0/110426/8c701b1d-77be-b1ea-23f0-910a002b320b.gif)

# ライフゲーム

基本的には、(conwayの)ライフゲーム（[wikipedia](https://www.wikiwand.com/ja/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)）のルールを踏襲しています。
一点違う点としては、セルにライフが付いている点です。
githubのcalendarのセル（カレンダーの一日分）にはcontributionの頻度によって、以下の５つの状態があります。

- `#ebedf0`
- `#c6e48b`
- `#7bc96f`
- `#239a3b`
- `#196127`

これらの状態をそれぞれとして

- `#ebedf0` → ライフ0(死)
- `#c6e48b` → ライフ1(生)
- `#7bc96f` → ライフ2(生、死ステータスへの変更後はライフ1状態へ移行）
- `#239a3b` → ライフ3(　　　　　　　　〃　　　　　ライフ2状態へ移行）
- `#196127` → ライフ4(　　　　　　　　〃　　　　　ライフ3状態へ移行）

セルの死亡判定時には、ライフが減っていく処理にしました。


# 動かし方

[uitspitss/github-calendar-lifegame](https://github.com/uitspitss/github-calendar-lifegame)

上記のリポジトリをクローンします。
そして、 Google Chromeで `chrome://extensions/` のページに行き、 `Load unpacked(パッケージ化されていない拡張機能を読み込む)` をクリックして、クローンしてきたディレクトリ(フォルダ)を選択。
あとは、github上の任意のユーザーページに飛べば動くと思います。


# コード

- github
[uitspitss/github-calendar-lifegame/content_script.js](https://github.com/uitspitss/github-calendar-lifegame/blob/master/content_script.js)

- code

```javascript
window.addEventListener("DOMContentLoaded", function(){
  if(!document.querySelector('.js-calendar-graph-svg')) return;
  const nickname = document.querySelector('.p-nickname').innerHTML;

  const LIFE = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
  let board = [];

  const push_banpei = () => {
    let l = [];
    for(let i=0; i < 9; i++){
      l.push({
        life: 0,
      });
    }
    board.push(l);
  };

  // initialize board
  push_banpei();
  document.querySelectorAll('g > g').forEach(g => {
    let week_rects = [];
    week_rects.push({life: 0});
    g.querySelectorAll('rect').forEach(rect => {
      week_rects.push({
        life: LIFE.findIndex(life => life === rect.getAttribute('fill')),
        date: rect.dataset.date
      });
    });
    week_rects.push({life:0});
    board.push(week_rects);
  });
  for(let i=0; i < 6 - new Date().getDay(); i++){
    board[board.length-1].push({
      life: 0
    });
  }
  push_banpei();

  // console.log(board);

  const loop = () => {
    let past_board = JSON.parse(JSON.stringify(board));
    let alive_num = 0;

    for(let x=1; x < 54; x++){
      for(let y=1; y < 8; y++){
        const past_life = past_board[x][y].life;
        const date = past_board[x][y].date;

        let cnt = 0;
        if(past_board[x-1][y].life >= 1) cnt++;
        if(past_board[x+1][y].life >= 1) cnt++;
        if(past_board[x][y-1].life >= 1) cnt++;
        if(past_board[x][y+1].life >= 1) cnt++;
        if(past_board[x-1][y-1].life >= 1) cnt++;
        if(past_board[x+1][y-1].life >= 1) cnt++;
        if(past_board[x-1][y+1].life >= 1) cnt++;
        if(past_board[x+1][y+1].life >= 1) cnt++;

        if(past_life >= 1){
          alive_num++;
          if(cnt <= 1){
            // dies by under population
            board[x][y].life -= 1;
          }else if(cnt === 2 || cnt === 3){
            // lives
          }else{
            // dies by overpopulation
            board[x][y].life -= 1;
          }
        }else{
          if(cnt === 3){
            // reproduce
            board[x][y].life = 1;
          }
        }

        // console.log(past_board[x][y].life, board[x][y].life);
        if(board[x][y].date && past_board[x][y].life !== board[x][y].life){
          document.querySelector(`rect[data-date="${date}"]`).setAttribute('fill', LIFE[board[x][y].life]);
        }
      }
    }
    let is_changed = true;
    if(JSON.stringify(board) === JSON.stringify(past_board)){
      is_changed = false;
    }
    return [is_changed, alive_num];
  }

  let gen = 0;
  const timer = setInterval(() => {
    _ret = loop();
    const is_changed = _ret[0];
    const alive_num = _ret[1];
    if(!is_changed){
      clearInterval(timer);
      if(alive_num === 0){
        alert(`congturations! ${nickname}'s calendar is alive to ${gen} generations.`);
      }else{
        alert(`congturations! ${nickname}'s calendar is alive to ${gen} generations, and ${alive_num} cells are alive forever.`);
      }
    }
    gen++;
  }, 1000);
});
```

カレンダー内にボックス等の固定物体パターンが生成されて、全体に変化がなくなった場合は、

- `congratulations! ${nickname}'s calendar is alive to ${gen} generations.`
- `congratulations! ${nickname}'s calendar is alive to ${gen} generations, and ${alive_num} cells are alive forever.`

というalertが出ます。
そして、以下のパルサーのような周期的なパターンが生まれた場合は、残念ながらずっと動き続けます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/110426/b72e5ac1-d7ae-d554-9a3f-3f978a72b6c1.png)


# 感想

ライフゲーム自体は以前から知ってはいましたが、作成したのは初めてでした。
漠然と全セル死滅して終わり、というイメージがあったのですが、
意外と生存期間が長く、永遠に続くパターンもあったりして楽しかったです。

生存期間としては、このライフゲームのリポジトリを作る前が20世代ほどで全滅。
このリポジトリを作ってプッシュしたら、パルサーパターンが出てきてforever。
他、数人を試した感じではほどほどにcontributeしてる人で70世代くらいでforeverという感じでした。

前述のコード内で、 `// lives` の箇所は生きている限りはセルは死に向かうだろうということでライフが2以上ある場合は1削るなどの処理を入れたりして遊んでました。いろいろとバリエーションができそうで楽しいですね。

# 参考ページ等

- [Chrome拡張を簡単に作れるテンプレとライブラリ造ったので紹介](https://qiita.com/waterada/items/7f5729e066714284ff72)
ファイル構成などを参考にさせていただきました。

- [giuliandrimba/ghub-game-of-life](https://github.com/giuliandrimba/ghub-game-of-life)
コードを見た感じでは、ライフなしのライフゲームになっているかなと…。ブックマークレットで、動作は未確認。

- [epic conway's game of life](https://www.youtube.com/watch?v=C2vgICfQawE)
ライフゲームの奥深さを知ることができる動画(youtube)。

- [GitHub Identiconでライフゲームする-その1~Identiconについて~](https://qiita.com/massss/items/4628fcb24788a0bfb940)
githubの初期ユーザーアイコンでライフゲームをするという面白い試み。

# 遊び終わったらgithub-calendar-lifegameをRemoveして片付けましょう。

おおまかなデバッグはしましたが、取り切れていないバグが多分にあると思います…。