LoginSignup
5
5

More than 3 years have passed since last update.

HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】

Last updated at Posted at 2020-10-26

1. 間違い探しゲームの概要

20×20のボタンの中から指定された文字が表示されたボタンを見つける、シンプルな間違い探しゲームです!解くのが簡単とは言っていない

以下の6つの記事で解説しています。


HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】(本記事)
HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】

1. 間違い探しゲームの概要
  1-1. ゲームの全体像
  1-2. 開発環境及びファイルの全体像
   1-2-1. 開発・動作確認済み環境
   1-2-2. 作成するファイル
    1-2-2-1. メインファイル
    1-2-2-2. サブファイル
  1-3. ソースコード
   1-3-1. メインファイルのソースコード
   1-3-2. サブファイルのソースコード
  1-4. 参考にさせて頂いた記事・サイトの一覧


HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】

2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移


HTML・PHP・MySQLだけで作る間違い探しゲーム【③問題表示ページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【③問題表示ページを作る】

3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写


HTML・PHP・MySQLだけで作る間違い探しゲーム【④DB接続設定ファイルを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【④DB接続設定ファイルを作る】

4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について


HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】

5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録


HTML・PHP・MySQLだけで作る間違い探しゲーム【⑥ランキングページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【⑥ランキングページを作る】

6. ランキング表示ページ
  6-1. 表示切り替え機能
  6-2. 入力値の保持 - タグ
  6-3. ランキングの表示
   6-3-1. テーブルの表示
   6-3-2. テーブルデザイン

まず、どのようなものなのか概要を説明します。
(サーバー構築やDB構築に関しては本記事群では扱いません)

1-1. ゲームの全体像

初期画面にアクセスすると、

  • 名前の入力欄
  • 難易度の選択ラジオボタン
  • ランキングへの登録認否の選択ラジオボタン

が表示されます。
スタート画面.png

後で解説しますが、名前入力欄には

  • 空白文字のみ無効
  • 10文字以内
  • 名前の前後に空白文字/制御文字無効

のバリデーションを入れており、引っかかった場合はエラーが表示されます。
スタート画面_エラーメッセージ.png
適切な名前を入力し、「問題に挑戦!」をクリックすると選択した難易度に応じた問題が表示されます。
問題ページ_易しい.png
問題ページ_難しい.png
正解・不正解を問わず、ボタンをクリックすると結果表示ページへ遷移します。
結果には

  • 正解か不正解か
  • 回答に要した時間
  • 難易度
  • リセット回数

が表示されます。

結果ページ.png
結果表示ページの「ランキングページへ」をクリックすると回答時間ランキング表示画面へ遷移します。
ランキングページ.png
ランキングは難易度ごとに切り替えることもできます。

1-2. 開発環境及びファイルの全体像

1-2-1. 開発・動作確認済み環境

  • PHP 7.1.29 (cli)
  • MySQL Ver 14.14 Distrib 5.6.44

1-2-2. 作成するファイル

メインとなる5つと補助的な2つの合計7つです。そこまで数が多くないのとファイル名のみ(相対パス)で参照できるようにする為、全て同一ディレクトリ内に配置します。

1-2-2-1. メインファイル

  • start.php
    • プレイヤーに名前/難易度/ランキング登録認否を選択してもらうページを生成するファイルです。
  • find_the_mistake.php
    • 問題を作成・表示するファイルです。
    • 20×20の文字が書かれたボタンを出現させ、その中に1つだけ正解を紛れ込ませます。
  • db_connect.php
    • DB接続設定用のファイルです。
    • 下記result.phpとranking.phpで使用します。
  • result.php
    • 回答が正解していたかどうかを示す結果画面です。
    • 回答時間とリセット回数をカウントする機能を実装し、その結果も示します。
  • ranking.php
    • ランキングを表示するファイルです。
    • ここでは難易度別にランキングを表示切り替えできる機能も実装します。

1-2-2-2. サブファイル

  • create_ranking_table.sql
    • ランキングデータを保存するテーブルを作成するSQL文を記述したSQLファイルです。
  • style.css
    • ランキングページのテーブルの見た目を整えるCSSファイルです。

1-3. ソースコード

GitHubのソースコードはこちらからどうぞ。

1-3-1. メインファイルのソースコード

start.php
<?php

// セッションの開始
session_start();

// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

    // 変数への代入
    $name = $_POST['name'];
    $difficulty = $_POST['difficulty'];
    $permission = $_POST['permission'];

    // 名前のバリデーション
    if (empty(trim($name))) {
        $error_msg = '名前に空白は無効です';
    } elseif (mb_strlen($name, 'UTF-8') > 10) {
        $error_msg = '名前は10字以内にしてください';
    } elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
        $error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
    }

    // セッション変数への格納と問題表示ページへの遷移
    if (empty($error_msg)) {
        $_SESSION['name'] = $name;
        $_SESSION['difficulty'] = $difficulty;
        $_SESSION['permission'] = $permission;
        header('Location:find_the_mistake.php');
        exit();
    }
}

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1>間違い探し</h1>
        <p>20×20個のボタンが表示されるので、指定されたボタンを見つけ出して押しましょう!</p>
        <?php if (isset($error_msg)): ?>
        <hr>
        <ul>
            <li><?= $error_msg; ?></li>
        </ul>
        <hr>
        <?php endif; ?>
        <form method="POST">
            <label>名前
                <small> (10字以内)</small><br>
                <input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
            </label>
            <p>
                <span>難易度</span><br>
                <label>
                    <input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
                    易しい(絵文字)
                </label>
                <label>
                    <input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
                    難しい(漢字)
                </label>
            </p>
            <p>
                <span>ランキングへの登録<small>(正解だった場合のみ)</small></span><br>
                <label>
                    <input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
                    許可しない
                </label>
                <label>
                    <input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
                    許可する
                </label>
            </p>
            <input type="submit" value="問題に挑戦!(時間計測が開始されます)">
        </form>
        <p>
            <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
        </p>
    </body>
</html>
find_the_mistake.php
<?php

// セッションの再開
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION)) {
    header('Location:start.php');
    exit();
}

// リセット回数の計測開始
if (isset($_SESSION['count'])) {
    $_SESSION['count']++;
} else {
    $_SESSION['count'] = 0;
}

// ターゲット配列の設定
if ($_SESSION['difficulty'] === '難しい(漢字)') {
    $chars = [
        ['猫', '描'],
        ['犬', '大'],
        ['幸', '辛'],
        ['白', '臼'],
        ['矢', '失'],
        ['力', '刀'],
        ['防', '妨'],
        ['土', '士'],
        ['卵', '卯'],
        ['巨', '臣'],
        ['寒', '塞'],
        ['旅', '族'],
        ['車', '東'],
        ['釘', '針']
    ];
} else {
    $chars = [
        ['&#x1f415;', '&#x1f408;'],
        ['&#x1f405;', '&#x1f406;'],
        ['&#x1f98e;', '&#x1f40d;'],
        ['&#x1f433;', '&#x1f42c;'],
        ['&#x1f339;', '&#x1f337;'],
        ['&#x1f34a;', '&#x1f34b;'],
        ['&#x1f34e;', '&#x1f351;'],
        ['&#x1f955;', '&#x1f336;'],
        ['&#x1f96f;', '&#x1f95e;'],
        ['&#x1f358;', '&#x1f359;'],
        ['&#x1f341;', '&#x1f342;'],
        ['&#x1f332;', '&#x1f333;'],
        ['&#x1f47b;', '&#x1f47d;'],
        ['&#x1f396;', '&#x1f3c5;']
    ];
}

// ターゲット配列のシャッフル
shuffle($chars);
for ($i = 0; $i < count($chars); $i++) {
    shuffle($chars[$i]);
}

// 正解と選択対象配列を設定
$correct = $chars[0][0];
$_SESSION['correct'] = $correct;
for ($j = 0; $j <= 19; $j++) {
    for ($k = 0; $k <= 19; $k++) {
        $targets[$j][$k] = $chars[0][1];
    }
}

// 正解のターゲットを選択対象配列に1つだけ挿入
$key = range(0, 19);
$key1 = array_rand($key);
$key2 = array_rand($key);
$targets[$key1][$key2] = $correct;

// 開始時刻を記録
$_SESSION['start_time'] = microtime(true);

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><?= $correct; ?>を見つけよう!</h1>
        <p>
            <small>難易度: <?= $_SESSION['difficulty']; ?></small>
            <button type="button" onclick="location.href='find_the_mistake.php'">分かるか!(リセット)</button>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
        <form action="result.php" method="POST">
            <table>
                <?php foreach ($targets as $target): ?>
                <tr>
                    <?php for ($l = 0; $l < count($target); $l++): ?>
                    <td><input type="submit" name="answer" value="<?= $target[$l]; ?>"></td>
                    <?php endfor; ?>
                </tr>
                <?php endforeach; ?>
            </table>
        </form>
    </body>
</html>
db_connect.php
<?php

// 定数定義
const PDO_DSN = 'mysql:host=localhost;dbname=[FILTERED];charset=utf8mb4';
const USERNAME = '[FILTERED]';
const PASSWORD = '[FILTERED]';

// DB接続
try {
    $dbh = new PDO(PDO_DSN, USERNAME, PASSWORD, [
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    exit('DB接続に失敗しました' . PHP_EOL . $e->getMessage() . PHP_EOL);
}
result.php
<?php

// セッションの開始
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
    header('Location:start.php');
    exit();
}

// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', $end_time - $start_time) . '秒';

// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
    $time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
    $count = '100回以上';
}

// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
    $result = '正解です!';
} else {
    $result = '不正解です。。。';
}

// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です!' && $permission === '許可する') {

    // db接続
    require_once('db_connect.php');

    // 新規登録処理
    $sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$name, $difficulty, $time, $count]);
}

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><small>結果は...<?= $result ?></small></h1>
        <h2><small>難易度: <?= $difficulty; ?></small></h2>
        <h2><small>回答時間: <?= $time; ?></small></h2>
        <h2><small>リセット回数: <?= $count; ?></small></h2>
        <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
    </body>
</html>
ranking.php
<?php

// 難易度がPOSTされている場合は変数に格納
if (isset($_POST['show_method'])) {
    $select = $_POST['show_method'];
} else {
    $select = 'all';
}

// db接続
require_once('db_connect.php');

// 選択された表示形式によってSQL文を分岐
$sql = 'SELECT * FROM rankings ';
switch ($select) {
    case 'difficult':
        $sql .= "WHERE difficulty = '難しい(漢字)' ORDER BY time LIMIT 10";
        break;
    case 'easy':
        $sql .= "WHERE difficulty = '易しい(絵文字)' ORDER BY time LIMIT 10";
        break;
    case 'all':
        $sql .= 'ORDER BY time LIMIT 10';
        break;
}
$stmt = $dbh->query($sql);
$players = $stmt->fetchAll();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <title>間違い探し ランキング</title>
    </head>
    <body>
        <h1>回答時間ランキング</h1>
        <form method="POST">
            <select name="show_method" size="1">
                <option value="all" <?php if ($select === 'all') echo 'selected' ?>>全て</option>
                <option value="easy" <?php if ($select === 'easy') echo 'selected' ?>>易しい</option>
                <option value="difficult" <?php if ($select === 'difficult') echo 'selected' ?>>難しい</option>
            </select>
            <input type="submit" value="表示">
        </form>
        <div class="table">
            <table class="s-tbl">
                <thead>
                    <tr>
                        <th>順位</th>
                        <th>名前</th>
                        <th>難易度</th>
                        <th>回答時間</th>
                        <th>リセット回数</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($players as $key => $player): ?>
                    <tr>
                        <td><?= ++$key; ?></td>
                        <td><?= $player['name']; ?></td>
                        <td><?= $player['difficulty']; ?></td>
                        <td><?= $player['time']; ?></td>
                        <td><?= $player['reset']; ?></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <p>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
    </body>
</html>

1-3-2. サブファイルのソースコード

create_rankings_table.sql
CREATE TABLE `rankings` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `difficulty` varchar(10) NOT NULL,
  `time` varchar(10) NOT NULL,
  `reset` varchar(15) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
style.css
.table {
  margin-top: 10px;
}
.s-tbl {
  border-collapse: collapse;
}
.s-tbl th, .s-tbl td {
  border: 1px solid #000;
  padding: 0.5em;
}
.s-tbl tr:nth-child(even) {
  background: #eee;
}
.s-tbl tr:hover {
  background: #ffffe0;
}

1-4. 参考にさせて頂いた記事・サイトの一覧

もちろん、公式マニュアル! RTFM!
PHPマニュアル「PHP: PHP マニュアル - Manual

PDOによるDB接続に関して体系的に書かれており非常に勉強になりました
Qiita「PHPでデータベースに接続するときのまとめ - Qiita」 by @mpyw さん

クラスの基礎から例外処理までとても丁寧に解説されておりとても参考になりました
Qiita「【PHP超入門】クラス~例外処理~PDOの基礎 - Qiita」 by @7968 さん

エミュレータのON/OFFに関する疑問が解決されました
Teratail「`PDO::ATTR_EMULATE_PREPARES => false`は必要か?

フロントに関する事なら外せないドキュメント
MDN web docs「開発者向けのウェブ技術 | MDN

ずっと疑問に思っていたことが解消されました
Teratail「なぜsession_startより前に何も出力があってはいけない?

正規表現について新たな知見を得られました
Qiita「【PHP】マルチバイト(全角スペース等)対応のtrim処理 - Qiita」 by @fallout さん

ReDoSに関してとても勉強になりました
Qiita「正規表現の落とし穴(ReDoS - Regular Expressions DoS) - Qiita」 by @prograti さん

絵文字のHTMLエンティティを調べるのに活用させて頂きました
Let's EMOJI「Unicode 13.0 絵文字 (Unicode 13.0 Emoji) | Let's EMOJI

sprintfに関してとても詳細に解説されています
Let'sプログラミング ~初心者の方を対象としたプログラミングの総合学習サイト~
指定の形式にフォーマット(sprintf) - 文字列関数 - PHP関数

文字コードと照合順序に関しとても勉強になりました
Qiita「寿司ビール問題① 初心者→中級者へのSTEP20/25 - Qiita」 by @kamohicokamo さん

この記事のおかげで命名の迷いがなくなりました
Qiita「データベースオブジェクトの命名規約 - Qiita」 by @genzouw さん

テーブルのデザインの参考にさせていただきました
いつか誰かの役に立つかもしれないweb制作屋の備忘録
css tableで背景色を交互に変える方法

どの記事・ページもとても参考になりました!
この場を借りて感謝を申し上げます🙇


次の記事 >> 【②スタートページを作る】
2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5