プログラミング初心者のための「神経衰弱風フリップカードアプリ」開発入門
今回は「神経衰弱風フリップカードアプリ」の作り方についてご紹介していきたいと思います。
ルールはシンプルで「カード」をひっくり返して「同じ番号のカード」を当てていくゲームです。
現在の自分の「記憶力」を「正解率」を元にした10段階の「ランク」で確認することができます。
「16マス(4×4)」と「36マス(6×6)」の2つのゲームモードがあり、「36マス」は記憶力に自信がある方でも、クリアまである程度時間がかかるモードとなっています。
このページの、最下部に実際にゲームで遊べる「リンク」を用意しておきますので、「自分の記憶力が知りたい!」「記憶力の限界に挑戦したい!」方はぜひ一度遊んでみてください。
このゲームの流れや操作方法は、下の動画から確認できます。
この「カードアプリ」の作り方についてご説明していきたいと思います。
画面構成
「カードアプリ」には、次の3つの画面があります。
- メイン画面
- ゲーム画面
- スコア&ランク画面
「メイン画面」のボタンをクリックすると、「ゲーム画面」と「スコア画面」に遷移することができます。

これらの画面は、1つのHTMLファイル内に収められています。
<!-- スタート画面 --> <div id="start_screen"> <p class="rank_16_frame">16マスの現在のランク:<span class="rank_16"></span></p> <p class="rank_36_frame">36マスの現在のランク:<span class="rank_36"></span></p> <div id="select_game_mode_frame"> <p>■ゲームモード選択</p> <div> <p>マス目の数を選択してください。</p> <select id="game_mode"> <option value="16">16マス</option> <option value="36">36マス</option> </select> </div> </div> <button id="start_btn">スタート</button> <hr> <button id="score_btn">スコア&ランク</button> </div> <!-- ゲーム画面 --> <div id="game_screen"> <p id="message"></p> <p>現在のカード選択回数:<span id="select_card_count">0</span></p> <div id="cards_container" class="clear"></div> <button id="next_btn">次へ</button> </div> <!-- スコア&ランク画面 --> <div id="score_screen"> <p id="score_title">スコア&ランク</p> <p class="rank_16_frame">16マスの現在のランク:<span class="rank_16"></span></p> <table id="score_16_list"></table> <hr> <p class="rank_36_frame">36マスの現在のランク:<span class="rank_36"></span></p> <table id="score_36_list"></table> <hr> <button class="to_start_btn">スタート画面</button> </div>
画面間の遷移は、「div要素」に「id属性」を設定し、それぞれの画面の表示を「CSS」の「display属性」の値で切替えています。
画面を切り替えるための関数は、
/** * 指定画面を表示 */ function displayScreen(screen_type) { hideScreen(); switch (screen_type) { case 'start': //スタート画面を表示 getElmId('start_screen').style.display = 'block'; displayScoreList(); //スコア一覧を表示 break; case 'game': //ゲーム画面を表示 getElmId('game_screen').style.display = 'block'; break; case 'score': //スコア一覧画面を表示 getElmId('score_screen').style.display = 'block'; break; } } /** * 画面を非表示 */ function hideScreen() { getElmId('start_screen').style.display = 'none'; //スタート画面を非表示 getElmId('game_screen').style.display = 'none'; //ゲーム画面を非表示 getElmId('score_screen').style.display = 'none'; //スコア画面を非表示 }
のようになります。
「hideScreen」関数で全ての画面を非表示にし、「displayScreen」関数で、引数に指定した画面を表示しています。
「GetElmId」関数は、「documen.getElementById」メソッドの別名関数です。
/** * 指定したIDの要素を取得 */ function getElmId(val) { return document.getElementById(val); }
また、「displayScoreList」関数は、スコア一覧を表示するための関数です。
この関数は、「メイン画面」の「スコア&ランク」ボタンをクリックすると実行されます。
/** * スコア一覧を表示 */ function displayScoreList() { //16マスのランクを表示 let score_16_rank = getRankVal(score_16_data, 16); //「ランク値」を計算 Array.from(getElmClass('rank_16')).forEach( element => isNaN(score_16_rank) ? element.innerHTML = score_16_rank : element.innerHTML = createRankStar(score_16_rank) ); //36マスのランクを表示 let score_36_rank = getRankVal(score_36_data, 36); //「ランク値」を計算 Array.from(getElmClass('rank_36')).forEach( element => isNaN(score_36_rank) ? element.innerHTML = score_36_rank : element.innerHTML = createRankStar(score_36_rank) ); //16マスの「スコア一覧」テーブルを作成&表示 createScoreTable('score_16_list', score_16_data); //36マスの「スコア一覧」テーブルを作成&表示 createScoreTable('score_36_list', score_36_data); } /** * 「スコア一覧」テーブルを作成&表示(最新のデータから10プレイ分を表示) */ function createScoreTable(list_type, score_data) { getElmId(list_type).innerHTML = ''; //現在の表示内容をクリア //テーブルヘッダーの作成&表示 let tr = document.createElement('tr'); let td = null; let th = document.createElement('th'); th.innerHTML = 'プレイ日時'; tr.appendChild(th); th = document.createElement('th'); th.innerHTML = '正解率'; tr.appendChild(th); getElmId(list_type).appendChild(tr); if (score_data.length !== 0) { //スコア一覧データを作成&表示 for (var i = score_data.length - 1; i > (score_data.length - 1) - 10; i--) { if (i >= 0) { tr = document.createElement('tr'); td = document.createElement('td'); td.innerHTML = score_data[i]['date']; tr.appendChild(td); td = document.createElement('td'); td.innerHTML = Math.round((16 / parseInt(score_data[i]['select_count'])) * 100) + "%"; tr.appendChild(td) getElmId(list_type).appendChild(tr); } } } } /** * 「ランク値」を計算 */ function getRankVal(scoreData, rankType) { { if (scoreData.length < 10) { //プレイ回数が足りない場合 let remain_game = 10 - scoreData.length; //スコア判定までの残りゲーム数 return 'あと' + remain_game + 'ゲームでランクが判定できます。'; //表示用メッセージの作成 } else { let sum = 0; //合計値格納用 let rank = 0; //ランク数格納用 //最新のデータから10プレイ分を元に「ランク値」を算出 for (var i = scoreData.length - 1; i > (scoreData.length - 1) - 10; i--) { sum += parseInt(scoreData[i]['select_count']); //スコアの合計値を計算 } let div_val = rankType === 16 ? 160 : 360; let judge_val = (div_val / sum) * 100; //スコアを算出 rank = Math.floor(judge_val / 10) + 1; //ランクの計算 return rank; } } /** * 「ランクスター」を作成 */ function createRankStar(rank_val) { let rank_star = ''; //ランクスター文字列格納用 for (var i = 1; i <= 10; i++) { i <= rank_val ? rank_star += "★" : rank_star += "☆"; //ランクスター文字列を作成 } //表示用ランク文字列を作成 return 'Low ' + rank_star + ' High (Rank:' + rank_val + ')'; }
ゲームの進行
ゲームを進めるための処理は、次のようになります。
- マス目の表示
- カード選択時の処理
- 結果の表示&結果のローカルストレージへの保存
マス目の表示
「メイン画面」で「マス目の数」を選択して「スタート」ボタンをクリックすると、「startGame」関数が実行されます。
/** * 「スタート」ボタンをクリック時 */ function startGame() { displayScreen('game'); //ゲーム画面を表示 getElmId("select_card_count").innerHTML = selectCount; //カード選択回数を更新 game_mode_num = parseInt(getElmId('game_mode').value); //ゲームモードを取得 createCards(); //カードの生成 cardsObj = getElmClass("card"); //カードオブジェクトを取得 getElmId("message").innerHTML = "1枚目のカードを選択してください。"; //表示メッセージを設定 getElmId("next_btn").disabled = true; //「次へ」ボタンを無効化 }
「createCards」関数で「カードを作る処理」を行っていますが、「createCards」関数の内容は次のようになります。
/** * カードを生成 */ function createCards() { getElmId("cards_container").innerHTML = ''; //カードをクリア cardsNums = createRandomNums(); //カードのランダムな番号を生成 for (var i = 0; i < game_mode_num; i++) { let frame = document.createElement("div"); //カードフレームの作成 let card = document.createElement("div"); //カードの作成 let card_num = document.createElement("span"); //カード番号用 let card_cover_img = document.createElement("img"); //カバー画像表示用 card_cover_img.dataset.id = i; //データIDを設定 card_cover_img.src = "images/cover.png"; //カバー画像ファイル名を設定 card.classList.add("card"); //クラスを追加 card_num.classList.add("card_num"); //クラスを追加 card_num.style.display = "none"; //カードの番号を非表示 card.appendChild(card_num); //カード番号をカードフレームに追加 card.appendChild(card_cover_img); //カバー画像をカードフレームに追加 frame.appendChild(card); //カードフレームへカードを追加 getElmId("cards_container").appendChild(frame); //カードコンテナにカードフレームを追加 } let cards = Array.from(document.getElementsByClassName("card")); //カードを配列として取得 cards.forEach(element => { element.addEventListener("click", selectCard, false); //カードクリック時のイベントリスナーを設定 }); //カードの横幅を設定 let cards_frame = Array.from(getElmId('cards_container').children); //全カード要素を取得 if (game_mode_num === 16) { //16マスのゲームの場合 cards_frame.forEach( element => element.style.width = "25%" ); } else if (game_mode_num === 36) { //36マスのゲームの場合 cards_frame.forEach( element => element.style.width = "16%" ); } }
この関数では、「for文」の中で「カード」の要素を作成していますが、下図のような構造の要素が作成されます。

「カードフレームのdiv要素」の中に「カードのdiv要素」があり、その中に、「カードの番号」を表示するための「span要素」と「カードのカバー画像」を表示するための「img要素」があります。
カード選択時の処理
カードを選択すると、「selectCard」関数が実行されます。関数の内容は下記のようになります。
プログラムのコメントにある「1サイクル」は、
- 1枚目のカードを選択
- 2枚目のカードを選択
- カードの番号の合致判定
の「1サイクル文の処理の流れ」のことを表しています。
/** * カード選択時の処理 */ function selectCard(e) { if (!finishedOneCycle) { //1サイクルのゲームが終了していない場合 selectCount++; //カード選択カウントを+1カウントアップ getElmId('select_card_count').innerHTML = selectCount; //カード選択回数を表示 let selectCard = cardsObj[e.target.dataset.id]; //選択カードの取得 selectCard.getElementsByTagName('img')[0].style.display = "none"; //カバー画像を非表示 selectCard.getElementsByTagName('span')[0].style.display = "block"; //カード番号用要素を表示 selectCard.getElementsByTagName('span')[0].innerHTML = cardsNums[e.target.dataset.id]; //カード番号を設定 if (firstPickFlg) { //1回目のカードを選択しているか? firstCard = selectCard; //1回目の「選択カード」を変数に設定 firstPickFlg = false; //1回目の「カード選択フラグ」を設定 getElmId("message").innerHTML = "2枚目のカードを選択してください。"; //表示メッセージを設定 } else { //2回目のカードを選択しているか? getElmId("next_btn").disabled = false; //「次へ」ボタンを有効化 getElmId("next_btn").style.backgroundColor = "#0000ff"; //「次へ」ボタンの背景色を青に変更 secondCard = selectCard; //2回目の選択カードを変数に設定 firstPickFlg = true; //1回目のカード選択フラグを設定 finishedOneCycle = true; //1サイクル分のゲーム実行フラグを設定 getElmId("message").innerHTML = "「次へ」ボタンをクリックしてください。"; //表示メッセージを設定 if (successCount === (game_mode_num / 2) - 1) { //全てのカードが正解になっているか? //「ゲーム結果」をローカルストレージへ保存 var d = new Date(); //日付オブジェクトを生成 //日付文字列を生成 var d_save_str = d.getFullYear() + "年" + (d.getMonth() + 1) + "月" + d.getDate() + "日" + d.getHours() + "時" + d.getMinutes() + "分"; //「配列データ」を作成 let regist_data = { 'select_count': selectCount, 'date': d_save_str } if (game_mode_num === 16) { //16マスのゲーム実行時 score_16_data.push(regist_data); //スコアデータをスコア用配列に追加 saveScoreData('score_16_data', score_16_data); //スコア用配列をローカルストレージへ保存 } else if (game_mode_num === 36) { //36マスのゲーム実行時 score_36_data.push(regist_data); //スコアデータをスコア用配列に追加 saveScoreData('score_36_data', score_36_data); //スコア用配列をローカルストレージへ保存 } getElmId("message").innerHTML = "終了"; //メッセージを表示 getElmId("next_btn").disabled = true; //「次へ」ボタンを無効化 getElmId("next_btn").style.backgroundColor = "#afafaf"; //「次へ」ボタンの背景色を変更 getElmId('modal_frame').style.display = 'block'; //モーダル画面を表示 getElmId('success_rate').innerHTML = Math.round((16 / selectCount) * 100); //モーダル画面へ正解率を表示 successCount = 0; //正解率をリセット selectCount = 0; //カード選択回数をリセット finishedOneCycle = false; //1サイクル分のゲーム実行フラグを設定 } } } else { alert("「次へ」ボタンをクリックしてください。"); } }
処理の内容は、コメントを付記しておきましたので、「どのような処理を行っているのか?」を意識しながら、プログラムを読んでみてください。
「saveScoreData」関数は、ローカルストレージにスコアを保存するための関数です。
/** * 「スコアデータ」をセーブ */ function saveScoreData(data_type, save_data) { let save_json = JSON.stringify(save_data); //「スコアデータ(配列)」を「JSON」へ変換 localStorage.setItem(data_type, save_json); //「ローカルストレージ」へセーブ }
カードを2枚選択すると画面下部の「次へ」ボタンをクリックします。

「次へ」ボタンをクリックした時の処理は次のようになります。
/** * 次のカードを選択 */ function nextSelectCard() { getElmId("message").innerHTML = "1枚目のカードを選択してください。"; //表示メッセージを設定 //選択カード間違い時 if (firstCard.getElementsByTagName('span')[0].innerHTML !== secondCard.getElementsByTagName('span')[0].innerHTML) { firstCard.getElementsByTagName('img')[0].style.display = "block"; //1回目の選択カードのカバー画像を表示 firstCard.getElementsByTagName('span')[0].style.display = "none"; //1回目の選択カードの番号を非表示 secondCard.getElementsByTagName('img')[0].style.display = "block"; //2回目の選択カードのカバー画像を表示 secondCard.getElementsByTagName('span')[0].style.display = "none"; //2回目の選択カードの番号を非表示 } else { //選択カード正解時 successCount++; //正解カウントを1カウントアップ } finishedOneCycle = false; //1サイクル分のゲーム実行フラグを設定 getElmId("next_btn").disabled = true; //「次へ」ボタンを無効化 getElmId("next_btn").style.backgroundColor = "#afafaf"; //「次へ」ボタンの背景色を変更 }
プログラムの全体は下記のリンクからご覧いただけます。
実際にゲームをプレイしてみたい方は、下記のリンクからアクセスできます。
プログラミング初心者の方が「プログラムを書ける」ようになるためには、たくさんのプログラムを読んで、プログラムの作り方や考え方を身に付けていく必要があります。
今回はシンプルな「カードゲーム」でしたが、世の中にはさまざまな「カードゲーム」がありますのでゲームをプレイしながら、「このゲームはどんなプログラムが書かれているんだろう?」と考えながら「プログラムの作り方を考える習慣」を身に付けていきましょう。