プログラミング初心者のための「神経衰弱風フリップカードアプリ」開発入門
今回は「神経衰弱風フリップカードアプリ」の作り方についてご紹介していきたいと思います。
ルールはシンプルで「カード」をひっくり返して「同じ番号のカード」を当てていくゲームです。
現在の自分の「記憶力」を「正解率」を元にした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"; //「次へ」ボタンの背景色を変更
}
プログラムの全体は下記のリンクからご覧いただけます。
実際にゲームをプレイしてみたい方は、下記のリンクからアクセスできます。
プログラミング初心者の方が「プログラムを書ける」ようになるためには、たくさんのプログラムを読んで、プログラムの作り方や考え方を身に付けていく必要があります。
今回はシンプルな「カードゲーム」でしたが、世の中にはさまざまな「カードゲーム」がありますのでゲームをプレイしながら、「このゲームはどんなプログラムが書かれているんだろう?」と考えながら「プログラムの作り方を考える習慣」を身に付けていきましょう。