Contents
HTML部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかる神経衰弱</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel = "stylesheet" href = "./style.css" type = "text/css" media = "all"> </head> <body> <div id = "container"> <div id = "message1"></div> <div id = "message2"></div> <div id = "cards"></div> <button id = "start">START</button> </div> <script src= "./index.js"></script> </body> </html> |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
body { background-color: black; color: #fff; } #container { width: 360px; } .card { width: 50px; height: 80px; margin-top: 10px; margin-left: 8px; background-color: transparent; border: 0px solid #fff; padding: 0; } #start{ position: absolute; width: 150px; height: 60px; left:110px; top:100px; font-size: 20px; } #cards { position: relative; } #message1 { padding-top: 10px; padding-left: 10px; } #message2 { padding-left: 10px; } |
JavaScript部分
グローバル変数と定数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const $container = document.getElementById('container'); const $cards = document.getElementById('cards'); const $message1 = document.getElementById('message1'); const $message2 = document.getElementById('message2'); const $start = document.getElementById('start'); // 効果音 const ngSound = new Audio('./sounds/ng.mp3'); const okSound = new Audio('./sounds/ok.mp3'); const openSound = new Audio('./sounds/open.mp3'); const winSound = new Audio('./sounds/win.mp3'); const loseSound = new Audio('./sounds/lose.mp3'); // Cardオブジェクトを格納する配列 const cards = []; const CARDS_COUNT = 26; // スマホだと52枚全部は表示できないのでスペードとハートの2種類全26枚 let firstCard = null; // プレイヤーが開いた1枚目のCardオブジェクトを格納する変数 let ignoreClick = true; // trueの場合はカードをクリックしてもなにもおきない let playerScore = 0; let cpuScore = 0; const momories = []; // コンピュータ側が記憶しているカードの情報 |
Cardクラスの定義
Cardクラスを定義します。メンバーはカードのスート(スペードかハートか?)と数字、卓上に存在するカードなのか、0からはじまる通し番号、シャッフルするときにつかわれる乱数です。
1 2 3 4 5 6 7 8 9 |
class Card { constructor(sute, number){ this.Number = number; this.Suit = sute; this.Exist = true; this.ID = 0; this.RandomValue = Math.random(); } } |
ページが読み込まれたときの処理
ページが読み込まれたらカードを初期化してスタートボタンがクリックされたらプレイを開始できるようにイベントリスナを追加します。
1 2 3 4 5 6 7 |
window.onload = () => { initCards(); // カードの初期化(後述) showScore(); // スコアの表示(後述) $message1.innerText = 'STARTボタンをクリックしてください'; $start.addEventListener('click', (ev) => gameStart()); } |
カードの初期化
カードを初期化する処理を示します。
最初にカードが表示されている部分にある要素をすべて削除し、生成しなおします。そしてゲーム開始時はカードはすべて裏向きなので裏向きのカードのイメージを生成して追加します。
同時にCardオブジェクトを生成して配列内に格納します。これはどこにどのカードがあるかを管理するためのものです。そして配列momoriesをクリアします。ここにはコンピュータ側が知っているカードの情報が格納されます。ゲーム開始時は当然のことながらどこに何があるかはまったく知らないので空にしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
function initCards(){ // カードを表示させている要素をすべて削除 const count = $cards.childElementCount; for(let i = 0; i < count; i++) $cards.children[0].remove(); // カードを表示させる要素を生成する // カードはすべて裏向き for(let i = 0; i < CARDS_COUNT; i++){ const $card = document.createElement('button'); // 要素は'button' $card.className = "card"; $card.id = "card-id-" + i; $card.addEventListener('click', (ev) => { onClickCard(i); // プレイヤーの手番のときにカードをクリックするとカードが開く }); $cards.appendChild($card); // 裏向きのカードのイメージを生成して要素に追加 const image = new Image(); image.src = './card-images/back.png'; image.width = 50; image.height = 80; $card.appendChild(image); } // Cardオブジェクトの生成と配列への格納 cards.length = 0; for(let i = 0; i < CARDS_COUNT; i++){ const suit = Math.floor(i / 13); const number = i % 13 + 1; cards.push(new Card(suit, number)); } // 乱数をつかってシャッフルする cards.sort((a,b) => a.RandomValue - b.RandomValue); // カードに通し番号をつける for(let i = 0; i < CARDS_COUNT; i++) cards[i].ID = i; // コンピュータ側が知っているカードの情報(ゲーム開始時は当然のことながら何も知らない) momories.length = 0; } |
スコアを表示する処理を示します。
1 2 3 |
function showScore(){ $message2.innerHTML = `あなた:${playerScore} <span style = "margin-left: 50px;">私:${cpuScore}</span>`; } |
ゲーム開始の処理
ゲームを開始する処理を示します。ゲームはプレイヤーの先攻で始まります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function gameStart(){ // カード初期化 initCards(); // プレイヤーの先攻なのでクリックに反応するようにする ignoreClick = false; firstCard = null; // 双方のスコアをリセットして表示 playerScore = 0; cpuScore = 0; showScore(); $message1.innerText = 'あなたの手番です'; // スタートボタンを非表示に $start.style.display = 'none'; } |
カードをクリックしたときの処理
プレイヤーの手番のときにカードをクリックするとカードが開きます。その処理を示します。
ゲームが開始される前やコンピュータ側の手番のとき、2枚目を選ぶときに1枚目と同じカードを選んだ場合、卓上に存在しないカードを選んだときは警告音を鳴らします。
カードを開ける処理をしたあと、それが1枚目のカードの場合は対応するオブジェクトをグローバル変数に保存します。2枚目のカードを開けた場合は数字があっているか調べます。
数字があっている場合は引き続きプレイヤーの手番です。合っていない場合はコンピュータ側の手番になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
async function onClickCard(id){ if(ignoreClick || !cards[id].Exist || (firstCard != null && firstCard == cards[id])){ ngSound.currentTime = 0; ngSound.play(); return; } // 一連の処理が完了するまでクリックには無反応とする ignoreClick = true; openCard(id); // カードを開ける(後述) // 効果音を鳴らす openSound.currentTime = 0; openSound.play(); // 1枚目のカードを開けた場合はグローバル変数に保存する // 2枚目のカードを開けた場合は数があっているか調べる if(firstCard == null){ firstCard = cards[id]; ignoreClick = false; } else{ $message1.innerText = '判定中'; await sleep(500); // 数字が合っている場合は加点処理をする。 // 合っていない場合はコンピュータ側の手番とする if(firstCard.Number == cards[id].Number){ okSound.currentTime = 0; okSound.play(); await sleep(500); // 数字が合っている場合は2枚のカードを非表示にする removeCard(firstCard.ID); // 後述 removeCard(id); playerScore++; showScore(); } else { ngSound.currentTime = 0; ngSound.play(); // 数字が合っていない場合は2枚のカードを伏せる await sleep(500); closeCard(id); // 後述 closeCard(firstCard.ID); // コンピュータ側の手番 await think(); // 後述 } firstCard = null; // 数字が合っている場合、またはコンピュータ側の手番が終了した場合で、 // 卓上にカードが残っている場合はプレイヤーの手番とする // 卓上のカードが存在しない場合はゲーム終了 const existCards = cards.filter(card => card.Exist); if(existCards.length < 2) onFinishGame(); // 後述 else { ignoreClick = false; $message1.innerText = 'あなたの手番です'; } } } // 待機処理 async function sleep(ms){ await new Promise(resolve => setTimeout(() => { resolve(''); }, ms)); } |
カードを開く処理と伏せる処理
カードを開く処理を示します。クリックされたボタンに相当するオブジェクトを参照すればそのカードのスートと番号がわかります。クリックされたボタンの子要素を削除し、スートと番号から生成されたイメージと置き換えます。これでカードが開いたように見せることができます。
また開かれたカードをコンピュータ側に記憶させます。記憶に重複が生まれないようにする方法もあるのですが、あえて重複が発生するようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function openCard(id){ const $card = document.getElementById('card-id-' + id); $card.children[0].remove(); const image = new Image(); //const arr = ['c', 'd', 'h', 's', ] // スマホだと52枚全部は表示できないのでスペードとハートの2種類 const arr = ['s', 'h']; image.src = `./card-images/${arr[cards[id].Suit]}${cards[id].Number}.png`; image.width = 50; image.height = 80; $card.appendChild(image); const card = cards.filter(card => card.ID == id)[0]; momories.push(card); // 記憶に重複が生まれないようにするにはこうすればよい // if(momories.filter(c => c == card).length == 0) // momories.push(card); } |
カードを伏せる処理を示します。
カードの裏側のイメージを生成し、カードがある位置に存在するボタンの子要素を削除してこれと置き換えます。
1 2 3 4 5 6 7 8 9 10 |
function closeCard(id){ const image = new Image(); image.src = `./card-images/back.png`; image.width = 50; image.height = 80; const $card = document.getElementById('card-id-' + id); $card.children[0].remove(); $card.appendChild(image); } |
カードを取り除く処理を示します。
要素を取り除いてしまうわけではなく、背景色と同じイメージと置き換えることでカードがなくなったように見せかけているだけです。また対応するCardオブジェクトのExistプロパティをfalseに変更しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
function removeCard(id){ cards[id].Exist = false; const image = new Image(); image.src = `./card-images/none.png`; image.width = 50; image.height = 80; const $card = document.getElementById('card-id-' + id); $card.children[0].remove(); $card.appendChild(image); } |
コンピュータ側の着手
コンピュータ側の着手の処理を示します。
コンピュータ側はすでに開かれたカードをすべて覚えていることにします(これではコンピュータ側が強すぎるのでランダムに忘れさせる処理をいれてもいいかも……)。
コンピュータ側のアルゴリズムですが、記憶のなかに番号が同じカードのペアがあればそれを開きます。ない場合はまず記憶にないカードを開き、2枚目のカードとして記憶されているものがあればそれを開きます。ない場合は明らかに番号が合っていないカード以外からランダムに選びます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
async function think(){ $message1.innerText = '私の手番です'; await sleep(500); while(true){ const existCards = cards.filter(card => card.Exist); let id1 = -1; let id2 = -1; // 存在するカードで記憶にあるものをまとめる(重複排除) const tempMomories = []; const momories2 = momories.filter(mem => mem.Exist); for(let i = 0; i < momories2.length; i++){ if(tempMomories.filter(mem => mem == momories2[i]).length == 0) tempMomories.push(momories2[i]); } // 2枚目とも記憶のなかにあれば2枚をそこから選ぶ。 const pairs = []; for(let i=1; i<=13; i++){ const ms2 = tempMomories.filter(mem => mem.Number == i); if(ms2.length >= 2) pairs.push({first:ms2[0].ID, second:ms2[1].ID}); } if(pairs.length > 0){ const pair = selectRandom(pairs); id1 = pair.first; id2 = pair.second; } // 記憶されているカードのID const ids = tempMomories.map(mem => mem.ID); // 記憶されていないカードのID const ids2 = cards.filter(card => card.Exist).map(card => card.ID).filter(i => ids.indexOf(i) == -1); if(id1 == -1 || id2 == -1){ // 1枚目は記憶していないカードを選ぶ if(ids2.length > 0){ id1 = selectRandom(ids2); } // 2枚目が記憶のなかにあればそこから選ぶ。ない場合はあきらかに違うカード以外から選ぶ const ms = tempMomories.filter(mem => mem.ID != id1 && mem.Number == cards[id1].Number); if(ms.length > 0){ id2 = ms[0].ID; } else { const ids3 = ids2.filter(id => id != id1); id2 = selectRandom(ids3); } } // 上記のいずれにも該当しない場合は適当に2つ選ぶ if(id1 == -1 || id2 == -1){ id1 = selectRandom(existCards).ID; id2 = selectRandom(existCards).ID; while(id1 == id2) id2 = selectRandom(existCards).ID; } // コンピュータ側が選んだカードを開く openCard(id1); await sleep(500); openCard(id2); await sleep(500); // 数字が合っていればカードを取り除き、引き続きコンピュータ側が着手する // 外れていればカードを伏せ、プレイヤー側の手番とする if(cards[id1].Number == cards[id2].Number){ removeCard(id1); removeCard(id2); cpuScore++; showScore(); // カードのペアが取り除かれることで卓上のカードはすべてなくなっているかもしれない // その場合はゲーム終了 if(cards.filter(card => card.Exist).length < 2) return; await sleep(500); } else { closeCard(id1); closeCard(id2); break; } } } // 配列のなかから要素をランダムに選択する関数 function selectRandom(arr){ const r = Math.floor(Math.random() * arr.length); return arr[r]; } |
ゲーム終了の処理
卓上のカードがなくなったらゲーム終了です。スコアを比較して大きいほうが勝ちです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function onFinishGame(){ ignoreClick = true; // クリックに反応しないようにする $message1.innerText = '終了'; if(playerScore >= cpuScore){ // プレイヤーの勝ち winSound.currentTime = 0; winSound.play(); } else { // コンピュータ側の勝ち loseSound.currentTime = 0; loseSound.play(); } // 3秒後にスタートボタンを再表示する setTimeout(() => { $start.style.display = 'block'; }, 3000); } |