Jumpaku Dinner
はじめに
夜ご飯決定アプリ「Jumpaku Dinner」を開発・公開し,その成果をLT大会で発表しました. 以下では,
- Jumpaku Dinnerとは何か
- どのように使えば良いか
- どのように動作しているか
- なぜ制作したか
- 感想
- 質疑応答
などを述べます.
Jumpaku Dinnerとは何か
Jumpaku Dinnerは,ある日の夜ご飯として何を作るかを決定するためのWebアプリケーションです. ページ(https://dinner.jumpaku.net)を開くと,ランダムに一つの料理が選択され,その料理の料理名,写真,材料,一言コメントが表示されます.
どのように使えば良いか
「今日の夜ご飯何食べたい?」「なんでも良い」「なんでも良いは困る!」
そんな会話に覚えはありませんか? 夜ご飯で作るものを決められない親,食べられればなんでも良い子供,そういった方がこのアプリに従うことで,喧嘩することなく夜ご飯を決定することができます. 他にも自炊したいけど,何を作ったら良いか分からない一人暮らしの大学生も同様です.
使い方は以下の通りで非常に簡単です.
- 今日の夜ご飯はJumpaku Dinnerに従うと心に決める.
- Jumpaku Dinnerのページを開く.
- Jumpaku Dinnerに提示された夜ご飯を作って食べる.
どのように動作しているか
https://dinner.jumpaku.netで公開しているJumpaku Dinnerの概要を以下に示します. Jumpaku Dinnerのフロントエンドは,TypeScriptとReactで記述され,webpackによりビルドされます. Jumpaku Dinnerのバックエンドではhttpdが動作しており,ビルドされたリソースをサーブしています. これらをまとめたDockerイメージはDocker Hubで公開しています.
なぜ制作したか
2020年5月頃より,私は自炊した料理の写真をTwitter(Jumpaku@Jumpaku)にアップロードするようになりました. そして写真は2020年9月頃には50枚程度まで増えていましたが,一方で,作りたい料理は無くなっていきました.
また,その頃,プログラミングに関する活動として競技プログラミング(AtCoder)やVPSの管理などしかしておらず,久しくアプリ開発をしていませんでした. そのため,私の開発欲は高まっていました.
そんな中で,私のフォロワのツイートによりあるイベント(【学生限定】秋のLT大会 2020 Online)が開催されること知りました. cist-lt.connpass.com そしてこのLT大会の開催日は,私がそれを知った日の翌日でした.
増えた写真,減りゆく自炊,高まる開発欲,迫る締め切り,VPSの活用,そして発表機会. その時,すべての点が線でつながりました.
「明日までに一人ハッカソンで夜ご飯決定アプリ「Jumpaku Dinner」を制作してVPSで公開し,LT大会で発表しよう!」
そう思い至った私はすぐに参加登録をし,環境構築から開発をスタートしたのでした.
以上の経緯により,Jumpaku Dinnerを制作しました.
感想
- 自分で撮った写真を活用したアプリを制作できて良かった.
- 自分にとって役に立つアプリを制作できて良かった.
- VPSで動作させるサービスが増えて良かった.
- 締め切りに追われながらの一人ハッカソンは,スリリングで楽しかった.
- 完成した成果を発表でき,さらにTwitter等で反応を頂けて嬉しかった.
質疑応答
LT大会で発表した際に,「短期間で完成させるためにはどうしたら良いか?」という質問をいただきました. これに対し,「短期間で完成させられる程度まで要求を低くしていけば良い.」と答えました. これは,言い換えると,とりあえず動くものを作成するということです. このことは,開発時に意識してやっていたわけではないのですが,質問されたことにより,何かを完成させるために大事なことの一つとして自覚することができました. こういったことも含めて発表して良かったと思います.
まとめと展望
- 夜ご飯決定アプリ「Jumpaku Dinner」を制作しました.
- 自分のVPSでJumpaku Dinnerを公開しました.
- 開発成果をLT大会で発表しました.
今後,今回の成果をプロトタイプとして,構成の見直しや機能の追加をしてさらに発展させていきたいと考えています. 具体的にはデータをデータベースで管理することとした上で,「My夜ご飯管理機能」と「今日の夜ご飯共有機能」をWeb APIとして実装したいと考えています.
リンク
- Jumpaku Dinner:https://dinner.jumpaku.net
- GitHub:https://github.com/Jumpaku/JumpakuDinner
- Docker Hub:https://hub.docker.com/r/jumpaku/jumpaku-dinner
- 【学生限定】秋のLT大会 2020 Online:https://cist-lt.connpass.com/event/187301/
- 発表資料(ダウンロードすると読めます.):https://nextcloud.jumpaku.net/s/7tgpizFxkbZzHW4
C++におけるabs関数のオーバーロードについて調べた
目的
https://ja.cppreference.com/w/cpp/header によると,cmath
やcstdlib
などのC互換ヘッダはstd
名前空間で宣言した関数をグローバル名前空間でも宣言するかもしれない.
実際,以下のソースコードをコンパイルしてみると,グローバル名前空間のabs
関数とstd
名前空間のabs
関数の両方を参照できることが分かる.
#include <cmath> #include <cstdlib> #include <iostream> using ll = long long int; int main() { ll x = 1e9; std::cout << ::abs(x * x) << std::endl; std::cout << std::abs(x * x) << std::endl; }
- コンパイルコマンド
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment -o main main.cpp
しかし,以下のように生成された実行ファイルを実行してみると,グローバル名前空間のabs
関数とstd
名前空間のabs
関数が異なる結果を出力していることが分かる.
- 実行コマンド
./main
- 実行結果
1486618624 1000000000000000000
このプログラムは(109) * (109)の絶対値を求めようするものであるが,グローバル名前空間のabs
関数を使うと正しい値を求められない.
本記事では,グローバル名前空間のabs
関数とstd
名前空間のabs
関数がどのように解決されるかを確かめ,この問題の対策を考える.
検証
https://cpprefjp.github.io/reference/cstdlib.html によると,cstdlib
はint
型の絶対値を求めるint abs(int)
を宣言している.
また,https://cpprefjp.github.io/reference/cmath/abs.html によると,cmath
はint abs(int)
に加えて,long long int
型の絶対値を求めるlong long int abs(long long int)
を宣言している.
そこで,グローバル名前空間のabs
関数とstd
名前空間のabs
関数に対して,上のプログラムのようにlong long int
型の引数を渡した時,これらの関数がそれぞれint abs(int)
とlong long int abs(long long int)
のどちらに解決されるのかを確かめるために,以下のソースコードをコンパイルしてみた.
ソースコード
#include <cmath> #include <cstdio> #include <cstdlib> #include <type_traits> using std::is_same_v, std::declval; using ll = long long int; int main() { static_assert(is_same_v<decltype(::abs(declval<ll>())), int>); static_assert(is_same_v<decltype(std::abs(declval<ll>())), ll>); }
コンパイル
検証は以下の環境で行った.
以下のコマンドによりコンパイルを行い,実行ファイルmain
を得た.
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment -o main main.cpp
結果
上のソースコードがコンパイルに成功したことから,以下のことが分かる.
- グローバル名前空間の
abs
関数にlong long int
型の引数を渡すとint abs(int)
に解決される. std
名前空間のabs
関数にlong long int
型の引数を渡すとlong long int abs(long long int)
に解決される.
考察
検証結果から最初に示したプログラムで,グローバル名前空間のabs
関数を使った場合に正しい値を求められなかった原因は次のようなものだと考えられる.
グローバル名前空間のabs
にlong long int
型の引数を渡すと,abs
がint abs(int)
に解決されるため,引数がint
型に変換される.
この時,(109) * (109) == 1018はint型で表現するには大きすぎるためオーバーフローが発生し,正しくない値がabs
に渡ってしまう.
対策
グローバル名前空間のint abs(int)
関数にlong long int
型の引数を渡してしまう誤りはコンパイルエラーとして検出できない.
また,long long int
型の値の絶対値を求めるときはllabs
を使うように意識するという方法や,abs
関数の前に常に「std::」を付けるように意識するという方法は,ミスの無い完璧な人間は存在しないということを考えれば,対策として十分とは言えない.
したがって,一般的な対策はとても難しいと考えられる.
しかし,対象をプログラミングコンテストなどに限れば以下のような対策が実施可能である.
予めテンプレートとなるソースコードを用意しておくことが可能なAtCoder(https://atcoder.jp)のようなプログラミングコンテストでは,そのテンプレートに次の記述を追加しておくことで,グローバル名前空間のint abs(int)
関数にlong long int
型の引数を渡してしまう誤りをなくすことができる.
#include<cmath> using std::abs;
このusing
宣言により,グローバル名前空間にstd
名前空間のabs
関数が導入されるため,long long int
型をグローバル名前空間のabs
関数の引数にしようとした場合でもlong long int abs(long long int)
が参照されるようになる.
実際,最初のソースコードにこれを追加して実行すると正しい結果が得られる.
#include <cmath> #include <cstdlib> #include <iostream> using ll = long long int; using std::abs; int main() { ll x = 1e9; std::cout << ::abs(x * x) << std::endl; std::cout << std::abs(x * x) << std::endl; }
- 実行結果
1000000000000000000 1000000000000000000
JavaFXのウィンドウやSVG画像への図の描画
本記事はMuroran Institute of Technology Advent Calendar 2019 12/14の記事です.
概要
本記事では
について具体的なGradleプロジェクトの例を用いて説明します.
JavaFXはJavaのGUIライブラリの一つです. JavaFXはJava8で標準ライブラリに追加されてJDKの一部となりましたが,Java11からはJDKに同梱されなくなり,代わりにOpenJFXを利用することができるようになりました. 1.では,JavaFXのCanvasクラスに図の描画を行うためにorg.jfreeのFXGraphics2DというライブラリのFXGraphics2Dクラスを利用します. FXGraphics2Dクラスは標準ライブラリのGraphics2Dクラスを継承しているため,Graphics2Dクラスと同じように扱うことができます.
SVG画像はベクタ画像(品質の劣化なくサイズを変更可能な形式の画像)の一つです. SVG画像の形式はXML形式で,仕様はW3Cによって勧告されています. 2.では,SVG画像に図の描画を行うためにorg.jfreeのJFreeSVGというライブラリのSVGGraphics2Dクラスを利用します. SVGGraphics2Dクラスも標準ライブラリのGraphics2Dクラスを継承しており,Graphics2Dクラスと同じように扱うことができます.
3.では,FXGraphics2DクラスとSVGGraphics2DクラスがGraphics2Dクラスを継承していることを生かたポリモーフィックなプログラムによって,JavaFXウィンドウとSVG画像への同じ図の描画を行います.
1.,2.,3.はOpenJFX,FXGraphics2D,JFreeSVGといった標準のJDKに含まれないライブラリを利用しますが,これらのライブラリの管理を含めたプロジェクトのビルドを行うために,本記事ではGradleというビルドツールを使用します. Gradleはbuild.gradleという設定ファイルに従って,コンパイル,ライブラリの用意,実行といった操作を自動的に行うソフトウェアです.
以下では,まず,本記事の実行環境について説明し,Graphics2Dクラスの基本的な使い方を示します. その後1.,2.,3.についてそれぞれ,Gradleプロジェクトの例を示します. 本記事ではJavaの環境としてJava13を利用しました.
実行環境
本記事のプログラムは,次のようにGitHubのリポジトリからクローンして実行することができます.
git clone https://github.com/Jumpaku/AdventCalendar_2019-12-14.git cd AdventCalendar_2019-12-14 # JavaFXウィンドウへの図の描画 cd example1 ./gradlew run # ./gradlew.bat run (Windowsの場合) # SVG画像への図の描画 cd ../example2 ./gradlew run # ./gradlew.bat run (Windowsの場合) # JavaFXウィンドウとSVG画像への同じ図の描画 cd ../example3 ./gradlew run # ./gradlew.bat run (Windowsの場合)
ただし,Java13とGitがインストールされていることが前提となります.
このリポジトリは以下のように3つのプロジェクトexample1,example2,example3で構成されています.
例えば,example1
ディレクトリで./gradlew run
を実行すると,Gradleがexample1/build.gradle
に書かれたプロジェクトの設定に従ってプロジェクトをビルドし,起動します.
プログラムが起動されるとexample1/src/main/java/example1/Main.java
のmain
メソッドが呼ばれます.
このmain
メソッドはFXGraphics2D
クラスを利用します.
FXGraphics2D
クラスはorg.jfreeのFXGraphics2Dというライブラリに含まれています.
必要となるライブラリをbuild.gradle
に書いておくと,ビルドする時にGradleによってダウンロードされてライブラリを利用できるようになります.
Graphics2Dの基本的な使い方
Graphics2Dの基本的な使い方として
- 基本図形のストローク描画とフィル描画
- パス操作
- 領域操作
- アフィン変換
を示します.
基本図形のストローク描画とフィル描画
public static void exampleDrawAndFill(Graphics2D g) { // 楕円を表現するEllipse2Dオブジェクトを生成する // 基本図形は他にLine2D(線分),Rectangle2D(長方形),Arc2D(弧)などもある Ellipse2D ellipse = new Ellipse2D.Double(0.0, 25.0, 100.0, 50.0); // Ellipseオブジェクトの内部の塗りつぶし色を設定する g.setColor(new Color(255, 75, 0)); // Ellipseオブジェクトの内部を塗りつぶす g.fill(ellipse); // Ellipseオブジェクトの輪郭線の色を黒に設定する g.setColor(Color.BLACK); // Ellipseオブジェクトの輪郭線を太さ1の線に設定する g.setStroke(new BasicStroke(1f)); g.draw(ellipse); }
パス操作
public static void examplePath(Graphics2D g) { // 複雑なパス図形を表現できるPath2Dオブジェクトを生成する Path2D curve = new Path2D.Double(); // Path2Dオブジェクトの開始点を設定する curve.moveTo(125.0, 25.0); // Path2Dオブジェクトの開始点に,3次Bezier曲線を接続する curve.curveTo(200.0, 25.0, 100.0, 75.0, 175.0, 75.0); // Path2Dオブジェクトの輪郭線を描画する g.setColor(new Color(3, 175, 122)); g.setStroke(new BasicStroke(1f)); g.draw(curve); // Path2Dオブジェクトを生成する Path2D polyline = new Path2D.Double(); // Path2Dオブジェクトの開始点を設定する polyline.moveTo(125.0, 25.0); // Path2Dオブジェクトの開始点に,線分を接続する polyline.lineTo(200.0, 25.0); // Path2Dオブジェクトに,さらに線分を接続する polyline.lineTo(100.0, 75.0); // Path2Dオブジェクトに,さらに線分を接続する polyline.lineTo(175.0, 75.0); // Path2Dオブジェクトの輪郭線を黒い太さ1線に設定し,描画する // BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUNDは線分の端点や接続部分を丸くするための設定 g.setColor(Color.BLACK); g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(polyline); }
領域操作
public static void exampleArea(Graphics2D g) { // 3個のEllipse2Dオブジェクトを生成する Ellipse2D ellipse0 = new Ellipse2D.Double(0.0, 100.0, 100.0, 50.0); Ellipse2D ellipse1 = new Ellipse2D.Double(0.0, 100.0, 50.0, 100.0); Ellipse2D ellipse2 = new Ellipse2D.Double(0.0, 100.0, 50.0, 50.0); // 3個のEllipse2Dオブジェクトの和集合領域を表現するAreaオブジェクトを生成する Area union = new Area(ellipse0); union.add(new Area(ellipse1)); union.add(new Area(ellipse2)); // 3個のEllipse2Dオブジェクトの共通集合領域を表現するAreaオブジェクトを生成する Area intersection = new Area(ellipse0); intersection.intersect(new Area(ellipse1)); intersection.intersect(new Area(ellipse2)); // 和集合領域と共通集合領域を塗りつぶす g.setColor(new Color(77, 196, 255)); g.fill(union); g.setColor(new Color(0, 90, 255)); g.fill(intersection); // 和集合領域と共通集合領域の輪郭線を描画する g.setColor(Color.BLACK); g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(union); g.draw(intersection); }
アフィン変換
public static void exampleTransform(Graphics2D g) { // 弧を表現するArc2Dオブジェクトを生成する Arc2D arc = new Arc2D.Double(-1.0, -1.0, 2.0, 2.0, -45.0, 270.0, Arc2D.PIE); // アフィン変換を表現するAffineTransformオブジェクトを生成する AffineTransform t = new AffineTransform(); t.translate(150.0, 150.0); // 平行移動 t.rotate(Math.PI / 2); // 回転 t.scale(50.0, 50.0); // 拡大 // Arc2Dオブジェクトにアフィン変換した図形オブジェクトを生成する Shape transformed = t.createTransformedShape(arc); // アフィン変換した図形オブジェクトを塗りつぶす g.setColor(new Color(255, 241, 0)); g.fill(transformed); // アフィン変換した図形オブジェクトの輪郭線を描く g.setColor(Color.BLACK); g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(transformed); }
JavaFXウィンドウへの図の描画
JavaFXウィンドウへの図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.
build.gradle
plugins { id 'java' id 'application' // JavaFXを利用するためにOpenJFXのプラグインを追加する id 'org.openjfx.javafxplugin' version '0.0.8' } group 'jumpaku' version 'SNAPSHOT' // 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する repositories { mavenCentral() } // JavaFXに関する設定をする javafx { // 使用するJavaFXのバージョンを指定する version = "13.0.1" // JavaFXで使用するモジュールを指定する(モジュールはJava9の機能) modules = ['javafx.controls', 'javafx.swing'] } // example2.Mainクラスをメインクラスとして指定する mainClassName = 'example1.Main' // FXGraphics2Dライブラリを追加する dependencies { implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8' }
Main.java
package example1; /*import...*/ public class Main { /** * JavaFXウィンドウに図を描画する * さらに描画した図をpng画像としてファイルに書き出す * @param args */ public static void main(String[] args) { // Appクラスを指定してアプリケーションを起動する Application.launch(App.class, args); } public static class App extends Application { /** * アプリケーション起動時に呼び出される * @param primaryStage */ @Override public void start(Stage primaryStage) throws IOException { // Canvasオブジェクトを用意する int width = 320; int height = 240; Canvas canvas = new Canvas(width, height); // CanvasオブジェクトのGraphicsContext2DオブジェクトをFXGraphics2Dクラスのコンストラクタに渡す GraphicsContext ctx = canvas.getGraphicsContext2D(); Graphics2D g = new FXGraphics2D(ctx); // Graphics2Dクラスを通してCanvasオブジェクトに図を描画する exampleDrawAndFill(g); examplePath(g); exampleArea(g); exampleTransform(g); // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する primaryStage.setScene(new Scene(new Pane(canvas))); primaryStage.show(); // CanvasオブジェクトのPNG画像を書き出す SnapshotParameters params = new SnapshotParameters(); WritableImage img = canvas.snapshot(params, new WritableImage(width, height)); File file = new File("example.png"); ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file); } } public static void exampleDrawAndFill(Graphics2D g) {/*...*/} public static void examplePath(Graphics2D g) {/*...*/} public static void exampleArea(Graphics2D g) {/*...*/} public static void exampleTransform(Graphics2D g) {/*...*/} }
実行結果
example1
ディレクトリにおいて,./gradlew run
を実行して表示されたウィンドウを以下に示します.
また,書き出されたPNG画像(example1/example.png)を以下に示します.
SVG画像への図の描画
SVG画像への図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.
build.gradle
plugins { id 'java' id 'application' } group 'jumpaku' version 'SNAPSHOT' // 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する repositories { mavenCentral() } // example2.Mainクラスをメインクラスとして指定する mainClassName = 'example2.Main' // JFreeSVGライブラリを追加する dependencies { implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4' }
Main.java
package example2; /*import...*/ public class Main { /** * SVG画像に図を描画しファイルに書き出す * @param args */ public static void main(String[] args) { // SVGGraphics2Dオブジェクトを用意する SVGGraphics2D g = new SVGGraphics2D(320, 240); // SVGGraphics2Dオブジェクトに図を描画する exampleDrawAndFill(g); examplePath(g); exampleArea(g); exampleTransform(g); // SVGGraphics2DオブジェクトからSVG画像の文字列を取得しファイルに書き出す String svg = g.getSVGDocument(); try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) { writer.print(svg); } catch (FileNotFoundException exp) { exp.printStackTrace(); } } public static void exampleDrawAndFill(Graphics2D g) {/*...*/} public static void examplePath(Graphics2D g) {/*...*/} public static void exampleArea(Graphics2D g) {/*...*/} public static void exampleTransform(Graphics2D g) {/*...*/} }
実行結果
example2
ディレクトリにおいて,./gradlew run
を実行して書き出されたSVG画像(example2/example.svg)を以下に示します.
JavaFXウィンドウとSVG画像への同じ図の描画
JavaFXウィンドウとSVG画像への同じ図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.
build.gradle
plugins { id 'java' id 'application' // JavaFXを利用するためにOpenJFXのプラグインを追加する id 'org.openjfx.javafxplugin' version '0.0.8' } group 'jumpaku' version 'SNAPSHOT' // 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する repositories { mavenCentral() } // JavaFXに関する設定をする javafx { // 使用するJavaFXのバージョンを指定する version = "13.0.1" // JavaFXで使用するモジュールを指定する modules = ['javafx.controls', 'javafx.swing'] } // example3.Mainクラスをメインクラスとして指定する mainClassName = 'example3.Main' // FXGraphics2DライブラリとJFreeSVGライブラリを追加する dependencies { implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8' implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4' }
Main.java
package example3; /*import...*/ public class Main { /** * JavaFXウィンドウ上にドラッグ軌跡を描画するPNG画像に書き出す * JavaFXウィンドウに描画されたものと同じドラッグ軌跡をSVG画像にも書き出す */ public static void main(String[] args) { // Appクラスを指定してアプリケーションを起動する Application.launch(App.class, args); } public static class App extends Application { /** 直前のドラッグ位置 */ Point2D previousPoint; /** * アプリケーション起動時に呼び出される * @param primaryStage */ @Override public void start(Stage primaryStage) { int width = 640; int height = 480; // Canvasオブジェクトを生成する Canvas canvas = new Canvas(width, height); // FXGraphics2Dオブジェクトを生成する FXGraphics2D fxGraphics2D = new FXGraphics2D(canvas.getGraphicsContext2D()); // SVGGraphics2Dオブジェクトを生成する SVGGraphics2D svgGraphics2D = new SVGGraphics2D(width, height); // FXGraphics2DオブジェクトとSVGGraphics2Dオブジェクトをまとめたリストを生成する List<Graphics2D> graphics2DList = Arrays.asList(fxGraphics2D, svgGraphics2D); // Canvasオブジェクトにマウスプレス時のイベントハンドラを設定する canvas.setOnMousePressed(e -> { // マウスプレス時には図をクリアして現在のカーソル位置を保存する graphics2DList.forEach(g -> { g.setBackground(Color.WHITE); g.clearRect(0, 0, width, height); }); previousPoint = new Point2D.Double(e.getX(), e.getY()); }); // Canvasオブジェクトにマウスドラッグ時のイベントハンドラを設定する canvas.setOnMouseDragged(e -> { // マウスドラッグ時には直前のカーソル位置と現在のカーソル位置の間に線分を描画して現在のカーソル位置を保存する Point2D.Double currentPoint = new Point2D.Double(e.getX(), e.getY()); graphics2DList.forEach(g -> g.draw(new Line2D.Double(previousPoint, currentPoint))); previousPoint = currentPoint; }); // Canvasオブジェクトにマウスリリース時のイベントハンドラを設定する canvas.setOnMouseReleased(e -> { // マウスリリース時にはSVG画像とPNG画像の書き出しを行う // PNG画像を書き出す SnapshotParameters params = new SnapshotParameters(); WritableImage img = canvas.snapshot(params, new WritableImage(width, height)); try { File file = new File("example.png"); ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file); } catch (IOException ex) { ex.printStackTrace(); } // SVG画像を書き出す String svg = svgGraphics2D.getSVGDocument(); try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) { writer.print(svg); } catch (FileNotFoundException exp) { exp.printStackTrace(); } }); // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する primaryStage.setScene(new Scene(new Pane(canvas))); primaryStage.show(); } } }
実行結果
example3
ディレクトリにおいて,./gradlew run
を実行しました.
表示されたウィンドウにドラッグ軌跡が描画される様子を以下に示します.
書き出された(example3/example.png)を以下に示します. 書き出されたSVG画像(example3/example.svg)を以下に示します.
まとめ
ICPC 2019 Asia Yokohama Regionalに参加
本記事はMuroran Institute of Technology Advent Calendar 2019 12/13の記事です.
はじめに
私はこれまで,2016年,2017年,2018年のICPC国内予選に選手として参加しています. 今回も選手として参加したかったのですが,今回は選手としての参加資格がありませんでした. 他の参加選手からコーチになるように頼まれたので,引き受けることにしました. 今回,私の大学から5チームが国内予選に参加し,そのうち1チームが横浜のアジア地区予選に出場することとなり,コーチとして一緒に行ってきました. 以下ではICPC 2019 Asia Yokohama Regionalに参加した感想を時系列順に述べます.
ICPC 2019 Yokohama Regional 国内予選
準備
コーチの最も重要な任務はチーム登録と実行委員会からのメールの伝達です. 特にチーム登録は意外と大変でした. 選手はアカウントを作成し,これをコーチに伝えます. コーチは選手を検索し,作成したチームに選手を追加します. その後,選手たちは登録情報の補充をします. あるチームでは,アカウント登録の完了がなかなか終わらずチーム登録を完了できません. また,あるチームでは,チーム登録まで完了しても,登録情報の補充が完了しない選手がいます. 催促をしてもなかなか反応がないこともありました. 登録期限が迫る中,私は選手登録とチーム登録が完了できるか心配でした. 最終的には全員参加できたので良かったです. 私が,過去にICPC国内予選に参加した時のコーチも同じような思いをしていたと思うと,改めて感謝の念を抱きました.
他にも,選手たちと一緒に監督を引き受けてくれる先生を探し,引き受けてくれるようにお願いしました. 監督の仕事は,試験監督のような感じで,国内予選当日に問題のコピーを配り,選手が不正しないようにを監視することです. また,監督は必要に応じて審判団との連絡も行います. 私の指導教員が引き受けてくれてありがたかったです.
本番
ICPC国内予選当日,コーチの仕事はありませんでした. 室蘭工業大学からはSataniChoが横浜のアジア地区予選に出場することとなりました.
問題を解いた
選手たちと同じモチベーションで打ち上げに臨むためには,同じ問題を解いて同じ熱を纏う必要があります. 研究室でICPCに参加していないメンバ2人を誘って問題を解く会を開催しました. 私たちは正式な参加ではないため,パソコンは1人1台使用可,インターネット接続可,複数ディスプレイ可で解きました. 他の2人がA,Bを担当し,私はCを担当しました. 最初C++で解こうと思いましたが,組み合わせを全て列挙すれば良いことに気がついてPythonのitertoolsを使うことにしました. 他の2人がA,Bを解き終わってからは3人で協力して解きました. ほぼ3時間以内に解き終わることができました. Pythonが遅すぎたため,最後はPyPyで実行しました. 協力しながら解きごたえのある問題を解くことができて楽しかったです.
ICPC 2019 Asia Yokohama Regional
横浜で開かれるアジア地区予選に出場するSataniChoの選手たちには旅費が支給されます. なんと,コーチにも旅費が支給され,一緒に行くことができました. 日程は3日間で,1日目はリハーサル,2日目は本番と立食パーティ,3日目は会社見学でした.
1日目
5時半くらいに起床し,荷物の準備をしました. 6:15に待ち合わせをして,選手の車に乗せていただき,室蘭工業大学から新千歳空港まで行きました. 自転車では半日かかる約70kmの道のりですが,車だったため9:30出発の飛行機に余裕を持って到着しました. 昼過ぎに羽田空港に到着し蒙古タンメンを食べに行きました. 客が並んでいたため,リハーサルの集合時刻に間に合うか際どいところでしたが,結果的には間に合ったと思います. 会場の横浜産貿ホール マリネリアに到着すると英語で質問されました. 大学名をきかれたのだと解釈し,"Muroran Institute of Technology"と回答しましたが,ピンとこなかったようだったため,"From Hokkaido"と言い直したら,受付へ通されました. あとで考え直すと,もしかしたら北海道大学と勘違いされていたかもしれません. アジア地区予選の公用語は英語なのかと察しました. 受付では,スタッフにStudent IDを見せてくれと英語で言われましたが,よく聞き取れずにいるともう1人のスタッフが"Gakusei shou"と分かりやすい英語で教えてくれました.
リハーサル会場にはチームごとにブースが設けられており,水,弁当,しおり,Tシャツ,参加証明書,ネームタグが支給されました. Tシャツとネームタグは大会の間はずっと付けなければいけないようでした. 今まで選手登録の度に,なぜTシャツのサイズが要求されるのか疑問でしたが,会場で支給されるからだと知ることができました. また,朝時間がなくて3日間の着替えの準備が不十分だったのですが,Tシャツが支給されたことによりこの問題が解決しました.
リハーサルでは,まず諸注意があり,デモンストレーションがあり,その後,選手たちが実際にリハーサル問題を解きます. 国内予選を勝ち抜いた選手たちからは,多くのことを学びました. C++のラムダ式は変数に代入した上で,引数で自分自身を受け取ることで再帰することができることを知った時は衝撃を受けました. また,アジア地区予選ではジャッジシステムを用いるため,実行時間に何時間も使って強引に解くことはできないことも知りました. そして,ACする度にスタッフが風船を持ってやってきて,ブースの机の横に引っ掛けていきます. これはとてもにぎやかな感じがしました. しかし,コーチがその場でするべきことは特にありません. 自分はなぜここにいるのか,という当初から存在していた答えの無い問いと向き合う決心をしました. 起床が早朝だったため,気がつくとウトウトしていました.
リハーサル後,私たちは会場の近くの中華街へ夕食を食べにいきました. まず,インターネットで調べて人気そうだった中華料理屋に並びました. 行列に並ぶのが嫌になったため,その向かいの中華料理屋へいくことになりました. その店は2階建てで,2階のテーブルに案内されました. そこにはスタッフが1人いました. 2階には客用のテーブルが並んでいましたが,厨房などは無いようでした. いったい料理はどこから来るのか,食器はどこへ帰っていくのかと思いましたが,料理用エレベータで上ってきた料理を客に出し,片付けた食器を料理用エレベータで下ろしていたのでした. 私は色々な料理を食べることのできるコース料理を注文しようかとも思いましたが,高かったため,回鍋肉定食を注文しました. とても美味しく,また,お腹も満たされました.
ホテルに着いて21時になるとAtCoder Beginner Contest 145が始まりました. そのE問題のTLEを修正することができないまま終わってしまいました. atcoder.jp 提出したプログラムはソートとDPを組み合わせた解法で,計算量も解法も想定解法と同じはずでしたがTLEとなってしまいました. 原因の究明をしたいとは思いましたが,あまり寝るのが遅くなって寝坊したら選手たちに申し訳ないため,すぐに寝ました.
2日目
本番では,コーチは会場である横浜産貿ホール マリネリアに入ることができず,会場横の神奈川県民ホールで待機することになっていました. ここで,遂にコーチが活躍する機会が訪れました. 選手たちは時計やスマートフォンなどを会場に持ち込めないため,コーチが預かることになったのです. 選手の役に立つことができました.
神奈川県民ホールに着いたのは8:50くらいでしたが,開館時刻が9:00と書いてあったため待つこととしました. 入り口前で待っているとスタッフが1人やってきました. 公用語が英語だと察した私は,挨拶は"Good morning"か"Hello"どちらが良いかと迷いましたが,「おはようございます」言われました. そこは日本語なのか,と混乱した私から1呼吸遅れて出てきたのはカタコトの"Ohayou gozaimasu"でした. そのスタッフが言うには,神奈川県民ホールは開館が9:00であるため,そこから準備が始まるらしく,大変なんだなと思いました.
開館後どうして良いか分からなかった私は周りの人についていくことにしました. みんなの向かう先には,ステージのある大きなホールがあり,多くの椅子が並んでいました. 座っていると着々と準備が進んでいき,ステージ上で大会の中継が始まりました. しばらくすると問題用紙も配布されました. コーチはこのホールで大会が終わるまで中継を見たり,問題を読んだりしながら待機するのかと思いかけたところで,周りを見渡すと,あまりにも人が少ないことに気がつきました. 少し飽きた私はお手洗いのついでに,ホールの外のロビーの様子を見に行くことにしました.
ロビーには軽食が並べられており,問題用紙を広げながら楽しそうに議論をしている他のコーチたちが,何人かいました. ロビーの椅子の方が座りやすため,そこで軽食をいただきながら昨日のAtCoderのE問題を解き直そうと思いました. しかし,AtCoder後に充電するのを忘れていたため,私のMacBookProの充電は風前の灯火でした. 使用可能な電源がないようだったため,パソコンの使用は諦めることにしました. 大会終了までまだまだ時間があります. やることが無くて困った私はSataniChoのその時点での順位の確認をしながら,気がつくと鞄を抱き締めたままウトウトしていました. 周りが賑やかになってきたことに気がついて周囲を見ると選手たちがいて,大会が終了したことを知りました.
その後ホールに移動し,解説と最終順位の発表,協賛企業からのお話などを聞きました. そして,みんなで横浜産貿ホール マリネリアへ向かい,そこで表彰式と立食パーティがありました. SataniChoも何かの賞を受賞していました.
立食パーティでは,まず,ご飯を食べた後,各企業の紹介ブースを回りました. 初めて見る会社も多くあり視野が広がりました. また,鞄に入り切らないほど多くの景品をいただきました. 他にも,タピオカミルクティを獲得するために数独を解いたり,笑顔度を測定したりしました.
3日目
最終日は会社見学でした. 私たちのグループはHauwei,NEC,しながわ水族館の順に見学をしました.
Hauwei
8:15分にホテルをチェックアウトし,Hauweiへ向かいました. そこでは,Hauweiの日本の研究所の会社説明を受け,エンジニアたちからそれぞれの仕事内容を聞き,エンジニアと昼食をいただきました. 私たちと一緒に昼食を食べたエンジニアは,写真が好きで,カメラに関する仕事をしているということで,写真やカメラに対する熱い想いを感じました.
また,しながわ水族館に先に行った他のグループは昼食をどうしているのだろうか,といった会話をSataniChoのチームメイトとした時に,私が「魚料理でも食べているんじゃないか」とジョークを言うと向かいで座っていた他チームの方が笑ってくれて良い人だなと思いました.
NEC
Hauweiの次にNECへ行きました. 移動は個人行動ということになっていたと思いますが,他の選手たちについて行きました. その選手たちも途中で道が分からなくなり,NECと書かれた大きなビルをに向かって歩きました. 一応,選手の監視役のようなICPCスタッフもいましたが,選手の引率役ではないようでした.
NECではAIの話とセキュリティの話を聞いた後で,顔認証技術を利用したシステムのデモンストレーションを見ました.
AIの話では「あの頃は CHOCOLATE」というお菓子の開発について聞きました. これは各時代の新聞から取り出した単語と単語から連想される味を組み合わせたデータを学習して,ある時代のムードをチョコレートの味で再現するというもののようです. その時に流された動画もとてもお洒落でした. しかし,AIが再現した味が本当にその時代のムードを表現できているのかどうかは,確かめられないのでは? と思い,質問しました. これに対しNECの社員は,待ってましたと言わんばかりに回答を始め,私は一本取られたなと思いました.
また,セキュリティの話ではCTFの紹介があり,興味を持ちました.
顔認証技術を利用したシステムのデモンストレーションでは,まるでPSYCHO-PASSや攻殻機動隊のような近未来感のあるシステムが紹介され,とてもワクワクしました.
しながわ水族館
NECの後はしながわ水族館へ向かいました. その時には,私は監視役について行けば,しながわ水族館にたどり着けると気がついていました. 他の選手たちもそのことに気づいたようでした. 私は降りる駅もわからないまま監視役について行きました. まるで,ハンター試験のようだと思いました. しかし,一部の選手たちが途中で違う電車に乗ったり,別の駅で降りたりしようとした際には,監視役は選手たちが間違わないように指示を出し,最後には先頭を歩いてしながわ水族館まで選手たちを導きました. 実は優しい人だったのかと思いました.
また,移動途中の電車内においては,Hauweiで私のジョークに笑ってくれた選手が,僕たちはいつまでこのネームタグをつけていなければいけないのか,という話の中で,「ACした時の風船をつけるよりマシだ」とジョークを言っていて面白いと思いました.
しながわ水族館では,自由行動ということだったので,閉館まで見て回りました. まず,アシカかセイウチのショーを見ました. 水族館スタッフがアシカとセイウチを見分けるには耳を見れば良いと言っていた気がしますが,どっちがどっちかは,もう忘れてしまいました. 他にもクラゲ,イカ,エイ,ペンギン,サメ,ドクターフィッシュなどが印象的でした.
特にドクターフィッシュのコーナーでは水槽に手を入れると,ドクターフィッシュが群がってきて意外と高い周波数で手をつつきます. 痛くもなく,痒くもなく,くすぐったくもなく,とても不思議な感覚でした. ちょうどそこで,昼食で私のジョークに笑ってくれて,電車でAC風船のジョークを言っていた方と再会しました. 彼は,ドクターフィッシュの感覚が気に入ったようで,「家でドクターフィッシュを飼って顔を入れたい」とユーモアのあることを言っていました. 彼と話したのそれが最後ですが,TwitterのIDを聞くなど,もっと仲良くなりたかったなと少し後悔しています.
しながわ水族館の会社見学では,ショーの準備をしたり,展示物を工夫したりといった,客を楽ませるための心遣いを感じて,尊敬しました. また,水族館の仕事は展示の他にも環境保全や生態調査もあることを知りました.
帰路
しながわ水族館を出た後は,研究室の在室表のマグネットとして使用できるマグネット付きチンアナゴを記念に買い,その後夕食としてラーメンを食べました.
余裕を持って遅い出発時刻の飛行機を予約していたため,時間が余りましたが,早めに羽田空港へ行って,お土産を買ったり,プログラムを書いたりすることにしました.
空港ではさっさと保安検査場を通過した後,お土産としてすいーとぽてたまごを買い,それから搭乗時刻まで一昨日のAtCoderでTLEのまま放置してあったE問題の解き直しました.
アルゴリズム自体は想定解法通りであったにも関わらず,実行時間がかかってしまうボトルネックは,DPのメモ化に用いているstd::unordered_map
だろうと考えました.
実際,std::unordered_map
の代わりに2次元にしたstd::vector
を用いるとTLEしませんでした.
よく調べてみるとキーの衝突が頻発いていることが分かりました.
私はどうしてもDPのメモ化にハッシュマップを用いたかったため,次の三つを試してみました.
std::unordered_map::reserve
を呼んでメモリ確保を最初の一回だけにする.std::array
とstd::map
を組み合わせてシンプルな自前のハッシュマップを実装する.std::tuple
に対して実装しているハッシュ関数を変更する.
まず,1.のみの場合は特に効果を感じませんでした. atcoder.jp 次に,2.のみの場合は,結果はTLEとなりましたが,TLEとなるテストケースが2個に減り,効果があることが分かりました. atcoder.jp さらに,2.と3.を同時に行うと,無事ACできました. atcoder.jp ハッシュ関数は元々Effective Javaを参考に実装していたのですが,よりハッシュ値が分散するようにチューニングしました. 最後に,標準ライブラリがある場合には出来るだけそれを使うべきだと考えて,3.のみの場合についても試してみました. atcoder.jp 結果はTLEでしたが,先ほどの2.のみの場合に残ったTLEのテストケースのうち一つがAC,もう一つがTLEであり,ACした方は実行時間が1986 msだったため,もう一度提出してみると今度は1999 msでACすることができました. atcoder.jp
新千歳空港から室蘭へは再び選手の車に乗せていただきました.
まとめ
ASP.NETお試し会
本記事はMuroran Institute of Technology Advent Calendar 2019 12/7の記事です.
はじめに
唐突にASP.NETお試し会を開催しました. 参加者は3名(表示名:参加者1,参加者2,参加者3)です. 内容はASP.NETを用いたジャンケンAPIの試作です. 以下では,開催に至った経緯,ASP.NETお試し会でやったこと,試作したジャンケンAPIの動作例,3名の感想を書きます.
開催に至った経緯
参加者1は普段からUnityでゲーム開発をしています. さらに,参加者1はオンラインゲームの開発に挑戦しようとしています. 参加者1は,特にサーバサイドにおける通信を扱うプログラミングについて,参加者2から教えてもらうことにしました. 参加者2は,参加者1がC#に慣れていると考え,ASP.NETを利用してサーバプログラムを開発したら良いと考えました. しかし,参加者2はサーバプログラムの開発経験はKtorを利用した簡単なWebAPIの開発だけであり,ASP.NETについては全く知らないため教えることができません. 参加者2はKtorを用いないサーバプログラム開発にも興味を持っていたため,参加者1と一緒に調べながら簡単なサーバプログラムを試作し,理解を深めることにしました. そこに参加者3が合流して,ASP.NETお試し会が開催されることとなりました.
ASP.NETお試し会でやったこと
ASP.NETお試し会では以下の作業を行いました.
- VisualStudioの起動とプロジェクトの作成
- ジャンケンAPIの試作
- モデルの構成
- コントローラーの構成
VisualStudioの起動とプロジェクトの作成
VisualStudioの起動して,「Welcome Page」から「New Project...」,「ASP.NET Core Web API」,「Next」と進み,「Project Name」と「Solution Name」を埋め,「Create」を選択してプロジェクトを作成しました.
実行し,ブラウザからhttp://localhost: 24635/api/values
にアクセスすると["value1","value2"]
と表示されました.
ポート番号は人にプロジェクトによって異なるようです.
初期状態ではこのURLにアクセスするとControllers/ValuesController.cs
のpublic IEnumerable<string> Get()
メソッドが呼ばれているようです.
Controllers/ValuesController.cs
を以下に示します.
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace Test3.Controllers { [Route("api/[controller]")] public class ValuesController : Controller { // GET api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public void Post([FromBody]string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
ジャンケンAPIの試作
ASP.NETのプロジェクトの初期動作を確認した後,以下のモデルとコントローラを実装し,ジャンケンAPIを試作しました.
モデルの構成
モデルはゲームのリストとゲームを操作するコマンドで構成されます. ゲームはプレイヤ2人の名前と出した手(グー,チョキまたはパー)を保持します. ゲームを操作するコマンドは以下の通りです.
- make:
- 入力:プレーヤー0の名前
- 出力:ルームID
- 新たなゲームを作成する
- enter :
- 入力:入室する部屋のルームID,プレーヤー1の名前
- 出力:なし
- 既存の部屋に入室する
- tryPlay :
- 入力:ルームID
- 出力:プレイを開始できるかどうか
- プレイできるか確認する
- play :
- 入力:ルームID,プレーヤー名,グー,チョキまたはパー
- 出力:なし
- グー,チョキまたはパーを出す
- tryResult:
- 入力:ルームID
- 出力:勝敗を判定できるかどうか
- 勝敗を判定できるか確認する
- result :
- 入力:ルームID
- 出力:勝者と敗者
- 勝敗を確認する
以上のモデルを実装したModelクラスを以下に示します.
using System; using System.Collections.Generic; namespace Aspdotnet.Models { public enum Move { Rock, Paper, Scissors, None } public class Result { String Winner; String Loser; public Result(String winner, String loser) { Winner = winner; Loser = loser; } public bool IsTie() { return Winner == null && Loser == null; } public override string ToString() { return "Winner: " + Winner + ", " + "Loser: " + Loser; } } class Game { public String player0; public Move move0 = Move.None; public String player1; public Move move1 = Move.None; public bool IsReady() { return player0 != null && player1 != null && move0 == Move.None && move1 == Move.None; } public bool HasFinished() { return player0 != null && player1 != null && move0 != Move.None && move1 != Move.None; } public Result GetResult() { if (!HasFinished()) { return null; } if (move1 == Move.Paper && move0 == Move.Scissors || move1 == Move.Scissors && move0 == Move.Rock || move1 == Move.Rock && move0 == Move.Paper) { return new Result(player0, player1); } if (move0 == Move.Paper && move1 == Move.Scissors || move0 == Move.Scissors && move1 == Move.Rock || move0 == Move.Rock && move1 == Move.Paper) { return new Result(player1, player0); } return new Result(null, null); } } public class Model { List<Game> games = new List<Game>(); static Model instance = new Model(); public static Model GetInstance() { return instance; } Model() { } public override string ToString() { string s = ""; for (int i = 0; i < games.Count; ++i) { var g = games[i]; s += i.ToString() + " : "; s += "player0 = " + g.player0 + "; "; s += "player1 = " + g.player1 + "; "; s += "move0 = " + g.move0 + "; "; s += "move1 = " + g.move1 + "; "; } return s; } public int Make(String player0) { if (player0 == null) { return -1; } int roomId = games.Count; var game = new Game(); game.player0 = player0; games.Add(game); return roomId; } public void Enter(int roomId, String player1) { if (player1 == null) { return; } if (roomId < 0 || roomId >= games.Count) { return; } var game = games[roomId]; game.player1 = player1; } public bool TryPlay(int roomId) { if (roomId < 0 || roomId >= games.Count) { return false; } var game = games[roomId]; return game.IsReady(); } public void Play(int roomId, String player, Move move) { if (roomId < 0 || roomId >= games.Count || move == Move.None) { return; } var game = games[roomId]; if (game.player0 == player && game.move0 == Move.None) { game.move0 = move; } if (game.player1 == player && game.move1 == Move.None) { game.move1 = move; } } public bool TryResult(int roomId) { if (roomId < 0 || roomId >= games.Count) { return false; } return games[roomId].HasFinished(); } public Result Result(int roomId) { if (roomId < 0 || roomId >= games.Count) { return null; } var game = games[roomId]; if (!game.HasFinished()) { return null; } return games[roomId].GetResult(); } } }
コントローラーの構成
クライアントがHTTPのGETメソッドを用いてジャンケンAPIにアクセスして,上のコマンドを実行できるようにします. コマンドへの入力はURLに含めることにしました. これを実装したJankenControllerクラスを以下に示します.
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; namespace Aspdotnet.Controllers { [Route("api/[controller]")] public class JankenController : Controller { // GET api/janken/make/player0Name [HttpGet("make/{player0}")] public int GetMake(string player0) { var model = Models.Model.GetInstance(); int roomId = model.Make(player0); Console.WriteLine(model); return roomId; } // GET api/janken/enter/roomId/player1Name [HttpGet("enter/{roomId}/{player1}")] public string GetEnter(int roomId, string player1) { var model = Models.Model.GetInstance(); model.Enter(roomId, player1); Console.WriteLine(model); return ""; } // GET api/janken/tryPlay/roomId/ [HttpGet("tryPlay/{roomId}")] public bool GetTryPlay(int roomId) { var model = Models.Model.GetInstance(); var isReady = model.TryPlay(roomId); Console.WriteLine(model); return isReady; } // GET api/janken/play/roomId/player0Name/Rock [HttpGet("play/{roomId}/{player}/{move}")] public string GetPlay(int roomId, string player, string move) { var model = Models.Model.GetInstance(); Models.Move m = Models.Move.None; switch (move) { case "Rock": m = Models.Move.Rock; break; case "Paper": m = Models.Move.Paper; break; case "Scissors": m = Models.Move.Scissors; break; default: return ""; } model.Play(roomId, player, m); Console.WriteLine(model); return ""; } // GET api/janken/tryResult/roomId/ [HttpGet("tryResult/{roomId}")] public bool GetTryResult(int roomId) { var model = Models.Model.GetInstance(); bool hasFinished = model.TryResult(roomId); Console.WriteLine(model); return hasFinished; } // GET api/janken/result/roomId/ [HttpGet("result/{roomId}")] public string GetResult(int roomId) { var model = Models.Model.GetInstance(); Models.Result result = model.Result(roomId); Console.WriteLine(model); return result.ToString(); } } }
試作したジャンケンAPIの動作例
最後に試作したジャンケンAPIの動作確認をしました. 動作例を以下に示します.
curl http://localhost:24635/api/janken/make/Sankasha1 # => 1 curl http://localhost:24635/api/janken/enter/0/Sankasha2 curl http://localhost:24635/api/janken/tryPlay/0 # => true curl http://localhost:24635/api/janken/play/0/Sankasha1/Paper curl http://localhost:24635/api/janken/play/0/Sankasha2/Rock curl http://localhost:24635/api/janken/tryResult/0 # => true curl http://localhost:24635/api/janken/result/0 # => Winner: Sankasha1, Loser: Sankasha2
3名の感想
参加者1
ASP.NETを通じて、HTTP通信を組み込んだソフトウェアをどう組み立てるか、どう実装した方が良いかを考える経験が出来ました
参加者2
見様見真似でとりあえず,動作するジャンケンAPIを作成することができた. その点では,やはりVisualStudioとASP.NETはすごいと思った. ただ,実際にVPSなどで動作させる場合にどうすれば良いか想像できなかった. 当たり前だが,今後ASP.NETを利用する場合には使い方をきちんと調べる必要がある.
ジャンケンAPIは,最初は単純なように思えたが,実際に試作してみると状態を管理するのは意外と面倒だった. また,入力が全てURLに含まれているため,不正なプレイが可能かもしれない.
ASP.NETお試し会を開催した結果,アドベントカレンダーを書くこともできため良かった.
参加者3
もう少し難しいと思ってたのですが,実装自体は予想以上に楽だったので驚きました.使い方次第でいろいろ面白いことが出来そうなので,自作ゲームで生かしてみたいです.