【JavaScript】「じゃんけんゲーム」を作ってみた

# はじめに

こんにちは。
今回は、HTML、CSS、JavaScriptを使ってオリジナルゲームを作りました。
ゲームの名は「じゃんけんゲーム」です。

![じゃんけんゲーム.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309633/db442ff0-8e3f-a07d-913a-07b8fb002fcc.gif)


私個人の中で、勉強している言語が使えるようになってきたら、確認または一つの到達度のラインとして、じゃんけんゲームを作るようにしています。
ロジックはとても簡単なので、すぐ作れてしまうものなのですが、プログラムをより工夫しようと考えると結構大変でした。
とりあえず自分なりに終止符を打ち、完成させたので、記事にしていこうと思います。

完成したものは次のURLからプレイできます！

https://michimichix521.github.io/JankenGame/

ファイル等はGitHubにあげていますので、[こちらから](https://github.com/michimichix521/JankenGame)ダウンロードすることもできます。


# 環境

- Windows 10 home
- Google Chrome


# じゃんけんゲームの制作

じゃんけんゲームで使う素材については、ぜひこちらを使ってください。
このゲームではimgフォルダを作って、そこに以下の画像ファイルを入れています。
これら素材は[Piskel](https://www.piskelapp.com)で描いています。

![rock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309633/ec252fbd-a742-6eef-b664-238d6b8dc7e6.png)

![scissors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309633/9c0a4565-4e89-a957-0156-0e96caa800d7.png)

![paper.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309633/b6bb7b53-94b8-bab6-cd6f-2a1991239ce7.png)


## 1.構造（HTML）

まずは構造についてです。
このじゃんけんゲームでは、大きく分けると、ディーラーとプレイヤーがいて、その間に、勝敗を表すテキストとリセットボタンがあります。
ソースコードは以下のようになります。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>じゃんけんゲーム</title>
	<link rel="stylesheet" href="styles.css">
</head>
<body>
	<main>
		<div class="dealer"></div>
		<div class="container">
			<div class="result"></div>
			<div id="resetButton" class="hidden">もう一度！</div>
		</div>
		<div class="player"></div>
	</main>
	<script src="main.js"></script>
</body>
</html>
```


## 2.見た目（CSS）

CSSのソースコードは以下のようになります。
あまり解説はしませんが、HTMLやJavaScriptの処理に対して、その都度確認してみてください。

```styles.css
body{
	background-color:#088A4B;
}

main{
	width:500px;
	margin:0 auto;
	text-align:center;
	font-weight:bold;
}

.dealer{
	height:160px;
}

.dealer img{
	width:150px;
	height:160px;
}

.player,.container{
	display:flex;
}

.player{
	justify-content:space-between;
}

.player img{
	width:100px;
	height:110px;
	cursor:pointer;
	user-select:none;
}

.container{
	margin:70px 0;
	height:100px;
	line-height:100px;
	justify-content:space-around;
}

.result{
	font-size:48px;
	font-family:Arial, sans-serif;
}

#resetButton{
	width:100px;
	font-size:18px;
	border-radius:50%;
	background-color:pink;
	border:3px solid #fff;
	box-sizing: border-box;
}

#resetButton:hover{
	opacity:0.8;
	cursor:pointer;
	user-select:none;
}

.hidden{
	display:none;
}
```


## 3.振る舞い（JavaScript）

動作としては、ディーラーのじゃんけんのアイコンは、プレイヤーにあるグー・チョキ・パーのアイコンを１つクリックするまで、ランダムに入れ替わるようになっています。
クリックされたタイミングで、ディーラーのアイコンの入れ替わりが止まり、プレーヤーがクリックしたアイコンと、ディーラーの止まったアイコンで勝敗を決めるといったものになります。

ソースコードは以下の通りです。
プログラムの構造では、親クラスとして、Userクラスがあり、それを継承し、子クラスとして、DealerクラスとPlayerクラスができている形になっています。


```main.js
"use strict";

{
	class User{ //親クラス
		constructor(tag,type){
			this.tag=tag;
			this.type=type;
			this._img=document.createElement("img");

			this._img.addEventListener("click",()=>{
				this.click();
			});
		}

		addImage(){ //画像ソースの追加
			this._img.src="img/"+this.type+".png";
		}

		addHTML(){ //HTMLに生成したタグの追加
			this.tag.appendChild(this._img);
		}

		click(){ //クリックされた時の処理
			if(!RESET_BUTTON.classList.contains("hidden")){ //何度もクリックできないようにする
				return;
			}

			clearTimeout(dealer._timeoutId);
			RESET_BUTTON.classList.remove("hidden");
			this.result();
		}

		result(){ //じゃんけんの結果判定
			let _dealerType=dealer.type[dealer._rdNum];

			switch(this.type){
				case "rock": //グーをクリックした場合
					if(_dealerType === "rock"){
						RESULT.textContent="引き分け";
					}else if(_dealerType === "scissors"){
						RESULT.textContent="勝ち！";
					}else{
						RESULT.textContent="負け";
					}
					break;

				case "scissors": //チョキをクリックした場合
					if(_dealerType === "rock"){
						RESULT.textContent="負け";
					}else if(_dealerType === "scissors"){
						RESULT.textContent="引き分け";
					}else{
						RESULT.textContent="勝ち！";
					}
					break;

				case "paper": //パーをクリックした場合
					if(_dealerType === "rock"){
						RESULT.textContent="勝ち！";
					}else if(_dealerType === "scissors"){
						RESULT.textContent="負け";
					}else{
						RESULT.textContent="引き分け";
					}
					break;

				default:
					console.log("タイプなし");
					break;
			}
		}
	}

	class Dealer extends User{ //子クラス:ディーラー
		constructor(tag,type){
			super(tag,type);

			this._rdNum=Math.floor(Math.random()*type.length);
			this._timeoutId;

			this.addImage();
			this.addHTML();
		}

		//オーバーライド
		addImage(){ //画像ソースの追加
			this._img.src="img/"+this.type[this._rdNum]+".png";
		}

		shuffleImage(){ //ランダムにじゃんけん画像ソースの入れかえ
			this._rdNum=Math.floor(Math.random()*this.type.length);
			this.addImage();

			this._timeoutId = setTimeout(()=>{
				this.shuffleImage();
			},20); //50fps

		}

		//オーバーライド
		click(){return;} //クリックしても何も起こらない
	}

	class Player extends User{ //子クラス:プレイヤー
		constructor(tag,type){
			super(tag,type);

			this.addImage();
			this.addHTML();
		}
	}

	const DEALER_TAG=document.querySelector(".dealer"); //タグの取得
	const PLAYER_TAG=document.querySelector(".player"); //タグの取得
	const RESET_BUTTON=document.getElementById("resetButton"); //タグの取得
	const RESULT=document.querySelector(".result"); //タグの取得

	const JANKEN_TYPE=["rock","scissors","paper"]; //じゃんけんタイプの配列

	init();

	const dealer=new Dealer(DEALER_TAG,JANKEN_TYPE) //ディーラーのインスタンスの生成

	let player=new Array(3);
	for(let i=0;i<player.length;i++){
		player[i]=(new Player(PLAYER_TAG,JANKEN_TYPE[i])); //プレイヤーのインスタンスの生成
	}

	dealer.shuffleImage();

	RESET_BUTTON.addEventListener("click",()=>{ //リセットボタンが押された時の処理
		dealer.shuffleImage();
		init();
	});

	function init(){ //初期化
		RESULT.textContent="";
		RESET_BUTTON.classList.add("hidden");
	}
}
```

じゃんけんゲームは以上で終了になります。




### 2019/09/12 修正
コメントでクラスの責務に関して指摘を受けたので、以下が修正したソースコードになります。
上記のmain.jsのファイルを入れ替えるだけで動くと思いますので、ぜひ試してみてください。


```main.js
"use strict";

{
  class GameMaster{ //ゲームの進行用のクラス
    constructor(){
      this.init(); //初期化

      this._opponent=new Opponent(DEALER_TAG,JANKEN_TYPE) //相手

      //プレイヤー側のじゃんけんのアイコンの表示
      this._player=new Array(3);
       for(let i=0;i<this._player.length;i++){
          this._player[i]=document.createElement("img");
          this._player[i].src="img/"+JANKEN_TYPE[i]+".png";
          PLAYER_TAG.appendChild(this._player[i]);

          this._player[i].addEventListener("click",()=>{
            this.click(JANKEN_TYPE[i]); //プレイヤー側のアイコンがクリックされた時に動作するメソッド
          });
      }

      this._opponent.shuffleImage(); //相手のアイコンをシャッフルする

      RESET_BUTTON.addEventListener("click",()=>{ //リセットボタンが押された時の処理
        this._opponent.shuffleImage();
        this.init();
      });
    }

    init(){ //初期化
      RESULT.textContent="";
      RESET_BUTTON.classList.add("hidden");
    }

    click(clickType){ //クリックされた時の処理
      if(!RESET_BUTTON.classList.contains("hidden")){ //何度もクリックできないようにする
        return;
      }

      clearTimeout(this._opponent._timeoutId);
      RESET_BUTTON.classList.remove("hidden");
      this._opponent.result(clickType);
    }
  }

  class Opponent{ //相手のクラス
    constructor(){
      this._img=document.createElement("img");

      this._rdNum=Math.floor(Math.random()*JANKEN_TYPE.length);
      this._img.src="img/"+JANKEN_TYPE[this._rdNum]+".png";
      DEALER_TAG.appendChild(this._img);

      this._timeoutId;
    }

    shuffleImage(){ //ランダムにじゃんけん画像ソースの入れかえ
      this._rdNum=Math.floor(Math.random()*JANKEN_TYPE.length);
      this._img.src="img/"+JANKEN_TYPE[this._rdNum]+".png";

      this._timeoutId=setTimeout(()=>{
        this.shuffleImage();
      },20); //50fps
    }

    result(clickType){ //じゃんけんの結果判定
      let _dealerType=JANKEN_TYPE[this._rdNum];

      switch(clickType){
        case "rock": //グーをクリックした場合
          if(_dealerType === "rock"){
            RESULT.textContent="引き分け";
          }else if(_dealerType === "scissors"){
            RESULT.textContent="勝ち！";
          }else{
            RESULT.textContent="負け";
          }
          break;

        case "scissors": //チョキをクリックした場合
          if(_dealerType === "rock"){
            RESULT.textContent="負け";
          }else if(_dealerType === "scissors"){
            RESULT.textContent="引き分け";
          }else{
            RESULT.textContent="勝ち！";
          }
          break;

        case "paper": //パーをクリックした場合
          if(_dealerType === "rock"){
            RESULT.textContent="勝ち！";
          }else if(_dealerType === "scissors"){
            RESULT.textContent="負け";
          }else{
            RESULT.textContent="引き分け";
          }
          break;

        default:
          console.log("タイプなし");
          break;
      }
    }
  }

  const DEALER_TAG=document.querySelector(".dealer"); //タグの取得
  const PLAYER_TAG=document.querySelector(".player"); //タグの取得
  const RESET_BUTTON=document.getElementById("resetButton"); //タグの取得
  const RESULT=document.querySelector(".result"); //タグの取得

  const JANKEN_TYPE=["rock","scissors","paper"]; //じゃんけんタイプの配列

  const gameMaster=new GameMaster(); //ゲームの進行用のクラスのインスタンスの生成
}
```



# おわりに
resultメソッドで勝敗に関する処理をしているのですが、もっときれいにできると思います。自分的に他のところで力尽きてしまいました。(笑)

興味があれば、これを参考に改良してみてください。
こっちの方がもっときれいにプログラムが書けるよ！とかあれば、ぜひコメントをください！

ここまで読んでいただき、ありがとうございました。