JavaScriptでビンゴを作る

講義でくじ引きをするにあたり、いい機会なので作ってみました。プログラミングはてんで駄目なので目新しい機能はありませんが、復習用にメモしておきます。以下自分用メモなので諸々雑ですがうっかり通りすがった方はすみません、ご了承ください。
基本的な作り方は末尾にまとめた記事を参考にさせていただきました。

#概要
仕様は概ね下記の通り。スクリーンに写すのでPC前提です。クリックミスで同じ人が二度引かないように、スタートボタンとストップボタンを分けましたが、スマホ前提なら一個にした方が良いかも。
なお、即席なのでセットできる数値は半角数字のみです。来期も使う機会があったら入力チェックをつけよう。

- ビンゴに使う数字の最大数を入力してセットする
- スタートを押すと数字が周り、ストップで止まる
- 同じ数字が出ない
- 一度でた数字は色がつく

[→DEMO](http://sasadesignworks.com/sample/jscont/bingo/demo_bingo.html)
（不親切設計なので半角数字での指定以外は受け取りません。）
（音がなりますのでご注意ください。）

# HTML
そんなに壮大な必要はなかったのでinput要素は半角数字2桁までに限定。多分CSS的に3桁まではなんとか？
ビンゴの番号用リスト（出た番号は黄色にする）`id=numList`、出た順に数字を並べる履歴用リスト`id=result`の２種類を用意。遊ぶだけならresultはいらないけど、今回は一応つけておこう。

```html:html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="reset.css">
<link rel="stylesheet" type="text/css" href="style.css">
<script type="text/javascript" src="bingo.js"></script>
<title>Bingo slot</title>
</head>
<body>
  <header>
    <div class="inner">
      <h1>BINGO SLOT</h1>
    </div>
  </header>
  <nav class="menu">
    <div class="inner">
      <form name="setmenu">
        <input type="text" name="dat" id="value" pattern="[1-9][0-9]" maxlength="2">
        <button type="button" id="setBtn">SET</button>
        <button type="button" id="resetBtn">RESET</button>
      </form>
    </div>
  </nav>
  <section class="main">
    <div class="inner">
      <div id="roulette"></div>
      <form id="playBtn">
        <button id="startBtn">START</button>
        <button id="stopBtn">STOP</button>
      </form>
      <audio id="dramroll" preload="auto">
        <source src="dramroll.mp3" type="audio/mp3">
      </audio>
      <audio id="pingpong" preload="auto">
        <source src="pingpong.mp3" type="audio/mp3">
      </audio>
    </div>
  </section>
  <section class="listSec">
    <div class="inner">
      <div>
        <ul class="clearfix" id="numList">
        </ul>
      </div>
      <div>
        <ul class="clearfix" id="result">
        </ul>
      </div>
    </div>
  </section>
  <fotter>
    <div class="inner">
      <address>Copyright(c) --, Allright Reserved.</address>
    </div>
  </fotter>
</body>
</html>
```
# CSS
丸いとビンゴ感が出るかなという安直さ。即席感満載だけどま、まあ今回きりだから...。

```CSS:CSS
@charset "UTF-8";
@import url('https://fonts.googleapis.com/css?family=Montserrat&display=swap');

html {
  background: #fff;
  font-size: 100%;
}
body {
  margin:0 auto;
  width: 100%;
  color: #0b3c5d;
  font-family: 'Montserrat', 'Hiragino Kaku Gothic Pro', Osaka, 'メイリオ', 'ＭＳ Ｐゴシック', 'MS PGothic', Verdana,Arial, sans-serif;
  text-align: center;
}
header{
  background: #333;
  color: #fff;
}
section.main {
  margin: 0 auto;
}
section.listSec {
  min-height: 300px;
  background: #f2eee2;
}
div.inner {
  margin: 0 auto;
  padding: 30px 0;
  width: 80%;
  max-width: 1000px;
}
fotter {
  clear: both;
}
h1 {
  font-size: 3.5rem;
  font-weight: bold;
}
form {
  display: flex;
  justify-content: center;
}
#value, #setBtn, #resetBtn {
  height: 2.5rem;
  box-sizing: border-box;
}
#value {
  padding: 10px 5px;
  width: 4rem;
  background-color: #fff;
  border: 2px solid #0b3c5d;
  font-size: 1.4rem;
  ime-mode: disabled;
  line-height: 2.5rem;
}
#setBtn, #resetBtn {
  margin-left: 10px;
  padding: 0px 20px;
  background: #0b3c5d;
  border: 2px solid #0b3c5d;
  color: #fff;
  font-size: 1.2rem;
}
#setBtn:hover, #resetBtn:hover {
  background: #f2eee2;
  color: #0b3c5d;
}
#setBtn:disabled, #resetBtn:disabled {
  border: 2px solid #f2eee2;
  background: #f2eee2;
  color: #fff;
}
#roulette {
  margin: 0 auto;
  width: 400px;
  height: 400px;
  background: #f2eee2;
  border-radius: 200px;
  font-size: 12rem;
  line-height: 400px;
}
#startBtn, #stopBtn {
  margin:20px;
  width: 120px;
  height: 120px;
  border-radius: 60px;
  font-size: 2rem;
  font-weight: bold;
  color: #fff;
}
#startBtn {
  background: #008f95;
}
#startBtn:disabled {
  border: 2px solid #008f95;
  background: #C6EBEC;
  color: #008f95;
}
#stopBtn {
  background: #e24e42;
}
#stopBtn:disabled {
  border: 2px solid #e24e42;
  background: #F0CCCA;
  color: #e24e42;
}
ul {
  margin-top: 30px;
  list-style-type: none;
  display: block;
  overflow: hidden;
}
li {
  margin: 8px;
  width:60px;
  height: 60px;
  line-height: 60px;
  border-radius: 30px;
  background: #fff;
  display: inline-block;
  font-size: 1.8rem;
}
li.hit {
  background: #fedc3d;
}
ul#numList li {
  float: left;
}
ul#result {
  color: #f2eee2;
}
ul#result li {
  float:left;
}
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}
```

# JavaScript

```JavaScript:JavaScript
window.addEventListener('load', function bingo() {
  let setBtn = document.getElementById('setBtn');
  let startBtn = document.getElementById('startBtn');
  let stopBtn = document.getElementById('stopBtn');
  let resetBtn = document.getElementById('resetBtn');
  let result = document.getElementById('result');
  let position = 'beforeend';
  let dramroll = document.getElementById('dramroll');
  let pingpong = document.getElementById('pingpong');
  // ボタンの起動
  resetBtn.disabled = true;
  startBtn.disabled = true;
  stopBtn.disabled = true;
  // セットクリック
  function setBingo(){
    let max = parseInt(document.setmenu.dat.value, 10);    // 入力された値を数値に変換
    let numList = [...Array(max).keys()].map(i => ++i);    // max分の番号を入れた配列
    let el01 = document.querySelector('#numList');
    let el02 = document.querySelector('#result');
    // ボタンの起動
    setBtn.disabled = true;
    resetBtn.disabled = false;
    startBtn.disabled = false;
    // 番号リストを出力
    for(i = 0; i < max; i++) {
      let value = '<li>' + numList[i] + '</li>';
      el01.insertAdjacentHTML(position, value);    // １からmaxまでのくじ番号リストを出力
    }
    // くじを作成
    for (i = max - 1; i >= 0; i--) {    // 配列内の整数をシャッフル
      let random = Math.floor(Math.random() * (i + 1));
      [numList[i], numList[random]] = [numList[random], numList[i]];
    }
    // スタートクリック
    function startBingo() {
      if (0 < numList.length) {
        stopBtn.disabled = false;
        startBtn.disabled = true;
        pingpong.pause();
        pingpong.currentTime = 0;
        dramroll.play();
        let status = true;    // ルーレットを起動
        roulette = setInterval(function() {
          let rouletteNum = Math.floor((Math.random() *max) + 1);    // maxまでのランダムな数を作成
          document.getElementById('roulette').innerHTML = rouletteNum;    // 表示
        }, 30);
      } else {
        ;
      }
    }
    startBtn.addEventListener('click', startBingo);
    // ストップクリック
    function stopBingo() {
      status = false;    // ルーレットを停止
      dramroll.pause();
      dramroll.currentTime = 0;
      pingpong.play();
      if (0 < numList.length) {
        stopBtn.disabled = true;
        startBtn.disabled = false;
        clearInterval(roulette);
        let current = numList.shift();    // 結果を取り出す
        document.getElementById('roulette').innerHTML = current;    // 結果をルーレット上に出力
        let value = '<li>' + current + '</li>';
        result.insertAdjacentHTML(position, value);    // 結果リストに当選番号を出力
        let hitNum = document.getElementById('numList').getElementsByTagName('li');    // くじ番号リストのli要素
        let hit = hitNum[current - 1];    // 当選番号と同じ番号のli要素
        hit.classList.add('hit');
        if(numList.length == 0) {
          startBtn.disabled = true;
        } else {
          ;
        }
      } else {
        ;
      }
    }
    stopBtn.addEventListener('click', stopBingo);
    // リセットクリック
    function resetBingo() {
      window.location.reload();
    }
    resetBtn.addEventListener('click', resetBingo);
  }
  setBtn.addEventListener('click', setBingo);
  // テキストボックスでエンターキーが押された場合にビンゴをセットする
  function keydown(e) {
    if(e.keyCode == 13) {
      document.getElementById('value').nextElementSibling.focus();
    }
  }
  document.getElementById('value').addEventListener('keydown', keydown);
}, false);
```
## 順番に確認
### id取得とボタンの初期値を設定
まずはロードとともにボタンのidを取得。ついでに音源用と配置用も用意しちゃってますね。
セットボタン以外のボタンは`disabled`を`true`にして無効状態に。

```JavaScript:JavaScript
// JavaScript Document
window.addEventListener('load', function bingo() {
  let setBtn = document.getElementById('setBtn');
  let startBtn = document.getElementById('startBtn');
  let stopBtn = document.getElementById('stopBtn');
  let resetBtn = document.getElementById('resetBtn');
  let result = document.getElementById('result');
  let position = 'beforeend';
  let dramroll = document.getElementById('dramroll');
  let pingpong = document.getElementById('pingpong');
  // ボタンの起動
  resetBtn.disabled = true;
  startBtn.disabled = true;
  stopBtn.disabled = true;
```
### セットボタンをクリック
`parseInt`の第一引数に入力された値`document.setmenu.dat.value`を、第二引数に10進数を表す`10`を指定して、最大数`max`の値を取得。
抽選結果を入れる配列`postList`とmax分のくじを入れる配列`ｎｕｍList`を用意する。`numList`はスプレッド演算子でmax個分の要素を展開、keys（）でインデックスを返してくれるので、map()で各要素に１ずつ足したら１からmaxまでの値が順に入ったお行儀のいい配列が誕生。これで番号リスト用の配列ができたので、あとはfor文で出力。

- [Array.prototype.keys()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/keys)
- [Array.prototype.map()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
- [JavaScript - 配列を連番で初期化する](https://infoteck-life.com/a0066-js-array-initial-serial/)
- [連番の数字の配列を作成（es2015 ver)](https://qiita.com/sakymark/items/710f0b9a632c375fbc31)

今度はくじ用。`Math.random()`で`ｎｕｍList`の中身をシャッフル。フィッシャー–イェーツのアルゴリズムについては下記参考。

- [JavaScript アルゴリズムで配列をシャッフルする](https://qiita.com/artistan/items/9eb9a0fb14f4ec3a8764)

```JavaScript:JavaScript
  // セットクリック
  function setBingo(){
    let max = parseInt(document.setmenu.dat.value, 10);    // 入力された値を数値に変換
    let numList = [...Array(max).keys()].map(i => ++i);    // max分の番号を入れた配列
    let el01 = document.querySelector('#numList');
    let el02 = document.querySelector('#result');
    // ボタンの起動
    setBtn.disabled = true;
    resetBtn.disabled = false;
    startBtn.disabled = false;
    // 番号リストを出力
    for(i = 0; i < max; i++) {
      let value = '<li>' + numList[i] + '</li>';
      el01.insertAdjacentHTML(position, value);    // １からmaxまでのくじ番号リストを出力
    }
    // くじを作成
    for (i = max - 1; i >= 0; i--) {    // 配列内の整数をシャッフル
      let random = Math.floor(Math.random() * (i + 1));
      [numList[i], numList[random]] = [numList[random], numList[i]];
    }
```
### スタートクリック
スタート用の関数`startBingo`を作る。もし`numList.length`（配列の要素数）が0より大きければ、実行する。２回目以降繰り返すときにストップ時の「デデン！」が重なったままドラムロールが鳴ると姦しいのでまずは`pingpong`を止めて、ついでに`pingpong.currentTime = 0;`で次のデデン！のために巻き戻しておく。そして`dramroll`を鳴らす。デレレレレレ...
ルーレットも回す。詳細は参考文献を確認のこと。1からmaxまでの数字をグルグル...速さはとりあえず30ミリ秒に。
`add.EventListener`でクリックしたら動いておくれ。

```JavaScript:JavaScript
    // スタートクリック
    function startBingo() {
      if (0 < numList.length) {
        stopBtn.disabled = false;
        startBtn.disabled = true;
        pingpong.pause();
        pingpong.currentTime = 0;
        dramroll.play();
        let status = true;    // ルーレットを起動
        roulette = setInterval(function() {
          let rouletteNum = Math.floor((Math.random() *max) + 1);    // maxまでのランダムな数を作成
          document.getElementById('roulette').innerHTML = rouletteNum;    // 表示
        }, 30);
      } else {
        ;
      }
    }
    startBtn.addEventListener('click', startBingo);
```
### ストップクリック
ストップ用の関数`stopBingo`を作る。目と耳がうるさいのでルーレットとドラムロールを止めて「デデン！」を鳴らす。もし`numList.length`（配列の要素数）が0より大きければ、実行する。ルーレットをクリアしてシャッフル済みの`ｎｕｍList`から要素をひとつ取り出してルーレット上にドーンと出力。
履歴用の`<ul id="result"></ul>`のli要素を出力しておく。
出力しておいた`numList`の番号に色をつけたい。li要素を取得して配列に突っ込んだ`hitNum`の誕生を宣言する。`hit`で`current-1`番目の要素をさらに宣言して、`class="hit"`をつけて無事色付け終わり。
`numList`が０になったら、スタートボタンを止めておく。`add.EventListener`でクリックしたらよろしく。

```JavaScript:JavaScript
// ストップクリック
    function stopBingo() {
      status = false;    // ルーレットを停止
      dramroll.pause();
      dramroll.currentTime = 0;
      pingpong.play();
      if (0 < numList.length) {
        stopBtn.disabled = true;
        startBtn.disabled = false;
        clearInterval(roulette);
        let current = numList.shift();    // 結果を取り出す
        document.getElementById('roulette').innerHTML = current;    // 結果をルーレット上に出力
        let value = '<li>' + current + '</li>';
        result.insertAdjacentHTML(position, value);    // 結果リストに当選番号を出力
        let hitNum = document.getElementById('numList').getElementsByTagName('li');    // くじ番号リストのli要素
        let hit = hitNum[current - 1];    // 当選番号と同じ番号のli要素
        hit.classList.add('hit');
        if(numList.length == 0) {
          startBtn.disabled = true;
        } else {
          ;
        }
      } else {
        ;
      }
    }
    stopBtn.addEventListener('click', stopBingo);
```
### リセットボタン
リセットボタンは単純にクリックしたらリロードにしました。ここまででセットボタンの関数がおしまい。`add.EventListener`でクリックしたらよろしく。

```JavaScript:JavaScript
// リセットクリック
    function resetBingo() {
      window.location.reload();
    }
    resetBtn.addEventListener('click', resetBingo);
  }
  setBtn.addEventListener('click', setBingo);
```
### テキストボックスのエンターキーダウン処理
動作テスト時にうっかりキーダウンでリロードしてあ...ってなったので一応これだけはつけて置く。本当はきちんと入力制限とエラー表示つけたほうがいいでしょう。

```JavaScript:JavaScript
setBtn.addEventListener('click', setBingo);
  // テキストボックスでエンターキーが押された場合にビンゴをセットする
  function keydown(e) {
    if(e.keyCode == 13) {
      document.getElementById('value').nextElementSibling.focus();
    }
  }
  document.getElementById('value').addEventListener('keydown', keydown);
}, false);
```
# 参考記事
これがなければ始まらない。本当にありがとうございました。

- [HTML / CSS / JavaScript（jQuery）でビンゴゲームを作成](https://www.terakoya.work/javascript-jquery-bingo-game-build/)
- [JavaScript - JavaScriptでビンゴゲームを作る](https://mitakalab.tumblr.com/post/40755495293/javascript-javascript%E3%81%A7%E3%83%93%E3%83%B3%E3%82%B4%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%92%E4%BD%9C%E3%82%8B)
- [JavaScriptでビンゴマシンを作ってみた。](https://qiita.com/Yuta_Fujiwara/items/076b6ea61f456d9649e9)
- [JavaScriptでビンゴをつくる](https://qiita.com/otsukayuhi/items/149f493dbaab6697a84c)

配列についてはこちらが大変わかりやすくまとめられていて素敵です。

- [JavaScriptの配列の使い方まとめ。要素の追加,結合,取得,削除。](https://qiita.com/takeharu/items/d75f96f81ff83680013f)