Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

じゃんけんゲーム.gif

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

完成したものは次のURLからプレイできます!
https://michimichix521.github.io/JankenGame/

ファイル等はGitHubにあげていますので、こちらからダウンロードすることもできます。

環境

  • Windows 10 home
  • Google Chrome

じゃんけんゲームの制作

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

rock.png

scissors.png

paper.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)

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

ソースコードは以下の通りです。
プログラムの構造では、親クラスとして、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メソッドで勝敗に関する処理をしているのですが、もっときれいにできると思います。自分的に他のところで力尽きてしまいました。(笑)

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away