「憂鬱な勇者」を C# で



C# でも書いてみました.コンパイルには Achiral 1.1.0.0 が必要です.(追記)単体で動くようにしてみました.以下のソースを UTF-8 (BOM付き) でセーブし,gmcs 1.9.1.0 にてコンパイルできることを確認しました.

using System;
using System.Linq;

using System.Threading;
using System.Collections.Generic;

static class Program
{
    public static IEnumerable<T> Repeat<T>(T element) { while (true) yield return element; }

    public static IEnumerable<int> UpToInfinity(this int initial)
    {
        for (var i = initial; ; i = checked(i + 1))
        {
            yield return i;
        }
    }
    public static IEnumerable<TResult> Scan<TSource, TAccumulate, TResult>(
        this IEnumerable<TSource> source,
        TAccumulate seed,
        Func<TAccumulate, TSource, TAccumulate> func,
        Func<TAccumulate, TResult> resultSelector)
    {
        var result = seed;
        yield return resultSelector(result);
        foreach (var item in source)
        {
            result = func(result, item);
            yield return resultSelector(result);
        }
    }
    public static IEnumerable<TResult> ZipWith<T1, T2, TResult>(
        this IEnumerable<T1> source1,
        IEnumerable<T2> source2,
        Func<T1, T2, TResult> func)
    {
        using (var enumerator1 = source1.GetEnumerator())
        using (var enumerator2 = source2.GetEnumerator())
        {
            while (enumerator1.MoveNext() && enumerator2.MoveNext())
            {
                yield return func(enumerator1.Current, enumerator2.Current);
            }
        }
    }
    public static IEnumerable<TResult> ZipWith<T1, T2, TResult>(
        this IEnumerable<T1> source1,
        IEnumerable<T2> source2,
        Func<T1, T2, int, TResult> func)
    {
        using (var enumerator1 = source1.GetEnumerator())
        using (var enumerator2 = source2.GetEnumerator())
        {
            int index = 0;
            while (enumerator1.MoveNext() && enumerator2.MoveNext())
            {
                yield return func(enumerator1.Current, enumerator2.Current, index);
                index = checked(index + 1);
            }
        }
    }
    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source){action(item);}
    }

    static string MultAsDecimal(this string a, string b)
    {
        var revA = a.Select(c => int.Parse(c.ToString())).Reverse().ToArray();
        var revB = b.Select(c => int.Parse(c.ToString())).Reverse().ToArray();
        return string.Concat(
            revA.SelectMany((n1, dig1) =>
                    revB.Select((n2, dig2) => new { N = n1 * n2, Digit = dig1 + dig2 }))
                .Concat(new[] { new { N = 0, Digit = revA.Length + revB.Length - 1 } })
                .GroupBy(r => r.Digit, r => r.N)
                .OrderBy(g => g.Key)
                .Select(g => g.Sum())
                .Scan(0, (rest, digitsum) => rest / 10 + digitsum, rest => rest % 10)
                .Skip(1)
                .Reverse()
                .SkipWhile(n => n == 0)
                .Select(n => n.ToString())
                .ToArray());
    }

    static void Main(string[] args)
    {
        //モンスター用配列
        var monsters = new[]
        {
            "焼きたてパン", "強いシャチホコ", "もんじゃ焼き一年生", "怪人ホタテ男",
            "ニセ勇者", "逃げ足の早いアレ", "睡魔", "煩悩",
            "愛らしい子犬の中の人", "恋するスズメバチ", "勇敢なクマンバチ", "信じられない物",
            "勇者の師匠", "浮遊する鎧", "怪盗ドボン", "闇の招き猫", "誘惑のカスタードクリーム",
            "しょっぱすぎる籠手", "カレー味の兜", "光沢だけは一流の盾", "若葉マークのモンスター",
            "新緑の季節", "梅雨時の車両のニオイ", "暑すぎる夏", "新宿らしき何か", 
            "やたら発達したドーナツ", "育ちすぎたクマー",
            "なごやかな雰囲気", "凍り付いた気配", "忍び寄る恐怖",
        };

        //Lv up時に習得するもの
        var skills = new[]
        {
            "お豆腐の買い方","鉛筆の買い方", "消しゴムの使い方", "メモの取り方",
            "攻撃に使えないこともない呪文", "裏町の歩き方", "森林浴",
            "珈琲の味", "しじみのみそ汁の作り方", "回覧板の回し方", "郵便物の投函方法",
            "立ち話のコツ", "猫の呼び方", "犬の呼び方", "カラスの呼び方",
            "鳩専用豆鉄砲", "秘密の趣味", "速く走るコツ", "剣の使い方",
            "斧の使い方", "まきわりで、まっきわりわり", "聖なる祈り",
            "孤独", "涼しく過ごすコツ", "お洒落のコツ", "卵をふわっと焼く方法",
            "ごはんの研ぎ方", "油汚れの対応方法", "大人の振るまい", "Suicaの使い方",
        };

        const string template1 =
@"*-----
{0}を倒した!
{1}の経験値を得た。
勇者は{2}にレベルが上がった!
勇者は、{3}を覚えた。
";
        const string template2 =
@"*-----
{0}を倒した!
{1}の経験値を得た。
勇者は、また、レベルが上がった!
勇者は、ふと空しさを覚えた。
";
        const string intermission =
@"
そして、
かくかくしかじかで、山あり谷ありの冒険が続いたが割愛。
";

        // factorials 
        var exps = 1.UpToInfinity()
                    .Scan("1", (fact, n) => fact.MultAsDecimal(n.ToString()), _=>_)
                    .Skip(1);

        var seq = Repeat(new Random())
                      .ZipWith(exps, (rand, exp, i) => new
                      {
                          Number = i,
                          Exp = exp,
                          Monster = monsters[rand.Next(monsters.Length - 1)],
                          Skill = skills[rand.Next(skills.Length - 1)]
                      });

        const int NumButtles = 30;

        Enumerable.Repeat(template1, NumButtles - 1)
                  .Concat(Enumerable.Repeat(template2, 1))
                  .ZipWith(seq, (template, param) => string.Format(template,
                                                                   param.Monster,
                                                                   param.Exp,
                                                                   param.Number + 1,
                                                                   param.Skill))
                  .SelectMany(msg => new[] { msg, intermission })
                  .Take(NumButtles * 2 - 1)
                  .ForEach(msg => { Console.WriteLine(msg); Thread.Sleep(2000); });
    }
}

今回は変数名も関数名もインデントもやたら適当です.ごめんなさい.
しかし可変長整数が使えないのは困りました.結局文字列型を使って 10 進乗算を実装しました.



とまあご覧の通り C# でループを使わないコーディングスタイルの練習なわけですが,今まで何気なく書いてきたループ処理が新鮮に見えてくるから面白いですね.
異なる処理を交互に行いたい (今回はレベルアップの表示と,かくかくしかじかの表示) とか,最後の一回だけ別の処理をしたいとか.確かに結構あるかもしれません.とりあえず今回は SelectMany や Take,ZipWith で書いてみましたが,このあたりは追々別解も考えていくことにしましょう.
あと,Scan の 3 引数バージョンの挙動は後でちょっと弄るかもしれません.何か微妙に使いにくい.

外伝: 憂鬱な勇者の 3 分クッキング (2008年6月3日追記)

Mono 1.9.1 インストール済み openSUSE 10.3 Live CD を使って 3 分ぐらいで憂鬱な勇者を実行してみます.
OS 起動後,デスクトップ右下のネットワークアイコンからネットワークを有効化し,ターミナルを開いて,以下のように実行します.3 分クッキングらしく,ソースは作り置きのものを利用しましょう.

wget http://www.dwahan.net/nyaruru/hatena/melancholic.cs
gmcs melancholic.cs
mono melancholic.exe