タイピングゲーム

アプリ概要

テーマに関連する英単語を10問タイピング

プロンプト

## 依頼
入力されたテーマに基づく英単語のリストを出力してください。

## 役割
あなたは英単語に詳しい先生です。

## ルール
- 英単語は**カンマ区切り**で出力してください。
- 英単語は**100個以上**出力してください。
- 空白や記号が含まれる単語は除いてください。
- 英単語は重複させないでください。
- 不快な単語は出力しないでください。

入力ソース

<style>
#title {
    font-size: 2rem;
    padding-bottom: 10px;
}

#inputArea {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100svh;
    text-align: center;
}

#theme {
    font-size: 1.2em;
    padding: 10px;
    width: 300px;
    margin-bottom: 20px;
    border: 2px solid #ccc;
    border-radius: 5px;
    outline: none;
    transition: border-color 0.3s;
}

#theme:focus {
    border-color: #007BFF;
}

#error {
    color: red;
}

#startButton {
    font-size: 1.2em;
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    background-color: #007BFF;
    color: #fff;
    cursor: pointer;
    transition: background-color 0.3s;
}

#startButton:hover {
    background-color: #0056b3;
}

#startButton:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
}

#gameArea {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100svh;
    text-align: center;
}

#word {
    font-size: 3em;
    margin-bottom: 20px;
}

#typedWord {
    font-size: 1.5em;
    padding: 10px;
    width: 300px;
    border-bottom: 2px solid #000;
    text-align: center;
    height: 2.1em;
}

.info {
    display: flex;
    justify-content: center;
    margin-top: 20px;
    font-size: 1.1em;
}

.info > div {
    margin: 0 10px;
    min-width: 3em;
}
</style>

<div id="inputArea">
    <h1 id="title">Typing Game</h1>
    <input type="text" id="theme" maxlength="30">
    <button id="startButton">Start</button>
    <div id="error"></div>
</div>
<div id="gameArea" style="display: none;">
    <div id="word"></div>
    <div id="typedWord"></div>
    <div class="info">
        <div id="correctWords"></div>
        <div id="mistakes"></div>
        <div id="accuracy"></div>
        <div id="time"></div>
    </div>
    <div class="info">
        <div id="score"></div>
        <div id="cpm"></div>
    </div>
</div>

<script>
const GENERATED_QUESTION_COUNT = 50;
const QUESTION_COUNT = 10;
const MAX_INPUT_LENGTH = 30;

const getElementById = (id) => {
    const element = document.getElementById(id);

    if (element == null) {
        throw new Error(`Element with id ${id} not found.`);
    }

    return element;
};

 const getWords = async (input) => {
    if (input == null || input.trim().length === 0 || input.length > MAX_INPUT_LENGTH) {
        return [];
    }

    try
    {
        const serverAi = new ServerAI();
        const response = await serverAi.getAnswerText('6710797e8f7cb9905ce6eea3', '', input);

        const words = response.split(',')
            .map(x => x.trim())
            .filter(x => /^[a-zA-Z]+$/.test(x.trim()));
        return [... new Set(words)];
    } catch (error) {
        return [];
    }
};

const shuffleArray = (array) => {
    for (let i = array.length - 1; i >= 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
};

document.addEventListener('DOMContentLoaded', (event) => {
    const elements = {
        input: {
            inputArea: getElementById('inputArea'),
            startButton: getElementById('startButton'),
            theme: getElementById('theme'),
            error: getElementById('error'),
        },
        game: {
            gameArea: getElementById('gameArea'),

            word: getElementById('word'),
            typedWord: getElementById('typedWord'),

            correctWords: getElementById('correctWords'),
            mistakes: getElementById('mistakes'),
            accuracy: getElementById('accuracy'),       
            time: getElementById('time'),

            score: getElementById('score'),
            cpm: getElementById('cpm'),
        },
    };

    const gameState = {
        words: [],
        wordIndex: 0,
        typedWord: '',
        key: '',
        startTime: 0,
        mistakeTime: 0,
        gameCleared: false,
    };
    const gameStats = {
        correctWords: 0,
        mistakes: 0,
        accuracy: 0,
        time: 0,
        score: 0,
        cpm: 0,
    };

    const setErrorText = (text) => {
        elements.input.error.textContent = text;
    };

    const showGameScreen = () => {
        elements.input.inputArea.style.display = 'none';
        elements.game.gameArea.style.display = 'flex';
    };

    const showThemeScreen = () => {
        elements.input.inputArea.style.display = 'flex';
        elements.game.gameArea.style.display = 'none';

        elements.input.theme.focus();
    };

    const startGame = async (theme) => {
        elements.input.startButton.disabled = true;
        gameState.words = await getWords(theme);
        elements.input.startButton.disabled = false;

        if (gameState.words.length < GENERATED_QUESTION_COUNT) {
            setErrorText('Failed to get words. Please try again.');
            return;
        }

        shuffleArray(gameState.words);
        gameState.words.length = QUESTION_COUNT;
    
        setErrorText('');
        resetGame();
        showGameScreen();

        requestAnimationFrame(gameLoop);
    };

    const resetGame = () => {
        gameState.wordIndex = 0;
        gameState.typedWord = '';
        gameState.key = '';
        gameState.startTime = performance.now();
        gameState.mistakeTime = 0;
        gameState.gameCleared = false;

        gameStats.correctWords = 0;
        gameStats.mistakes = 0;
        gameStats.accuracy = 0;
        gameStats.time = 0;
        gameStats.score = 0;
        gameStats.cpm = 0;

        stateHasChanged();
    };

    const gameLoop = () => {
        if (gameState.gameCleared && gameState.key === 'Enter') {
            showThemeScreen();
            return;
        }

        if (gameState.key === 'Escape') {
            showThemeScreen();
            return;
        }
  
        update();
        stateHasChanged();
        requestAnimationFrame(gameLoop);
    };

    const update = () => {
        if (gameState.gameCleared) {
            return;
        }

        if (gameState.key.length === 1) {
            const currentWord = gameState.words[gameState.wordIndex].toLowerCase();
            const typedWord = currentWord[gameState.typedWord.length];
  
            if (currentWord[gameState.typedWord.length].toLowerCase() !== gameState.key.toLowerCase()) {
                updateMistakes();
            }      
            else {
                updateCorrectWords();

                if (gameState.typedWord === currentWord) {
                    updateWord();
                }
            }
        }

        if (performance.now() > gameState.mistakeTime + 300) {
            gameState.mistakeTime = 0;
        }

        updateTime();
        updateAccuracy();
        updateCPM();
        updateScore();
    };

    const updateWord = () => {
        gameState.wordIndex++;
        gameState.typedWord = '';

        if (gameState.wordIndex >= gameState.words.length) {
            gameState.gameCleared = true;
            return;
        }
    };

    const updateCorrectWords = () => {
        gameStats.correctWords++;
        gameState.typedWord += gameState.key.toLowerCase();
        gameState.key = '';
    };

    const updateMistakes = () => {
        gameStats.mistakes++;
        gameState.key = '';
        gameState.mistakeTime = performance.now();
    };

    const updateAccuracy = () => {
        const totalCount = gameStats.correctWords + gameStats.mistakes;
        gameStats.accuracy = totalCount == 0 ? 0 : (gameStats.correctWords / totalCount) * 100;      
    };

    const updateTime = () => {
        gameStats.time = (performance.now() - gameState.startTime) / 1000;
    };

    const updateScore = () => {
        gameStats.score = gameStats.cpm * ((gameStats.accuracy / 100) ** 3);
    };

    const updateCPM = () => {
        const minutes = gameStats.time / 60;
        gameStats.cpm = minutes === 0 ? 0 : gameStats.correctWords / minutes;
    };

    const stateHasChanged = () => {
        if (gameState.gameCleared) {
            elements.game.word.textContent = 'Game Clear!';
            elements.game.typedWord.textContent = 'Press Enter to return';
        }
        else {
            elements.game.word.textContent = gameState.words[gameState.wordIndex];
            elements.game.typedWord.textContent = gameState.typedWord;
        }

        elements.game.correctWords.textContent = `Correct: ${gameStats.correctWords}`;
        elements.game.mistakes.textContent = `Mistakes: ${gameStats.mistakes}`;
        elements.game.accuracy.textContent = `Accuracy: ${gameStats.accuracy.toFixed(2)}%`;
        elements.game.time.textContent = `Time: ${gameStats.time.toFixed(1)}s`;
        elements.game.score.textContent = `Score: ${gameStats.score.toFixed(0)}`;
        elements.game.cpm.textContent = `CPM: ${gameStats.cpm.toFixed(0)}`;

        if (gameState.mistakeTime === 0 || gameState.gameCleared) {
            elements.game.typedWord.style.color = 'black';
            elements.game.typedWord.style.borderBottomColor = 'black';
        }
        else {
            elements.game.typedWord.style.color = 'red';
            elements.game.typedWord.style.borderBottomColor = 'red';
        }
    };

    elements.game.gameArea.addEventListener('click', () => {
        if (gameState.gameCleared) {
            showThemeScreen();
        }
    });

    elements.input.theme.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') {
            elements.input.startButton.click();
        }
    });
    
    elements.input.startButton.addEventListener('click', () => {
        const theme = elements.input.theme.value;
        startGame(theme);
    });

    document.addEventListener('keydown', (event) => {
        if (elements.game.gameArea.style.display !== 'flex') {
            return;
        }

        gameState.key = event.key;  
    });

    showThemeScreen();
});
</script>