本記事は C# その2 Advent Calender2018 17日目の記事です。
はじめに
こんにちは@yoship1639です。
普段は個人ゲーム開発を行っています。
本記事は内容は技術的ですが、個人的にはネタ記事に入ります。
アドベントカレンダーを埋めたくて記述したのですが、何か書こうと思っても時間が足りなかったので、逆に短いコードを追求したものを作成しようと思い立ち26行で解読、コンソール表示可能なライフゲームをコーディングしてみました。
ライブラリを一切使わず標準機能のみなので、コピペすればそのまま動きます。
それでは見てみましょう。
ライフゲームのルールを確認
その前に一応、ライフゲームのルールを確認しておきたいと思います。
ライフゲームは好きなサイズの縦x横の盤面に生命「セル」を置き、そのセルの生死をシミュレーションするゲームです。
セルは自分の周り8近傍のセルの数で次世代の生死が決まります。
誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次世代のセルが誕生。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次世代でもセルは生存。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎によりセルは死滅。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密によりセルは死滅。
という単純なシミュレーションゲームです。セルの置き方によっては様々なバリエーションを見せるので、見ていて飽きない面白いゲームとなっています。
ライフゲームを書く
早速、10分クオリティですが書いてみました。以下の機能を有しています。
- コンソール表示可能
- [Enter]で次世代に移行
- X Yでフィールド[Y, X]にセルの設置
- qで終了
26行にしては高性能だと思います。私自身も画面に収まる程度のコード量でライフゲームが実現できるのは意外でした。
ソースコード
using System;
class Program {
static int W = 10; // ヨコ
static int H = 10; // タテ
static void Main(string[] args) {
var f = new int[H, W]; // フィールド
var pf = new int[H, W]; // 前のフィールド
string[] sc = null; // コマンド
while ((sc = Console.ReadLine().Split(' '))[0] != "q")
if (sc.Length == 2) pf[int.Parse(sc[1]), int.Parse(sc[0])] = 1;
else if (sc[0] == "") {
for (var y = 0; y < H; y++, Console.WriteLine())
for (var x = 0; x < W; x++) {
var c = 0;
for (var yy = y - 1; yy <= y + 1; yy++)
for (var xx = x - 1; xx <= x + 1; xx++) {
if (yy < 0 || yy >= H || xx < 0 || xx >= W || (x == xx && y == yy)) continue;
c += pf[yy, xx];
}
f[y, x] = ((c == 2 && pf[y, x] == 1) || c == 3) ? 1 : 0;
Console.Write(f[y, x] == 1 ? " o" : " .");
}
pf = (int[,])f.Clone();
}
}
}
実行1 (ブリンカー)
実行2 (グライダー)
如何でしょうか。これだけ短くてもちゃんとライフゲームになっています。中括弧をなくしたりjavaスタイルにしたりしましたが、それを考慮しても相当短くなったのではないかと思います。
コードを読み解く
コードだけ見せても納得しないと思うので、コードの処理を簡単に解説したいと思います。
var f = new int[H, W]; // フィールド
セルをint型で定義しています。1がセルが生きている。0がセルが死んでいるです。
while ((sc = Console.ReadLine().Split(' '))[0] != "q")
ライフゲームのメインループです。コマンド引数を受け取りスペースで分割し、第一引数がqだったら終了、それ以外だったら続行します。
if (sc.Length == 2) pf[int.Parse(sc[1]), int.Parse(sc[0])] = 1;
セルを入力引数で誕生させる処理です。第一、第二引数でセットする場所を指定し、セルを誕生させます。
else if (sc[0] == "") {
何も入力しない空欄Enterで、次世代の処理を進めます。
for (var y = 0; y < H; y++, Console.WriteLine())
for (var x = 0; x < W; x++) {
...
}
フィールドを走査します。この操作で次世代のセルの生死をそれぞれシミュレートします。行を稼ぐためにコンソール表示の改行をここで行ってしまいます。
var c = 0;
for (var yy = y - 1; yy <= y + 1; yy++)
for (var xx = x - 1; xx <= x + 1; xx++) {
if (yy < 0 || yy >= H || xx < 0 || xx >= W || (x == xx && y == yy)) continue;
c += pf[yy, xx];
}
8近傍の生存セルの数を数えています。本来ならばif分8つで処理しますが、ここではfor文で8近傍を走査し生存セルの数を数えます。if文で境界外の処理を除いています。
f[y, x] = ((c == 2 && pf[y, x] == 1) || c == 3) ? 1 : 0;
生存セルの周りに2個あったら、または生存関係なく3個あったら生存セルにします。それ以外を死滅させます。この1行で「誕生・生存・過疎・過密」のロジックを制御しています。
Console.Write(f[y, x] == 1 ? " o" : " .");
セルの生死をコンソール表示します。走査と同時にコンソール表示を行ってしまい行を稼いでいます。ここを置き換えれば表示を変更させる事が出来ます。今回は「o」が生存「.」が死滅です。
pf = (int[,])f.Clone();
前世代のフィールドを使わないと処理がうまくいかないのでコピーし、次世代のセルの生死のデータに使います。
以上です。
いかがでしたでしょうか、非常に短い濃密コードでもライフゲームのロジックを読み解けましたよね!
最後に
26行で解読、コンソール表示可能なライフゲームを実装してみました。非常に短い処理でもちゃんとしたライフゲームのロジックを組むことが可能であることがよく分かったのではないでしょうか。
是非とも皆さん、改行しない等の小細工は無しでもっと短くしてみてください。
ありがとうございました。