CSS javascript PHP ゲーム

JavaScriptでタイピング練習ゲームを作成しよう!その2 オンラインランキングの実装

概要

この記事では、初心者でも簡単に作成できるタイピング練習ゲームのプログラムを紹介します。このゲームは、オンラインランキング機能を備えており、サーバーにデータを保存し、プレイヤーのスコアを競うことができます。特に、SEO対策を考慮し、検索エンジンでの上位表示を狙った構成になっています。

下記サイトからゲームのサンプルを実行できます。

https://chemtoollab.com/game/typev2/index.html

使用例

タイピング練習ゲームは、プログラミング初心者から中級者まで幅広く利用できます。特に、タイピングスキルを向上させたい人や、オンラインでスコアを競いたい人に適しています。ゲームはブラウザ上で動作し、複雑なインストール作業は不要です。

必要なプログラム

このタイピング練習ゲームには、以下のプログラムとファイルが必要です。

  1. index.html: ゲームのHTMLファイル
  2. style.css: ゲームのスタイルシート
  3. script.js: ゲームのJavaScriptファイル
  4. word.json: ゲームで使用する単語リスト
  5. save_ranking.php: ランキングを保存するPHPスクリプト
  6. get_rankings.php: ランキングを取得するPHPスクリプト
  7. get_lowest_ranking.php: 10位のスコアを取得するPHPスクリプト
  8. create_db.php: SQLiteデータベースの作成スクリプト
  9. reset_db.php: データベースをリセットするスクリプト
  10. clear_rankings.php: ランキングをクリアするスクリプト
  11. alter_db.php: 既存のデータベースに列を追加するスクリプト

導入方法

  1. サーバーの準備: まず、PHPが動作するWebサーバーを準備します。多くのレンタルサーバーやVPSが利用可能です。ここでは、Conohaサーバーを例に説明します。
  2. ファイルのアップロード: 上記のファイルをすべてサーバーの指定フォルダにアップロードします。FTPクライアント(例:FileZilla)を使用すると便利です。
  3. データベースの設定:
    • create_db.php を実行して、データベースと必要なテーブルを作成します。
    • 既存のデータベースに date 列を追加するために alter_db.php を実行します。
    • reset_db.php を使用して、データベースをリセットできます。
    • clear_rankings.php を使用して、ランキングデータをクリアできます。

使用手順

  1. ゲームの開始:
    • ブラウザで index.html を開き、「ゲームを開始」ボタンをクリックします。
    • タイピング練習ゲームが開始され、指定された単語をタイピングします。
  2. オンラインランキングの表示:
    • 「ランキング表示」ボタンをクリックして、現在のランキングを確認できます。
  3. ゲーム終了後:
    • ゲームが終了すると、スコアと時間が表示されます。
    • ランクインした場合、名前入力画面が表示され、名前を入力してランキングに登録します。
  4. データベースの管理:
    • 必要に応じて reset_db.php を実行してデータベースをリセットします。
      https://ドメイン/reset_db.phpを開くと実行されます。
    • ランキングデータをクリアする場合は clear_rankings.php を実行します。
      https://ドメイン/clear_rankings.phpを開くと実行されます。
    • 必要に応じて alter_db.php を実行してデータベースを更新します。

注意点

  1. ファイルの配置:
    • すべてのファイルは同じディレクトリに配置してください。特にPHPスクリプトとデータベースファイルは同じフォルダ内に置く必要があります。
  2. パーミッション設定:
    • アップロードしたPHPファイルとデータベースファイルに適切なパーミッションを設定してください。一般的に、PHPファイルは644、データベースファイルは666が推奨されます。
  3. セキュリティ:
    • データベースへの不正アクセスを防ぐため、適切なセキュリティ設定を行ってください。特に、データベースファイルにはアクセス制限をかけることをお勧めします。

プログラム

以下のファイルをメモ帳などに丸々コピーして、それぞれファイル名を指定してください。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>タイピング練習ゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>タイピング練習ゲーム</h1>
        <div id="title-screen">
            <p>ゲームを始める前に、入力モードが半角英数字になっていることを確認してください。</p>
            <button id="start-button">ゲームを開始</button>
            <button id="show-ranking-button">ランキング表示</button>
        </div>
        <div id="game-screen" style="display: none;">
            <div id="word-container">
                <div id="japanese-word"></div>
                <div id="romaji-word"></div>
            </div>
            <input type="text" id="input-field" autofocus>
            <div id="score">スコア: 0</div>
            <div id="progress">1/10</div>
            <div id="timer">タイマー: 0 秒</div>
            <button id="restart-button" style="display: none;">再スタート</button>
        </div>
        <div id="ranking-screen" style="display: none;">
            <h2>ランキング</h2>
            <ol id="ranking-list"></ol>
            <button id="back-button">戻る</button>
        </div>
        <div id="name-input-screen" style="display: none;">
            <h2>ランキング入り!</h2>
            <p>名前を入力してください:</p>
            <input type="text" id="name-input-field">
            <button id="submit-name-button">送信</button>
            <div id="name-input-score">スコア: 0</div>
            <div id="name-input-timer">タイマー: 0 秒</div>
            <div id="name-input-date">日時: </div>
            <div id="name-input-rank"></div>
            <p>ランクが反映されるまでには1分程度かかります。</p>
        </div>
        <div id="game-over-screen" style="display: none;">
            <h2>ゲーム終了</h2>
            <div id="game-over-score">スコア: 0</div>
            <div id="game-over-timer">タイマー: 0 秒</div>
            <button id="title-button">タイトルに戻る</button>
            <button id="new-game-button">ゲームを新しく始める</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

style.css

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

.container {
    width: 80%;
    max-width: 600px;
    margin: 50px auto;
    padding: 20px;
    background-color: #ffffff;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1, h2 {
    margin-bottom: 20px;
}

#word-container {
    margin: 20px 0;
}

#japanese-word {
    font-size: 24px;
    margin-bottom: 10px;
}

#romaji-word {
    font-size: 20px;
    color: #555;
}

#input-field, #name-input-field {
    width: calc(100% - 20px); /* 右側の余白を調整 */
    padding: 10px;
    font-size: 18px;
    box-sizing: border-box; /* 幅計算を含める */
}

#score, #progress, #timer {
    margin: 10px 0;
}

#restart-button, #start-button, #show-ranking-button, #back-button, #submit-name-button, #title-button, #new-game-button {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
    margin: 10px;
}

ol {
    text-align: left;
    padding-left: 40px;
}

#game-over-screen {
    display: none;
}

script.js

let words = [];
let currentWordIndex = 0;
let score = 0;
let timer = 0;
let timerInterval;
const totalRounds = 10;
let rankings = [];

const japaneseWordElement = document.getElementById('japanese-word');
const romajiWordElement = document.getElementById('romaji-word');
const inputField = document.getElementById('input-field');
const scoreElement = document.getElementById('score');
const progressElement = document.getElementById('progress');
const timerElement = document.getElementById('timer');
const restartButton = document.getElementById('restart-button');
const startButton = document.getElementById('start-button');
const showRankingButton = document.getElementById('show-ranking-button');
const titleScreen = document.getElementById('title-screen');
const gameScreen = document.getElementById('game-screen');
const rankingScreen = document.getElementById('ranking-screen');
const rankingList = document.getElementById('ranking-list');
const backButton = document.getElementById('back-button');
const nameInputScreen = document.getElementById('name-input-screen');
const nameInputField = document.getElementById('name-input-field');
const submitNameButton = document.getElementById('submit-name-button');
const gameOverScreen = document.getElementById('game-over-screen');
const titleButton = document.getElementById('title-button');
const newGameButton = document.getElementById('new-game-button');
const gameOverScoreElement = document.getElementById('game-over-score');
const gameOverTimerElement = document.getElementById('game-over-timer');
const nameInputScoreElement = document.getElementById('name-input-score');
const nameInputTimerElement = document.getElementById('name-input-timer');
const nameInputDateElement = document.getElementById('name-input-date');
const nameInputRankElement = document.getElementById('name-input-rank');

let newEntry = null;

async function loadWords() {
    const response = await fetch('word.json');
    const data = await response.json();
    if (data.words.length < totalRounds) {
        alert('ファイル内の単語が不足しています。');
        return;
    }
    words = data.words.sort(() => 0.5 - Math.random()).slice(0, totalRounds);
    startGame();
}

async function getRankings() {
    const response = await fetch('get_rankings.php');
    const data = await response.json();

    // Debugging output
    console.log('Fetched data:', data);

    // Check if data is an array
    if (Array.isArray(data)) {
        rankings = data;
    } else {
        console.error('Invalid data format', data);
        throw new Error('Invalid data format');
    }
}

function startGame() {
    titleScreen.style.display = 'none';
    rankingScreen.style.display = 'none';
    nameInputScreen.style.display = 'none';
    gameOverScreen.style.display = 'none';
    gameScreen.style.display = 'block';
    currentWordIndex = 0;
    score = 0;
    timer = 0;
    inputField.value = '';
    inputField.disabled = false;
    restartButton.style.display = 'none';
    resetSubmitButton();
    updateScore();
    updateProgress();
    updateTimer();
    startTimer();
    nextWord();
}

function resetSubmitButton() {
    submitNameButton.disabled = false;
    submitNameButton.style.backgroundColor = '';
}

function startTimer() {
    timerInterval = setInterval(() => {
        timer++;
        updateTimer();
    }, 1000);
}

function stopTimer() {
    clearInterval(timerInterval);
}

function updateScore() {
    scoreElement.textContent = `スコア: ${score}`;
}

function updateProgress() {
    progressElement.textContent = `${Math.min(currentWordIndex + 1, totalRounds)}/${totalRounds}`;
}

function updateTimer() {
    timerElement.textContent = `タイマー: ${timer} 秒`;
}

function nextWord() {
    if (currentWordIndex >= totalRounds) {
        endGame();
        return;
    }
    const wordPair = words[currentWordIndex];
    japaneseWordElement.textContent = wordPair.japanese;
    romajiWordElement.textContent = wordPair.romaji;
    inputField.value = '';
    inputField.focus();
}

function checkInput() {
    const userInput = inputField.value.trim();
    const currentWord = words[currentWordIndex].romaji;

    if (currentWord.startsWith(userInput)) {
        if (userInput === currentWord) {
            score += 10;
            currentWordIndex++;
            updateScore();
            if (currentWordIndex < totalRounds) {
                updateProgress();
                nextWord();
            } else {
                endGame();
            }
        } else {
            romajiWordElement.textContent = currentWord.slice(userInput.length);
        }
    } else {
        score -= 5;
        updateScore();
        inputField.value = userInput.slice(0, -1);
    }
}

function endGame() {
    stopTimer();
    updateProgress();
    japaneseWordElement.textContent = 'ゲーム終了';
    romajiWordElement.textContent = '';
    inputField.disabled = true;
    restartButton.style.display = 'block';
    gameOverScoreElement.textContent = `スコア: ${score}`;
    gameOverTimerElement.textContent = `タイマー: ${timer} 秒`;
    checkRanking();
}

function checkRanking() {
    newEntry = { score: score, time: timer, date: new Date().toLocaleString() };
    let rank = -1;

    if (rankings.length < 10) {
        rank = rankings.length + 1;
    } else {
        for (let i = 0; i < rankings.length; i++) {
            if (score > rankings[i].score || (score === rankings[i].score && timer < rankings[i].time)) {
                rank = i + 1;
                break;
            }
        }
    }

    if (rank !== -1 && rank <= 10) {
        nameInputScoreElement.textContent = `スコア: ${score}`;
        nameInputTimerElement.textContent = `タイマー: ${timer} 秒`;
        nameInputDateElement.textContent = `日時: ${newEntry.date}`;
        nameInputRankElement.textContent = `${rank}位にランクイン!`;
        nameInputScreen.style.display = 'block';
        gameScreen.style.display = 'none';
    } else {
        gameOverScreen.style.display = 'block';
        gameScreen.style.display = 'none';
    }
}

submitNameButton.addEventListener('click', async () => {
    submitNameButton.disabled = true;
    submitNameButton.style.backgroundColor = 'grey';
    newEntry.name = nameInputField.value;
    const response = await fetch('save_ranking.php', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(newEntry)
    });

    const result = await response.json();
    if (result.status === 'success') {
        nameInputScreen.style.display = 'none';
        showRanking();
    } else {
        alert('ランキングの保存に失敗しました。');
    }
});

startButton.addEventListener('click', async () => {
    try {
        await getRankings();
        loadWords();
    } catch (error) {
        console.error('Error during game start:', error);
        alert('ゲームの開始に失敗しました。');
    }
});

restartButton.addEventListener('click', loadWords);
showRankingButton.addEventListener('click', showRanking);
backButton.addEventListener('click', () => {
    rankingScreen.style.display = 'none';
    titleScreen.style.display = 'block';
});

titleButton.addEventListener('click', () => {
    gameOverScreen.style.display = 'none';
    titleScreen.style.display = 'block';
});

newGameButton.addEventListener('click', loadWords);

inputField.addEventListener('input', checkInput);

async function showRanking() {
    try {
        const response = await fetch('get_rankings.php');
        const data = await response.json();

        // Debugging output
        console.log('Fetched data:', data);

        // Check if data is an array
        if (Array.isArray(data)) {
            rankings = data;
        } else {
            console.error('Invalid data format', data);
            throw new Error('Invalid data format');
        }

        rankingList.innerHTML = '';
        rankings.forEach((entry, index) => {
            const listItem = document.createElement('li');
            listItem.textContent = `${index + 1}位. ${entry.name} - ${entry.score}点 (${entry.time}秒) ${entry.date}`;
            rankingList.appendChild(listItem);
        });
        rankingScreen.style.display = 'block';
        titleScreen.style.display = 'none';
    } catch (error) {
        console.error('Error fetching rankings:', error);
        alert('ランキングの取得に失敗しました。');
    }
}

word.json

以下はサンプルのword.jsonです。10個以上のペアを用意してください。

{
"words": [
{"japanese": "りんご", "romaji": "ringo"},
{"japanese": "ばなな", "romaji": "banana"},
{"japanese": "さくらんぼ", "romaji": "sakuranbo"},
{"japanese": "なし", "romaji": "nashi"},
{"japanese": "ぶどう", "romaji": "budou"},
{"japanese": "みかん", "romaji": "mikan"},
{"japanese": "いちご", "romaji": "ichigo"},
{"japanese": "すいか", "romaji": "suika"},
{"japanese": "もも", "romaji": "momo"},
{"japanese": "めろん", "romaji": "meron"}
]
}

save_ranking.php

<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $input = json_decode(file_get_contents('php://input'), true);
    $name = $input['name'];
    $score = $input['score'];
    $time = $input['time'];
    $date = $input['date'];

    try {
        $db = new PDO('sqlite:rankings.db');
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        $stmt = $db->prepare("INSERT INTO rankings (name, score, time, date) VALUES (:name, :score, :time, :date)");
        $stmt->bindParam(':name', $name);
        $stmt->bindParam(':score', $score);
        $stmt->bindParam(':time', $time);
        $stmt->bindParam(':date', $date);
        $stmt->execute();

        echo json_encode(['status' => 'success']);
    } catch (PDOException $e) {
        echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
    }
}
?>

get_rankings.php

<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');

try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $stmt = $db->query("SELECT name, score, time, date FROM rankings ORDER BY score DESC, time ASC LIMIT 10");
    $rankings = $stmt->fetchAll(PDO::FETCH_ASSOC);

    echo json_encode($rankings);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

get_lowest_ranking.php

<?php
try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $stmt = $db->query("SELECT score, time FROM rankings ORDER BY score DESC, time ASC LIMIT 1 OFFSET 9");
    $lowestRanking = $stmt->fetch(PDO::FETCH_ASSOC);

    echo json_encode($lowestRanking);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

reset_db.php

<?php
try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // ランキングテーブルを削除して再作成
    $db->exec("DROP TABLE IF EXISTS rankings");
    $db->exec("CREATE TABLE IF NOT EXISTS rankings (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        score INTEGER NOT NULL,
        time INTEGER NOT NULL,
        date TEXT NOT NULL
    )");

    echo "Database reset successfully.";
} catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
?>

clear_rankings.php

<?php
try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // ランキングテーブルのデータを削除
    $db->exec("DELETE FROM rankings");

    echo "Rankings cleared successfully.";
} catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
?>

create_db.php

<?php
try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Create the rankings table with the date column
    $db->exec("CREATE TABLE IF NOT EXISTS rankings (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        score INTEGER NOT NULL,
        time INTEGER NOT NULL,
        date TEXT DEFAULT CURRENT_TIMESTAMP
    )");

    echo "Database and table created successfully.";
} catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
?>

alter_db.php

<?php
try {
    $db = new PDO('sqlite:rankings.db');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Add the date column to the rankings table
    $db->exec("ALTER TABLE rankings ADD COLUMN date TEXT DEFAULT CURRENT_TIMESTAMP");

    echo "Column added successfully.";
} catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
?>

なお、まとまったファイルを用意しましたので、よろしければダウンロードして使ってください!

まとめ

この記事では、初心者でも簡単に作成できるタイピング練習ゲームのプログラムとオンラインランキング機能の実装方法を紹介しました。このゲームを通じて、タイピングスキルの向上とプログラミングの楽しさを実感していただければ幸いです。サーバー設定やセキュリティにも気を配り、安心して利用できるゲームを提供しましょう。

-CSS, javascript, PHP, ゲーム