オリジナルビンゴを作ると、プレイ時間は調節可能か？

クリスマス、忘年会シーズンですが、この時期イベントでビンゴゲームが行われることも多いかと思います。

先日、結婚式二次会でオリジナルのビンゴを作ってやってみたところ、作り方を教えて欲しいという声をいくつか頂いたので、その作り方と、表題の疑問『オリジナルビンゴをつくると、プレイ時間は調節可能か？』について考えていきたいと思います。


# 目次
- オリジナルビンゴカードを作る
 - 無地のビンゴカードの準備
 - データの準備
 - 印刷
 - ビンゴアプリの準備

- ビンゴのシミュレーション
 - 普通のビンゴのビンゴまでの平均ターン回数は？
 - ひらがな46文字だとどうなる？
 - FREEの存在
 - 結果

- 結論

# オリジナルビンゴカードを作る
### 無地のビンゴカードの準備

オリジナルビンゴを作ろうと思ったそもそものモチベーションは、一般的な数字の5x5のビンゴではなく、何か特徴的なものをビンゴのネタに使いたく、ひらがなを使いたいという所から始まりました。同じひらがなを使ったゲームに[『ひらがなポーカー』](https://hiraganapoker.com/)というものがあり、縦横で単語や文章になったりする偶然も狙っています。

さて、そのようなビンゴは存在しないので作ろうということになるのですが、数字の入っていない5x5のビンゴカードなんてあるのかと探したらありました、メルカリに。

<img width="378" alt="スクリーンショット 2018-12-19 3.10.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/197729/12640586-f76b-a953-f713-0f209fa6ab20.png">
[https://item.mercari.com/jp/m10806867979/](https://item.mercari.com/jp/m10806867979/)
こちらのページから、コメントで枚数を相談すると専用のページを作ってもらえます。

書き込み式の3x3のビンゴカード(名前ビンゴなどに使われる)はいくつかamazonや楽天で売られていますが、5x5の穴を開けるタイプの無地のものはここしか見当たらず。

ちなみに、これ以外ですと、市販の数字が入ったビンゴカードの裏を使う、という若干微妙な手段はあります。

### データの準備
こちらは、人数分のそれぞれ違う配置のカードが必要ですので、pythonで生成することにしました。

 - メルカリで入手したカードのサイズが88mm x 110mmだったので 880x1100の画像データを作ります。
 - imagesフォルダの中に、1.png〜46.pngの画像を用意しておきます。
 - 左上に通し番号を入れるために、フォントファイルをコード中に指定しました。
 - せっかくなので上部にロゴ画像を入れました。

```
import numpy as np
import sys
import time
from PIL import Image,ImageOps,ImageDraw,ImageFont

BOARD_NUM = 140
BOARD_W = 880
BOARD_H = 1100
KANA_SIZE = 80
KANA_DISTANCE = 170
KANA_CENTER_Y = 660
KANA_NUM = 46
BINGO_SIZE = 5

CHARA_CENTER_Y = 130
CHAEA_SIZE = 180
CROP = 5

KANA_LIST = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん".decode("utf-8")

def main(argv):
    kana = []
    result = {}
    chara = Image.open("./chara.png")#せっかくなので上部にロゴ画像を入れました。
    charaW,charaH = chara.size
    charaW = int(1. * CHAEA_SIZE * charaW / charaH)
    charaH = CHAEA_SIZE
    for i in range(KANA_NUM):
        #imagesフォルダの中に、1.png〜46.pngの画像を用意しておきます。
        kana.append([i, Image.open("images/"+str(i+1)+".png") ])
        result[i]=0
    checkList=[]
    saveNpy = np.zeros((BOARD_NUM,BINGO_SIZE,BINGO_SIZE))
    for i in range(BOARD_NUM):
        img = Image.new("RGB",(BOARD_W,BOARD_H),(255,255,255))
        np.random.shuffle(kana)
        check = ""
        for j in range(BINGO_SIZE):
            for k in range(BINGO_SIZE):
                pasteX = BOARD_W / 2 + KANA_DISTANCE * (j - 2) - KANA_SIZE / 2
                pasteY = KANA_CENTER_Y + KANA_DISTANCE * (k - 2) - KANA_SIZE / 2
                img.paste(kana[j+k*BINGO_SIZE][1].resize((KANA_SIZE,KANA_SIZE)),(pasteX,pasteY))
                result[kana[j+k*BINGO_SIZE][0]] += 1
                saveNpy[i,k,j] = kana[j+k*BINGO_SIZE][0]
                check+= str(kana[j+k*BINGO_SIZE][0])
                check+= ':'
        print check
        print saveNpy[i]
        if check in checkList:
            print "error"
            #念の為、たまたま同じ配列のカードができていないかはチェックしています。
            quit()
        else:
            checkList.append(check)
        
        img.paste(chara.resize((charaW,charaH)).crop((CROP,CROP,charaW-CROP,charaH-CROP)),(CROP+BOARD_W/2-charaW/2, CROP+CHARA_CENTER_Y-charaH/2))
        draw = ImageDraw.Draw(img)
        font = ImageFont.truetype("フォントファイル.ttf", 32)
        draw.text((50,50), "No."+str(i+1), fill="#000",font=font)
        img.save("./output/"+str(i)+".png")
    np.save("save.npy",saveNpy)#あとでシミュレーション用に配列を保存
    return

if __name__ == "__main__":
    main(sys.argv)
```

こんな感じの画像ができます。
<img width="200" src="https://qiita-image-store.s3.amazonaws.com/0/197729/7de411ad-678f-4aee-a864-4bd5fdb5205e.png">


### 印刷

年賀状を印刷する要領で、家庭用のプリンターを使います。
プリンターがなかったので良い機会なので買いました。

2018年12月現在、この辺りがコスパが高いようです。
[エプソン EPSON PX-049A (価格.com)](http://kakaku.com/item/K0000904983/)
<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/197729/1374b5c6-eefe-5b59-8a17-c654d605cd27.jpeg">

印刷ズレもなく140枚あっというまにできました！

<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/197729/03f42e02-52c5-336a-f9c1-f174723caa5b.jpeg">


### ビンゴアプリの準備

さて、カードができたのでビンゴを回すアプリもつくっていきましょう。
こちらは面倒であれば、ひらがなを振った紙を箱に入れて引いていくのでも良いでしょう。

 - 引いたものが一覧で見える(見落とし、聞き落とし対策)
 - 引いてしまったのを戻せる
 - 途中でアプリが落ちてしまった時のためにマニュアルで、出るものを選べる
 - 商品の画像も同じアプリで一緒に出せる

以上のことができると、とてもスムーズです。
参考までに、openframeworksで以上の機能を実装したものを上げておきます。
[oFBingoMachine(Github)](https://github.com/mizumasa/oFBingoMachine)


# ビンゴのシミュレーション

さて、これでビンゴの準備は万端ですが、オリジナル故に、実際にゲームとして成り立つのか気になります。
特に、ひらがなの場合、一般的な数字ビンゴよりも75->46と引ける数が少ないので途中でビンゴになる人が大量発生してしまうとやっかいです。

### 普通のビンゴのビンゴまでの平均回数は？

以下のコードを実行して、1〜75までの数字、中央のFREEあり、参加者140人でゲームを10000回やった場合の、
ターンが進むに従ってビンゴを達成する人数の、平均ヒストグラムを計算します。

```
import numpy as np

BOARD_NUM = 140
KANA_NUM = 75
BINGO_SIZE = 5
TEST_NUM = 10
USE_FREE = True

def main():
    #board = np.uint8(np.load("先ほど保存したカードの情報.npy"))
    board = np.uint8(np.load("save75.npy"))
    key = range(KANA_NUM)
    averageTimes = []
    totalTimesToFinishHist = np.zeros((KANA_NUM),dtype="int")
    for itr in range(TEST_NUM):
        np.random.shuffle(key)
        print key
        state = np.zeros((BOARD_NUM,BINGO_SIZE,BINGO_SIZE),dtype="uint8")
        if USE_FREE:
            state[:,2,2]=1
        finishedPlayerNum = np.zeros((KANA_NUM),dtype="uint8")
        finishedFlag = np.zeros((BOARD_NUM),dtype="uint8")
        for i,j in enumerate(key):
            for k in range(BOARD_NUM):
                state[k],ret = check(board[k],state[k],j)
                if ret>0:
                    if finishedFlag[k] == 0:
                        finishedFlag[k] = i+1
                        finishedPlayerNum[i]+=1
        print "When each player finished ?"
        print finishedFlag
        print "Average times to finish :",np.average(finishedFlag)
        averageTimes.append(np.average(finishedFlag))
        print "Times to finish histgram"
        print finishedPlayerNum
        totalTimesToFinishHist += finishedPlayerNum
    print "==========================="
    print "Times to finish histgram"
    print totalTimesToFinishHist * 1. / TEST_NUM
    print "Average times to finish :",np.average(averageTimes)

def check(ary,state,key):
    ret = np.nonzero(ary == key)
    if len(ret[0]) > 0:
        state[ret[0][0],ret[1][0]] = 1
        return state,checkBingo(state)
    else:
        return state,checkBingo(state)

def checkBingo(state):
    score = 0
    if min(state[0,:]) == 1:score+=1
    if min(state[1,:]) == 1:score+=1
    if min(state[2,:]) == 1:score+=1
    if min(state[3,:]) == 1:score+=1
    if min(state[4,:]) == 1:score+=1
    if min(state[:,0]) == 1:score+=1
    if min(state[:,1]) == 1:score+=1
    if min(state[:,2]) == 1:score+=1
    if min(state[:,3]) == 1:score+=1
    if min(state[:,4]) == 1:score+=1
    if (state[0,0]+state[1,1]+state[2,2]+state[3,3]+state[4,4])==5:score+=1
    if (state[4,0]+state[3,1]+state[2,2]+state[1,3]+state[0,4])==5:score+=1
    return score

if __name__ == "__main__":
    main()
```

### ひらがな46文字だとどうなる？

さて、これが、ひらがな46文字だとどうなるでしょうか。とりあえずビンゴは出やすそうです。
KANA_NUM = 46と書き換えて再度実行します。


### FREEの存在

市販されているビンゴカードは、大抵真ん中の部分が最初から空いています。これによって少しビンゴになりやすくなっていると思うのですが、実際の所どうなのでしょうか。

### 結果

それでは一斉に結果です。
ビンゴになるまでに必要な平均ターン数　(140人10000回の平均)

|| 数字(1〜75) | ひらがな(46文字) |
|:------------------:|:------------------:|:------------------:|
|FREEあり| 41.38回 | 25.59回 |
|FREEなし| 43.54回 | 26.93回 |

**FREEがあっても2回ぐらいしか変わらない！！**
意外と影響は少ないんですね。

各ターン毎のビンゴが出る人数の分布は次の通りです。
![hist.png](https://qiita-image-store.s3.amazonaws.com/0/197729/558c6a40-8d99-6c5c-afd4-dc37f8bef3b3.png)


一方で、実際のビンゴでは最後までめくりきることはなく、先着順で一定数ビンゴが出たら終了してしまいます。それを踏まえて、累積でビンゴになった人をカウントしていくと、次のようなグラフになります。

![ruiseki.png](https://qiita-image-store.s3.amazonaws.com/0/197729/b7b9d15a-7ed4-6b10-461f-1ebafc1cb0f6.png)

今回、商品は１５個だったため、15人ビンゴが出るまでのターン回数を数えると

|| 数字(1〜75) | ひらがな(46文字) |
|:------------------:|:------------------:|:------------------:|
|FREEあり| 18回 | 29回 |
|FREEなし| 20回 | 31回 |

この時、同時にビンゴになる人は３〜4人ぐらい出てくる見込みです。じゃんけんですね。

数字のビンゴに比べて、約2/3の時間でゲームが終了することが分かりました。


# 結論

今回、オリジナルビンゴを、外注せずに自前でやることで用紙3000円＋プリンター6000円と1万円以内に収めることができました。出る種類が少ないため、通常のビンゴよりも2/3ぐらいの時間になってしまいましたが、時間が押すことを考えるとこれぐらいでも良いのかもしれません。最終的に「ひらがな、FREEなし」で、20回程引いて無事にゲームは終了しました。

![DSC02711.jpg](https://qiita-image-store.s3.amazonaws.com/0/197729/e0af8e95-4ab4-b530-0f4f-7786fad0c36a.jpeg)
![DSC00176.jpg](https://qiita-image-store.s3.amazonaws.com/0/197729/0b1f3e9b-5623-c69f-710b-e4f97f9b2e48.jpeg)
![DSC00183.jpg](https://qiita-image-store.s3.amazonaws.com/0/197729/b72f4331-cf4c-fe33-4c16-5f11c290e627.jpeg)