Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
522 changes: 522 additions & 0 deletions css/main.css

Large diffs are not rendered by default.

558 changes: 558 additions & 0 deletions css/word-reorder.css

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<title>SQL学習ゲーム</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/editor.css">
<link rel="stylesheet" href="css/word-reorder.css">
</head>
<body>
<div id="app">
Expand Down Expand Up @@ -66,6 +67,11 @@ <h3 id="slide-title">解説スライド</h3>
<iframe id="slide-iframe" src="" frameborder="0"></iframe>
</div>
</div>

<!-- 単語並び替えセクション -->
<div id="word-reorder-section" class="word-reorder-section hidden">
<!-- WordReorderUIクラスがここにコンテンツを動的に生成します -->
</div>
</section>

<section class="results-section">
Expand Down Expand Up @@ -105,6 +111,68 @@ <h3>💡 ヒント</h3>
</div>
</div>

<!-- ゲームオーバーレイ(アニメーション用) -->
<div id="game-overlay" class="game-overlay hidden">
<div class="overlay-content">
<!-- ロボット判定アニメーション -->
<div id="robot-animation" class="robot-animation">
<div class="robot-container">
<div class="robot">
<div class="robot-head">
<div class="robot-eye left-eye"></div>
<div class="robot-eye right-eye"></div>
<div class="robot-mouth"></div>
</div>
<div class="robot-body">
<div class="robot-chest"></div>
<div class="robot-arm left-arm"></div>
<div class="robot-arm right-arm"></div>
</div>
<div class="robot-legs">
<div class="robot-leg left-leg"></div>
<div class="robot-leg right-leg"></div>
</div>
</div>
<div class="thinking-bubbles">
<div class="bubble bubble-1">?</div>
<div class="bubble bubble-2">SQL</div>
<div class="bubble bubble-3">🤔</div>
</div>
</div>
<div class="robot-text">AIが回答を分析中...</div>
</div>

<!-- 結果アニメーション -->
<div id="result-animation" class="result-animation hidden">
<div id="result-icon" class="result-icon">🎉</div>
<div id="result-text" class="result-text">正解!</div>
</div>

<!-- 成功時のクラッカーエフェクト -->
<div id="success-effects" class="success-effects hidden">
<div class="cracker left-cracker"></div>
<div class="cracker right-cracker"></div>
<div class="rocket-container">
<div class="rocket rocket-1">🚀</div>
<div class="rocket rocket-2">🎆</div>
<div class="rocket rocket-3">✨</div>
</div>
</div>

<!-- 失敗時のエフェクト -->
<div id="failure-effects" class="failure-effects hidden">
<div class="shake-container">
<div class="error-icon">❌</div>
<div class="error-waves">
<div class="wave wave-1"></div>
<div class="wave wave-2"></div>
<div class="wave wave-3"></div>
</div>
</div>
</div>
</div>
</div>




Expand Down
157 changes: 157 additions & 0 deletions js/game-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,163 @@ export class GameEngine {
};
}

/**
* word-reorderチャレンジの回答をチェック
* @param {string} userSQL - ユーザーが構築したSQL
* @returns {Promise<Object>} 結果オブジェクト
*/
async checkWordReorderAnswer(userSQL) {
const challenge = this.getCurrentChallenge();
this.attempts++;

if (!userSQL || userSQL.trim() === '') {
return {
correct: false,
message: "SQLを構築してください"
};
}

try {
// データベースマネージャーが利用可能かチェック
if (!window.dbManager) {
throw new Error('データベースが初期化されていません');
}

// ユーザーのSQLと正解SQLの両方を実行
const [userResult, correctResult] = await Promise.all([
window.dbManager.executeQuery(userSQL),
window.dbManager.executeQuery(challenge.solution)
]);

// ユーザーのSQLでエラーが発生した場合
if (!userResult.success) {
return {
correct: false,
message: `SQLエラー: ${userResult.error}`
};
}

// 正解SQLでエラーが発生した場合(チャレンジデータの問題)
if (!correctResult.success) {
console.error('正解SQLでエラーが発生:', correctResult.error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Description: Log Injection occurs when untrusted user input is directly written to log files without proper sanitization. This can allow attackers to manipulate log entries, potentially leading to security issues like log forging or cross-site scripting. To prevent this, always sanitize user input using encodeURIComponent() or DOMPurify.sanitize() before logging. Learn more - https://cwe.mitre.org/data/definitions/117.html

Severity: High

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remediation is made by using DOMPurify.sanitize() to sanitize the correctResult.error before logging it, preventing potential log injection attacks.

Suggested change
console.error('正解SQLでエラーが発生:', correctResult.error);
// 正解SQLでエラーが発生した場合(チャレンジデータの問題)
if (!correctResult.success) {
// import DOMPurify from 'dompurify'; // DOMPurify is used to sanitize user input before logging
console.error('正解SQLでエラーが発生:', DOMPurify.sanitize(correctResult.error));
return {
correct: false,
message: "チャレンジデータに問題があります。管理者に報告してください。"

return {
correct: false,
message: "チャレンジデータに問題があります。管理者に報告してください。"
};
}

// 結果を比較
const isCorrect = this.compareQueryResults(userResult, correctResult);

if (isCorrect) {
this.calculateScore();
return {
correct: true,
message: "正解です!素晴らしい!",
score: this.score,
userResult: userResult,
correctResult: correctResult
};
} else {
return {
correct: false,
message: "結果が正解と一致しません。もう一度確認してください。",
userResult: userResult,
correctResult: correctResult
};
}

} catch (error) {
console.error('SQL実行エラー:', error);
return {
correct: false,
message: `実行エラー: ${error.message}`
};
}
}

/**
* 2つのクエリ結果を比較
* @param {Object} result1 - 1つ目の結果
* @param {Object} result2 - 2つ目の結果
* @returns {boolean} 結果が一致するかどうか
*/
compareQueryResults(result1, result2) {
// 両方とも成功している必要がある
if (!result1.success || !result2.success) {
return false;
}

// カラム数の比較
if (result1.columns.length !== result2.columns.length) {
return false;
}

// カラム名の比較(順序も考慮)
for (let i = 0; i < result1.columns.length; i++) {
if (result1.columns[i] !== result2.columns[i]) {
return false;
}
}

// 行数の比較
if (result1.data.length !== result2.data.length) {
return false;
}

// データの比較(行ごと)
for (let i = 0; i < result1.data.length; i++) {
const row1 = result1.data[i];
const row2 = result2.data[i];

// 各カラムの値を比較
for (const column of result1.columns) {
const value1 = row1[column];
const value2 = row2[column];

// 値の比較(型も考慮)
if (!this.compareValues(value1, value2)) {
return false;
}
}
}

return true;
}

/**
* 2つの値を比較(型変換も考慮)
* @param {any} value1 - 1つ目の値
* @param {any} value2 - 2つ目の値
* @returns {boolean} 値が一致するかどうか
*/
compareValues(value1, value2) {
// 完全一致の場合
if (value1 === value2) {
return true;
}

// null/undefined の処理
if (value1 == null && value2 == null) {
return true;
}

if (value1 == null || value2 == null) {
return false;
}

// 数値の比較(文字列として格納されている場合も考慮)
const num1 = Number(value1);
const num2 = Number(value2);

if (!isNaN(num1) && !isNaN(num2)) {
return Math.abs(num1 - num2) < 1e-10; // 浮動小数点の誤差を考慮
}

// 文字列として比較
return String(value1) === String(value2);
}

calculateScore() {
const baseScore = 100;
const attemptPenalty = Math.max(0, (this.attempts - 1) * 10);
Expand Down
Loading
Loading