<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bono's Fret Blaster Launcher</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
text-align: center;
background-color: #f9f9f9;
}
#launchBtn {
font-size: 1.5em;
padding: 15px 30px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
#launchBtn:hover {
background-color: #45a049;
}
#gameContainer {
margin-top: 40px;
}
</style>
</head>
<body>
<h1 id="mainTitle">Welcome to Bono's Fret Blaster</h1> <button id="launchBtn">Launch the Game</button>
<div id="gameContainer"></div>
<script>
// HTML content for the game
const gameHTML = `
<style>
#game-container {
display: flex;
flex-direction: column;
align-items: center;
}
/* Styles for the main fretboard container */
#fretboard {
display: grid; /* Enables grid layout */
/* Defines 9 columns for frets (80px each) - no string label column */
grid-template-columns: repeat(9, 80px);
/* Defines 1 row for fret numbers (30px) and 6 rows for strings (50px each) */
grid-template-rows: 30px repeat(6, 50px);
grid-gap: 2px; /* Space between grid cells */
margin-top: 20px;
user-select: none;
border: 2px solid #333; /* Outer border of the fretboard */
padding: 5px; /* Padding inside the fretboard border */
background-color: #555; /* Background of the fretboard itself */
}
/* Base style for all cells in the grid (labels and frets) */
.fret-cell {
border: 1px solid #444; /* Border for each individual cell */
box-sizing: border-box; /* Ensures padding and border don't increase cell size */
font-weight: bold;
text-align: center;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
width: 100%; /* Make cell fill its grid area */
height: 100%; /* Make cell fill its grid area */
}
/* Style for fret number labels (top row) */
.fret-label {
background-color: #666; /* Darker background for labels */
color: white;
cursor: default;
pointer-events: none; /* Not interactive */
font-size: 1em;
}
/* String labels are now removed, but keeping this class for potential future use or if styling is applied elsewhere */
.string-label {
background-color: #f0f0f0; /* Light background for labels */
cursor: default;
pointer-events: none; /* Not interactive */
font-size: 1.1em;
}
/* Style for the clickable fret cells */
.fret {
background-color: #eee; /* Light background for clickable frets */
cursor: pointer;
transition: background-color 0.3s;
font-size: 1.2em;
}
/* Style for Open (Fret 0) notes - initially correct and unclickable for guessing */
.initial-correct {
background-color: #e0ffe0 !important; /* Light green */
cursor: default !important;
pointer-events: none !important;
}
/* Hover effect for clickable frets that are not yet answered or wrong */
.fret:hover:not(.correct):not(.wrong):not(.initial-correct) {
background-color: #ddd; /* Slightly darker on hover */
}
/* Style for correctly guessed frets */
.correct {
background-color: #a8e6a1 !important; /* Green for correct */
cursor: default; /* No longer needs pointer cursor */
}
/* Style for incorrectly guessed frets */
.wrong {
background-color: #f99 !important; /* Red for wrong */
}
/* Style for the prompt message (e.g., "Find the note: C") */
#prompt {
font-size: 1.5em;
margin-top: 10px;
color: #333; /* Ensure prompt text is visible */
}
/* Style for the prompt box */
.prompt-box {
background-color: #e0f2f7; /* Light blue */
border: 1px solid #a7d9ed; /* Slightly darker blue border */
padding: 10px 20px;
border-radius: 8px;
margin-bottom: 20px; /* Space below the box */
display: inline-block; /* To make the box fit its content */
}
/* Style for the score box */
.score-box {
background-color: #e6ffe6; /* Very light green */
border: 1px solid #a8e6a1; /* Slightly darker green border */
padding: 10px 20px;
border-radius: 8px;
margin-top: 15px; /* Space above the box */
display: inline-block; /* To make the box fit its content */
font-size: 1.2em;
font-weight: bold;
color: #333;
}
#message {
margin-top: 10px;
font-weight: bold;
height: 1.5em; /* Reserve space for messages */
color: #333; /* Ensure message text is visible */
}
/* Removed #score style as it's now handled by .score-box */
#playAgainBtn {
margin-top: 20px;
padding: 10px 20px;
font-size: 1em;
display: none; /* Hidden initially */
cursor: pointer;
}
#chromatic-scale {
border: 2px solid #444;
padding: 10px 15px;
max-width: 150px;
text-align: left;
user-select: none;
margin-top: 30px;
margin-left: auto;
margin-right: auto;
}
#chromatic-scale h2 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.3em;
text-align: center;
}
.note-box {
display: inline-block;
width: 28px;
height: 30px;
line-height: 30px;
margin: 2px;
border: 1px solid #444;
background-color: #eee;
font-weight: bold;
text-align: center;
font-size: 1em;
border-radius: 4px;
user-select: none;
}
.sharp {
background-color: #ccc;
}
</style>
<div id="game-container">
<h1>Bono's Fret Blaster</h1>
<p>Click the fret on the guitar fretboard that matches the note shown.</p>
<div id="prompt" class="prompt-box"></div>
<div id="fretboard"></div>
<div id="message"></div>
<div id="score" class="score-box"></div>
<button id="playAgainBtn">Play Again</button>
</div>
<div id="chromatic-scale">
<h2>Chromatic Scale</h2>
</div>
`;
// JavaScript content for the game logic
const gameScriptContent = `
(function() {
'use strict';
// --- Constants ---
const STANDARD_TUNING = ['E', 'A', 'D', 'G', 'B', 'E'];
const CHROMATIC_NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const NUM_STRINGS = STANDARD_TUNING.length;
const NUM_FRETS_TO_DISPLAY = 8; // For frets 0 (Open) to 7
const MAX_TRIES = 20;
// DOM Element IDs
const DOM_ID_FRETBOARD = 'fretboard';
const DOM_ID_PLAY_AGAIN_BTN = 'playAgainBtn';
const DOM_ID_PROMPT = 'prompt';
const DOM_ID_MESSAGE = 'message';
const DOM_ID_SCORE = 'score';
const DOM_ID_CHROMATIC_SCALE = 'chromatic-scale';
// CSS Classes
const CSS_CLASS_FRET_CELL = 'fret-cell';
const CSS_CLASS_FRET_LABEL = 'fret-label';
const CSS_CLASS_FRET = 'fret';
const CSS_CLASS_INITIAL_CORRECT = 'initial-correct';
const CSS_CLASS_CORRECT = 'correct';
const CSS_CLASS_WRONG = 'wrong';
const CSS_CLASS_PROMPT_BOX = 'prompt-box';
const CSS_CLASS_SCORE_BOX = 'score-box';
const SOUND_VOLUME_CORRECT_PRIMARY = 0.22;
const SOUND_VOLUME_CORRECT_SECONDARY = 0.18;
const SOUND_VOLUME_INCORRECT = 0.18;
const CHROMATIC_NOTE_INDICES = {};
CHROMATIC_NOTES.forEach((note, index) => CHROMATIC_NOTE_INDICES[note] = index);
function getNoteFor(stringIndex, fretNumber) {
const openNote = STANDARD_TUNING[stringIndex];
const openNoteIndex = CHROMATIC_NOTE_INDICES[openNote];
if (openNoteIndex === undefined) return '?';
return CHROMATIC_NOTES[(openNoteIndex + fretNumber) % CHROMATIC_NOTES.length];
}
// --- DOM Elements ---
const fretboardDiv = document.getElementById(DOM_ID_FRETBOARD);
const playAgainBtn = document.getElementById(DOM_ID_PLAY_AGAIN_BTN);
const promptDiv = document.getElementById(DOM_ID_PROMPT);
const messageDiv = document.getElementById(DOM_ID_MESSAGE);
const scoreDiv = document.getElementById(DOM_ID_SCORE);
const chromaticScaleDiv = document.getElementById(DOM_ID_CHROMATIC_SCALE);
if (!fretboardDiv || !promptDiv || !playAgainBtn || !messageDiv || !scoreDiv || !chromaticScaleDiv) {
console.error("CRITICAL ERROR: One or more essential game HTML elements are missing. Check IDs like 'fretboard', 'prompt', etc.");
alert("Error: Game elements missing. Cannot start Bono's Fret Blaster.");
return;
}
// --- Game State ---
let gameState = {
currentNoteToFind: '',
correctAnswers: 0,
attemptsMade: 0,
isGameActive: true
};
// --- Audio Context and Sound Functions ---
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;
try {
audioCtx = new AudioContext();
} catch (e) {
console.warn("Web Audio API is not supported or could not be initialized. Sound will be disabled.", e);
var playSound = function() {};
var playCorrectSound = function() {};
var playIncorrectSound = function() {};
}
if (audioCtx) {
function playSoundOriginal(frequency, duration, type = 'sine', volume = 0.1) {
if (!audioCtx || audioCtx.state === 'suspended') {
audioCtx && audioCtx.resume().catch(err => console.warn("Failed to resume audio context:", err));
}
if (!audioCtx || audioCtx.state !== 'running') {
return;
}
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = type;
oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(volume, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + duration);
}
playCorrectSound = function() {
playSoundOriginal(880, 0.15, 'triangle', SOUND_VOLUME_CORRECT_PRIMARY);
setTimeout(() => playSoundOriginal(1320, 0.15, 'triangle', SOUND_VOLUME_CORRECT_SECONDARY), 50);
};
playIncorrectSound = function() {
playSoundOriginal(150, 0.2, 'square', SOUND_VOLUME_INCORRECT);
};
playSound = playSoundOriginal;
}
// --- Fretboard Building ---
function buildFretboard() {
fretboardDiv.innerHTML = ''; // Clear previous board
// Fret labels (0/Open through NUM_FRETS_TO_DISPLAY - 1)
for (let fret = 0; fret < NUM_FRETS_TO_DISPLAY; fret++) {
const fretLabel = document.createElement('div');
fretLabel.textContent = fret === 0 ? 'Open' : fret;
fretLabel.classList.add(CSS_CLASS_FRET_CELL, CSS_CLASS_FRET_LABEL);
fretLabel.style.gridRow = 1;
fretLabel.style.gridColumn = fret + 1;
fretboardDiv.appendChild(fretLabel);
}
// String rows (high E string at the top visually)
for (let stringIdx = NUM_STRINGS - 1; stringIdx >= 0; stringIdx--) {
const visualGridRow = 2 + ((NUM_STRINGS - 1) - stringIdx);
// Fret cells for this string
for (let fret = 0; fret < NUM_FRETS_TO_DISPLAY; fret++) {
const fretDiv = document.createElement('div');
fretDiv.classList.add(CSS_CLASS_FRET_CELL, CSS_CLASS_FRET);
fretDiv.dataset.string = stringIdx;
fretDiv.dataset.fret = fret;
// Set text content based on fret number
if (fret === 0) { // Open string
fretDiv.classList.add(CSS_CLASS_INITIAL_CORRECT);
fretDiv.textContent = STANDARD_TUNING[stringIdx];
} else {
fretDiv.textContent = '';
}
fretDiv.style.gridRow = visualGridRow;
fretDiv.style.gridColumn = fret + 1;
fretboardDiv.appendChild(fretDiv);
}
}
}
// --- Game Logic ---
function pickRandomNote() {
const randomStringIdx = Math.floor(Math.random() * NUM_STRINGS);
const randomFret = Math.floor(Math.random() * NUM_FRETS_TO_DISPLAY);
return getNoteFor(randomStringIdx, randomFret);
}
function updateScore() {
scoreDiv.textContent = \`Score: \${gameState.correctAnswers} / \${gameState.attemptsMade} (Best out of \${MAX_TRIES})\`;
}
function resetCellStatesForNewGame() {
document.querySelectorAll(\`.\${CSS_CLASS_FRET}\`).forEach(el => {
el.classList.remove(CSS_CLASS_CORRECT, CSS_CLASS_WRONG);
});
}
function newRound() {
if (gameState.attemptsMade >= MAX_TRIES) {
promptDiv.textContent = \`Game Over! Final score: \${gameState.correctAnswers} / \${MAX_TRIES}.\`;
messageDiv.textContent = 'Well done!';
gameState.isGameActive = false;
playAgainBtn.style.display = 'inline-block';
updateScore();
return;
}
document.querySelectorAll(\`.\${CSS_CLASS_FRET}.\${CSS_CLASS_WRONG}\`).forEach(el => {
el.classList.remove(CSS_CLASS_WRONG);
if (Number(el.dataset.fret) !== 0 && !el.classList.contains(CSS_CLASS_CORRECT)) {
el.textContent = '';
} else if (Number(el.dataset.fret) === 0) {
el.textContent = STANDARD_TUNING[Number(el.dataset.string)];
}
});
gameState.currentNoteToFind = pickRandomNote();
promptDiv.textContent = \`Find the note: \${gameState.currentNoteToFind}\`;
messageDiv.textContent = '';
gameState.isGameActive = true;
updateScore();
}
fretboardDiv.addEventListener('click', e => {
if (!gameState.isGameActive || !e.target.classList.contains(CSS_CLASS_FRET) || e.target.classList.contains(CSS_CLASS_INITIAL_CORRECT)) {
return;
}
const clickedFretCell = e.target;
const stringNum = Number(clickedFretCell.dataset.string);
const fretNum = Number(clickedFretCell.dataset.fret);
const selectedNote = getNoteFor(stringNum, fretNum);
if (clickedFretCell.classList.contains(CSS_CLASS_CORRECT)) {
const previouslyCorrectNoteText = clickedFretCell.textContent;
messageDiv.textContent = \`That's \${previouslyCorrectNoteText}, found earlier. Still looking for \${gameState.currentNoteToFind}.\`;
return;
}
gameState.attemptsMade++;
if (selectedNote === gameState.currentNoteToFind) {
clickedFretCell.classList.remove(CSS_CLASS_WRONG);
clickedFretCell.classList.add(CSS_CLASS_CORRECT);
clickedFretCell.textContent = selectedNote;
messageDiv.textContent = 'Correct! 🎉 Finding next note...';
gameState.correctAnswers++;
if (typeof playCorrectSound === 'function') playCorrectSound();
newRound();
} else {
clickedFretCell.classList.add(CSS_CLASS_WRONG);
clickedFretCell.textContent = selectedNote;
messageDiv.textContent = \`Incorrect. That fret is \${selectedNote}. Target: \${gameState.currentNoteToFind}.\`;
if (typeof playIncorrectSound === 'function') playIncorrectSound();
updateScore();
setTimeout(() => {
if (clickedFretCell.classList.contains(CSS_CLASS_WRONG)) {
clickedFretCell.classList.remove(CSS_CLASS_WRONG);
if (fretNum !== 0 && !clickedFretCell.classList.contains(CSS_CLASS_CORRECT)) {
clickedFretCell.textContent = '';
} else if (fretNum === 0) {
clickedFretCell.textContent = STANDARD_TUNING[stringNum];
}
}
}, 1000);
if (gameState.attemptsMade >= MAX_TRIES) {
newRound();
}
}
});
playAgainBtn.addEventListener('click', () => {
gameState.correctAnswers = 0;
gameState.attemptsMade = 0;
gameState.isGameActive = true;
playAgainBtn.style.display = 'none';
resetCellStatesForNewGame();
buildFretboard();
newRound();
});
function renderChromaticScale() {
chromaticScaleDiv.innerHTML = '<h2>Chromatic Scale</h2>';
CHROMATIC_NOTES.forEach(note => {
const noteDiv = document.createElement('div');
noteDiv.className = 'note-box' + (note.includes('#') ? ' sharp' : '');
noteDiv.textContent = note;
chromaticScaleDiv.appendChild(noteDiv);
});
}
// --- Initialize Game ---
buildFretboard();
renderChromaticScale();
newRound();
})();
`;
const launchBtn = document.getElementById('launchBtn');
const gameContainer = document.getElementById('gameContainer');
const mainTitle = document.getElementById('mainTitle'); // Get the main title element
launchBtn.addEventListener('click', () => {
if (gameContainer.innerHTML !== '') return;
launchBtn.disabled = true;
launchBtn.textContent = 'Loading Game...';
// Hide the main title when the game launches
if (mainTitle) {
mainTitle.style.display = 'none';
}
if (window.AudioContext || window.webkitAudioContext) {
let tempAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (tempAudioCtx.state === 'suspended') {
tempAudioCtx.resume().then(() => {
tempAudioCtx.close();
}).catch(err => {
tempAudioCtx.close();
});
} else {
tempAudioCtx.close();
}
}
gameContainer.innerHTML = gameHTML;
const gameScriptElement = document.createElement('script');
gameScriptElement.textContent = gameScriptContent;
gameContainer.appendChild(gameScriptElement);
launchBtn.style.display = 'none';
});
</script>
</body>
</html>