LINE Messaging API を使って C# で LINE にメッセージを送る

LINE の Messaging API アカウントを使って、C# からメッセージを送る方法のメモです。これは前回の記事の内容にも関連しています。

Messaging API を作る

まずはこれがないと何もできません。LINE Developers にアクセスして作成します。

developers.line.biz

作成方法はこちらを参考にしてください。

developers.line.biz

こんな感じに作成できれば OK です。

f:id:takunology:20210301143556p:plain

リファレンスを読む

Messaging API は Webhook を投げることで動かすことができます。今回はブロードキャストメッセージ(友達登録した相手にメッセージを送る)を使用します。まずはリファレンスを読んで、どんな形式で Webhook を投げれば良いかを確認しておきます。

developers.line.biz

リファレンスの目次に、「ブロードキャストメッセージを送る」という項目があるので確認します。

f:id:takunology:20210301153329j:plain

どうやらこんな感じに JSON 形式で送れば良さそうです。

//ここはヘッダー
{
    "messages":[
        {
            "type":"text",
            "text":"Hello, world1"
        },
        {
            "type":"text",
            "text":"Hello, world2"
        }
    ]
}

-h と書いてあるのはリクエストヘッダーで、-d はリクエストボディーです。

C# プログラムを書く

まずは Messaging API アカウントのチャネルアクセストークンをコピーしておきましょう。Messaging API 設定 のタブを選択し、下にスクロールするとあります。

f:id:takunology:20210301155123j:plain

あとはリファレンスに沿った形式で Webhook を投げれるように、JSONシリアライズしていきます。シリアライズに関してはこの記事を参考にどうぞ。

blog.takunology.jp

WebClient を使うと簡単に POST できます。また、Headers.Add メソッドを使ってリクエストヘッダーを指定しています。 UploadString にて LINE のブロードキャスト(共通の)アドレスへ接続し、シリアライズされた JSON 文字列がアップロードされます。Headers.Add にはチャネルアクセストークンがあるので、アカウントを識別してメッセージが送信されます。

using System.Collections.Generic;
using System.Runtime.Serialization;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Net;
using System.Text;

class Program
{
    public static string LineToken = @"チャネルアクセストークンの文字列";
    public static string LineURL = @"https://api.line.me/v2/bot/message/broadcast";

    static void Main(string[] args)
    {
        SendMessage("やぁ、たくのろじぃだよ");
        SendMessage("C# が好きなイカだよ");
    }

    public static void SendMessage(string TextMessage)
    {
        var LineMessage = new LineMessage();
        LineMessage.Message.Add(new Message { Type = "text", Text = TextMessage });

        var Serializer = new DataContractJsonSerializer(typeof(LineMessage));
        var ms = new MemoryStream();
        Serializer.WriteObject(ms, LineMessage);

        using (WebClient client = new WebClient())
        {
            client.Encoding = Encoding.UTF8;
            client.Headers.Add("Content-Type", "application/json");
            client.Headers.Add("Authorization", $"Bearer {LineToken}");
            client.UploadString(LineURL, Encoding.UTF8.GetString(ms.ToArray()));
        }
    }

    [DataContract]
    public class LineMessage
    {
        [DataMember(Name = "messages")]
        public IList<Message> Message { get; set; } = new List<Message>();
    }

    [DataContract]
    public class Message
    {
        [DataMember(Name = "type")]
        public string Type { get; set; }
        [DataMember(Name = "text")]
        public string Text { get; set; }
    }
}

実行結果

イカからメッセージが送られてくるのってなかなかにシュールですね(笑)。ちなみに、このイカは「いかのろじぃ」といいます。(聞いてない)

f:id:takunology:20210301154927p:plain

f:id:takunology:20210301154940p:plain

実はSDKがあった

今回は全て自分の手でコーディングしましたが、どうやら LINE 用の拡張機能があったみたいです。

github.com

おわりに

チャネルアクセストークンを用いて C# からメッセージを送ることができました。これを Azure Functions と組み合わせたのが前回の記事の内容でした。もしよろしければご覧ください。

blog.takunology.jp

宣伝的なもの

LINE Developers Community の方々による LINE API を用いた開発や最新情報などを共有するコミュニティがあります。私も先日、コミュニティのイベントに参加してきました。

linedevelopercommunity.connpass.com

また、学生による学生のためのMicrosoft技術勉強会コミュニティ Microsoft Student Partners Japan もあります。日本に所属する Microsoft Learn Student Ambassadors が月1でイベントを開催しています。

mspjp.connpass.com

私も今年から Ambassador として活動し始めました。一応専門は Minecraft を用いた C# プログラミングなのですが、どのようにイベントを企画しようか検討中です。

また、Minecraft を使いながら楽しく学べるコンテンツも製作中です。

www.mcwithcode.com

【C#】Uno Platformを使ってみた

1. Uno Platform

Uno Platform は Windows, iOS, Android, Web 上で動くアプリケーションを開発できるクロスプラットフォームです。

platform.uno

Xamarinと似ていますが、いくつか違う点があります。一番はUWP/WPFと同じような形式でXAMLが書けることです。これによって、今までWindowsアプリを作っていた人が簡単にモバイル向けアプリのコーディングができるようになります。他にもいい点がありますが、ちょまどさんの記事を参考にするといいと思います。必要な環境構築も丁寧に書いてありますので...。

qiita.com

2. アプリをつくってみた

今回作成したアプリは「新型コロナウイルス感染症対策サイトへアクセスするためのツール」です。

github.com

このサイトは東京をメインに北海道や鹿児島県などの多岐にわたる地域に派生しています。派生先のURLはGithub上で共有されており、一般の人が探すにはちょっとハードルが高いと思います。そこで、見たいときに他県の情報もみれるようなアプリを作りたいと思います。

2.1 デザイン - XAMLコード

まずはデザインですね。Uno Platformには4つの各プラットフォーム向けプロジェクトと統合された1つの Shared というプロジェクトが生成されます。この Shared に書き込むことによって、どのプラットフォームにも反映されます。

Gridで要素を分割することで、WebViewによる全画面表示を防いでいます。

<Page
    x:Class="StopCOVID19ViewerApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:StopCOVID19ViewerApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
  
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
     <Grid.RowDefinitions>
            <RowDefinition Height="12*"/>
            <RowDefinition Height="1*"/>
     </Grid.RowDefinitions>
      <WebView x:Name="webView" Grid.Row="0"/>
      <TextBlock Text="地域の選択" Grid.Row="1" Margin="30 15" FontSize="16"/>
      <ComboBox x:Name="SelectBox" PlaceholderText="東京都" Grid.Row="1" Width="150" Height="30" Margin="130 10"/>       
      <Button Content="更新" x:Name="buttonRefresh" Grid.Row="1" Click="ButtonRefresh_Click" Margin="300 10 10 30"/>
     </Grid>
</Page>

2.2 ロジック - C#コード

今のところ例外処理などはあまり考えていません...。Dictionary に公開されている県とそのURLを追加して、それをXAMLから参照しています。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace StopCOVID19ViewerApp
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        WebSiteURLs webSite = new WebSiteURLs();

        public MainPage()
        {
            this.InitializeComponent();
            this.SetComboBoxItems();
        }

        protected void SetComboBoxItems()
        {
            foreach (var item in webSite.prefecture.Keys)
                SelectBox.Items.Add(item.ToString());
        }

        private void ButtonRefresh_Click(object sender, RoutedEventArgs e)
        {
            string valueFromCB = SelectBox.SelectedValue.ToString(); //コンボボックスから選択されたアイテムの取得
            string valueFromDic = webSite.prefecture[valueFromCB];
            var uri = new Uri(valueFromDic);
            webView.Navigate(uri);
        }
    }

    public class WebSiteURLs
    {
        public Dictionary<string, string> prefecture = new Dictionary<string, string>();

        public WebSiteURLs()
        {
            //都道府県を登録しておく
            prefecture.Add("北海道", "https://stopcovid19.hokkaido.dev/");
            prefecture.Add("埼玉県", "https://stopcovid19.e-toda.jp/");
            prefecture.Add("千葉県", "https://chiba-covid19.mypl.net/");
            prefecture.Add("東京都", "https://stopcovid19.metro.tokyo.lg.jp");
            prefecture.Add("神奈川県", "https://www.pref.kanagawa.jp/osirase/1369/");
            prefecture.Add("山梨県", "https://stopcovid19.yamanashi.dev/");
            prefecture.Add("岐阜県", "https://covid19-gifu.netlify.com/");
            prefecture.Add("愛知県", "https://stopcovid19.code4.nagoya/");
            prefecture.Add("三重県", "https://covid19-mie.netlify.com/");
            prefecture.Add("兵庫県", "https://stop-covid19-hyogo.org/");
            prefecture.Add("愛媛県", "https://ehime-covid19.com/");
            prefecture.Add("岡山県", "https://covid19-okayama.netlify.com/");
            prefecture.Add("鹿児島県", "https://covid19.codeforkagoshima.dev/");
        }
    }
}

3. 実行結果

プラットフォーム(OS)によってプロジェクトがわかれているので、スタートアッププロジェクトの設定を行ってやります。

3.1 Androidで実行してみた

ボタンが隠れてしまっていますね...。ロジックは順調に動いています。

f:id:takunology:20200318153942p:plain

3.2 iOS で実行してみた

実はXAMLiOSに合わせているので、当然ながら問題ないですね。

f:id:takunology:20200319023507p:plain

3.3 UWPで実行してみた

ボタンやコンボボックスは左下表示されています。問題なく動きます。

f:id:takunology:20200318153829p:plain

3.4 Webで実行してみた

一番下のウィンドウが Edge, 左が Chrome, 右が Firefoxです。 更新ボタンを押しても何も表示されません。Webに対してWebViewは対応していないのでしょうか?

f:id:takunology:20200319023710p:plain

4. おわりに

1つのファイルを書きかえるだけですべてのプラットフォームに対応するなんて素晴らしいです!感動しました。ただWebで表示できなかった理由が分かりませんでした。

マジですごいですね。これ。

参考ページ

Uno Platformを初めて使うならこちらのページがオススメです。後はWPFまたはUWPのXAMLとコードビハインドの書き方を調べれば作れます!

qiita.com

qiita.com

【C#】マイクラのプロセス名がMinecraftではなかった話

イクラ自動化をするにあたり、プロセスに干渉して自動操作みたいなことができないかを試したくて、プロセス名を取得してみたら意外な結果を得られました。

実行環境

  • .NET Core 3.1 コンソールアプリケーション
  • Minecraft 1.12.2

1. プロセスの取得

プロセスは簡単に言えば「実行中のプログラム」のことです。メモ帳でもペイントでも、何かしらのアプリを起動していると、それらはシステムによって「プロセス」と認識されます。

イクラ自動化をするにあたり、どうせならプレイヤーの動きも自動化してみたいと思いました。そのためには起動中のプロセスにアクセスしてコマンド、あるいはキー入力をしないといけません。そこで、まずはプロセス名を調べる必要があります。

プロセスを取得するには Process クラスを使用します。このクラスはコンストラクタを持っているので初期化してそのまま使えます。ちなみに、この子は配列で扱います。

イクラのプロセス名は minecraft とかだと思いますが、どんな名前かは分からないので、まずはすべてのプロセスを確認してみます。すべてのプロセスにアクセスするには GetProcesses メソッドを用います。あとは配列の中をみんな大好き foreach でぶん回します。

public static void Processes()
{
    Process[] processes = Process.GetProcesses();
    foreach (var process in processes)
    {
        Console.WriteLine($"{process.Id}:{process.ProcessName}");
    }
}

とりあえずこれでプロセスIDとプロセス名を取得してみます。

f:id:takunology:20200309033643p:plain

あんれぇ~?Minecraft が見つからないですなぁ...。launcherは見つかりましたが、タスクマネージャーを見る限り違うようです。

f:id:takunology:20200309033957p:plain

イクラにはメモリを大量に与えているので、メモリ量で並び替えて探してみると...。

f:id:takunology:20200309034301p:plain

ん? javaw のメモリ使用量がエグいような...???

2. "javaw.exe" は何者なのか?

Javaをベースに動いているのと、メモリ使用量がエグいという点から javaw がマインクラフトのプロセス名なのではと推測しました。プロセス名を指定して詳細情報を得るための方法があります。

プロセス名を指定して情報を得るには GetProcessesByName メソッドを使用します。引数にはプロセス名を入れることでプロパティを参照することができます。配列で宣言する必要がありますが、今回はプロセス名 javaw が1つしかないので配列 [0] を参照します。

メソッドの詳細はドキュメントを参照してみてください。

docs.microsoft.com

public static void GetMinecraftProcess()
{
    //マイクラのプロセス名が "javaw" かもしれないので詳細を得る
    Process[] process = Process.GetProcessesByName("javaw");

    Console.WriteLine($"プロセスID : {process[0].Id}");
    Console.WriteLine($"プロセス名 : {process[0].ProcessName}");
    Console.WriteLine($"ウィンドウタイトル : {process[0].MainWindowTitle}");
    Console.WriteLine($"メインモジュール : {process[0].MainModule}");
    Console.WriteLine($"プロセスの優先順位 : {process[0].BasePriority}");
    Console.WriteLine($"順位の種類 : {process[0].PriorityClass}");
    Console.WriteLine($"メモリ割り当てサイズ : {process[0].PagedMemorySize64}");
    Console.WriteLine($"仮想メモリサイズ : {process[0].VirtualMemorySize64}");
}

3. 結論

まずは実行結果です。

f:id:takunology:20200309035537p:plain

ビンゴ!!
ということで、マインクラフトのプロセス名は javaw でした!

いろいろ気になって java のアプリについて調べてみました。
java はコンソールアプリケーションで使用するための実行ファイルだそうです。また、 javaw はウィンドウアプリケーションで使用するための実行ファイルでした。つまり、マインクラフトは javaw という実行形式で実行されているため、プロセス名が javaw だったと言えます。

4. おわりに

プロセス名 = アプリ名ではなかったことを知れたのは大きな収穫でした。まさか javaw という名前で動いているとは思いませんでした。意外な結果で面白かったです。今回得られたことをマイクラ自動化で活かせると良いと思いました。

参考サイト

参考にさせていただいたサイトです。ありがとうございます。

www.gwtcenter.com

dobon.net

【C#】画素ごとの輝度を得て、テキスト形式で保存する

機械学習や画像処理にて解析を行う際には、画像を数値化しなければなりません。今回は手書き数字の画像の輝度情報を読み取り、テキストファイルにして保存する方法をメモります。

実行環境

この環境でやってみました。特別なライブラリは使用していません。

  • Visual Studio 2019
  • .NET Core 3.0 コンソールアプリケーション
  • ペイント (Win10 付属のやつ)

1. 手書き数字を用意する

ペイントなどのソフトを用いて 32×32 ピクセルで数字を書きます。今回は試しに2を書きました。これを100個ほど用意します(鬼畜)。 保存形式は Jpeg をおススメします。

f:id:takunology:20200307220653p:plain

用意したら、Visual Studio のプロジェクト内にフォルダごと移動します。私は手書き数字の入ったディレクトリ名を training にしました。

2. 画素から輝度を得る

追加したファイルたちはプロジェクト直下にあります。しかし、実行する際に実行ファイルは bin/debug/netcoreapp3.0 にて生成されるので、ディレクトリ参照を行う際には3つ上の階層を指定する必要があります。

画像を開くためには Bitmap クラスを用います。インスタンスを生成する際に、ファイルパスを指定します。画像サイズはこちらから指定してもよいですが、bitmapクラスには幅と高さを取得するためのメソッドがあるので、これを利用します。幅は bitmap.Widthと高さは bitmap.Height で指定可能です。

輝度を得るためには Color 構造体を用います。この構造体に対して、bitmapインスタンスの GetPixel メソッドを実行し、その画素データを取得します。構造体にはその画素に関する様々なデータ(色やサイズ、輝度など)が入るので、この構造体の輝度を参照します。そのためには GetBrightness メソッドを実行します。この値を保持しておくには、あらかじめ変数が必要です。

画像データは幅と高さの2次元で表現されるので 2重 for文を用いて処理します。ちなみに、二次元配列に対して輝度データも代入しているので全体では3次元のデータを扱うことになります。配列以外にも、Vectorを用いる方法があるようです。(おそらくC++かな...?)

using System.Drawing;

static float[,] JpegData = new float[0, 0]; //クラス内にて定義
static int Width = 0;
static int Height = 0;

static void GetData(int label, int num)
{
    FilePath = $"../../../training/{label.ToString()}/two{num.ToString()}.jpg";
    Bitmap bitmap = new Bitmap(FilePath);

    Width = bitmap.Width;
    Height = bitmap.Height;

    JpegData = new float[Height, Width];

    for (int i = 0; i < Height; i++)
    {
        for (int j = 0; j < Width; j++)
        {
            Color pixel = bitmap.GetPixel(i, j); //[i, j]の色情報にアクセス
            JpegData[i, j] = pixel.GetBrightness();
        }
    }
}

3. テキスト形式に書き出す

得た輝度データをテキスト形式にして書き出すには StreamWriterクラスを用います。インスタンスを生成する際にファイルパス、上書き許可、エンコード等を指定できます。エンコードEncoding クラスの GetEncode() メソッドで指定可能です。 あと、StreamWriter による書き込みが終わった後は 必ず Close() メソッドでファイルを閉じて ください。

また、生成するテキストをまとめておくためのディレクトリも作成します。ディレクトリを新しく作るには Directory.CreateDirectory メソッドを用い、引数には作成したい階層およびディレクトリ名を入れます。

今回はカンマ区切りで画素ごとにデータを区切っていきます。また、書き込みが終わる最後の画素になった場合(今回は0から始まって31番目で終わる場合)はカンマを付けずに終わります。

static void WriteToFile(int label, int num)
{
    Encoding encode = Encoding.GetEncoding("utf-8");
    FilePath = $"../../../data/{label.ToString()}/{num.ToString()}.txt";
    Directory.CreateDirectory("../../../data/2");

    StreamWriter streamWriter = new StreamWriter(FilePath, false, encode);

    for (int i = 0; i < Height; i++)
    {
        for (int j = 0; j < Width; j++)
        {
            if (i == 31 && j == 31)
            {
                streamWriter.Write($"{JpegData[i, j]}");
                break;
            }
            streamWriter.Write($"{JpegData[i, j]},");
        }
    }

    streamWriter.Close();
}

4. 実行

あとは実行するためのコードを書きます。

static void Main(string[] args)
{
    Console.WriteLine("Get Brihtness.");

    int label = 2; //2の手書き数字を変換する
    int num = 100; //手書き数字の枚数

    for (int i = 1; i <= num; i++)
    {
        GetData(label, i);
        WriteToFile(label, i);
        Console.WriteLine($"File {i}.txt Created.");
    }

    Console.WriteLine("Finish.");
}

今回は100枚の画像データがあるので100回繰り返して輝度を取得し、ファイルを書き込みます。つまり、実行後は100個のデータが生成されます。

f:id:takunology:20200307224425p:plain

やばいですね。こんなにたくさんのファイルがわずか5秒程度で生成されました。

ファイルの生成場所はソースコード内にあるように、プロジェクトディレクトリの data/2 に入っています。

f:id:takunology:20200307224830p:plain

できたファイルの中身はどうなっているでしょうか???

f:id:takunology:20200307224940p:plain

うへぇ~すごく文字列です... (*´ω`)

ちなみにこのデータは1番目の画像データの輝度です。先ほど大量に書いた数字データの一番左上(1番目)がこれです。

5. おわりに

ファイルの中身を見ると画素の輝度が1に近いほど白で、0に近いほど黒くなるようです。驚きだったのは小数で表されていたことです。確かOpenCVとかだと 256 段階で表現されていたと思います。

今回は輝度の書き出しをやりましたが、これが最終目標ではありません。このデータを活用してこそ意味があります。

今回作ったコード

参考にどうぞ。リポジトリディープラーニングになっていますが、その途中で作ったものなので中に入っています。そのままディレクトリを移動せずに Program.cs を参照してもらえれば今回のコードが見れます。

github.com

【C#】深層学習入門 #8 損失関数とミニバッチ学習法

ニューラルネットワークの性能を示すのに損失関数を使用します。最近では交差エントロピー誤差(クロスエントロピー)がよく用いられているようです。また、学習するための手法にミニバッチ学習法があります。大量の訓練データの中で無作為に選んで学習を行います。その際、誤差の総和を正規化することで安定した誤差を得ることができます。

【C#】深層学習入門 #7 学習データを用意する

手書き数字認識をやるためにMNISTのデータセットを用いますが、画像ファイルではなく特殊なファイル形式であるため、TensorFlowなどのライブラリが必要です。また、Python用として提供されているためC#でやるには自作する必要があります。今回はMNISTを使用せず、自分で学習データを作る方法についてまとめてみました。

【C#】深層学習入門 #6 出力層の設計

ニューラルネットワークでは解を得る際に確率として得たいときがあります。これはソフトマックス関数を用いることによって解決できます。ソフトマックス関数は中間層よりも出力層に対して有効であり、指数関数を用いることで数値自体をいじらずに表現をシフトすることができます。

【C#】行列の積を計算する

行列の積はいろんなところで使えるので、練習がてら実装してみます。

1. 行列と型

行列は数字や文字を羅列したもので、行と列に並べて表現します。

f:id:takunology:20200303132433p:plain

例えば図の例では「3行3列の行列」といいます。行列式という言葉もありますが、行列とは違うモノです。

他にも、3行3列の行列を [3×3]型の行列と表現することがあります。また、行列の形は様々でいろんな型があります。

f:id:takunology:20200303133641p:plain

f:id:takunology:20200303134558p:plain

2. 行列の積

行列同士の掛け算は「行×列の和」 で進めていきます。ただし、ルールがあります。掛けられる行列の列の数と、掛ける行列の行の数が同じでないと計算ができません。つまり、[a × n]型 × [n × b]型 でしか計算できません。

f:id:takunology:20200303134613p:plain

これを計算をさせるプログラムを書いていきます。

行列の計算を本格的に勉強したい方はこの本をおススメします。

3. 実装

.NET Core 3.0 のコンソールアプリで実装します。フローはこんな感じです。

  1. 入力する行列の行数と列数を定義
  2. これを掛けられる行列A、掛ける行列Bの2つ分定義する
  3. 行列を入力した値で初期化
  4. 行列AとBの積をとり、その和を結果として結果行列に代入
  5. 結果行列を出力する(必要なら入力行列も表現)

必要なパラメータは行列A, Bとその行数と列数、結果を格納するための結果行列とその行数と列数です。

static int ROW_A = 0;
static int COL_A = 0;

static int ROW_B = 0;
static int COL_B = 0;

static int[,] MatrixA = new int[ROW_A, COL_A];
static int[,] MatrixB = new int[ROW_B, COL_B];
static int[,] MatrixResult = new int[ROW_A, COL_B];

行と列を2重for文で初期化して行列を作ります。初期化するパラメータは任意の入力値なのでコンソール入力を行います。

static void Input()
{
    Console.Write("行列Aの行数:");
    ROW_A = int.Parse(Console.ReadLine());
    Console.Write("行列Aの列数:");
    COL_A = int.Parse(Console.ReadLine());
    Console.WriteLine("行列Aのパラメータ入力");

    MatrixA = new int[ROW_A, COL_A];

    for (int i = 0; i < ROW_A; i++)
    {
        for (int j = 0; j < COL_A; j++)
        {
            MatrixA[i, j] = int.Parse(Console.ReadLine());
        }
    }

    Console.WriteLine("行列Bの行数:");
    ROW_B = int.Parse(Console.ReadLine());
    if (COL_A != ROW_B)
    {
        Console.WriteLine("行列Aの列数と行列Bの行数が異なるため計算できません。");
        return;
    }

    Console.WriteLine("行列Bの列数:");
    COL_B = int.Parse(Console.ReadLine());
    Console.WriteLine("行列Bのパラメータ入力");

    MatrixB = new int[ROW_B, COL_B];

    for (int i = 0; i < ROW_B; i++)
    {
        for (int j = 0; j < COL_B; j++)
        {
            MatrixB[i, j] = int.Parse(Console.ReadLine());
        }
    }

}

ここからがポイントですね。行列の積は [i × j] だけでは計算できません。なぜなら共通の要素を回せないからです。よって、行数と列数を回すfor文に加えて、共通要素を回すためのfor文が必要です。つまり、[i × k]型 × [k × j]型 で計算できるようにしなければなりません。

行列の積の要素数は、掛ける行列の行数と掛けられる行列の列数すなわち、行列Aの行数と行列Bの列数で結果行列の行数と列数を定義します。共通要素は行列Aの列数または行列Bの行数のどちらでも良いです。

static void Calc()
{
    MatrixResult = new int[ROW_A, COL_B]; //結果行列は行列Aの行数と行列Bの列数で再定義
    for (int i = 0; i < ROW_A; i++)
    {
        for (int j = 0; j < COL_B; j++)
        {
            for (int k = 0; k < COL_A; k++) //ROW_BでもOK
            {
                MatrixResult[i, j] += MatrixA[i, k] * MatrixB[k, j];
            }
        }
    }
}

結果を表示するには結果行列の成分をそのまま回せば表示できますが、GetLength() メソッドを用いれば定義された行列(配列)の次元の要素数にアクセスできます。

static void ShowResult()
{
    Console.WriteLine("====== 計算結果 =====");
    for (int i = 0; i < MatrixResult.GetLength(0); i++)
    {
        Console.Write("|");
        for (int j = 0; j < MatrixResult.GetLength(1); j++)
        {
            Console.Write(string.Format("{0,4}", MatrixResult[i, j]));
        }
        Console.WriteLine(" |");
    }
    Console.ReadKey();
}

4. 実行結果

行列AとBの要素数、およびパラメータを入力することで計算が可能です。

f:id:takunology:20200303151210p:plain


行列計算は活用方法も様々あるので、やっておいて損はないと思います。

今回作成したコード

参考にどうぞ

github.com

【C#】Minecraft自動化 #7 建築自動化

はじめに

いよいよ建築自動化を行います。この内容が個人的に一番やりたかったことです。

建築は設計図をもとにブロックを配置していきたいと考えていますが、設計図をどうするかが問題ですね。3次元的に表したいので、CADと行きたいところですが、それはちょっとレベルが高いのでEXCELで作りたいと思います。

EXCELシートの行と列はX方向とZ方向、シートの枚数はY方向として、建築自動化プログラムを書いていきます。

1. EXCELファイルを読み込む

Googleで調べていると、COM参照すべきかしないべきかでいろいろ議論されており、やめたほうがいいという記事を見つけました。

qiita.com

そこで、今回はNPOIパッケージを使います。

www.nuget.org

f:id:takunology:20191116225252p:plain

あとはEXCELデータを読み込む処理を書いていきます。

public class ReadExcelFile
{
    public void OpenExcelFile()
    {
        var Path = "../../../Excel/TestBook.xlsx";
        var Book = WorkbookFactory.Create(Path);
        var Sheet = Book.GetSheetAt(0); //1枚目

        GetValue(Sheet, 0, 0); //シート1枚目の1行A列を取得

    }

    private void GetValue(ISheet Sheet, int Column, int Row)
    {
        var row = Sheet.GetRow(Row) ?? Sheet.CreateRow(Row); //例外対策
        var cell = row.GetCell(Column) ?? row.CreateCell(Column);
        string value;

        value = cell.StringCellValue;

        Console.WriteLine(value);
    }
}

動かしてみます。動かすときはExcelを閉じないとアクセス権を得られないので注意しましょう。

f:id:takunology:20191117010237p:plain

さて、文字列はこのままでいいのですが、数値の場合はちょっと工夫が必要です。NumericCellValueメソッドを用います。しかし、これはdouble型の返り値を返すので他の型へ変換する必要があります。文字列にしたいときはこのようにします。

value = cell.NumericCellValue.ToString();

他の型ならParseメソッドを使うと良いかもしれません。

また、シートごとにデータを見るためのメソッドを作ってみました。Valueを3次元配列で宣言してそれぞれシート数、行、列の順にアクセスできるようにしました。

public static void ShowSheet()
{
    for (int y = 0; y < Value.GetLength(0); y++)
    {
        Console.WriteLine($"{y + 1}枚目のシート");
        for (int x = 0; x < Value.GetLength(1); x++)
        {
            for (int z = 0; z < Value.GetLength(2); z++)
            {
                Console.Write(string.Format("{0, 3}", ($"{Value[y, x, z]}")));
            }
            Console.WriteLine();
        }
    }
}

f:id:takunology:20191117035452p:plain

さて、これでExcelファイルの読み込みはOKです。

2. ブロック名の定義

次に、得た値をマイクラのブロックIDに変換します。といっても、switch文で場合分けすればいいと思います。

public static void Convert()
{
    for (int y = 0; y < Value.GetLength(0); y++)
    {
        for (int x = 0; x < Value.GetLength(1); x++)
        {
            for (int z = 0; z < Value.GetLength(2); z++)
            {
                string value = Value[y, x, z];
                switch (value)
                {
                    case "0":
                        Value[y, x, z] = "air";
                        break;

                    case "1":
                        Value[y, x, z] = "stone";
                        break;

                    case "2":
                        Value[y, x, z] = "grass";
                        break;

                    case "3":
                        Value[y, x, z] = "glass";
                        break;

                    case "4":
                        Value[y, x, z] = "planks";
                        break;

                    case "5":
                        Value[x, y, z] = "sandstone";
                        break;
                }
            }
        }
    }
}

これをマイクラのコマンド送信メソッドに投げればいい感じに動きそうです。

3. Excel設計図をもとにした自動建築

Excelに設計図を書きます。例えば上のプログラムに合わせるならば、1は石、2は草ブロックというようにしてセルを埋めていきます。

セルを埋めたらプログラムを動かしていきます。ブロックのIDはWikiで調べるなどして登録しておくと、建築バリエーションが増えます。ちなみに色々改善して動かした結果はこんな感じです。

www.youtube.com

今回作ったコード

参考にどうぞ。

github.com

参考

NPOIの使い方で参考になりました。ありがとうございます。

www.sejuku.net

【C#】Minecraft自動化 #6 迷路の生成

はじめに

イクラ自動化はもともとC#でどこまでできるかを検証するためにやってきましたが、どうやら本当に色々できるようです。

とりあえず座標移動(テレポート)と配置(ブロック配置)ができれば何でもできそうです。

今回は迷路を作ります。これは建設自動化のための準備みたいなものです。

1. 外壁をつくる

迷路を作るためには、まずは外壁を作る必要があります。

外壁ロジックは、最初と最後の座標だけ囲みます。座標を二次元配列として扱ったほうが分かりやすいので、二次元配列を使ってみました。GetLengthメソッドを用いると、その配列の要素数を得ることができます。

ロジックとしては、まずはX方向の座標から考えます。最初と最後のX方向は壁にするので、そのX座標が最初か最後かを判定させます。Z方向も同様です。これをぶん回せば迷路の壁が生成されます。

static void Wall()
{
    for (int i = 0; i < Maze.GetLength(0); i++)
    {
        if (i == 0 || i == (Maze.GetLength(0) - 1)) // X方向の始めと終り
        {
            for (int j = 0; j < Maze.GetLength(1); j++)
            {
                Maze[i, j] = 1;
                Console.Write(Maze[i, j]); //壁生成
            }
            Console.WriteLine();
        }
        else
        {
            for (int j = 0; j < Maze.GetLength(1); j++)
            {
                if (j == 0 || j == (Maze.GetLength(1) - 1)) // Z方向の始めと終り
                {
                    Maze[i, j] = 1;
                    Console.Write(Maze[i, j]); //壁生成
                }
                else
                {
                    Maze[i, j] = 0;
                    Console.Write(Maze[i, j]);
                }
            }
            Console.WriteLine();
        }
    }
}

f:id:takunology:20191111171230p:plain

2 壁伸ばし法の実装

これは自分のアルゴリズムに対する理解力が十分でないため、不完全なところがあります。

壁伸ばし法は2通りあります。外壁から壁をランダムな方向に伸ばしていく方法と、ランダムな座標から外壁に向かって伸ばしていく方法です。私は後者を選択しました。

手順というかロジックはこんな感じですかね...。

  • ランダムに壁伸ばし開始座標を選ぶ
  • その座標からランダムな方向へ壁を伸ばす
  • 1マスずつ伸ばすと隣接した壁がくっつく可能性があるので、2マスずつ伸ばす
  • このとき、隣接した座標とその隣の座標が通路でないと伸ばせない
  • ただし、その座標の2つ先が外壁だったら1マス伸ばして開始座標を選びなおす
  • 座標が外壁に達したら、再びランダムな開始座標を選びなおす
  • 開始座標候補がすでに全て壁なら迷路完成

ただ、これだと不完全で伸ばしている最中の壁の座標を保持しておき、新しく伸ばそうとしている壁の座標が、すでに存在すればその方向には伸ばさないという手法も必要らしいです。が、今現在理解できていないので組み込んでいません。スタックに保持する必要があるみたいですが...。

一応ロジックの一部を載せますが、微妙です。

static void Generate()
{
    int PositionX; //開始座標
    int PositionY; //開始座標
    int count = 0;
    Random rnd = new Random();
ReGenerate:
    while (true)
    {
        PositionX = rnd.Next(2, (Maze.GetLength(0) - 2));
        PositionY = rnd.Next(2, (Maze.GetLength(1) - 2));
        count++;
        if (PositionX % 2 == 0 && PositionY % 2 == 0)
        {//偶数になるまで再抽選
            if (Maze[PositionX, PositionY] != "■") //その座標に壁が存在しないか
                break;
        }
    }
    int direction = Direction();

    while (PositionX != 0 || PositionY != 0 || PositionX != (Maze.GetLength(0) - 1) || PositionY != (Maze.GetLength(1))) //その座標が外壁でない限り繰り返す
    {
        direction = Direction();
        Maze[PositionX, PositionY] = "■";

        switch (direction)
        {
            case 0://上
                if ((Maze[PositionX - 1, PositionY] == " ") && (Maze[PositionX - 2, PositionY] == " "))
                { //隣接が通路かつその隣も通路
                    Maze[PositionX - 1, PositionY] = "■";
                    Maze[PositionX - 2, PositionY] = "■";
                    PositionX -= 2;
                }
                else if (PositionX - 2 == 0) //もし現在の座標の2マス上が壁だったら、1マス上を壁にする
                {//壁に隣接した部分を壁にしたので作り直し
                    Maze[PositionX - 1, PositionY] = "■";
                    break;
                }
                else if ((Maze[PositionX - 2, PositionY] == "■") && (Maze[PositionX + 2, PositionY] == "■") && (Maze[PositionX, PositionY - 2] == "■") && (Maze[PositionX, PositionY + 2] == "■"))
                { //うずまき対策
                    goto ReGenerate;
                }
                else
                {
                    break;
                }
                break;
        //下、左、右の方向ロジックも書く。
        }
        Console.Clear();
        Console.WriteLine($"X:{PositionX} Y:{PositionY} 方向:{direction}");
        ShowMaze();
    }
}

迷路の途中経過を表示する関数と方向を決める関数

static void ShowMaze()
{
    for (int x = 0; x < Maze.GetLength(0); x++)
    {
        for (int y = 0; y < Maze.GetLength(1); y++)
        {
            Console.Write(Maze[x, y]);
        }
        Console.WriteLine();
    }
}

static int Direction() //方向を決める
{
    Random rnd = new Random();
    int direction = rnd.Next(0, 4);
    return direction;
}

実行してみるとこんな感じですね。

f:id:takunology:20191114200441p:plain

ちょっと不完全ですが、このまま続けます。いつか必ずリベンジを...。

3 Minecraft で実行してみる

あとは Console.WriteLineの部分を RCON 送信メソッドに置き換えるなりすれば迷路が生成できます。

あと、いままで遅延を発生するために

Thread.Sleep(10);

と書いていましたが、正しくは

await Task.Delay(10);

でした。どうやら非同期処理中に非同期でない遅延メソッドを使うとそこで止まってしまうようです。なぜかはわかりませんが...。

動かしてみるとこんな感じになります。

www.youtube.com

おわりに

今回は迷路(?)を作りました。まだアルゴリズムが未熟な部分があるのでいつか修正したいと思います。

次は建築やってみたいですね。エクセルシートの設計図をもとに自動建築を行いたいと考えています。

今回作ったコード

少し汚いコードですが、参考にどうぞ。

github.com

参考

壁伸ばし法で参考になりました。ありがとうございます。

www5d.biglobe.ne.jp

algoful.com

こちらは非同期遅延メソッドで参考にしました。ありがとうございます。

qiita.com