4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

コンソールで動くライフゲームを創る

Posted at

ライフゲーム

ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。
生物集団においては、過疎でも過密でも個体の生存に適さないという個体群生態学的な側面を背景に持つ。セル・オートマトンのもっともよく知られた例でもある。
誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

ライフゲーム - Wikipedia

↓こんな感じのが完成しました↓

開発環境構築

PS > dotnet --version
2.1.300
<< COMMENTOUT
ライフゲーム本体はコンソールアプリ以外でも使えるように、ライブラリとして制作します。
出力はコンソールに行います。
COMMENTOUT
PS > mkdir GameOfLife
PS > cd GameOfLife
# ソリューションとプロジェクト
PS > dotnet new sln
PS > dotnet new classlib -o GameOfLife.Lib
PS > dotnet sln add GameOfLife.Lib/GameOfLife.Lib.csproj
PS > dotnet new console -o GameOfLife.ConsoleApp
PS > dotnet sln add GameOfLife.ConsoleApp/GameOfLife.ConsoleApp.csproj
# 参照設定
PS > dotnet add GameOfLife.ConsoleApp/GameOfLife.ConsoleApp.csproj reference GameOfLife.Lib/GameOfLife.Lib.csproj
# ビルド
PS > dotnet build
GameOfLife.ConsoleApp.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <ProjectReference Include="..\GameOfLife.Lib\GameOfLife.Lib.csproj" />
  </ItemGroup>
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <!-- C#のバージョンを最新(7.3)にする -->
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
</Project>
GameOfLife.Lib.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <!-- C#のバージョンを最新(7.3)にする -->
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
</Project>

ライブラリ側

コンソールに依存しないライブラリ側を作成します。
それぞれのクラスが依存するオブジェクトはクラスの外から注入(DI)し、出来るだけ疎結合を目指したいと思います。

  • セル
    • 世代交代時にグリッド側に生死判定させるのではく、セル自身に判定させるようにしました。
Cell.cs
using System.Collections.Generic;

namespace GameOfLife.Lib {
    public class Cell {
        // 生死
        public enum State { Alive, Dead }

        // 現在の生死
        internal State Current { get; private set; }

        // 次の世代の生死
        State Next { get; set; }

        // 周囲のセル
        IList<Cell> Neighbours { get; }

        // 次の世代の生死を判定するルール
        Rule Rule { get; }

        // 初期状態を受け取る。ルールはDIします。
        public Cell(State state, Rule rule) {
            Current = state;
            Rule = rule;
            Neighbours = new List<Cell>();
        }

        // 次の世代の生死を設定します。
        internal void SetNextState() =>
            Next = Rule.GetNextState(self: Current, Neighbours);

        // 世代を進めます。
        internal void ToNextGen() => Current = Next;

        // 周囲のセルを設定します。
        internal void AddNeighbour(Cell neighbour) => Neighbours.Add(neighbour);

        // コンソール出力時の描画
        public override string ToString() => Current == State.Alive ? "o " : ". ";
    }
}
  • ルール
    • 冒頭で説明した生死判定のルールです。
    • DI するためにstaticクラスにしていません。
Rule.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace GameOfLife.Lib {
    public class Rule {
        // 周囲の生きているセルの数から次代の生死を判定する
        internal Cell.State GetNextState(Cell.State self, IList<Cell> neighbours) {
            int alives = neighbours.Count(cell => cell.Current == Cell.State.Alive);
            bool willBorn = alives == 3;
            bool isSurvive = alives == 2 && self == Cell.State.Alive;
            return (willBorn || isSurvive) ? Cell.State.Alive : Cell.State.Dead;
        }
    }
}
  • グリッド
    • 2次元のセルを持つ盤面です。
Gird.cs
using System.Linq;

namespace GameOfLife.Lib {
    public class Grid {
        public Cell[][] Cells { get; }

        public Grid(Cell[][] cells) => Cells = cells;

        // セル達を次の世代へ進めます。
        public void ToNextGen() {
            var flattened = Cells.SelectMany(row => row);
            foreach (var cell in flattened)
                cell.SetNextState();
            foreach (var cell in flattened)
                cell.ToNextGen();
        }
    }
}
  • セルビルダー
    • セルを組み立てます。
    • あまり出来が良くない(債務の分割ができていない気がする):sweat:
CellBuilder
using System;
using System.Collections.Generic;
using System.Linq;

namespace GameOfLife.Lib {
    public class CellBuilder {
        Rule Rule { get; }

        // 各セルに設定するルールをコンストラクタで受け取ります。
        public CellBuilder(Rule rule) => Rule = rule;

        // 縦横の長さと生きたセルの割合を受け取りグリッド(配列の配列)を返します。
        public Cell[][] BuildCells(int rowLength, int colLength, int percentOfLivingCells) {
            var random = new Random();
            var cells = Enumerable.Range(1, rowLength)
                .Select(_ => Enumerable.Range(1, colLength)
                    .Select(__ => {
                        var state = (random.Next(1, 100) < percentOfLivingCells)
                            ? Cell.State.Alive
                            : Cell.State.Dead;
                        return new Cell(state, Rule);
                    }).ToArray()
                ).ToArray();
            return ConnectNeighbours(cells);
        }
        // 各セルに周囲のセルを接続します。
        Cell[][] ConnectNeighbours(Cell[][] cells) {
            foreach (var (row, rowIndex) in cells.Select((row, i) =>(row, i))) {
                foreach (var (cell, colIndex) in row.Select((cell, i) =>(cell, i))) {
                    foreach (Cell neighbour in GetNeighbours(rowIndex, colIndex, cells.Length, row.Length))
                        cell.AddNeighbour(neighbour);
                }
            }
            return cells;
            // 周囲のセルを取得します。ローカル関数です。
            IEnumerable<Cell> GetNeighbours(int rowIndex, int colIndex, int rowLength, int colLength) {
                for (int i = -1; i < 2; i++) {
                    for (int j = -1; j < 2; j++) {
                        if (i == 0 && j == 0) continue;
                        int offsetRow = circlate(rowIndex + i, rowLength);
                        int offsetCol = circlate(colIndex + j, colLength);
                        yield return cells[offsetRow][offsetCol];
                    }
                }
            }
        }

        // はみ出るインデクスを循環させます。(C#ではマイナスのインデクスで配列の後ろからn番目でアクセスができない)
        // 周囲のセルを取得する際にグリッド端のセルはハミ出るのでこれで逆の端と繋げます。
        int circlate(int index, int length) {
            if (index < 0) return index + length;
            if (index >= length) return index - length;
            else return index;
        }
    }
}

コンソール側

ライブラリを利用したコンソール動くライフゲームを作ります。

  • 現在のグリッドの状態を描画するクラスです。
    • 名前がびみょい
using System;
using System.IO;
using GameOfLife.Lib;

namespace GameOfLife.ConsoleApp {
    public class ConsoleDrawer {
        TextWriter StdOut { get; }

        // TextWriterは外部からDIします。
        public ConsoleDrawer(TextWriter writer) => StdOut = writer;

       // グリッドをTextWriter.Flushで一気に出力します。
        public void DrawCells(Cell[][] cells) {
            Console.CursorTop = 0;
            Console.CursorLeft = 0;
            foreach (var row in cells) {
                foreach (var cell in row) {
                    StdOut.Write(cell);
                }
                StdOut.WriteLine();
            }
            StdOut.Flush();
        }
    }
}
  • メイン
    • DI コンテナーは使わず、依存オブジェクトは手動で注入することにします。
Program.cs
using System;
using System.IO;
using System.Text;
using System.Threading;
using GameOfLife.Lib;

namespace GameOfLife.ConsoleApp {
    class Program {
        static void Main(string[] args) {
            var rule = new Rule();
            var builder = new CellBuilder(rule);
            var cells = builder.BuildCells(
                rowLength: 20,
                colLength: 20,
                percentOfLivingCells: 30);
            var grid = new Grid(cells);
            var writer = new StreamWriter(Console.OpenStandardOutput(), Encoding.ASCII);
            var drawer = new ConsoleDrawer(writer);
            while (!Console.KeyAvailable) {
                drawer.DrawCells(grid.Cells);
                grid.ToNextGen();
                Thread.Sleep(200);
            }
        }
    }
}

実際に動かす

コンソールが立ち上がるように設定します。
vscodeで開発したのでそのままだとvscodeのターミナルが立ち上がり動きません。

{
  // Use IntelliSense to find out which attributes exist for C# debugging
  // Use hover for the description of the existing attributes
  // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
  "version": "0.2.0",
  "configurations": [
    {
      "name": ".NET Core Launch (console)",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "build",
      // If you have changed target frameworks, make sure to update the program path.
      "program":
        "${workspaceFolder}/GameOfLife.ConsoleApp/bin/Debug/netcoreapp2.1/GameOfLife.ConsoleApp.dll",
      "args": [],
      "cwd": "${workspaceFolder}/GameOfLife.ConsoleApp",
      // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
-     "console": "internalConsole",
+     "console": "externalTerminal",
      "stopAtEntry": false,
      "internalConsoleOptions": "openOnSessionStart"
    },
    {
      "name": ".NET Core Attach",
      "type": "coreclr",
      "request": "attach",
      "processId": "${command:pickProcess}"
    }
  ]
}

F5もしくは以下のコマンドで動かします。

PS > dotnet run --project GameOfLife.ConsoleApp/GameOfLife.ConsoleApp.csproj
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?