pg09

テーマ シミュレーションとプログラム

ネタ:

大須のガチャ

ゲームのガチャの排出率の表記のバグで大損害 バグの内容

来年用のネタ: Scalaでテキスト処理 アナグラムを探す

開発環境 Scala IDE (eclispe) を使用する。

eclipseはプログラム言語 Java の代表的な開発環境。Java以外のプログラム言語にも対応している。

今回は、

プログラム言語 Scala で 確率シミュレーション の実験をする。

Scala は オブジェクト指向言語 と 関数型言語の両方の特徴をもつ マルチパラダイムなプログラミング言語。

マーチン・オーダスキー教授により開発が進められている。無償で利用可能。

Twitterなど、メジャーなシステムに利用されている。

http://qiita.com/oubakiou/items/31fd45ca337d76113a1e Scala採用企業のまとめ

今回の演習では、Scalaで関数型スタイルでコードを作成し、ランダムな事象(クジ引き、サイコロ)に関するシミュレーション実験をする。

ScalaはJVM系のプログラム言語。参考記事

前振り: iPad のダイスアプリ紹介

ダイスふる

統合開発環境(IDE: Integrated Development Environment ) によるプログラム開発の演習を以下の手順で進める。

今回使用する IDE は

オープンソースの Eclipse

他のIDEとしては

Microsoft社 Visual Studio

Apple社 Xcode

Google社 Android Studio

などがある。

Javaのバージョンが上がったことにより、Scala IDE に不具合が出ているので対処する。(参考サイト

対処作業:

デスクトップの レポートフォルダ(ショートカット) を開き、

小橋一秀 → プログラミング入門 → PG2018_09 のフォルダにある次のファイルをコピー(マウス右クリックから)する。

eclipse.ini

ファイルエクスプローラーから

PC → ローカルディスク(C:) → eclipse を開き、

右クリック → 貼り付け → ファイルを置き換える

※C:ドライブのファイルはログイン時に初期化されるため、大学の実習室の環境では毎回この作業が必要となる。

IDE eclipse の起動と設定:

ファイルエクスプローラーから

PC → ローカルディスク(C:) → eclipseeclipse.exe をダブルクリックで起動

※発行元が不明というダイアログでは、 実行 を選択

プロジェクトの保存先の設定

標準の保存先が ../workspace となっているが、変更する。

Browse…ボタンを押して、アカウント名のフォルダ(氏名または学籍番号)を選択 →

ドキュメント(Documents) を選択 → 新しいフォルダの作成 ボタンを押す →

フォルダ名を pg09 に変更する → OK を押す

Launch を押す

eclipse が起動する。

Scala のアプリ開発を設定していく。

Scalaプロジェクトの作成:

eclipse

-> Package Explorer(パッケージエクスプローラ)を右クリック

-> New(新規)

-> Other...(その他)

->Scala Wizards(ウィザード)

-> ScalaProject(プロジェクト) → Next(次へ)

Project name(プロジェクト名) pg09 と入力

Finish(完了)

JVMの設定: 今回の演習では、JVMの設定は不要

Javaは半年に一度のVersion UPが計画されている。開発プロジェクトによって使用するJVMを切り替える必要がある場合がある。

Serachから JVMを検索する:

練習用アプリケーション作成:

パッケージエクスプローラ から

pg09 プロジェクト

src フォルダ

→右クリック → NewScala App(アプリ) 作成

Name: (名前) Test と入力

Finish

メッセージを出力するプログラムを作成して実行する。

次のコードをエディタに入力する(以下の画像参照)。

printf("aaa")

実行環境の設定:

緑の矢印の横の小さな下向き参画ボタンから、

Run Configurations

を選ぶ。

Scala Application をダブルクリック

Main class の設定欄に

Test

と入力する。

下の画像を参照:

プログラムの実行:

緑の矢印の実行ボタンを押す。

Scala Interpreter の利用

インタープリターモードでScalaのコードの動作を確認していく。

この様なコマンドを入力し直ちに実行して結果を表示するという操作環境を REPL (Read Eval Print Loop)という。

Scala Console の起動: 今回はScala Consoleは使用しない。

起動画面と、コードの実行の様子。

※今回はこちらを使用する。

ウインドウ(Window) → ビューの表示(Show View) → Scala Interpreter

プロジェクトを選択して OK を押す。

Scala Interpreter の利用方法:

コードを入力後、 Ctrl + Enter で実行。

複数行のコードを書いて、Ctrl + Enterでまとめて実行できる。カーソルの位置はどこにあってもOK

複数の式を評価できる。ブロックにしたり、; で複数の式を記述して実行できる。

※ 1+2;3+4;5+6 の実行結果は 11 になるが、1+2 と 3+4 の式は無駄なコードになるので、warning がでる。

Ctrl + ↑ ↓ で、履歴の利用が可能。(行単位でなく、実行単位)

メニューに、コードのリセットや再実行のボタンがある。

文字入力の練習

以下の例で、あ 以外は 漢字入力OFF で入力する。

"あ"

1+2*3

val a = 1234+5678*9012

a*3

一番下の行に文字が入力されるように、マウスでクリックする

文字を入力後、Ctrl + Enter を押すと入力内容がプログラムとして実行される

※1行ずつ実行してください。

Scala Interpreter で以下を実行:

コードを

操作対象(データ) 操作方法1 操作方法2 操作方法3…

とデータをメソッド(コマンド)で連続的に処理する 関数スタイル で書いていく。

例1)

先頭から6個のデータを取得する:

コード:

"nagoya bunri univ." take 6

結果:

nagoya

例2)

先頭から6個のデータを取得し、その後並べ替える:

コード:

"nagoya bunri univ." take 6 sorted

結果:

aagnoy

例3)

先頭から6個のデータを取得し、その後並べ替え、その後逆順にする:

コード:

("nagoya bunri univ." take 6 sorted) reverse

結果:

yongaa

※これはInterpreterではなく、ScalaコンソールのREPLで実行している様子。

■数式の計算

1+1

2*3

■文字列の操作

+ で結合

"abc" + "xyz"

* で繰り返しコピー

"abc" * 3

*と+の組み合わせ

"abc" * 3 + "xyz"

"abc" + "xyz" * 3

// Scalaでは + や * はメソッド(プログラムのコマンド)。メソッドの書式で記述すると ("abc".*(3)).+("xyz") や "abc".+("xyz".*(3)) になる。

(n) で n番目の文字(Char)を検索(省略形)

"abc"(1)

"abc"(0)

// 省略無しのコードは "abc".apply(1)

// . と ()は省略可能。 "abc" apply 1

// .apply は省略可能。 "abc"(1)

map で変換処理

処理方法は ( _ + " " ) の部分。 _ + " " で、1文字毎に " " を追加する処理になる。処理結果は、Vector というデータの並びになる。

"abc" map ( _ + " " )

処理 mkString でデータの並びを 文字列 に変換する

"abc" map ( _ + " " ) mkString

キーワード val で 値の変更が不可能な変数を宣言する。変数の値は初期化時に決まり、値の再代入や変更は不能になる。

val hw = "Hello world!"

キーワード size で データ列の長さ(要素数)を調べる。

hw size

hw(0)

■整数リストの操作

List(1,2,3)

キーワード take で 引数で指定した個数(2)の要素をリストから取り出す。

List(1,2,3) take 2

キーワード sum で、リストの全要素の合計値を求める。

List(1,2,3) sum

要素を先頭から2個取り出して、合計する。

List(1,2,3) take 2 sum

1~7の数字のリストを変数 a にセットする。

val a = List(1,2,3,4,5,6,7)

a(1)

a sum

関数 map による変換(繰り返し)処理

リスト a の各要素に 1を加えた値と、2倍した)値からなるリストを作成する。

a map ( _ + 1 )

a map ( _ * 2 )

キーワード if 条件式 値1 else 値2 で 条件式が True なら 値1 False なら 値2 に変換する。

a map ( x => if( x > 3) 1 else 0 )

0 と 1 の列に変換後に集計する。

a map ( x => if( x > 3) 1 else 0 ) sum

偶数なら 1 奇数なら 0 に変換する。

a map ( x => if( x % 2 == 0 ) 1 else 0 )

条件を満たす要素数を調べる。

a count ( _ > 4 )

仮保存:

ここまでの演習内容を、一旦、Webclassの第9回課題にコピペしておきます。

ここから下の実験では、バグなどによりEclipseがフリーズする可能性があります。

ランダムを利用したシミュレーション

ランダム処理用のオブジェクト Random を利用するための準備

import scala.util.Random

0~9 のランダム値を1つ求める。

Random.nextInt(10)

Random.nextInt(10)

0~99 のランダム値を1つ求める。

Random.nextInt(100)

Random.nextInt(100)

ランダム値を繰り返し生成する dice オブジェクトを作成する。dice は使用するたびに異なるランダムな値を生成する。値は必要に応じて何回でも生成可能。

val dice = Iterator.continually( Random.nextInt(100) )

※ここで、dice には無限個のランダム値は格納されない。diceは無限に繰り返し利用することができる乱数発生コードになっている。

diceからランダム値を10個や100個とりだし、リストにする。

dice take 10 toList

dice take 100 toList

diceからランダム値を100個とりだし、集計。 ※ 集計結果が 期待値 と比べて 上か下か 考えてみよう。

dice take 100 sum

dice take 100 sum

練習:

dice に対していろいろな処理を試す。

例)

dice take 10 sum

処理方法は、以下から選ぶとよい。

合計を求める sum

ある数値以下なら1に、そうでなければ0に変換する map ( x => if( x < 10 ) 1 else 0) toList

ある数値の個数を集計 map ( x => if( x == 0 ) 1 else 0) sum

ある数値の個数を集計 count ( _ == 0)

※map と sum を使用した個数の集計よりもシンプルなコード

文字列化して結合 mkString

ガチャガチャのシミュレーション

ランダム処理用のオブジェクト Random を利用するための準備が必要です。

import scala.util.Random

Rare 希少 大当り

Uncommon 珍しい 当り

Common 普通 はずれ

gacha という名前の当たり判定表を作成し、指定した位置の文字を調べたり、ランダムな位置の文字を調べるプログラムの例)

val gacha = "R"*3 + "U"*15 + "C"*82

gacha size

gacha(0)

gacha(50)

gacha(Random.nextInt(100))

何回かガチャを回してみよう

100連続ガチャのシミュレーション

ダイスを 100回振った結果を求めるコード dice take 100

ダイスの数結果(数値)に応じて対応する位置の当たり判定をするコード map gacha toList

当たり(R)の回数を数えるコード rg count (_ == 'R' )

val rg = dice take 100 map gacha toList

rg count( _ == 'R' )

種類ごとに集計する

val kekka = rg map { case 'R' => (1,0,0); case 'U' => (0,1,0); case 'C' => (0,0,1) }

kekka./: (0,0,0) { case ((ar,au,ac), (r,u,c)) => (ar+r, au+u, ac+c) }

解説:

rg map { case 'R' => (1,0,0); 引いたいたカードの各種類 R U C に応じて、 (Rが1枚, Uが1枚, Cが1枚) に変換する

kekka./: (0,0,0) 初期値 RUCの所持枚数は各0枚。 /: は kekkaの各要素に対して順番に処理を繰り返す畳み込み演算。map と異なり、各要素ごとに個別に結果を残さずに処理結果を累積して処理する。全要素の処理後に累積値を計算結果とする。

(ar,au,ac) 累積中のRUCの枚数。

(r,u,c) kekkaから順に取り出された要素。

(ar+r, au+u, ac+c) 累積方法。 kekkaから順に取り出した要素を、対応する累積値に加える。

例) 累積値 (0,3,10) に対して、 要素が(1,0,0) なら、累積値は (1,3,10)に更新される

仮提出:

ここまでの演習内容を、一旦、Webclassの第9回課題に提出しておく。下記の提出方法参照。

時間に余裕があるなら、条件を変えて実験する

例1)

ガチャの構成比率の修正。総数は100から変更せずに、比率を変える。

val gacha = "R"*1 + "U"*17 + "C"*82

例2)

ガチャの種類を増やす。集計方法も (1,0,0,0) のように4種類分で集計するように変更する。

val gacha = "S"*1 + "R"*3 + "U"*15 + "C"*81

※ ゲームなどでSR や UR など2文字で表現されるレアリティーは、今回のコードではうまく動かない

YやXなど1文字に置き換えて表現する

例3)

1%以下の確率のガチャを設定する。 0.1% の例を示す。

排出率を修正する。総数を1000にして構成する。

val gacha = "S"*1 + "R"*39 + "U"*150 + "C"*810

dice を修正する。カードの種類数対応した範囲でランダム値を発生させる。

val dice = Iterator.continually( Random.nextInt(gacha size) )

2000回、ガチャを回す。

val rg = dice take 2000 map gacha toList

Sが出た回数を数える。

rg count( _ == 'S' )

種類ごとに集計する。

val kekka = rg map { case 'S' => (1,0,0,0); case 'R' => (0,1,0,0); case 'U' => (0,0,1,0); case 'C' => (0,0,0,1) }

kekka./: (0,0,0,0) { case ((as,ar,au,ac),(s,r,u,c)) => (as+s,ar+r,au+u,ac+c) }

応用編: ガチャはこわい1

上記の例3の実験で、2000回以内にSが出てくるとは限らない。

そこで、Sが出るまで引き直した回数を求める。

dice は 0~999 の範囲のランダム値なので < 1 で 0.1に相当するので、

dice indexWhere ( _ < 1 )

※ < 1 を < 0 にしないように。0より小さいランダム値は発生させていないので無限ループになる。eclipseを止めるしか終了方法はない模様。

indexWhere は左側のデータについて右の式で指定した条件、上の例では _ < 1 になる値が登場した位置を求める関数。

応用編: ガチャはこわい2

上の実験に続けて、100人でSが出るまでガチャをまわして、回した回数の多い順に並べる実験は、以下で。

( Iterator.continually(dice indexWhere ( _ < 1 )) take 100 ).toList.sorted reverse

※ sorted はデータを昇順に並べ替える関数。reverse はデータを逆順に並べ替える関数。

応用編: ガチャガチャマシンのシミュレーション

今回の課題は、デジタルなガチャなので、アイテムの個数に制限は無く、いくらでもガチャを回せる。

このプログラムを、実在のガチャのように1つアイテムを出すと、機械の中のカプセルが1つ減るようにプログラムを修正する。作成例 → サブページ参照

提出方法

Scala Interpreter の実行結果ウィンドウ(上側)をクリックして、 Ctrl + A を押す。

入力したコードと実行結果が全選択される。

Ctrl + C でコピー、または右クリック → Copy する。

WebClass の第9回課題を開いて、問題1 に貼り付けて提出する。仮提出している場合は、以前の入力内容に上書きして提出する。

余談

乱数を利用した演奏

例)以下のコードをまとめてコピペする。Ctrl+Enter で実行する。ドレミと1秒間隔で再生される。

import javax.sound.midi._

val synthesizer = MidiSystem.getSynthesizer()

synthesizer.open()

val instruments = synthesizer.getDefaultSoundbank().getInstruments()

// 楽器 0 をシンセサイザーに設定

synthesizer.loadInstrument(instruments(0))

val channel = synthesizer.getChannels()(0)

// ド レ ミ と 1秒(1000ms) 間隔で 演奏。

channel.noteOn(60 , 64) // 60の高さで音声出力

Thread.sleep(1000) // 1000ms プログラムを停止

channel.noteOff(128) // 音声停止

channel.noteOn(62 , 64)

Thread.sleep(1000)

channel.noteOff(128)

channel.noteOn(64 , 64)

Thread.sleep(1000)

channel.noteOff(128)

ランダム演奏例)

0.3~0.1秒間音を出すコマンドを定義する。以下は、それぞれ +ド ド -ド。

def playR = {channel.noteOn(72 , 64); Thread.sleep(300); channel.noteOff(128)}

def playU = {channel.noteOn(60 , 64); Thread.sleep(200); channel.noteOff(128)}

def playC = {channel.noteOn(48 , 64); Thread.sleep(100); channel.noteOff(128)}

playR playU playC のコマンドを実行すると、音が出る。

ガチャガチャの結果に応じて、音を出す。

val rg = dice take 100 map gacha toList

rg foreach { case 'R' => playR; case 'U' => playU; case 'C' => playC }

旧資料 2016年度用:

REPL(Read Eval Print Loop)の準備:

Console(コンソール)(+マークの付いたアイコン) ▼メニュー

Scala Console をメニューから選択

トラブル対応1

// Entering paste mode (ctrl-D to finish)

コンソールに ↑ と表示されたら、入力が受け付けられなくなる。 赤い四角の停止ボタンを押して、REPLを止める。

できたところまでを、↑↑ の提出方法を参考に保存しておく。

Scalaコンソールを開きなおす

続けて演習を行う。

トラブル対応2

コンソールのログを保存する際に、「保存したファイルを、編集するかで 「はい」 を選択」した場合、Scalaコンソールが開けない。

eclipse の編集画面(コンソールの上の部分)のタブを、ログファイルから、Test.scala にクリックして切替える。

トラブル対応3

コンソールタブを閉じてしまった場合、 再び表示するには: eclipse のメニュー → ウインドウ → ビューの表示 → その他 → 一般 → コンソール

トラブル対応4

コンソール停止後に演習を再開するとプログラムにエラーが出る

import scala.util.Random

val で定義した行

などを再実行する。

トラブルの原因:

複数行でコピペしたときに発生する?

Webからコピペしたときに、先頭文字から選択してコピペすると、改行文字がコピペされることが原因かも。

メモ

素数+アニメーション

マルコフ連鎖

すごろく

ランダム演奏(ループで)