素数判定関数を使って簡単なゲームを作った話④

ご覧いただきありがとうございます。
この内容は、複数に分けて投稿しています。全体をご覧になりたい方は、以下のリンク先よりご確認ください。

https://qiita.com/hitoki_univ/items/8e3feab0add17befc700

前回の内容については、こちらからどうぞ。

https://qiita.com/hitoki_univ/items/96e45db86abc4dae71d7


# 今回やること
第3回までで、ゲームをプレイする人数の選択、プレイヤー名と制限時間の設定に関する部分を実装しました。
今回は満を持して、ゲームの動作部分を実装していきます。

# ゲームの実行画面
前回作成したプレイヤー名などの入力画面と同様、ゲームのプレイ人数に応じて異なるページを作成する必要があります。しかし、基本構造はどのページも共通ですので、今回は2人用のページを主に確認していきます。

### HTML
``` php
<?php
session_start();
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $_SESSION["player1"] = htmlspecialchars($_POST["player1"], ENT_QUOTES, 'UTF-8');
    $_SESSION["player2"] = htmlspecialchars($_POST["player2"], ENT_QUOTES, 'UTF-8');
    $_SESSION["timer"] = htmlspecialchars($_POST["timer"], ENT_QUOTES, 'UTF-8');
}
?>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="prime_game.css">
    <title>素数ゲーム</title>
  </head>
  <body>
    <script language="JavaScript"><!--

        let lastPrime = 0; // 直前の素数を保存する変数
        let players = ["<?php echo $_SESSION['player1']; ?>", "<?php echo $_SESSION['player2']; ?>"];
        let currentPlayer = players[0];    // 最初のプレイヤー

        // 素因数分解関数(2*2*2*3*5)
        // 素因数分解関数
        function primeFactorization(num) {
            // 素因数を格納する配列を初期化
            let factors = [];
            // 2からnumまでの数でループ
            for(let i = 2; i <= num; i++) {
                // numがiで割り切れる間、ループ
                while(num % i === 0) {
                    // iを素因数として配列に追加
                    factors.push(i);
                    // numをiで割る
                    num /= i;
                }
            }
            // 素因数の配列を'×'で連結して文字列として返す
            return factors.join('×');
        }

        // 素数判定関数
        function getPrimeStatus (num) {
            // 1以下の数は素数ではない
            if(num <= 1) {
                return `${num}は1以下です。`;
            } else if(num == 2){
                // 2は素数
                return true;
            }else{
                // 2以上の数で割り切れるかチェック
                for(let i = 2; i < num; i++){
                    if(num % i === 0){
                        // 割り切れる場合は素数ではない
                        return `${num}は${primeFactorization(num)}で表すことができます。`;
                    }
                    if(i+1 == num){
                        // 割り切れない場合は素数
                        return true;
                    }
                }
            }
        }

        // 素数判定とタイマー制御関数
        function PrimeGame(event){
            event.preventDefault(); // フォームの送信をキャンセル

            const numInput = document.getElementsByName("sosuu")[0].value;
            const num = parseInt(numInput, 10);

            const result = getPrimeStatus(num);

            // 入力値が数値でない場合のエラーハンドリング
            if(isNaN(num)) {
                alert("有効な数字を入力してください。");
                document.getElementsByName("sosuu")[0].value = ""; // 入力欄をクリア
            } else if(num <= lastPrime) {
                alert("直前に入力した素数より大きな素数を入力してください。");
                document.getElementsByName("sosuu")[0].value = ""; // 入力欄をクリア
            } else if(result === true){
                // 素数の場合はタイマーをリセットし、最後に入力した素数を表示
                alert(`${num}は素数です。`);
                clearInterval(timerId); // タイマーをクリア
                Timer(); // タイマーを再スタート
                document.getElementById("lastNum").innerText = `直前に入力した素数: ${num}`;
                lastPrime = num; // 直前の素数を更新
                
                // プレイヤーを切り替え
                switch(currentPlayer) {
                    case players[0]:
                        currentPlayer = players[1];
                        lastPlayer = players[0];
                        break;
                    case players[1]:
                        currentPlayer = players[0];
                        lastPlayer = players[1];
                        break;
                }
                displayCurrentPlayer(); // 現在のプレイヤーを表示
                document.getElementsByName("sosuu")[0].value = ""; // 入力欄をクリア
            } else if(typeof result === 'string'){
                // 素数でない場合はエラーメッセージを表示
                alert(result);
                // 素数でない場合はTimer()を呼び出さない
                document.getElementsByName("sosuu")[0].value = ""; // 入力欄をクリア
            }
        }

        function displayCurrentPlayer() {
            document.getElementById("currentPlayer").innerText = `現在のプレイヤー: ${currentPlayer}`;
        }

        let timerId;

        // タイマー制御関数
        function Timer() {
            let seconds = <?php echo $_SESSION['timer']; ?>; // セッションからタイマーの初期値を取得

            // タイマーを更新
            timerId = setInterval(function () {
                if (seconds <= 0) {
                    // 残り時間が0になったらタイマーを停止し、送信ボタンを無効化
                    clearInterval(timerId);
                    document.getElementById("submitBtn").disabled = true;
                    document.getElementById("submitBtn").style.backgroundColor = "red";

                    // データベースに記録を送信
                    var xhr = new XMLHttpRequest();
                    xhr.open("POST", "record.php", true);
                    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                    xhr.send("biggest_prime_num=" + lastPrime + "&player_name=" + lastPlayer);

                    document.getElementById("rankingBtn").style.display = "block"; // ランキングボタンを表示
                } else {
                    // 残り時間を表示
                    document.getElementById("timer").innerText = `残り時間: ${seconds}秒`;
                }
                seconds--; // 残り時間を1秒減らす
            }, 1000); // 1秒ごとにタイマーを更新
        }

        // ページ読み込み時にタイマーを開始
        window.onload = function () {
            Timer();
            displayCurrentPlayer(); // 現在のプレイヤーを表示
            document.getElementById("rankingBtn").style.display = "none"; // ランキングボタンを非表示
        };
        
    // --></script>
    <h1><a href="title.php">素数ゲーム</a></h1>
    <h3>前の人より大きな素数を入力しよう！</h3>
    <div class="input_field">
        <form>
            <input type="text" name="sosuu", autocomplete="off"><br>
            <p id="currentPlayer"></p> 
            <p id="lastNum"></p>
            <p id="timer"></p>
            <input type="submit" id="submitBtn" value="これは素数ですか？" onclick="PrimeGame(event)">
            <br>
        </form>

        <button id="rankingBtn" onclick="location.href='ranking.php'">ランキング表示画面へ</button>
    </div>
  </body>
</html>
```

まず、プログラム最上部のPHPによる記述で、前回作成した画面からのプレイヤー名とタイマーの設定時間をPOST方式で取得しています。

:::note warn
``` php
$_SESSION["player1"] = $_POST["player1"];
```
のように値を直接受け取ってしまうと、XSSの脆弱性になります。htmlspecialchars等を使ってエスケープ処理を行いましょう。
(ご指摘いただいた @rana_kualu 様、ありがとうございました。)
:::

script部では、ゲームの機能を構成する関数をいくつか作成しています。

- getPrimeStatus()
入力された数値の素数判定を行う関数です。素数判定部分についてはよくあるものですが、数値が素数の場合はtrue、素数ではなかった場合は数値に合わせてコメントを返却するようになっています。
素数でなかった場合に、その数値がどのような掛け算で表現できるのかを表示したかったので、素因数分解を行う関数を作成して、表示するようにしています。


- primeFactorization()
これは、素因数分解を行う関数です。入力された数値が素数ではなかった場合に、どのような掛け算で表現することができるのかを表示したかったので、この関数を作成しました。
素因数を配列に格納していき、最後に配列に入っている数値を'×'で連結して文字列として返却する、という動作をします。
ちなみに、この関数は、例えば24が入力されたときは"2×2×2×3"という形で表示するのですが、これを"2^3*3"といった風に乗数を使って表現する関数も作成しています。表示の都合上、乗数は使わないほうが良かったので今回は採用しませんでしたが、作成自体はしていたので公開します。

``` javascript
//素因数分解関数(2^3*3)
function primeFactorization(num) {
    let factors = [];
    let count = {};
    for(let i = 2; i <= num; i++) {
        while(num % i === 0) {
            factors.push(i);
            num /= i;
        }
    }
    factors.forEach(function(i) { count[i] = (count[i]||0) + 1;});
    let result = [];
    for(let prop in count){
        if(count[prop] === 1){
            result.push(prop);
        }else{
            result.push(prop + '^' + count[prop]);
        }
    }
    return result.join('×');
}
```

- PrimeGame()
この関数は、ゲームのベースとなる機能を構成しています。
まずは、入力された数値や、その数値の素数判定結果を受け取ります。
その後、入力値が数値ではない場合、前回の入力より大きな素数を入力するというゲームのルールに違反した入力だった場合のエラーハンドリングを作成しています。
入力が素数だった場合は、次のプレイヤーに操作を移し、タイマーや数値入力欄等をリセットします。素数ではなかった場合はエラーメッセージを表示します。プレイヤーの変更やタイマーのリセット等は行いません。

- displayCurrentPlayer()
現在のプレイヤーを表示するための関数です。

- Timer関数
タイマーを制御する関数です。
まず、セッションからタイマーの初期値(前回作成したページで入力したもの)を取得し、1秒ごとにタイマーを更新します。
素数が入力された場合は前述の関数によってタイマーが初期値にリセットされますが、素数が入力されないとタイマーの秒数が減っていきます。最終的にタイマーが0秒になった場合には、
1.素数判定を行うボタンを無効化し、ボタンの色を青→赤に変更
2.データベースに今回のゲームで記録された最大の素数とその入力者を送信
3.ランキング表示画面へ遷移するためのボタンを表示
します。

- function()
これは無名関数として定義しており、ページの読込み時に実行されます。この関数は、ページの読込み時に
1.タイマーの開始
2.プレイヤー表示の開始
3.ランキングページに遷移するためのボタンの非表示
を行っています。

<br>
プログラム最下部は、HTMLで、素数の入力欄や各種ボタンを定義しています。素数入力などに関する部分はformタグで、ランキングページへ遷移するためのボタンはbuttonタグで作成しています。

### PHP
データを送信する部分には、"record.php"というファイルを使っています。
その中身は以下の通りです。
``` php
<?php
// MySQLに接続する
$mysqli = new mysqli("localhost", "root", "", "prime_ranking");

// POSTから最大素数とプレイヤー名を取得する
$biggest_prime_num = $_POST['biggest_prime_num'];
$player_name = $_POST['player_name'];

// 現在の日付と時間を取得する
$date = date('Y-m-d H:i:s');

// データベースに新しいレコードを挿入するための準備をする
$stmt = $mysqli->prepare("INSERT INTO ranking (date, biggest_prime_num, player_name) VALUES (?, ?, ?)");

// パラメータをバインドする
$stmt->bind_param("sss", $date, $biggest_prime_num, $player_name);

// クエリを実行する
$stmt->execute();
?>
```

このプログラムでは、まずMySQLに接続し、ゲーム実行ページから最大の素数(biggest_prime_num)とプレイヤー名(player_name)をPOST方式で取得します。
また、保存用に時刻も取得しています。

データベースに新しいレコードを挿入するには、INSERT文を使います。

↓INSERT文の使い方
 ``` 
 INSERT INTO テーブル名 (列名1, 列名2,...) VALUES (値1, 値2,...);
 ```

 今回は、'ranking'というテーブルに、date, biggest_prime_num, player_nameの3つの列を保存するので、そのようなSQL文の雛形を作成します。
 その後、ゲームをプレイするページからから受け取った素数やプレイヤー名、先程取得した時刻を、作成したSQL文の雛形にバインドします。

::: note warn
バインドを行わず、送られてきたデータをそのままSQL文に代入してしまうと、SQLインジェクションなどの脆弱性になります。
バインドを行うことで、値は自動的にエスケープ（特殊文字を無効化）され、安全な形式に変換されるので、データベースとの安全なやり取りを確立することができます。
:::

最後に、作成したSQL文を実行することで、データベースに新しくレコードを送信しています。



### CSS
``` css
@charset 'UTF-8';

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f0f0f0;
}

h1 {
    text-align: center;
    padding: 20px;
    background-color: #007BFF;
    color: white;
}

h3 {
    text-align: center;
    padding: 10px;
}

p {
    text-align: center;
    font-size: 1.2em;
}

h1 a {
    color: white;
    text-decoration: none;
}


.input_field {
    max-width: 500px;
    margin: 0 auto;
    padding: 20px;
    background-color: white;
    box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
}

label {
    display: block;
    margin-bottom: 5px;
}

input[type="text"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 5px;
    border: 1px solid #ccc;
}

input[type="number"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 5px;
    border: 1px solid #ccc;
}

input[type="submit"], button {
    display: block;
    width: 100%;
    padding: 10px;
    border: none;
    border-radius: 5px;
    background-color: #007BFF;
    color: white;
    cursor: pointer;
}
```

CSSについては、前回作成したページなどと雰囲気が変わらないようにだけ気をつけています。
以前に投稿したものと共通する部分も多いかと思います。

# 表示結果
上記の文書をブラウザで読み込むと、このような表示になります。

![player_game_2 最初の画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/cde412a9-e2d9-a59b-657c-c3f92c4fba97.png)

数値を入力すると、その内容に応じてポップアップを表示します。

- 素数が入力された場合
![player_game_2 入力画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/200e6a2c-2be1-3f7c-e41a-9bbfe5758d9b.png)

- 素数ではない数値が入力された場合
![player_game_2 素因数分解.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/5a302460-9039-e635-b5c9-95e6b072f4a9.png)

- 無効な文字が入力された場合
![player_game_2 無効な数値の入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/7bae202c-2576-5db4-55af-a338af939c44.png)

- 前回の入力より小さい数値を入力した場合
![player_game_2 小さい素数を入力した場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/b07839fd-9523-0605-092a-ab151d7fef82.png)

制限時間をオーバーした場合、素数判定を行うボタンの無効化、色の変更を行った上で、ランキング表示画面に遷移するためのボタンを表示します。
![player_game_2 時間オーバー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684916/025b69ff-d089-6f37-20ae-b37b355ecce3.png)


# 次回の投稿について
次回は、ランキング表示機能を実装します。
~~投稿次第、リンクを追加するので、是非ご覧ください。~~
以下のリンクからぜひご覧ください。

https://qiita.com/hitoki_univ/items/48277ba380cf46ac45fe


このプロジェクト全体の内容をご覧になりたい方は、以下リンク先のまとめページよりご確認ください。

https://qiita.com/hitoki_univ/items/8e3feab0add17befc700