生存報告ヴァルキュリア

adventar.org

シンデレラガールズ Advent Calendar 2017

Dec. 19

導入

アドベントカレンダーにおいて,デレステをきっかけに人生が完全に変わった事例が報告されている. muscle-keisuke.hatenablog.com 本報告には関係が無いが,もしまだ上の記事を読んでいないなら,是非読んでいただきたい. これは,その記事の筆者がサークルの共用タブレットにインストールされていたデレステをきっかけにして,

  1. デレマスにハマり,
  2. 赤城みりあ担当のプロデューサとなり,
  3. LINEを通じて会話することを目指して,
  4. プロデューサおよびアイドルが技術的,機械学習的に成長する

という話を書いた記事であり,次元の壁を超えたドラマとも言える.

一方,その裏で別のドラマが進行していたことを知る者は少ない. そこで,本報告ではそのもう一つのドラマの報告を目的として, もう一つのデレステをきっかけに人生が完全に変わった話について解説する. さらに,その上で,私が学んだことについて考察し, 最後に,現状の課題と今後の展望について述べる.

もう一つのデレステをきっかけに人生が完全に変わった話

上で紹介した記事と同様に, 私の人生もデレステをきっかけに完全に変わってしまった. 以下では,

  1. デレステと出会い,
  2. 超えられない壁に直面し,
  3. アイドル達の協力によってこれを超え,
  4. ユニットおよびプロデューサ自身を強化し,
  5. 「生存本能ヴァルキュリア」をマスターする

という話について,時系列に沿って説明する.

デレステとの出会い

今から約1年前,私が,自分の所属するサークルの共用タブレットにインストールされていた 「アイドルマスター シンデレラガールズ スターライトステージ(デレステ)」 を発見するところから,物語は始まる. 私はゲーム制作は好きだが,プレイにはあまり興味が無かった. また,アイドルも知らなかった. しかし,誰かがインストールしたデレステがふと気になった私はデレステを起動し,チュートリアルを始めた. 慣れないUI,うろ覚えのルール,それでもなんとか選曲まで辿り着いた. 私が知る曲など当然存在しないが,格好良い名前に惹かれて「生存本能ヴァルキュリア」を選ぶ. 私はクリアしたが,それどころではなかった. 初めて遊ぶリズムゲーム,その後ろで動く3Dモデル達,そして素晴らしい楽曲. 私はその全てに感動し,「生存本能ヴァルキュリア」を遊び続けた.

その時のユニットを以下に示す.

「生存本能ヴァルキュリア」MASTERクリアの壁

しばらく「生存本能ヴァルキュリア」で遊んでいた私は,難易度の存在に気が付いた. DEBUT, REGULARおよびPROの3つがあり,それまで遊んでいたのはDEBUTだった. すぐにREGULARに挑戦してこれをクリアし,PROに挑戦した. PROは簡単にはクリアできなかった. 私は,最高難易度なだけあってPROは難しいな,と感じつつ,何度もトライした. やっとの思いでクリアし,達成感に浸っていた私は, 最高難易度だと思っていたPROの横にMASTERが増えていることに気がついた.
「あんなに難しかったPROの更に上の難易度があるのか」

ここから数か月,孤独な苦難の時代が続く. 私は毎日「生存本能ヴァルキュリア」MASTERを練習した. しかし,何度MASTERに挑戦しても,サビ前でライフが尽きる. スタミナ,スタミナドリンクおよびリハーサルチケット等の練習のために使えるものは全て使ったが上達しない. また,ライブを最後まで見ることもできない. 追い込まれた私は

  1. スタージュエルでスタミナを回復できること
  2. スタージュエルでライブをコンティニューできること
  3. 今までもらったスタージュエルがたくさん余っていること

を思い出した. 私は,時間のある時はスタミナを回復して練習し, ライフが尽きた時はライブをコンティニューすることにした. その結果,ものすごい勢いでスタージュエルが消費されていった.

アイドル達の協力

スタージュエルはすぐに無くなり,私は行き詰まった. そんな時にふとアイドル編成のおすすめ編成機能が気になり,これでユニットを編成してみた. すなわち,事務所に所属していたアイドル達がもっと自分達を頼っても良いと協力を申し出てくれたのだ. 途方に暮れていた私は藁にもすがる思いで彼女達を頼った. 結果として,彼女らはライブを成功させた. 今まで不可能とも思えた「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしてしまった.

その時のユニットを以下に示す.

ユニットおよびプロデューサの強化

友人でもある同僚に「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしたことを報告すると,
「ゲーム下手な君が遂にMASTERをクリアしたか,でも,フルコンはしていないんだね.」 と言われた. そこで,私は「生存本能ヴァルキュリア」のマスター,すなわち「生存本能ヴァルキュリア」MASTERのフルコンを決意した.

前節の経験より,この目標のためには,自分一人ではなくアイドル達の協力が不可欠と考えた. そして,事務所内のユニットを強化するために

  • アイドルの強化
  • 他の楽曲のライブ
  • オーディション
  • イベントへの参加
  • ストーリーコミュ

などを行い,活動の幅を広げていった.

アイドルの強化では,

  • ライフ回復の特技のスキルレッスン,
  • ライフ上昇のためのレッスン,特訓およびポテンシャル解放

を行なった. そして,他の楽曲のライブを行うことで, プロデューサおよびアイドルの視野が広がった. また,オーディションやイベントへ参加がきっかけとなって, 事務所に新たなアイドルが増えた. 特に,ライフ回復を特技とするアイドルはすぐにユニットへ加えた. さらに,ストーリーコミュによって,事務所のアイドルへの理解を深めた.

一方で,プロデューサ自身はライブ成功に貢献するためにリズムアイコンのスピードを調整した. 具体的には,リズムアイコンのスピードを9.8とした. まず,私が画面上のリズムアイコンを全て把握するためには9.5程度以上でなければならなかった. スピードがこれより低いとリズムアイコンが多すぎて把握できなくなる. 次に,9.9程度というスピードは,私がリズムアイコンの出現を確認してから親指で対応する位置をタップできる限界の速度である. したがって,この時,指を最速で動かせばタイミングを計らずとも,タイミング良くタップできることになる. そして,私の目がリズムアイコンの動きを捉えられる限界が9.8程度である. リズムアイコンのスピードを9.8とすることで画面上のリズムアイコン全てに反応し,それらの動きを捉え,タイミング良くタップできる. 実際,この設定により,コンボ数が格段に上がった. 様々な楽曲でライブを安定してクリアできるようになり,私のライブ力も向上した.

この頃活躍していたユニットの例を以下に示す.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [花園の春風]西園寺琴歌+
  • [シュガーリーボディ]榊原里美+
  • [寡黙の女王]高峯のあ+

このようにして,元々アイドルにもソシャゲにも興味の無かった私はデレステにどんどんハマっていった.

「生存本能ヴァルキュリア」のマスター

他の簡単な楽曲のMASTERをいくつかフルコンできるようになって, アイドル達および私のライブ力の向上を実感してきたところで, 私は「生存本能ヴァルキュリア」のマスターのためのチャレンジに本腰を入れた. そして,何度もトライして遂にフルコンを達成した. 以下の画像はその時のものである. f:id:Jumpaku:20171219155830j:plain この画像から分かるようにフルコンするまでに実に323回のクリアがあったことが分かる. 最初の2,3ヶ月の度重なるライブ失敗を考慮すれば, フルコンするまでの失敗を含めた総ライブ数は400-500回程度に上ると予想される.

また,フルコン時のユニットを以下に示す.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [ようせいのこ]遊佐こずえ+
  • [Sweet Witches' Night]森久保乃々+
  • [スクールデビル]小関麗奈+

この編成において,森久保乃々および小関麗奈がコンボを継続する一方で, 喜多見柚,岸部彩香および遊佐こずえがライフを回復するため, ライフ1でのコンボ切れによるライブ失敗を避けることが可能となっている. したがって,フルコンが途切れても,最後までライブを楽しむことができる.

すなわち,ユニット内のアイドル達のチームワークによって, ファンを最後まで楽しませるという最低限の仕事を放棄することなく, 「生存本能ヴァルキュリア」のマスターを目指すことが可能となる.

実際,フルコンするまでトライを続けることができたのは,例え途中でコンボが切れても, リズムゲーム,3Dモデルによる素晴らしいライブおよび好きな楽曲を最後まで楽しめたからである.

考察

以上の経験より, 私はプロデューサとアイドルの信頼関係の重要性について学んだ. プロデューサが超えられない壁に直面したとき,一人でできる努力には限界がある. それで嫌になって投げ出しては本末転倒である. そんな時は,プロデューサはアイドルに頼っても良いのである. 一方で,アイドルが成長するためには,プロデューサが視野を広く保ち, アイドル達に様々な経験をさせることも大切である.

課題と展望

MASTERより上の難易度ができた.MASTER+である. 「生存本能ヴァルキュリア」MASTER+も追加された. 以下のユニットによってなんとかクリアはできたが,まだフルコンはできていない.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [花園の春風]西園寺琴歌+
  • [シュガーリーボディ]榊原里美+
  • [ようせいのこ]遊佐こずえ+

現在はクリア数66回にしてコンボCすら達成できていない. リズムアイコンのスピードを9.9としても画面上のリズムアイコンが多すぎて把握できず, 10.0とすると速すぎてリズムアイコンの動きを捉えられず,指も追いつかないという状況である. クリアするのがやっとの状態なので,コンボ継続を特技とするアイドルをユニットに加える余裕もない.

今後は「生存本能ヴァルキュリア」MASTER+のフルコンに向けて, アイドル達と相談しながら作戦を練りつつ,動体視力と指の移動力を鍛えていきたい.

謝辞

本報告は「生存本能ヴァルキュリア」をマスターするまでの過程を報告するものである. 以下に,その過程で協力していただいたアイドル達への感謝を表する.

はじめのうち私とライブの練習をしていただいたイヴ・サンタクロース氏,星輝子氏,塩見周子氏,横山千佳氏および双葉杏氏に感謝します.

行き詰まっていた時に頼らせていただいた前川みく氏,栗原ネネ氏,大沼くるみ氏,双葉杏氏および城ヶ崎莉嘉氏に感謝します.

様々なイベントまたは楽曲のライブで活躍していただいた喜多見柚氏,岸部彩香氏,西園寺琴歌氏,榊原里美氏および高峯のあ氏に感謝します.

タイプ別のユニットで活躍していただいたアナスタシア氏,篠原礼氏,渋谷凛氏,川島瑞樹氏, 村上巴氏,佐藤心氏,片桐早苗氏,日野茜氏, 関裕美氏,櫻井桃華氏および大原みちる氏に感謝します.

付録 JumpakuUnit

本報告には直接関係はないが, JumpakuUnitを以下に示す.

  • [ハッピーホーリーナイト]イヴ・サンタクロース+
  • [Tulip]塩見周子+
  • [Nothing but You]アナスタシア+
  • [∀NSWER]星輝子+
  • [寡黙の女王]高峯のあ+

Kotlinの良いところ

adventar.org

Muroran Institute of Technology Advent Calendar 2017

Dec. 18

Kotlin のここが良い

Kotlinという名前

「Kotlinは可愛い.」

初めてその名を見た時,私はそれがプログラミング言語であるとは気付かなかった. Kotlinを書き始めて数週間,私は,Kotlinは実はキメラなのではないかと思い始めた. 現在,私はKotlinの魔力に囚われている.

実行または開発環境

  • JVM上で動く
  • 環境構築が楽
  • Javaのライブラリを利用できる
  • Androidアプリも開発できるらしい

KotlinはJetBrainsで開発されたJVM向けの言語です. IntelliJ IDEAをインストールするとKotlinをすぐに使うことができます. したがって,環境構築は非常に楽です.

また,Kotlinで書いたコードはコンパイルされた後,Javaと同様にJVM上で実行されます. さらに,JavaのライブラリをKotlinのプログラムから利用することもできます. Gradleのプラグインも存在しており,これを使用したMavenのライブラリの利用も可能です.

ちなみに,私は試していませんが,Androidアプリも開発できるようです.

文法に関して

Javaの無駄を排除
  • nullは無駄
  • セミコロンは無駄
  • newは無駄
  • 変数宣言時の型名は無駄
  • main関数にとってクラスは無駄

Javaによるプログラミングにおいて,nullは諸悪の根源であり, これを駆逐するためにはnullチェックを欠かしてはいけません. しかし,これほど面倒臭いことがありましょうか? そして,そもそもnullは必要なのでしょうか? Haskellを見れば,変数が無くても,プログラムを作成することが可能であると分かります. 読み取り専用の変数をnullで初期化することに意味は無く, したがって,読み取り専用の変数はnull以外の値で初期化されます. また,全ての変数が読み取り専用でもプログラムは正しく動くことが可能です. すなわち,nullを用いずともプログラムは正しく動くのです. nullは不要です. それどころか,バグの根またはnullチェックの手間以外の何物でも無いのです.

Kotlinにはnonnull型とnullable型があります. nonnull型の変数にnullを代入し,またはnullで初期化しようとするとコンパイルエラーとなります. このnull安全な文法により,プログラマは憎っくきnullに触ることなくコードを書くことができます. このnull安全性は私がScalaではなくKotlinを選ぶ決め手となりました.

1行に複数の文が存在する事は稀で,そもそも,そういう書き方は好まれません. 実際,Pythonは改行で文を区切ることとなっています. すなわち,文末のセミコロンは不要なのです.

Java, C++等では,当たり前のように何千何万何億ものセミコロンを書いてきましたが, Kotlinではその無駄な作業をせずに済むのです.

Javaにおいて,オブジェクト生成時にはコンストラクタの呼び出しおよびnewを記述しなければいけません. コンストラクタの呼び出しを見れば,生成するオブジェクトのクラスおよび呼び出すコンストラクタに渡す引数といった, オブジェクト生成に必要な全ての情報が分かります. では,newを見るとどんな情報が得られるでしょうか,いいえ,どんな情報も得られません. Javaにおいて,newは不要なはずなのです. Kotlinでは,この無駄も排除されるのです. 私はこの点においてKotlinはJavaおよびScalaより良いと思っています.

変数宣言時の型名を省略しても良い言語が多く存在します. Kotlinもその一つです. C++にもautoができましたね. Javaにおいて,なぜ1つの変数宣言で同じ型名を2度も書かなければならないのかと思った事はありませんか? 明らかに無駄な型名を書かなくてはいけないというJavaの苦しみから,型推論があなたを解放します.

Hello World!を書くためだけになぜクラスを作らなければいけないのか,なぜ標準出力 (System.out.println("Hello World!");)がめんどくさいのか? KotlinはC++のように関数をクラス外に書けます. ついでに言うと,標準出力もprintln("Hello World!")とシンプルです.

ここまでの内容をまとめると,

  1. Javaには記述の無駄が多い.
  2. Kotlinはそれらの無駄を削ぎ落としたものとなっている.

と言えるのではないでしょうか.

不変性のサポート
  • val
  • デフォルトの修飾子

不変クラスはクラスの理想の姿と言えます. スレッドセーフであり,意図しない変更を受けず,矛盾した状態となることもないからです. 例えば,不変クラスのインスタンスへの参照を保持する読み取り専用の変数は, いつでもどこでも初期化時と同じ値を保つため,取り扱いが非常に楽です. プログラムを書くときは,不変クラスを作成したり,変数を読み取り専用としたりことによって, できるだけ不変とすることが望ましいと思います.

しかし,Javaにおいて,これは生易しい話ではありません. 全ての変数にfinalを付けていく作業は心の折れる作業です. また,不変クラスを作成する際は,そのクラスのインスタンスが保持するオブジェクトが変更されないことを保証しなければいけません. これを実現するためには

  • 継承不可とする.
  • フィールドを読み取り専用とする.
  • 可変なオブジェクトを変更しない.
  • 可変なオブジェクトの参照を共有しない.

といったことに注意しなければいけません.

ここで,不変クラスは推奨されていますが,Javaでの実装が困難であるという問題があります. これに対して,Kotlinには不変性を意識したプログラミングを援護する文法があり, 不変クラスの実装を楽に行えます.

例えば,変数を宣言する時にval a = 1と書くと,変数aは読み取り専用となります. これはfinal int a = 1;をとするより,シンプルです.

また,クラスおよびメソッドはデフォルトでfinalが付いた状態となっております. 継承可能とするためまたはオーバーライド可能とするためにはopenを付けます. そして,C++とは逆に,アクセス指定はデフォルトでpublicとなります. はじめのうち,私は,カプセル化に反する感じがして慣れませんでした. しかし,よく考えると, フィールドはプロパティによってラップされているので, オブジェクトが不変である場合には困る事はありませんでした.

以上のように,Kotlinは言語として不変性をサポートしてくれます.

その他の便利機能
  • 演算子オーバーロード
  • 分解宣言
  • data class
  • 関数のデフォルト引数と名前付き引数
  • when式, if else式, throw catch式
  • Iterableの拡張関数

ここではKotlinの文法に関するその他の良いところを挙げていきます. まず,演算子オーバーロードです. やはり,ベクトル,点その他の数学的なクラスは数値型の演算子を同じように使いたくなります. そんなときに,演算子オーバーロードができる言語はありがたいです.

KotlinのクラスはcomponentN()を実装することで, オブジェクトの値を分解して,複数の変数を同時に初期化することができます.

class Vector(val x: Double, val y: Double, val z: Double) {
    operator fun component1(): Double = x
    operator fun component2(): Double = y
    operator fun component3(): Double = z
}

fun main(vararg args: String) {
    val (a, b, c) = Vector(1.0, 2.0, 3.0)
    println("$a, $b, $c")
    //1.0, 2.0, 3.0
}

さらに,Kotlinにはdata classというものがあり, 単に

data class Vector(val x: Double, val y: Double, val z: Double)

と書くだけで,Javaのよくある次のようなクラスに

public final class Vector {

    private final double x;

    private final double y;

    private final double z;

    public Vector(double x, double y, double z){
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public double getZ() {
        return z;
    }

    @Override
    public String toString() {
        return "Vector(x="x + ", y=" + y + ", z=" + z + ")";
    }
}

を実装したクラスに更に operator fun component1(): Double = x, operator fun component2(): Double = yおよびoperator fun component3(): Double = z を実装したこととなります.

Javaにおいて,例えば,コンストラクタなどで設定すべき引数が多くなってくると, 引数の順番が分からない,デフォルトの設定がある,といった事態が起きます. そんなときは組み合わせ爆発によりオーバーロードに限りがありません. その結果として,Builderを作ることとなるのではないでしょうか? Kotlinでは,以下のように,関数のデフォルト引数および名前付き引数を利用することができます.

data class Vector(val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0)

fun main(vararg args: String) {
    println(Vector(1.0, 2.0, 3.0))
    //Vector(1.0, 2.0, 3.0)
    println(Vector(1.0, 2.0))
    //Vector(1.0, 2.0, 0.0)
    println(Vector())
    //Vector(0.0, 0.0, 0.0)
    println(Vector(y = 2.0))
    //Vector(0.0, 2.0, 0.0)
    println(Vector(1.0, z = 3.0))
    //Vector(1.0, 0.0, 3.0)
    println(Vector(z = 3.0, y = 2.0))
    //Vector(0.0, 2.0, 3.0)
}

そして,Kotlinでは条件分岐のためのwhenおよびif elseならびに例外処理のためのtry catchが全て式となり値を持ちます. これらを値を持つ式として扱えば条件漏れなどを無くせるのではないでしょうか?

他に,Kotlinには拡張関数という機能があり,これによってIterableのメソッドが便利になっています. 以下にその例を挙げます.

  • filter
  • map
  • zip
  • take
  • drop
  • reduce
  • fold
  • sortedBy
  • zipWithNext
  • find

ここでは私が気に入っている機能について紹介させていただきました.

Bezier 曲線を描くならどっち?

KotlinとJavaで,どちらの方が楽にプログラムを書けそうか比べてみます. ここでは,Bezier曲線クラスの実装を例にします.

Bezier曲線とは

\(n\)次のBezier曲線 \( \boldsymbol{B} : [0, 1] \to \mathrm{E} \) はパラメータ\(t \in [0, 1]\)に対応する点\(\boldsymbol{B}(t) \in \mathrm{E}\)を返す関数で, \(n + 1\)個の制御点\(\boldsymbol{p}_{i} \ (0 \leq i \leq n)\)を用いて,

$$\boldsymbol{B}(t) = \boldsymbol{B}_{0}^{n}(t)$$

と評価します.ただし,

$$\boldsymbol{B}_{i}^{0}(t) = \boldsymbol{p}_{i} \ (0 \leq i \leq n)$$ $$\boldsymbol{B}_{i}^{j}(t) = (1-t) \boldsymbol{B}_{i}^{j-1}(t) + t \boldsymbol{B}_{i+1}^{j-1}(t) \ (1 \leq j \leq n, 0 \leq i \leq n - j)$$

です.

Intervalクラス

package jumpaku.kotlin

data class Interval(val begin: Double, val end: Double){

    operator fun contains(t: Double): Boolean = t in begin..end
}
package jumpaku.java;

public final class Interval {
    public Interval(final double begin, final double end) {
        this.begin = begin;
        this.end = end;
    }

    private final double begin;

    private final double end;

    public double getBegin() {
        return begin;
    }

    public double getEnd() {
        return end;
    }

    public boolean contains(final double t) {
        return begin <= t && t <= end;
    }

    @Override
    public String toString() {
        return String.format("jumpaku.kotlin.Interval(begin=%.1f, end=%.1f)", begin, end);
    }
}

Pointクラス

package jumpaku.kotlin

data class Point(val x: Double = 0.0, val y: Double = 0.0) {

    fun divide(t: Double, that: Point): Point {
        return Point(x.divide(t, that.x), y.divide(t, that.y))
    }

    private fun Double.divide(t: Double, that: Double): Double {
        return (1-t)*this + t*that
    }
}
package jumpaku.java;

import java.util.Objects;

public final class Point{

    public Point(final double x, final double y) {
        this.x = x;
        this.y = y;
    }

    public static Point ofX(final double x) {
        return new Point(x, 0.0);
    }

    public static Point ofY(final double y) {
        return new Point(0.0, y);
    }

    public static Point origin() {
        return new Point(0.0, 0.0);
    }

    private final double x;

    private final double y;

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public Point divide(final double t, final Point that) {
        Objects.requireNonNull(that);
        return new Point(divide(t, x, that.x), divide(t, y, that.y));
    }

    private double divide(final double t, final double d0, final double d1) {
        return (1-t)*d0 + t*d1;
    }

    @Override
    public String toString() {
        return String.format("jumpaku.kotlin.Point(x=%.1f, y=%.1f)", x, y);
    }
}

BezierCurveクラス

package jumpaku.kotlin

class BezierCurve(vararg controlPoints: Point){

    val controlPoints: List<Point>

    init {
        require(controlPoints.isNotEmpty()) { "control point is empty" }
        this.controlPoints = listOf(*controlPoints)
    }

    val domain: Interval = Interval(0.0, 1.0)

    fun evaluate(t: Double): Point {
        require(t in domain) { "t($t) is out of domain($domain)" }
        return decasteljau(t, controlPoints)
    }

    private fun decasteljau(t: Double, controlPoints: List<Point>): Point {
        return when {
            controlPoints.size == 1 -> controlPoints.first()
            else -> decasteljau(t, controlPoints.zipWithNext { a, b -> a.divide(t, b) })
        }
    }
}
package jumpaku.java;

import java.util.*;

public final class BezierCurve {

    public BezierCurve(Point... controlPoints) {
        Objects.requireNonNull(controlPoints);
        if (controlPoints.length == 0) {
            throw new IllegalArgumentException("control point is empty");
        }
        if (Arrays.stream(controlPoints).anyMatch(Objects::isNull)){
            throw new IllegalArgumentException("control point contains null");
        }
        this.controlPoints = Collections.unmodifiableList(Arrays.asList(controlPoints));
    }

    private final List<Point> controlPoints;

    private final Interval domain = new Interval(0.0, 1.0);

    public List<Point> getControlPoints() {
        return new ArrayList<>(controlPoints);
    }

    public Interval getDomain() {
        return domain;
    }

    public Point evaluate(double t) {
        if (!domain.contains(t)) {
            throw new IllegalArgumentException("t($t) is out of domain($domain)");
        }
        return decasteljau(t, controlPoints);
    }

    private Point decasteljau(final double t, final List<Point> points) {
        if (points.size() == 1) {
            return points.get(0);
        }
        else {
            ArrayList<Point> result = new ArrayList<>();
            for (int i = 0; i < points.size() - 1; i++) {
                result.add(points.get(i).divide(t, points.get(i + 1)));
            }
            return decasteljau(t, result);
        }
    }
}

動作実験

Kotlinのソースコード

package jumpaku.kotlin

fun main(vararg args: String) {
    println("Jumpaku")

    val bezierCurve = BezierCurve(
            Point(-1.0, -1.0),
            Point(-1.0, 1.0),
            Point(1.0, -1.0),
            Point(1.0, 1.0))
    (0..4).map { i -> i/4.0 }
            .map { bezierCurve.evaluate(it) }
            .map { (x, y) -> "$x, $y" }
            .forEach(::println)
}

実行結果

-1.0, -1.0
-0.6875, -0.125
0.0, 0.0
0.6875, 0.125
1.0, 1.0

Javaソースコード

package jumpaku.java;

import java.util.stream.IntStream;

public class Main {

    public static void main(String... args) {
        System.out.println("Jumpaku");

        BezierCurve bezierCurve = new BezierCurve(
                new Point(-1.0, -1.0),
                new Point(-1.0, 1.0),
                new Point(1.0, -1.0),
                new Point(1.0, 1.0));
        IntStream.rangeClosed(0, 4)
                .mapToDouble(i -> i/4.0)
                .mapToObj(t -> bezierCurve.evaluate(t))
                .map(p -> p.getX() + ", " + p.getY())
                .forEach(System.out::println);
    }
}

実行結果

-1.0, -1.0
-0.6875, -0.125
0.0, 0.0
0.6875, 0.125
1.0, 1.0

JavaとKotlinで同じ実行結果を出力するプログラムを書きましたが, Kotlinの方がシンプルで良いと思います. また,ソースコードはどちらも同じ設計に基づいていることも感じられると思います.

まとめ

Kotlinについて思ったことをひたすら書きました. 私は,KotlinはJavaと同じパラダイムの言語でありながら, Javaの無駄を削ぎ落とし, 推奨される書き方をサポートし, 様々な言語の良い部分を寄せ集めた言語であると思います.

Javaを使っている人,特に,室蘭工業大学情報系のあなた,是非Kotlinを使ってみてください.

Python で論理クイズ Solve Logic Quiz with Python

はじめに

Python は手軽に書けるスクリプト言語です. 論理クイズは与えられた説明と矛盾しない解答を論理的に導くクイズです. 今回は Python を使って論理クイズを解きます.

Python

文法が分かりやすく,シンプルに書くことができ,標準ライブラリも充実していて,とても使いやすいと感じています. 今回は itertools モジュールを使って,組み合わせを総当りすることによって,論理クイズを解きます.

論理クイズ

ここで扱う論理クイズでは,初期設定と容疑者達の証言をもとに事件の真相を論理的に導きます.

初期設定とは容疑者の集合および嘘つきの人数であり,最初に与えられます.

証言とは事件に関する主張を1つ以上並べたもので,容疑者は最大で1つの証言を行います.

嘘つきとは容疑者のうち,証言をし,それが真相と矛盾するような人物のことです.

正直者とは容疑者のうち,嘘つきではない人物のことです.

真相とは1人の真犯人と0人以上の嘘つきの組み合わせのうち,初期設定,嘘つきの証言の否定および正直者の証言のどれとも矛盾しない唯一のものです.

ソースコード

次のソースコードは論理クイズを解く Python3 のプログラムです. suspectsは容疑者とその証言を表します. 容疑者名をキーとし,lambda 式を値とする辞書で実装します. ここで,lambda 式は嘘つきの名前のタプルおよび犯人の名前を受け取り,証言の真偽を返します. liarsCountは嘘つきの人数です.

このプログラムは容疑者の中から犯人を1人選ぶ組み合わせおよび嘘つきを人数分選ぶ組み合わせの直積の中から, 初期設定,嘘つきの証言の否定および正直者の証言のどれとも矛盾しないものを標準出力に出力します. 出力されるものが1つならそれが真相です.

# -*- coding: utf-8 -*-
import itertools as it

def implies(p, q):
    return (not p) or q

def give_testimony(testimony, is_liar):
    return not testimony if is_liar else testimony

# 容疑者名と証言の辞書
suspects = {}
# 嘘つきの人数
liarsCount = 0

for truth in it.product(
        it.combinations(suspects.keys(), liarsCount),
        suspects.keys()):
    liars, criminal = truth
    isConsistent = all([give_testimony(t(liars, criminal), s in liars)
                        for s, t in suspects.items()])
    if isConsistent:
        print("嘘つきは{},犯人は{}.".format("と".join(liars), criminal))

実行

今回は説法系推理アドベンチャシリーズの3つのゲーム,

で出題される論理クイズを解きます. これらのゲームは私が作成し,ウディフェスまたはウディコンにエントリしたゲームです. 以下ではこれらのゲームのネタバレがあります. 未プレイの方は以下の文章を読む前に,プレイするかプレイ動画を見てください.

愛と血の修羅場(サスペンス)

嘘つきは1人,容疑者と証言をまとめると以下のようになります.

  • 恵伊 : 史衣と出井は嘘をつかず,史衣と出井は犯人ではない.
  • 美衣 : 恵伊と良威のどちらかは正直者.
  • 史衣 : 犯人は出井か恵伊.
  • 出井 : 出井と美衣はどちらも犯人ではない.
  • 良威 : 犯人は美衣.

本ゲームで正解となる真相は,「嘘つきは良威,犯人は恵伊.」です.

上のプログラムのsuspectsおよびliarsCount

#容疑者名と証言の辞書
suspects = {
    "恵伊": lambda ls, c: ("史衣" not in ls) and ("出井" not in ls) and ("史衣" != c) and ("出井" != c),
    "美衣": lambda ls, c: ("恵伊" not in ls) or ("良威" not in ls),
    "史衣": lambda ls, c: ("恵伊" == c) or ("出井" == c),
    "出井": lambda ls, c: ("美衣" != c) and ("史衣" != c),
    "良威": lambda ls, c: ("美衣" == c)
}
#嘘つきの人数
liarsCount = 1

と初期化し,実行すると,嘘つきは良威,犯人は恵伊.と出力されます. よって,「嘘つきは良威,犯人は恵伊.」が唯一の真相であることが確認できます.

恋と友情の常識(ファイト)

嘘つきは2人,容疑者と証言をまとめると以下のようになります.

  • 恵伊 : 良威は正直者.
  • 美衣 : 恵伊は嘘つきではない,または史衣は嘘つきではない.
  • 史衣 : 出井は嘘つき.
  • 出井 : 美衣と恵夫はどちらも犯人ではない.
  • 良威 : 犯人は恵夫.
  • 恵夫 : 良威は嘘つき.

本ゲームで正解となる真相は「嘘つきは出井と恵夫,犯人は恵夫.」です.

上のプログラムのsuspectsおよびliarsCount

#容疑者名と証言の辞書
suspects = {
    "恵伊": lambda ls, c: "良威" not in ls,
    "美衣": lambda ls, c: ("恵伊" not in ls) or ("史衣" not in ls),
    "史衣": lambda ls, c: "出井" in ls,
    "出井": lambda ls, c: ("美衣" != c) and ("恵夫" != c),
    "良威": lambda ls, c: "恵夫" == c,
    "恵夫": lambda ls, c: "良威" in ls,
}
#嘘つきの人数
liarsCount = 2

と初期化し,実行すると,嘘つきは出井と恵夫,犯人は恵夫.と出力されます. よって,「嘘つきは出井と恵夫,犯人は恵夫.」が唯一の真相であることが確認できます.

罪と幸せの四苦八苦(ノアズアーク)

嘘つきは2人,容疑者と証言をまとめると以下のようになります.

  • 美衣 : 犯人は嘘つきの中の誰かで,出井は犯人ではない.
  • 史衣 : 犯人は恵夫.
  • 出井 : 永一と史衣はどちらも嘘つき.
  • 恵夫 : (証言無し)
  • 永一 : 出井が正直者ならば出井は犯人ではなく,美衣は正直者.

本ゲームで正解となる真相は「嘘つきは史衣と出井,犯人は史衣」です.

上のプログラムのsuspectsおよびliarsCount

#容疑者名と証言の辞書
suspects = {
    "美衣": lambda ls, c: (c in ls) and ("出井" != c),
    "史衣": lambda ls, c: "恵夫" == c,
    "出井": lambda ls, c: ("永一" in ls) and ("史衣" in ls),
    "恵夫": lambda ls, c: True,
    "永一": lambda ls, c: implies("出井" not in ls, "出井" != c) and ("美衣" not in ls),
}
#嘘つきの人数
liarsCount = 2

と初期化し,実行すると,嘘つきは史衣と出井,犯人は史衣.と出力されます. よって,「嘘つきは史衣と出井,犯人は史衣」が唯一の真相であることが確認できます.

まとめ

辞書やリストをシンプルに記述でき,その取り扱いも標準ライブラリが充実しているため,とても楽でした. PyCharmで書けば,静的チェックもしてくれたので,助かりました.

罪と幸せの四苦八苦(ノアズアーク)

f:id:Jumpaku:20170724043656p:plain

仲間達との船旅で起こる悲劇.
船上で発見された仲間の死体.
容疑者達は己の信じる愛を貫く.
すれちがう証言と譲れない主張.
私は論理の果てに真実をつかむ.

ゲーム情報

本ゲームは登場人物の説法を聞くことと,論理クイズを組み合わせた説法系推理アドベンチャです.

  • タイトル : 罪と幸せの四苦八苦(ノアズアーク)
  • 読み : つみとしあわせののあずあーく
  • 作者 : Jumpaku
  • ジャンル : 説法系推理アドベンチャ
  • プレイ時間 : 30分程度
  • プラットフォーム : Windows
  • リリース : 2017年7月24日
  • 言語 : 日本語
  • 開発環境 : WOLF RPGエディター

遊び方 How to play

上のリンクからダウンロードしてください.

ゲームを始めるには"Game.exe"を実行して下さい. 詳しい操作方法は"README.pdf"を読んで下さい.

ウディコンにエントリ

このゲームをウディコン2017に提出しました.

コンテストのページはここです. WOLF RPGエディターコンテスト -ウディコン- / 力作フリーゲーム!

ヴァージョン Version

  • v1.3 : 誤字の訂正
  • v1.2 : 誤字脱字の訂正
  • v1.1 : README.pdfの間違いを訂正
  • v1.0 : 最初の完成品

±ZERO

f:id:Jumpaku:20170722214902j:plain

離れていると死んでしまう!!

紹介

私はProject ZEROという友人のゲーム開発に参加しました. そこで"±0" というゲームを作って,ウディコン2017に参加したので宣伝します.

このゲームは離れていると死んでしまう2人のキャラクタを切り替えながら, ダンジョンを攻略し,ストーリーを進めていくRPGです. ダンジョンには様々なギミックがあり,その攻略にはパズルを解く面白さがあります.

ウディコン2017のページからダウンロードして遊べます. http://www.silversecond.com/WolfRPGEditor/Contest/entry.shtml

ゲーム情報

リンク