この記事では、JavaScriptを使用した数字パズルゲームの実装について詳しく解説します。HTML、CSS、JavaScriptの各ファイルの役割と構造を理解し、モダンなウェブアプリケーション開発の基本的な概念と技術を学ぶことができます。
はじめに
今回は、以下のゲームを作成します。
開発環境がまだの方はこちら
コード例
ディレクトリ階層:
number-puzzle-game/
│
├── index.html
│
├── css/
│ └── styles.css
│
├── js/
│ ├── constants.js
│ ├── utils.js
│ ├── ui.js
│ ├── game.js
│ └── main.js
│
└── sounds/
├── correct.mp3
├── wrong.mp3
└── game-over.mp3
以下のサイトから効果音をダウンロードできます。
HTML (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="styles.css" />
</head>
<body>
<!-- ゲームのメインコンテナ -->
<div id="game-container">
<div id="game-instructions"></div>
<div id="game-info"></div>
<div id="high-score">ハイスコア: 0</div>
<div id="timer">時間: 60秒</div>
<div id="score">スコア: 0</div>
<!-- 難易度選択ドロップダウン -->
<select id="difficulty-select">
<option value="easy">Easy</option>
<option value="normal">Normal</option>
<option value="hard">Hard</option>
</select>
<!-- パズルグリッド -->
<div id="puzzle-grid"></div>
<!-- ゲーム操作ボタン -->
<button id="start-button">ゲーム開始</button>
<button id="mute-button">🔊</button>
<button id="reset-high-score">ハイスコアリセット</button>
</div>
<!-- JavaScriptファイルの読み込み -->
<script src="js/constants.js"></script>
<script src="js/utils.js"></script>
<script src="js/ui.js"></script>
<script src="js/game.js"></script>
<script src="js/main.js"></script>
</body>
</html>
CSS (styles.css)
/* ルート変数の定義 */
:root {
--main-bg-color: #f0f0f0;
--tile-bg-color: #3498db;
--selected-tile-color: #e74c3c;
--text-color: white;
--font-family: Arial, sans-serif;
--base-font-size: 16px;
--tile-size: 60px;
--grid-gap: 5px;
--container-margin: 10px;
}
/* 全体のスタイル */
body {
font-family: var(--font-family);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: var(--main-bg-color);
font-size: var(--base-font-size);
}
/* ゲームコンテナのスタイル */
#game-container {
text-align: center;
}
/* パズルグリッドのスタイル */
#puzzle-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--grid-gap);
margin-bottom: var(--container-margin);
}
/* パズルタイルのスタイル */
.puzzle-tile {
width: var(--tile-size);
height: var(--tile-size);
background-color: var(--tile-bg-color);
display: flex;
justify-content: center;
align-items: center;
font-size: calc(var(--base-font-size) * 1.5);
color: var(--text-color);
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
}
/* パズルタイルのホバー効果 */
.puzzle-tile:hover {
transform: scale(1.05);
}
/* 選択されたタイルのスタイル */
.puzzle-tile.selected {
background-color: var(--selected-tile-color);
}
/* 正解アニメーション */
.puzzle-tile.correct {
animation: correctAnimation 0.5s;
}
@keyframes correctAnimation {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
/* その他のUI要素のスタイル */
#timer,
#score,
#game-info,
#high-score {
font-size: calc(var(--base-font-size) * 1.125);
margin-bottom: var(--container-margin);
}
/* 難易度選択ドロップダウンのスタイル */
#difficulty-select {
margin-bottom: var(--container-margin);
font-size: var(--base-font-size);
padding: 5px;
}
/* ボタンのスタイル */
#start-button,
#mute-button {
font-size: var(--base-font-size);
padding: 10px 20px;
background-color: var(--tile-bg-color);
color: var(--text-color);
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
/* ボタンのホバー効果 */
#start-button:hover,
#mute-button:hover {
background-color: var(--selected-tile-color);
}
/* ゲーム説明のスタイル */
#game-instructions {
margin-bottom: var(--container-margin);
text-align: left;
}
/* ミュートボタンのスタイル */
#mute-button {
padding: 5px 10px;
margin-left: 10px;
}
/* レスポンシブデザイン */
@media (max-width: 600px) {
:root {
--base-font-size: 14px;
--tile-size: 50px;
}
}
JavaScript
constants.js:定数の定義
// 難易度設定の定義
const DIFFICULTY_SETTINGS = {
easy: { target: 15, description: "3つの数字を選んで合計15にしてください。" },
normal: {
target: 17,
description: "3つの数字を選んで合計17にしてください。",
},
hard: { target: 19, description: "3つの数字を選んで合計19にしてください。" },
};
// サウンドファイルのパス
const SOUND_PATHS = {
correct: "sounds/correct.mp3",
wrong: "sounds/wrong.mp3",
gameOver: "sounds/game-over.mp3",
};
utils.js:ユーティリティ関数
// ゲーム時間(秒)
const GAME_DURATION = 60;
// ランダムな数字を生成する関数
function getRandomNumber(max) {
return Math.floor(Math.random() * max) + 1;
}
// サウンドを再生する関数
function playSound(sound, isMuted) {
if (!isMuted) {
sound.play();
}
}
ui.js: UIの操作を担当するクラス
// UI管理クラス
class UI {
constructor() {
// DOM要素の取得
this.elements = {
puzzleGrid: document.getElementById("puzzle-grid"),
startButton: document.getElementById("start-button"),
timerDisplay: document.getElementById("timer"),
scoreDisplay: document.getElementById("score"),
highScoreDisplay: document.getElementById("high-score"),
difficultySelect: document.getElementById("difficulty-select"),
gameInfo: document.getElementById("game-info"),
gameInstructions: document.getElementById("game-instructions"),
muteButton: document.getElementById("mute-button"),
resetHighScoreButton: document.getElementById("reset-high-score"),
};
}
// パズルグリッドの作成
createPuzzleGrid(tileCount, onTileClick) {
// 既存のグリッドをクリア
this.elements.puzzleGrid.innerHTML = "";
const gridSize = Math.sqrt(tileCount);
this.elements.puzzleGrid.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`;
// タイルの生成
for (let i = 0; i < tileCount; i++) {
const tile = document.createElement("div");
tile.classList.add("puzzle-tile");
tile.textContent = getRandomNumber(this.getMaxNumber());
tile.addEventListener("click", () => onTileClick(tile));
this.elements.puzzleGrid.appendChild(tile);
}
}
// 難易度に応じた最大数を取得
getMaxNumber() {
const difficulty = this.elements.difficultySelect.value;
return difficulty === "easy" ? 9 : difficulty === "normal" ? 12 : 15;
}
// タイマーの更新
updateTimer(timeLeft) {
this.elements.timerDisplay.textContent = `時間: ${timeLeft}秒`;
}
// スコアの更新
updateScore(score) {
this.elements.scoreDisplay.textContent = `スコア: ${score}`;
}
// ハイスコアの表示
displayHighScore(highScore) {
this.elements.highScoreDisplay.textContent = `ハイスコア: ${highScore}`;
}
// 全難易度のハイスコアを表示
displayAllHighScores(highScores) {
this.elements.highScoreDisplay.innerHTML = `
<h3>ハイスコア</h3>
<p>Easy: ${highScores.easy || 0}</p>
<p>Normal: ${highScores.normal || 0}</p>
<p>Hard: ${highScores.hard || 0}</p>
`;
}
// ゲーム説明の表示
showGameInstructions() {
this.elements.gameInstructions.innerHTML = `
<h2>ゲーム説明</h2>
<p>このゲームは、制限時間内にできるだけ多くの正解を見つけるパズルゲームです。</p>
<h3>遊び方</h3>
<ol>
<li>難易度を選択し、「ゲーム開始」ボタンをクリックします。</li>
<li>表示された数字のタイルから3つを選びます。</li>
<li>選んだ3つの数字の合計が目標値と一致すれば正解です。</li>
<li>制限時間内にできるだけ多くの正解を見つけてください。</li>
</ol>
<p>難易度が上がるほど、タイルの数が増え、数字の範囲が広がり、目標値も大きくなります。</p>
`;
}
// ミュートボタンの切り替え
toggleMuteButton(isMuted) {
this.elements.muteButton.textContent = isMuted ? "🔇" : "🔊";
}
// ゲーム状態に応じたUI更新
setGameState(isPlaying) {
this.elements.startButton.disabled = isPlaying;
this.elements.difficultySelect.disabled = isPlaying;
this.elements.resetHighScoreButton.disabled = isPlaying;
this.elements.gameInstructions.style.display = isPlaying ? "none" : "block";
this.elements.gameInfo.style.display = isPlaying ? "block" : "none";
}
}
game.js: メインのゲームロジックを含むクラス
// ゲーム管理クラス
class Game {
constructor(ui) {
this.ui = ui;
this.state = {
timer: null,
score: 0,
timeLeft: GAME_DURATION,
selectedTiles: [],
difficulty: "easy",
highScores: JSON.parse(localStorage.getItem("highScores")) || {
easy: 0,
normal: 0,
hard: 0,
},
isMuted: false,
};
this.sounds = {
correct: new Audio(SOUND_PATHS.correct),
wrong: new Audio(SOUND_PATHS.wrong),
gameOver: new Audio(SOUND_PATHS.gameOver),
};
this.bindEvents();
}
// イベントリスナーの設定
bindEvents() {
this.ui.elements.startButton.addEventListener("click", () =>
this.startGame()
);
this.ui.elements.muteButton.addEventListener("click", () =>
this.toggleMute()
);
this.ui.elements.resetHighScoreButton.addEventListener("click", () =>
this.resetHighScore()
);
}
// ゲーム開始
startGame() {
this.state.difficulty = this.ui.elements.difficultySelect.value;
this.createPuzzleGrid();
this.state.score = 0;
this.state.timeLeft = GAME_DURATION;
this.ui.updateTimer(this.state.timeLeft);
this.ui.updateScore(this.state.score);
this.ui.displayHighScore(this.state.highScores[this.state.difficulty]);
this.ui.setGameState(true);
// タイマーの設定
if (this.state.timer) {
clearInterval(this.state.timer);
}
this.state.timer = setInterval(() => {
this.state.timeLeft--;
this.ui.updateTimer(this.state.timeLeft);
if (this.state.timeLeft <= 0) {
clearInterval(this.state.timer);
this.endGame();
}
}, 1000);
}
// パズルグリッドの作成
createPuzzleGrid() {
const tileCount =
this.state.difficulty === "easy"
? 16
: this.state.difficulty === "normal"
? 25
: 36;
this.ui.createPuzzleGrid(tileCount, (tile) => this.handleTileClick(tile));
this.ui.elements.gameInfo.textContent =
DIFFICULTY_SETTINGS[this.state.difficulty].description;
}
// タイルクリック時の処理
handleTileClick(tile) {
if (
this.state.selectedTiles.length < 3 &&
!this.state.selectedTiles.includes(tile)
) {
tile.classList.add("selected");
this.state.selectedTiles.push(tile);
if (this.state.selectedTiles.length === 3) {
setTimeout(() => this.checkSum(), 300);
}
}
}
// 選択された数字の合計チェック
checkSum() {
const sum = this.state.selectedTiles.reduce(
(acc, tile) => acc + parseInt(tile.textContent),
0
);
const target = DIFFICULTY_SETTINGS[this.state.difficulty].target;
if (sum === target) {
// 正解の場合
this.state.score +=
this.state.difficulty === "easy"
? 10
: this.state.difficulty === "normal"
? 15
: 20;
this.ui.updateScore(this.state.score);
playSound(this.sounds.correct, this.state.isMuted);
this.state.selectedTiles.forEach((tile) => {
tile.classList.add("correct");
setTimeout(() => {
tile.textContent = getRandomNumber(this.ui.getMaxNumber());
tile.classList.remove("selected", "correct");
}, 500);
});
} else {
// 不正解の場合
this.state.selectedTiles.forEach((tile) =>
tile.classList.remove("selected")
);
playSound(this.sounds.wrong, this.state.isMuted);
}
this.state.selectedTiles = [];
}
// ゲーム終了処理
endGame() {
playSound(this.sounds.gameOver, this.state.isMuted);
setTimeout(() => {
alert(`ゲーム終了!スコア: ${this.state.score}`);
this.ui.setGameState(false);
this.ui.elements.puzzleGrid.innerHTML = "";
this.ui.showGameInstructions();
this.updateHighScore();
this.ui.displayAllHighScores(this.state.highScores);
}, 100);
}
// ハイスコアの更新
updateHighScore() {
if (this.state.score > this.state.highScores[this.state.difficulty]) {
this.state.highScores[this.state.difficulty] = this.state.score;
localStorage.setItem("highScores", JSON.stringify(this.state.highScores));
}
}
// ミュート切り替え
toggleMute() {
this.state.isMuted = !this.state.isMuted;
this.ui.toggleMuteButton(this.state.isMuted);
}
// ハイスコアのリセット
resetHighScore() {
const difficulty = this.ui.elements.difficultySelect.value;
if (
confirm(
`${
difficulty.charAt(0).toUpperCase() + difficulty.slice(1)
}のハイスコアをリセットしますか?`
)
) {
this.state.highScores[difficulty] = 0;
localStorage.setItem("highScores", JSON.stringify(this.state.highScores));
this.ui.displayAllHighScores(this.state.highScores);
}
}
}
main.js: アプリケーションのエントリーポイント
// DOMコンテンツ読み込み完了時の処理
document.addEventListener("DOMContentLoaded", () => {
const ui = new UI();
const game = new Game(ui);
ui.showGameInstructions();
ui.displayAllHighScores(game.state.highScores);
});
index.htmlの解説
このHTMLコードは、数字パズルゲームのウェブページの構造を定義しています。重要な部分を解説します:
- DOCTYPE宣言とhtml要素:
HTML5文書であることを宣言し、言語が日本語であることを示しています。 - head セクション:
- 文字エンコーディングをUTF-8に設定
- モバイルデバイスでの表示を最適化
- ブラウザのタブに表示されるタイトルを設定
- CSSファイルをリンク
- body セクション:
- game-container: ゲームの主要な要素を含むコンテナ
- 各種情報表示用の div 要素(指示、情報、ハイスコア、タイマー、スコア)
- 難易度選択のドロップダウンメニュー
- パズルのグリッドを表示する場所
- ゲーム操作用のボタン要素(開始、ミュート、ハイスコアリセット)
- JavaScriptファイルの読み込み:
- 複数のJavaScriptファイル(constants.js, utils.js, ui.js, game.js, main.js)を読み込んでいます。これらのファイルがゲームの機能を実装します。
このHTMLは、ゲームの視覚的構造を提供し、必要なスクリプトを読み込むための骨組みとなっています。実際のゲーム機能はリンクされているJavaScriptファイルで実装されることになります。
styles.cssの解説
このCSSコードは、数字パズルゲームのスタイルを定義しています。
- ルート変数の定義:
:root
セレクタを使用して、カスタムCSS変数を定義しています。これらの変数は、色、フォント、サイズなどの共通の値を保存し、コード全体で再利用できます。 - 全体のスタイル:
body
要素に対して、フォント、レイアウト、背景色などの基本的なスタイルを設定しています。flexboxを使用してコンテンツを中央に配置しています。 - ゲームコンテナのスタイル:
#game-container
に対して、テキストを中央揃えにしています。 - パズルグリッドのスタイル:
#puzzle-grid
に対して、CSS Gridを使用して4列のグリッドレイアウトを作成しています。 - パズルタイルのスタイル:
.puzzle-tile
クラスに対して、サイズ、色、フォント、カーソルスタイルなどを設定しています。transitionプロパティでホバー効果をスムーズにしています。 - 選択されたタイルと正解アニメーション:
選択されたタイルの色を変更し、正解時のアニメーションを@keyframesを使用して定義しています。 - その他のUI要素のスタイル:
タイマー、スコア、ゲーム情報などの要素のフォントサイズとマージンを設定しています。 - 難易度選択ドロップダウンとボタンのスタイル:
ドロップダウンメニューとボタンの外観を設定し、ホバー効果を追加しています。 - ゲーム説明のスタイル:
ゲーム説明テキストの配置を調整しています。 - レスポンシブデザイン:
@media
クエリを使用して、画面幅が600px以下の場合にフォントサイズとタイルサイズを小さくしています。
このCSSは、ゲームの視覚的な一貫性を保ちながら、異なる画面サイズに対応するレスポンシブなデザインを実現しています。カスタムCSS変数を使用することで、色やサイズの変更が容易になり、コードの保守性が向上しています。
constants.jsの解説
このコードは、ゲームの設定を定義する定数オブジェクトを宣言しています。
- 難易度設定の定義:
const DIFFICULTY_SETTINGS = {
easy: { target: 15, description: "3つの数字を選んで合計15にしてください。" },
normal: { target: 17, description: "3つの数字を選んで合計17にしてください。" },
hard: { target: 19, description: "3つの数字を選んで合計19にしてください。" },
};
DIFFICULTY_SETTINGS
は、ゲームの難易度ごとの設定を含むオブジェクトです。- 各難易度(easy, normal, hard)に対して、以下の情報が定義されています:
target
: プレイヤーが達成すべき目標の合計値description
: プレイヤーへの指示文
- この構造により、難易度ごとに異なる目標値と説明文を簡単に管理できます。
- サウンドファイルのパス定義:
const SOUND_PATHS = {
correct: "sounds/correct.mp3",
wrong: "sounds/wrong.mp3",
gameOver: "sounds/game-over.mp3",
};
SOUND_PATHS
は、ゲーム中で使用する効果音のファイルパスを定義するオブジェクトです。- 各イベント(正解、不正解、ゲームオーバー)に対応する音声ファイルのパスが指定されています。
これらの定数オブジェクトを使用する利点:
- 集中管理: 設定値が一箇所にまとまっているため、変更や管理が容易です。
- 可読性の向上: 難易度設定やサウンドファイルの情報が明確に構造化されているため、コードの理解が容易になります。
- 拡張性: 新しい難易度や効果音を追加する際に、これらのオブジェクトに新しいエントリを追加するだけで済みます。
- 再利用性: これらの設定は、ゲームのさまざまな部分で簡単に参照できます。例えば:
const easyTarget = DIFFICULTY_SETTINGS.easy.target; // 15
const correctSoundPath = SOUND_PATHS.correct; // "sounds/correct.mp3"
- 定数としての宣言:
const
キーワードを使用することで、これらの設定が誤って変更されることを防ぎます。
このようなアプローチは、コードの保守性と拡張性を高め、ゲームの設定を効率的に管理するのに役立ちます。
utils.jsの解説
このJavaScriptコードは、ゲームの共通ユーティリティ関数と定数を定義しています。
- ゲーム時間の定義:
const GAME_DURATION = 60;
この定数はゲームの制限時間を60秒と設定しています。const
キーワードは、この値が変更されないことを示しています。
- ランダムな数字を生成する関数:
function getRandomNumber(max) {
return Math.floor(Math.random() * max) + 1;
}
この関数は、1からmax
までのランダムな整数を生成します。
Math.random()
: 0以上1未満の乱数を生成Math.floor()
: 小数点以下を切り捨て+ 1
: 結果が1からmax
までの範囲になるように調整
- サウンドを再生する関数:
function playSound(sound, isMuted) {
if (!isMuted) {
sound.play();
}
}
この関数は、指定されたサウンドを再生します。
sound
: 再生するサウンドオブジェクトisMuted
: ミュート状態を示すブール値- 条件分岐を使用して、ミュートされていない場合のみサウンドを再生
これらの要素は、ゲームの基本的な機能を支援します:
- GAME_DURATION: ゲームの制限時間を一箇所で管理し、必要に応じて簡単に変更できます。
- getRandomNumber(): パズルの数字をランダムに生成する際に使用されます。
- playSound(): ゲーム内の様々な場面(正解、不正解、ゲームオーバーなど)で効果音を再生する際に使用されます。
これらの関数と定数を別ファイルで定義することで、コードの再利用性が高まり、メインのゲームロジックがより整理されて読みやすくなります。また、将来的な変更や拡張も容易になります。
ui.jsの解説
- クラス定義とコンストラクタ:
class UI {
constructor() {
this.elements = {
puzzleGrid: document.getElementById("puzzle-grid"),
startButton: document.getElementById("start-button"),
// ... 他のDOM要素 ...
};
}
}
- このクラスは、ゲームのUI全体を管理します。
- コンストラクタでは、
document.getElementById()
を使用して、HTMLの各要素を取得し、this.elements
オブジェクトに格納しています。 - これにより、クラス内の他のメソッドから簡単にこれらの要素にアクセスできます。
- createPuzzleGrid メソッド:
createPuzzleGrid(tileCount, onTileClick) {
this.elements.puzzleGrid.innerHTML = "";
const gridSize = Math.sqrt(tileCount);
this.elements.puzzleGrid.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`;
for (let i = 0, i < tileCount; i++) {
const tile = document.createElement("div");
tile.classList.add("puzzle-tile");
tile.textContent = getRandomNumber(this.getMaxNumber());
tile.addEventListener("click", () => onTileClick(tile));
this.elements.puzzleGrid.appendChild(tile);
}
}
- このメソッドは、パズルグリッドを動的に生成します。
- まず既存のグリッドをクリアし、新しいグリッドのサイズを設定します。
- ループを使用して、指定された数のタイルを作成します。
- 各タイルにはランダムな数字が割り当てられ、クリックイベントリスナーが追加されます。
- getMaxNumber メソッド:
getMaxNumber() {
const difficulty = this.elements.difficultySelect.value;
return difficulty === "easy" ? 9 : difficulty === "normal" ? 12 : 15;
}
- 選択された難易度に基づいて、タイルに表示される最大数を返します。
- 三項演算子を使用して、難易度に応じた値を返しています。
- updateTimer と updateScore メソッド:
updateTimer(timeLeft) {
this.elements.timerDisplay.textContent = `時間: ${timeLeft}秒`;
}
updateScore(score) {
this.elements.scoreDisplay.textContent = `スコア: ${score}`;
}
- これらのメソッドは、ゲームの状態(残り時間とスコア)を画面上に表示します。
- テンプレートリテラルを使用して、動的な文字列を生成しています。
- displayHighScore と displayAllHighScores メソッド:
displayHighScore(highScore) {
this.elements.highScoreDisplay.textContent = `ハイスコア: ${highScore}`;
}
displayAllHighScores(highScores) {
this.elements.highScoreDisplay.innerHTML = `
<h3>ハイスコア</h3>
<p>Easy: ${highScores.easy || 0}</p>
<p>Normal: ${highScores.normal || 0}</p>
<p>Hard: ${highScores.hard || 0}</p>
`;
}
- これらのメソッドは、ハイスコアを表示します。
displayAllHighScores
は、全難易度のハイスコアをHTML形式で表示します。
- showGameInstructions メソッド:
showGameInstructions() {
this.elements.gameInstructions.innerHTML = `
<h2>ゲーム説明</h2>
// ... 説明文 ...
`;
}
- このメソッドは、ゲームの説明をHTML形式で表示します。
- toggleMuteButton メソッド:
toggleMuteButton(isMuted) {
this.elements.muteButton.textContent = isMuted ? "🔇" : "🔊";
}
- ミュート状態に応じて、ミュートボタンの表示を切り替えます。
- setGameState メソッド:
setGameState(isPlaying) {
this.elements.startButton.disabled = isPlaying;
this.elements.difficultySelect.disabled = isPlaying;
// ... 他の要素の状態変更 ...
}
- ゲームの状態(プレイ中かどうか)に応じて、UI要素の有効/無効や表示/非表示を切り替えます。
このUIクラスは、ゲームの視覚的な部分を一元管理し、メインのゲームロジックとUIの操作を分離することで、コードの構造を改善し、保守性を高めています。各メソッドは特定のUI更新タスクを担当しており、必要に応じて呼び出すことができます。
game.jsの解説
このJavaScriptコードは、ゲームの全体的なロジックを管理するGame
クラスを定義しています。
- クラス定義とコンストラクタ:
class Game {
constructor(ui) {
this.ui = ui;
this.state = { ... };
this.sounds = { ... };
this.bindEvents();
}
}
ui
パラメータを受け取り、UIクラスのインスタンスを保存します。state
オブジェクトでゲームの状態を管理します。sounds
オブジェクトで効果音を管理します。bindEvents()
メソッドを呼び出してイベントリスナーを設定します。
- bindEvents メソッド:
bindEvents() {
this.ui.elements.startButton.addEventListener("click", () => this.startGame());
// ... 他のイベントリスナー ...
}
- ゲーム開始、ミュート切り替え、ハイスコアリセットのイベントリスナーを設定します。
- startGame メソッド:
startGame() {
// ゲーム状態の初期化
this.state.difficulty = this.ui.elements.difficultySelect.value;
this.createPuzzleGrid();
// ... スコアとタイマーの初期化 ...
// タイマーの設定
this.state.timer = setInterval(() => {
// ... タイマー処理 ...
}, 1000);
}
- ゲームの初期状態を設定し、パズルグリッドを作成します。
- 1秒ごとにタイマーを更新し、時間切れでゲームを終了します。
- createPuzzleGrid メソッド:
createPuzzleGrid() {
const tileCount = /* 難易度に応じたタイル数 */;
this.ui.createPuzzleGrid(tileCount, (tile) => this.handleTileClick(tile));
// ... 説明文の設定 ...
}
- 難易度に応じてパズルグリッドを作成します。
- 各タイルにクリックイベントハンドラを設定します。
- handleTileClick メソッド:
handleTileClick(tile) {
if (/* タイルが選択可能な条件 */) {
// タイルを選択状態に
if (this.state.selectedTiles.length === 3) {
setTimeout(() => this.checkSum(), 300);
}
}
}
- タイルがクリックされたときの処理を行います。
- 3つのタイルが選択されたら合計をチェックします。
- checkSum メソッド:
checkSum() {
const sum = /* 選択されたタイルの合計 */;
const target = DIFFICULTY_SETTINGS[this.state.difficulty].target;
if (sum === target) {
// 正解の処理
} else {
// 不正解の処理
}
}
- 選択されたタイルの合計が目標値と一致するかチェックします。
- 正解の場合はスコアを加算し、タイルを更新します。
- endGame メソッド:
endGame() {
// ゲームオーバー音を再生
setTimeout(() => {
// 結果表示とゲーム状態のリセット
this.updateHighScore();
// ...
}, 100);
}
- ゲーム終了時の処理を行います。
- スコアを表示し、ハイスコアを更新します。
- その他のメソッド:
updateHighScore()
: ハイスコアを更新します。toggleMute()
: 音のオン/オフを切り替えます。resetHighScore()
: ハイスコアをリセットします。
このGameクラスは、ゲームの全体的なロジックとフローを管理しています。UIクラスと連携して、ユーザーの操作に応じてゲーム状態を更新し、画面表示を制御します。また、ローカルストレージを使用してハイスコアを永続化しているのも特徴的です。
main.jsの解説
このコードは、ウェブページのDOMコンテンツが完全に読み込まれた時点で実行される処理を定義しています。
- イベントリスナーの設定:
document.addEventListener("DOMContentLoaded", () => { ... });
document.addEventListener()
メソッドを使用して、“DOMContentLoaded”イベントのリスナーを設定しています。- このイベントは、HTMLドキュメントが完全に読み込まれ、DOMツリーが構築された時点で発火します。
- 画像やスタイルシートなどの外部リソースの読み込み完了は待ちません。
- UIとGameオブジェクトの生成:
const ui = new UI();
const game = new Game(ui);
UI
クラスとGame
クラスのインスタンスを作成しています。Game
オブジェクトの生成時にui
オブジェクトを渡しており、これによりゲームロジックとUI操作を連携させています。
- ゲーム説明の表示:
ui.showGameInstructions();
- UIオブジェクトのメソッドを呼び出して、ゲームの説明を表示します。
- ハイスコアの表示:
ui.displayAllHighScores(game.state.highScores);
- ゲームオブジェクトの状態からハイスコアを取得し、UIを通じて表示します。
このコードの重要なポイント:
- DOMContentLoadedイベントを使用することで、HTMLの解析が完了した時点で処理を実行できます。これにより、DOMの操作が安全に行えます。
- UIとゲームロジックの分離:UIクラスとGameクラスを別々に作成し、連携させることで、コードの構造が整理され、保守性が向上します。
- 初期化処理の集中:ゲームの説明表示やハイスコアの表示など、ページ読み込み後に必要な初期化処理をこのイベントリスナー内にまとめています。
このアプローチにより、ページの読み込みが完了した直後にゲームの初期状態を適切に設定し、ユーザーにゲーム情報を提供することができます。
まとめ
- HTML、CSS、JavaScriptの連携: ウェブアプリケーションの基本構造と各技術の役割
- モジュール化: 機能ごとにJavaScriptファイルを分割し、保守性と再利用性を向上
- オブジェクト指向プログラミング: クラスを使用したコード構造化と状態管理
- イベント駆動プログラミング: ユーザーの操作に応じたゲーム進行の制御
- ローカルストレージ: ブラウザにデータを保存し、セッション間でのハイスコア管理
このゲーム開発プロジェクトは、モダンなウェブ開発の基礎を実践的に学ぶいい例となっています。各ファイルの役割を理解し、UIとゲームロジックを適切に分離することで、拡張性が高く保守しやすいコードを書くことができます。また、ユーザー体験を向上させるための様々な技術(アニメーション、サウンド効果、難易度設定など)の実装方法紹介しました。
コメント