プログラミング初心者のための「神経衰弱風フリップカードアプリ」開発入門

今回は「神経衰弱風フリップカードアプリ」の作り方についてご紹介していきたいと思います。

ルールはシンプルで「カード」をひっくり返して「同じ番号のカード」を当てていくゲームです。

現在の自分の「記憶力」を「正解率」を元にした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 + ')';
}

ゲームの進行

ゲームを進めるための処理は、次のようになります。

  1. マス目の表示
  2. カード選択時の処理
  3. 結果の表示&結果のローカルストレージへの保存

マス目の表示

「メイン画面」で「マス目の数」を選択して「スタート」ボタンをクリックすると、「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. 1枚目のカードを選択
  2. 2枚目のカードを選択
  3. カードの番号の合致判定

の「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";    //「次へ」ボタンの背景色を変更
}

プログラムの全体は下記のリンクからご覧いただけます。

→「Flip The Card(プログラム)」

実際にゲームをプレイしてみたい方は、下記のリンクからアクセスできます。

→「Flip The Carp」

プログラミング初心者の方が「プログラムを書ける」ようになるためには、たくさんのプログラムを読んで、プログラムの作り方や考え方を身に付けていく必要があります。

今回はシンプルな「カードゲーム」でしたが、世の中にはさまざまな「カードゲーム」がありますのでゲームをプレイしながら、「このゲームはどんなプログラムが書かれているんだろう?」と考えながら「プログラムの作り方を考える習慣」を身に付けていきましょう。

HOMEへ