pg12

ネタとメモ

ネタ:

iPad のダイスアプリ ダイスふる の紹介

大須のガチャ

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

ガチャに廃課金した人の記事 苦しみの終わりと始まり

メモ:

今回利用している学習環境について

https://www.creativescala.org/

日本語版https://eed3si9n.com/creative-scala/creative-scala.html

Creative Scala の環境構築について https://twitter.com/_kobashi/status/1333914442422046720

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

プログラム言語 Scala で確率の実験をする。

Scalaオブジェクト指向言語関数型言語の両方の特徴をもつマルチパラダイムなプログラミング言語。マーチン・オーダスキー教授により開発が進められている。無償で利用可能。Twitterなどメジャーなシステムに利用されている。

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

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

開発環境(IDE: Integrated Development Environment )について:

ビルドツールの sbt を使用する。

他のIDEとしては

オープンソースの Eclipse

Microsoft社 Visual Studio

Apple社 Xcode

Google社 Android Studio

などがある。

演習準備:

レポートフォルダから以下のフォルダ PG2020_12 をコピーしてローカルディスク(D:)に置く。

レポートフォルダ → 小橋一秀 → プログラミング入門 → PG2020_12

既にPG2020_12フォルダがD:にある場合はこの作業は省略する。

sbtの起動:

Windowsのスタートメニューをクリックして cmd と入力する。

Enterキーを押す。

cmd はコマンドプロンプト と呼ばれる Windowsのシステム管理用のアプリ。

コマンドプロンプトの作業:

ネタ:コマンド プロンプト (チャット風)


入力内容

d:


cd PG2020_12


cd creative-scala-template-master


sbt console

記号に注意 「 : 」 コロン を使います。 「;」セミコロン と似ているので注意する。

cd はチェンジディレクトリコマンドです。コマンドプロンプトで作業するWindowsのフォルダを切り替えます。

Tabキーでフォルダ名を補完入力します。 cd p でTab を押す。 cd c でTabを押す。

sbt console はそのまま入力する。

最後に一回 Enter キーを押す。(反応があるか確認する。何回か押してもOK)。

Scala Interpreter の利用:

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

この様にコマンドを入力するたびに実行結果が表示されるツールを

REPL (Read Eval Print Loop)

という。REPLでは

コードを入力して Enter で実行させる。

キーボードの上下矢印キー ↑ ↓ で入力履歴を利用できる。

値や文字の入力の練習:

以下の例でひらがな「あ」 以外は 漢字入力OFF で入力する。

記号 " と ' は漢字入力オフで入力する。

エラーになる行については授業で解説する。

0


0.0


1/0


1/0.0


'0'


'0.0'


"0"


"0.0"


a


val a = 1


a


"あ"


'あ'

※1行ずつ実行してください。数値や文字のInt Double Char String など)が表示される


値(数値や文字)をコマンドで処理する以下を実行:

コードを

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

のようにメソッド(コマンド)と組み合わせて処理する。


演習1の準備

コピペして実行しておく。

import scala.language.postfixOps


先頭から6個のデータを取得する。"nagoya bunri univ." が操作対象のデータで take がコマンド(Scalaでは関数と呼ぶ)。6 はコマンドに対する指示(引数と呼ぶ)。

"nagoya bunri univ." take 6

結果:

nagoya


先頭から6個のデータを取得してその後並べ替える。sorted がデータを整列する関数。sortedは take 6の実行結果を操作対象のデータとして扱う。

"nagoya bunri univ." take 6 sorted

結果:

aagnoy


文字列を並べ替えてから先頭から6個取得する。sortedの前に . (ドット)が必要。理由は後述。

"nagoya bunri univ.".sorted take 6

結果:

" .aab"


文字列を並べ替えてから先頭から6個取得して逆順にする。

"nagoya bunri univ.".sorted take 6 reverse

結果:

"baa. "

sorted の前に . が必要な場合について

関数 sorted には省略可能な引数が1つある。この引数で並べ替える方法を指定できる。

引数を省略せずに書くと以下のようになる。文字列中の文字を並べ替えるには Char型の順番を使用している。

"nagoya bunri univ." sorted Ordering[Char]

この省略可能な暗黙の引数がsorted にあるので、sortedの後ろにコマンドを書いた場合に引数として扱われないように工夫が必要となる。


先頭から6個のデータを取得して並べ替えて逆順にする。この例では ( )で囲むことで sorted の後ろに引数が無いことになる。

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

次のように . で sorted と文字列を接続した場合も、実は省略形。

"nagoya bunri univ.".sorted

.を使用する書き方で関数の引数を省略せずに書くには 引数を ( )で囲む必要がある。

"nagoya bunri univ.".sorted(Ordering[Char])

そこで以下のように . を sorted の前に書いておけば以下のコードでは sorted に引数が無いことになる。reverse は sorted の引数として扱われずに コマンドとして処理される。

"nagoya bunri univ.".sorted reverse

次の例はエラーになる。reverse が sortedの引数として扱われるが reverse は並べ替え方法の指示には使えない。

"nagoya bunri univ." sorted reverse

以下は文字列を逆順に並べ替えるコードの例。ここで reverse の前の . は reverse を Ordering[Char]に作用させて並び替え方法を逆にしている。

"nagoya bunri univ." sorted Ordering[Char].reverse

演習1

■数式の計算

1+1


2*3

■文字列の操作

+ で結合

"abc" + "xyz"

* で繰り返しコピー

"abc" * 3

*と+の組み合わせ

"abc" * 3 + "xyz"


"abc" + "xyz" * 3


("abc" + "xyz") * 3

(n) で n番目の文字(Char)を検索させる

"abc"(1)


"abc"(0)

map で複数のデータをまとめて変換できる

mapコマンド(map関数とよぶ)につづく ( _ + "*" ) の部分が変換内容のコード。

_ + "*" で、1文字毎に "*" を追加する処理になる。

"abc" map ( _ + "*" )

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

"abc" map ( _ + "*" ) mkString


キーワード val で 値の変更が不可能な変数を宣言する。変数の値は初期化時に決まり値の再代入や変更は不能になる(Swift言語などと同じ概念)。

val hw = "Hello world!"

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

hw size


hw(0)


hw((hw size)-1)


■整数リストの操作

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 )

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

a count ( _ > 4 )


課題1の提出:

ここまでの演習内容をWebclassの第12回課題の問1にコピペして提出します。以下の画像のようにメニューから 1.すべて選択 して 2.コピー する。

3.Webclassの回答欄に張り付ける。

演習2

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

ランダム(Random)とはサイコロの出目やくじ引きの結果の様に物事が偶然に決まることです。


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

import scala.util.Random

0~9 のランダムな値を1つ求める。矢印キーで履歴を呼び出して何回か繰り返す。

Random.nextInt(10)

0~99 のランダム値を1つ求める。矢印キーで履歴を呼び出して何回か繰り返す。

Random.nextInt(100)

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

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

diceからランダム値を10個や100個取り出してリストにする。

dice take 10 toList

dice take 100 toList

diceからランダム値を100個とりだして集計する。矢印キーで履歴を呼び出して何回か繰り返す。 ※ 集計結果が 期待値 と比べて 上か下か 考えてみよう。

dice take 100 sum

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

ある数値以下なら1に、そうでなければ0に変換する

dice take 100 map ( x => if( x < 10 ) 1 else 0) toList

ある数値の個数を集計

dice take 100 count ( _ == 0)


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

Rare 希少 大当り

Uncommon 珍しい 当り

Common 普通 はずれ

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

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

gacha size

gacha(0)

gacha(50)

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

gacha(Random.nextInt(100))

100連続ガチャのシミュレーション結果を rg に保存する。

val rg = dice take 100 map gacha toList

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

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)に更新される


課題2の提出:

Webclassの第12回課題の問1にシミュレーション結果の部分をコピーして提出する。

ガチャの排出確率を変えて実験したあとで課題2を提出してもOK

例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

※ ゲームなどでSRUR など2文字で表現されるレアリティーは今回のコードではうまく動かないYXなど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に相当する。最初に0がでた位置を調べるには以下のように indexWhere を使えばよい。

dice indexWhere ( _ < 1 )

※ < 1 を < 0 にしないように。0より小さいランダム値は発生させていないので無限ループになる。なおWは大文字

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


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

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

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


余談1

乱数を利用した演奏

以下のコードをまとめてコピペする。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 のコマンドを実行すると一定時間の間で音が出る。

playR

playU

PlayC

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

val rg = dice take 100 map gacha toList

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

余談2

デジタルなガチャガチャのシミュレーションに興味がない受講生は

Creative Scala に掲載されているグラフィックスのコードを試してみてもよい。

例)

Example.image.draw()


Image.circle(100).draw()


val x = Image.circle(20)

val xs = x beside x beside x

xs.draw()


val y = Image.circle(40)

val xyx = x above y above x

xyx.draw()


val xy = x on y

xy.draw()

ランダムドット

dice を2つ使って x と y の座標として組み合わせ、circleの描画に利用した例。

(dice zip dice) take 10 toList


((dice zip dice) take 10 toList).map{ case (x,y) => x+y }


((dice zip dice) take 10 toList).map{ case (x,y) =>

Image.circle(10).at(x,y) }


((dice zip dice) take 10 toList).map{ case (x,y) => Image.circle(10).at(x,y) }./: (Image.empty) { case (a,c) => a on c }


((dice zip dice) take 10 toList).map{ case (x,y) => Image.circle(10).at(x,y) }./: (Image.empty) { case (a,c) => a on c }.draw()


((dice zip dice) take 1000 toList).map{ case (x,y) => Image.circle(10).at(x*5,y*5) }./: (Image.empty) { case (a,c) => a on c }.draw()