JavaScriptでビンゴゲームを作ってみた

JavaScriptでビンゴゲームを作ってみました。

#仕様
+ カードは25マス。
+ 「CARD SET」を押すと，カードにランダムな数字が入る。「CARD SET」は無効化する。
+ 「CHOOSE NUMBER」を押すと，ランダムな数字が結果として表示される。
+ 結果と一致するカードのマスは色が変わる。
+ 履歴欄に結果の数字を順番に追加していく。
+ 全ての数字を選び終わると，アラートで終了を知らせる。「CHOOSE NUMBER」は無効化する。
+ 「RESET」を押すと，最初の状態に戻る。

↓最初の画面
![bingo_img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178942/f9d81e8d-aaf2-dce2-96de-b803cf65fb1e.png)
↓ビンゴ中の画面
![bingo_img3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178942/2d43f245-dcb3-7e16-0352-38eda209b333.png)


#コード

####HTML

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="X-UA-Compatible" content="ie=edge">
   <title>ビンゴゲーム | JS</title>
   <link rel="preconnect" href="https://fonts.gstatic.com">
   <link href="https://fonts.googleapis.com/css2?family=Goldman&family=Press+Start+2P&display=swap" rel="stylesheet">   
   <link rel="stylesheet" href="styles/style.css"/>
</head>
<body>
   <h1 class="title">!!!!! BINGO GAME !!!!!</h1>
   <div class="container">
      <div class="left-container">
         <div class="left-container__btns">
            <button id="setBtn" class="btn">CAED SET</button>
            <button id="chooseBtn" class="btn">CHOOSE NUMBER</button>
            <button id="resetBtn" class="btn">RESET</button>
         </div>
         <div class="left-container__roulette" id="roulette">
         </div>
         <div class="left-container__history">
            <h2>HISTORY_</h2>
            <ul class="history-list" id="history-list"></ul>
         </div>
      </div>
      <div class="card-container">
         <h2>CARD</h2>
         <div id="card">
         </div>
      </div>
   </div>
   
<script src="scripts/selectNumber.js"></script>
<script src="scripts/main.js"></script>
</body>
</html>
```

####CSS(SCSS)
<details><summary>SCSS</summary>
<div>

```scss:styles/style.scss
$cWhite: #ffffff;
$cYellow: #f8cc2f;
$cBlackGray: #2c2c2c;
$cRed: #e00000;
$cGreen: #03a305;
$cGray: rgb(192, 191, 191);

body {
	margin: 0;
	text-align: center;
	background-color: $cYellow;
	width: 100vw;
	height: 100vh;
	box-sizing: border-box;
	position: relative;
}

.title {
	font-family: 'Press Start 2P', cursive;
	color: $cRed;
	font-size: 2rem;
	text-shadow: 3px 3px 0px $cWhite;
}

.container {
	width: 90%;
	margin: 0 auto;
	padding-top: 20px;
	max-width: 1000px;
	display: flex;
	justify-content: space-between;
	align-items: center;
}

.left-container {
	width: 48%;

	&__btns {
		display: flex;
		flex-direction: column;
	
		& .btn {
			display: inline-block;
			box-sizing: border-box;
			padding-top: 15px;
			padding-bottom: 15px;
			margin: 15px 0;
			width: 100%;
			background-color: #04c706;
			border: outset 6px $cGreen;
			outline: none;
			font-size: 18px;
			color: $cWhite;
			font-family: 'Press Start 2P', cursive;
			cursor: pointer;
			
			&:disabled {
				background-color: rgb(172, 172, 172);
				color: $cWhite;
				border: inset 6px $cGray;
			}
		}
	}

	&__roulette {
		display: flex;
		justify-content: center;
		align-items: center;   
		box-sizing: border-box;
		margin: 15px auto;
		width: 200px;
		height: 120px;
		background-color: $cWhite;
		border: solid 6px $cRed;
		border-radius: 50%;
		font-size: 42px;
		font-family: 'Press Start 2P', cursive;
		color: $cRed;
	}

	&__history {
		box-sizing: border-box;
		padding: 20px;
		width: 100%;
		height: 180px;
		background-color: $cBlackGray;
		border: solid 3px $cWhite;
		
		& h2 {
			margin: 0;
			text-align: left;
			font-family: 'Press Start 2P', cursive;
			font-size: 18px;
			color: $cWhite;
		}
		
		& ul {
			margin: 0 auto;
			padding: 10px 0 0 0;
			font-family: 'Press Start 2P', cursive;
			color: $cWhite;
			list-style: none;
			display: flex;
			flex-wrap: wrap;
			
			& li {
				width: 12%;
				font-size: 0.8rem;
				line-height: 1.5rem;
			}
		}
	}
}

.card-container {
	width: 48%;
	height: 600px;
	background-color: $cWhite;
	box-shadow: 9px 9px 0px $cBlackGray;
	
	& h2 {
		margin: 35px auto;
		padding-left: 0.7em;
		font-family: 'Press Start 2P', cursive;
		font-size: 35px;
		color: $cBlackGray;
		letter-spacing: 0.7em;
	}

	& #card {
		margin: 20px;
		width: 90%;
		display: flex;
		flex-wrap: wrap;

		& .cell {
			box-sizing: border-box;
			width: 20%;
			background-color: $cWhite;
			border: solid 3px $cGray;
			position: relative;
			
			&::before {
				content: '';
				display: block;
				padding-top: 100%;
			}
			
			& .cell-inner {
				font-family: 'Press Start 2P', cursive;
				display: flex;
				justify-content: center;
				align-items: center;
				position: absolute;
				top: 0;
				left: 0;
				width: 100%;
				height: 100%;
				font-size: 1.2rem;
				color: $cBlackGray;
			}
	
			&.opened {
				background-color: $cRed;
				& .cell-inner {
					color: $cWhite;
				}
			}
		}
	}
}

```

</div>
</details>

####JavaScript

```javascript:scripts/selectNumber.js
class SelectNumber {
   constructor(arrBox) {
      this.arrBox = arrBox;
      this.arr = [];
      this._setNumber();
      this._assignNumber(arrBox);
   }
   _setNumber() {
      for(let i = 1; i <= 75; i++) {
         this.arr.push(i);
      }
   }
   _assignNumber() {
      for(let j = 0, len = this.arr.length; j < 25; j++, len--) {
         let rdmNumber = Math.floor(Math.random() * len);
         this.arrBox.push(this.arr[rdmNumber]);
         this.arr[rdmNumber] = this.arr[len - 1];
      }
   }
}
```

```javascript:scripts/main.js
window.addEventListener('DOMContentLoaded', function() {
   
   const card = document.getElementById('card');
   const setBtn = document.getElementById('setBtn');
   const chooseBtn = document.getElementById('chooseBtn');
   const roulette = document.getElementById('roulette');
   const historyList = document.getElementById('history-list');
   const resetBtn = document.getElementById('resetBtn');
   
   let countClick = 0;
   let cardArr = [];
   let resultArr = [];
   setBtn.disabled = false;
   chooseBtn.disabled = false;

   setBtn.addEventListener('click', function() {
      const select1 = new SelectNumber(cardArr);
      const select2 = new SelectNumber(resultArr);

      for (let i = 0; i < 25; i++) {
         const cell = document.createElement('div');
         const cellInner = document.createElement('div');
         cell.classList.add('cell');
         cellInner.classList.add('cell-inner');
      
         cellInner.textContent = cardArr[i];
         cell.setAttribute('name', cardArr[i]);
      
         cell.appendChild(cellInner);
         card.appendChild(cell);
      };

      setBtn.disabled = true;
   });
   
   chooseBtn.addEventListener('click', function() {
      if(countClick === 25) {
         alert('FINISH!!!!');
         chooseBtn.disabled = true;
      } else {
         const result = resultArr[countClick];
         roulette.textContent = result;
         
         const li = document.createElement('li');
         li.textContent = result;
         historyList.appendChild(li); 

         const resultCell = document.querySelector(`.cell[name = "${result}"]`);
         if(resultCell !== null) {
            resultCell.classList.add('opened');
         }
         
         countClick++;
      }  
   }); 

   resetBtn.addEventListener('click', function() {
      window.location.reload();
   });
});

```

#解説

####ランダムな数字を選ぶ

```javascript:scripts/selectNumber.js
class SelectNumber {
   constructor(arrBox) {
      this.arrBox = arrBox;
      this.arr = [];
      this._setNumber();
      this._assignNumber(arrBox);
   }
   _setNumber() {
      for(let i = 1; i <= 25; i++) {
         this.arr.push(i);
      }
   }
   _assignNumber() {
      for(let j = 0, len = this.arr.length; j < 25; j++, len--) {
         let rdmNumber = Math.floor(Math.random() * len);
         this.arrBox.push(this.arr[rdmNumber]);
         this.arr[rdmNumber] = this.arr[len - 1];
      }
   }
}
```

1〜75のランダムな数字を選ぶclass。
正確には，1〜75の中から25個を重複なくランダムに選び，渡された配列に順に格納していくclass。
今回はこのclassで，「カード用の配列」と「結果用の配列」を作成。

>この仕組みは何方かのブログで見つけ利用させて頂いたのですが，この記事を書くためにもう一度探したら見当たりませんでした。見つけ次第記載します。申し訳ありません…


####「CARD SET」を押した時の動作

```javascript:scripts/main.js
setBtn.addEventListener('click', function() {
      const select1 = new SelectNumber(cardArr);
      const select2 = new SelectNumber(resultArr);

      for (let i = 0; i < 25; i++) {
         const cell = document.createElement('div');
         const cellInner = document.createElement('div');
         cell.classList.add('cell');
         cellInner.classList.add('cell-inner');
      
         cellInner.textContent = cardArr[i];
         cell.setAttribute('name', cardArr[i]);
      
         cell.appendChild(cellInner);
         card.appendChild(cell);
      };

      setBtn.disabled = true;
});
```

+ 「カード用の配列」と「結果用の配列」を準備する。
+ カードのマスになるHTMLを組み立てる。
+ この後で選ばれたマスの表示を変えるのに使う為，`cell`に自分の番号のname属性を付けておく。
+ 「CARD SET」は無効化する。

####「CHOOSE NUMBER」を押した時の動作

```javascript:scripts/main.js
chooseBtn.addEventListener('click', function() {
      if(countClick === 25) {
         alert('FINISH!!!!');
         chooseBtn.disabled = true;
      } else {
         const result = resultArr[countClick];
         roulette.textContent = result;
         
         const li = document.createElement('li');
         li.textContent = result;
         historyList.appendChild(li); 

         const resultCell = document.querySelector(`.cell[name = "${result}"]`);
         if(resultCell !== null) {
            resultCell.classList.add('opened');
         }
         
         countClick++;
      }  
}); 
```

+ if文でクリック数を監視。
+ 結果用の配列の「クリック数」番目の数字を結果として表示する。
+ 履歴欄に結果の数字を追加。
+ 結果と一致するname属性値を持つ`cell`を取得→classの追加で表示変更。
+ 25回クリックしていたら（26回目を押したら），「FINISH!!!!」のアラートが出る。
+ 「CHOOSE NUMBER」を無効化。 

####(2021/3/28更新 改善)

+ 数字がランダムで選ばれる範囲：**25→75**に変更
+ 結果と一致するname属性値を持つ`cell`が，**card上に存在するかif文で調べる。存在すれば（resultCellがnullではなければ）classを追加。**


#反省，改善点
+ 数字を選ぶ際に，ドラムロール式にしたかった。
+ カードのリセットがページを再読み込みする方法になっている。それぞれの要素をリセットするような方法にしたい。（どっちの方が負担や効率面で良いのか分かりませんが…）