JSによるゲーム制作-(神経衰弱2023ver)

JavaScript

トランプ画像を使って神経衰弱ゲームを作ってみよう。
これは以前の記事を2023年版としてhtml,css,jsを書き直したものだ。
以前はtableタグを使ってトランプを並べていたが、今回はgridを使ってトランプを配置している

[準備編]
1.まずは以下をダウンロードして、デスクトップに展開する

2.以下のようなフォルダ構成になっている。imagesフォルダにはトランプの画像素材が入っている

3.index.htmlはcssファイルとjsファイルの読み込み設定とボタン要素が一つとdiv要素が一つ配置されている

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>神経衰弱</title>
    <link rel="stylesheet" href="css/main.css">
    <script src="js/main.js"></script>
  </head>
  <body>
    <button id="startBt">START</button>
    <div id="cardgrid"> </div>
  </body>
</html>

4.main.jsはドキュメントの読み込みが完了したら処理を開始する記述をしてある。main.cssは現状空白の状態だ。

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  
});

5.index.htmlをブラウザで開いてみよう。以下のようにボタンが1つ表示されていればOKだ。

[作成]
1.main.jsを以下のように更新する。

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        div.textContent=card.suit+':'+card.num;
        //divにcardクラス追加
        div.classList.add('card');
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    initgrid();
  }); 
});

2.実行してみよう。以下のように要素が縦に並べばOKだ。開発者画面を見てみると、div#cardgird要素の中にdiv.cardが52個格納されているのがわかる

3.main.cssを以下のように追記する

body{
  background-color:lightgreen;
}
#cardgrid{
  /*テーブル全体をセンタリング*/
  width:90%;
  margin:10px auto;
  /*グリッドレイアウトの設定*/
  display:grid;
  /*13個の要素を均等の幅で配置*/
  grid-template-columns:repeat(13,1fr);
  /*隙間を画面の縦幅の0.5%に設定*/
  gap:0.5vh;
}
/*td要素でなおかつclassにcardがついている要素*/
div.card{
  /*要素の縦横比をトランプ画像と同じ2:3に設定*/
  aspect-ratio: 2 / 3;
  background-color:yellow;
}

4.実行してみよう。gridレイアウトによって13枚を1列に並べることに成功している。ブラウザの横幅を変えてみると連動してカードサイズが変わることがわかる。

5.現状はボタンを押す度にどんどんカードが追加されてしまうので以下の1行をmain.jsに追加する 。ボタンを押すたびに中身を空っぽにしてから要素を追加している。

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        div.textContent=card.suit+':'+card.num;
        //divにcardクラス追加
        div.classList.add('card');
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    initgrid();
  }); 
});

6.それではいよいよ画像を設定していこう。以下のmain.jsとmain.cssのハイライト部分を追記修正する

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
      //カードの画像
      this.front=`${suit}${num < 10 ? '0':''}${num}.gif`;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        //textContentは不要なのでコメント化
        //div.textContent=card.suit+':'+card.num;
        //背景画像に画像を設定
        div.style.backgroundImage=`url(images/${card.front})`;
        //divにcardクラス追加
        div.classList.add('card');
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    initgrid();
  }); 
});
body{
  background-color:lightgreen;
}
#cardgrid{
  /*テーブル全体をセンタリング*/
  width:90%;
  margin:10px auto;
  /*グリッドレイアウトの設定*/
  display:grid;
  /*13個の要素を均等の幅で配置*/
  grid-template-columns:repeat(13,1fr);
  /*隙間を画面の縦幅の0.5%に設定*/
  gap:0.5vh;
}
/*td要素でなおかつclassにcardがついている要素*/
div.card{
  /*要素の縦横比をトランプ画像と同じ2:3に設定*/
  aspect-ratio: 2 / 3;
  /* background-color:yellow; */
  /*コンテナいっぱいに画像を拡大縮小する*/
  background-size:contain;
}

7.実行してみよう。トランプが並べば成功だ。

[シャッフル]

8.このままでは神経衰弱にならないので、まずはカードをシャッフルしよう。
ここではシャッフルアルゴリズムの一つFisher–Yates shuffleを用いている
以下のようにmain.jsを修正

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
      //カードの画像
      this.front=`${suit}${num < 10 ? '0':''}${num}.gif`;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        //textContentは不要なのでコメント化
        //div.textContent=card.suit+':'+card.num;
        //背景画像に画像を設定
        div.style.backgroundImage=`url(images/${card.front})`;
        //divにcardクラス追加
        div.classList.add('card');
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //カードシャッフル関数(Fisher–Yates shuffle)
  const shuffle=()=>{
    let i=cards.length;
    while(i){
      let index=Math.floor(Math.random()*i--);
      [cards[index],cards[i]]=[cards[i],cards[index]]
    }
  };
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    shuffle();
    initgrid();
  }); 
});

9.実行してみよう。ボタンを押す度にカードがシャッフルされるのがわかる。

[カードを裏面に]

10.そもそも最初から表を向いていたらゲームにならない。最初は裏向きにしておこう。main.jsとmain.cssを以下のように追加修正

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
      //カードの画像
      this.front=`${suit}${num < 10 ? '0':''}${num}.gif`;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        //textContentは不要なのでコメント化
        //div.textContent=card.suit+':'+card.num;
        //背景画像に画像を設定
        div.style.backgroundImage=`url(images/${card.front})`;
        //divにcardクラスとbackクラス追加
        div.classList.add('card','back');
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //カードシャッフル関数(Fisher–Yates shuffle)
  const shuffle=()=>{
    let i=cards.length;
    while(i){
      let index=Math.floor(Math.random()*i--);
      [cards[index],cards[i]]=[cards[i],cards[index]]
    }
  };
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    shuffle();
    initgrid();
  }); 
});
body{
  background-color:lightgreen;
}
#cardgrid{
  /*テーブル全体をセンタリング*/
  width:90%;
  margin:10px auto;
  /*グリッドレイアウトの設定*/
  display:grid;
  /*13個の要素を均等の幅で配置*/
  grid-template-columns:repeat(13,1fr);
  /*隙間を画面の縦幅の0.5%に設定*/
  gap:0.5vh;
}
/*td要素でなおかつclassにcardがついている要素*/
div.card{
  /*要素の縦横比をトランプ画像と同じ2:3に設定*/
  aspect-ratio: 2 / 3;
  /* background-color:yellow; */
  /*コンテナいっぱいに画像を拡大縮小する*/
  background-size:contain;
}
/*div要素でclassにbackがついている要素は裏面画像(importantルール適用)*/
div.back{
  background-image:url(../images/z01.gif) !important;
}

ポイント解説

jsによってインラインに埋め込まれたスタイルはcssでは基本上書きできない。
それを覆してスタイルを当てるには!importを付与する必要がある

11.実行してみよう。カードがすべて裏面になって表示されれば成功だ。

[クリックしたら表になる]
12.クリックしたらカードが表になったり裏になったりする挙動を作成しよう。
これは簡単で、div要素からbackクラスを外せば表になる。以下のようにmain.jsを追加修正。

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
      //カードの画像
      this.front=`${suit}${num < 10 ? '0':''}${num}.gif`;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  //クリックした際の関数を定義
  const flip=(eve)=>{
    //クリックされた要素を特定
    let div=eve.target;
    //toggle(ついていたら外れ、外れていたら付く)
    div.classList.toggle('back');
  };
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        //textContentは不要なのでコメント化
        //div.textContent=card.suit+':'+card.num;
        //背景画像に画像を設定
        div.style.backgroundImage=`url(images/${card.front})`;
        //divにcardクラスとbackクラス追加
        div.classList.add('card','back');
        //要素をクリックした際の挙動を登録
        div.onclick=flip;
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //カードシャッフル関数(Fisher–Yates shuffle)
  const shuffle=()=>{
    let i=cards.length;
    while(i){
      let index=Math.floor(Math.random()*i--);
      [cards[index],cards[i]]=[cards[i],cards[index]]
    }
  };
  
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    shuffle();
    initgrid();
  }); 
});

[2枚開いたら判定]
13.二枚開いて同じだったらそのままにし、違っていたら裏面に戻す処理を作成しよう。ここではfirstCard,secondCardという変数を用意し、これらの状況を見て判断することとする。main.jsを以下のように追加修正。(34行目のコメントアウトを忘れないよう!)

'use strict';
document.addEventListener('DOMContentLoaded',()=>{
  //Cardクラス作成
  class Card{
    constructor(suit,num){
      //カードのスート(s:スペード、d:ダイヤ...)
      this.suit=suit;
      //カードの数字(1,2,...13)
      this.num=num;
      //カードの画像
      this.front=`${suit}${num < 10 ? '0':''}${num}.gif`;
    }
  }
  //カード配列作成
  const cards=[];
  //カードスーツ配列
  const suits=['s','d','h','c'];
  //2重forで52枚のカードを作成
  for(let i=0;i<suits.length;i++){
    for(let j=1;j<=13;j++){
      //カードインスタンス生成(s1,s2....c13)
      let card=new Card(suits[i],j);
      //配列の末尾に追加
      cards.push(card);
    }
  }
  let firstCard=null;//1枚目のカードを保持(引いてない場合はnull)
  let secondCard=null;//2枚目のカードを保持(引いてない場合はnull)
  //クリックした際の関数を定義
  const flip=(eve)=>{
    //クリックされた要素を特定
    let div=eve.target;
    //toggle(ついていたら外れ、外れていたら付く)
    //div.classList.toggle('back');//この処理はもういらないので削除してよい
    //表面のカードや3枚目のカードをクリックしても何も起こらない。
    if(!div.classList.contains('back') || secondCard !== null){
      return;
    }
    //表面にする
    div.classList.remove('back');
    //もしそれが1枚目だったらfirstCardに代入
    if(firstCard === null){
      firstCard=div;
    }else{
      //2枚目だったらsecondCardに代入
      secondCard=div;
      //2枚のカードの数字が同じだったら
      if(firstCard.num === secondCard.num){
        //正解だった場合fadeoutクラスを付与する
        firstCard.classList.add('fadeout');
        secondCard.classList.add('fadeout');
        //firstCard,secondカードを共にnullに戻す
        [firstCard,secondCard]=[null,null];
      }else{
        //不正回だった場合は1.2秒後に裏面に戻す
        setTimeout(()=>{
          firstCard.classList.add('back');
          secondCard.classList.add('back');
          [firstCard,secondCard]=[null,null];
        },1200);
      }
    }
  };
  //cardgridのDOM取得
  const cardgrid=document.getElementById('cardgrid');
  //gridを初期化する処理
  const initgrid=()=>{
    //cardgridに入っている要素をすべて削除
    cardgrid.textContent=null;
    for(let i=0;i<suits.length;i++){
      for(let j=0;j<13;j++){
        //1枚毎のトランプとなるdiv要素作成
        let div=document.createElement('div');
        //配列からcardを取り出す
        let card=cards[i*13+j];
        //divの<div>この部分</div>(textContent)を設定
        //textContentは不要なのでコメント化
        //div.textContent=card.suit+':'+card.num;
        //背景画像に画像を設定
        div.style.backgroundImage=`url(images/${card.front})`;
        //divにcardクラスとbackクラス追加
        div.classList.add('card','back');
        //要素をクリックした際の挙動を登録
        div.onclick=flip;
        //divにnumプロパティを定義して、そこに数字を保存
        div.num=card.num;
        //cardgrid要素に追加
        cardgrid.append(div);
      }
    }
  };
  //カードシャッフル関数(Fisher–Yates shuffle)
  const shuffle=()=>{
    let i=cards.length;
    while(i){
      let index=Math.floor(Math.random()*i--);
      [cards[index],cards[i]]=[cards[i],cards[index]]
    }
  };
  
  //ボタンのDOM取得
  const startBt=document.getElementById('startBt');
  //ボタンを押したときの処理
  startBt.addEventListener('click',()=>{
    shuffle();
    initgrid();
    //ゲーム開始時にfirstCard,secondCardをnullにする
    [firstCard,secondCard]=[null,null];
  }); 
});

ポイント解説

まず、85,86行目でdiv要素にnumというプロパティを定義して、トランプの数字を覚えさせておき、2枚目をめくったときにその2つの数字が等しいかで判定している。
もし等しかった場合はとりあえず、2枚のカードにfadeoutというクラスを付与して、後ほどcssでこのカードを非表示にする。そうでなかったらsetTimeout関数を使って1200ミリ秒後に裏面に戻す処理がしてある。

[実行]

14.それでは実行してみよう。2枚のカードを開いて違っていたら閉じて、同じだった場合はそのままになるという、神経衰弱のベースとなる処理が完成した。

[仕上げ]
15.では最後に揃った場合2枚のカードを消滅させよう。main.cssに以下を追記

body{
  background-color:lightgreen;
}
#cardgrid{
  /*テーブル全体をセンタリング*/
  width:90%;
  margin:10px auto;
  /*グリッドレイアウトの設定*/
  display:grid;
  /*13個の要素を均等の幅で配置*/
  grid-template-columns:repeat(13,1fr);
  /*隙間を画面の縦幅の0.5%に設定*/
  gap:0.5vh;
}
/*td要素でなおかつclassにcardがついている要素*/
div.card{
  /*要素の縦横比をトランプ画像と同じ2:3に設定*/
  aspect-ratio: 2 / 3;
  /* background-color:yellow; */
  /*コンテナいっぱいに画像を拡大縮小する*/
  background-size:contain;
}
/*div要素でclassにbackがついている要素は裏面画像(importantルール適用)*/
div.back{
  background-image:url(../images/z01.gif) !important;
}
/*アニメーションの定義(名前はfadeout)*/
@keyframes fadeout{
  40%{
    opacity: 1;
  }
  100%{
    opacity: 0;
  }
}
div.fadeout{
  /*使用するアニメーション名*/
  animation-name:fadeout;
  /*アニメーション時間*/
  animation-duration:2s;
  /*アニメーション終了時の挙動*/
  animation-fill-mode:forwards; 
}

ポイント解説

揃ったと同時に消え始めてしまうのは不自然だったのでcssアニメーションを使って0.8秒は何も行わずにその後1.2秒かけて徐々に消えていくという処理を実装した。

完成!

これで完成だ。
以前のテーブルを使ったバージョンからgridレイアウトを使ったものにしたことでレスポンシブ対応が格段にやりやすくなっている。またjsもclass構文を使ったりと内容がアップデートされている。
この後、考えられる処理は、効果音やBGMの追加、経過時間の表示、残り枚数の表示などがある。ぜひ、実装してみてもらいたい。

コメント

タイトルとURLをコピーしました