aboutsummaryrefslogtreecommitdiff
path: root/static/bs/tutorial/tutorial.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/bs/tutorial/tutorial.js')
-rw-r--r--static/bs/tutorial/tutorial.js1011
1 files changed, 0 insertions, 1011 deletions
diff --git a/static/bs/tutorial/tutorial.js b/static/bs/tutorial/tutorial.js
deleted file mode 100644
index 26bd261..0000000
--- a/static/bs/tutorial/tutorial.js
+++ /dev/null
@@ -1,1011 +0,0 @@
-class BracketCityTutorial {
- /**
- * Constructor for the tutorial mode
- * @param {HTMLElement} rootElement - The container element for the tutorial
- */
- constructor(rootElement) {
- this.root = rootElement;
-
- // Detect device type
- this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
-
- // Track keyboard layout for mobile
- this.isAltKeyboard = false;
-
- // Hard-coded tutorial puzzle data
- this.PUZZLE_DATA = {
- initialPuzzle: "[where [opposite of clean] dishes pile up] or [exercise in a [game played with a cue ball]]",
- puzzleDate: "Tutorial Mode",
- completionText: "🎉 Tutorial Complete! 🎉",
- solutions: {
- "exercise in a pool": "swim",
- "game played with a cue ball": "pool",
- "where dirty dishes pile up": "sink",
- "opposite of clean": "dirty"
- }
- };
-
- // Initialize the game state
- this.state = {
- displayState: this.PUZZLE_DATA.initialPuzzle,
- solvedExpressions: new Set(),
- solvedOrder: [], // New array to track order
- message: '',
- totalKeystrokes: 0,
- activeClues: this.findActiveClues(this.PUZZLE_DATA.initialPuzzle),
- hintModeClues: new Set(),
- peekedClues: new Set(),
- megaPeekedClues: new Set(),
- tutorialStep: 0
- };
-
- // Tutorial guidance messages for each step
- this.tutorialSteps = [
- {
- message: "In Bracket Shitty you can solve <strong>any clue</strong> just by submitting an answer<div class='small-break'></div>No need to click, just type the answer to any <span class='spotlight'>highlighted clue</span> and hit enter!<div class='small-break'></div>Keep guessing until you get one!",
- highlight: "clue"
- },
- {
- message: "Nice! <span class='correct'>pool</span> is correct!<div class='small-break'></div>Clues are often <strong>nested</strong> within other clues<div class='small-break'></div>You'll need to solve <span class='spotlight'>opposite of clean</span> to reveal its parent clue about dishes",
- condition: expressions => expressions.size === 1 && expressions.has("game played with a cue ball"),
- highlight: "none"
- },
- {
- message: "Nice! <span class='correct'>dirty</span> is correct!<div class='small-break'></div>Clues are often <strong>nested</strong> within other clues<div class='small-break'></div>Now solve <span class='spotlight'>game played with a cue ball</span> to reveal the parent clue about exercise",
- condition: expressions => expressions.size === 1 && expressions.has("opposite of clean"),
- highlight: "none"
- },
- {
- message: "Excellent! <span class='correct'>pool</span> is correct!<div class='small-break'></div>Now both <strong>parent clues</strong> are revealed, so they are both <span class='spotlight'>highlighted</span> and solvable<div class='small-break'></div>Go ahead and solve one",
- condition: expressions => expressions.size === 2 && expressions.has("opposite of clean") && expressions.has("game played with a cue ball") && !expressions.has("where dirty dishes pile up") && !expressions.has("exercise in a pool") && this.state.solvedOrder[0] === "opposite of clean",
- highlight: "none"
- },
- {
- message: "Excellent! <span class='correct'>dirty</span> is correct!<div class='small-break'></div>Now both <strong>parent clues</strong> are revealed, so they are both <span class='spotlight'>highlighted</span> and solvable<div class='small-break'></div>Go ahead and solve one",
- condition: expressions => expressions.size === 2 && expressions.has("opposite of clean") && expressions.has("game played with a cue ball") && !expressions.has("where dirty dishes pile up") && !expressions.has("exercise in a pool") && this.state.solvedOrder[0] === "game played with a cue ball",
- highlight: "none"
- },
- {
- message: "Ok fine you got <span class='correct'>sink</span> instead<div class='small-break'></div>You still need to solve <span class='spotlight'>game played with a cue ball</span><div class='small-break'></div>Looking at the <strong>parent clue</strong> text can help...",
- condition: expressions => expressions.size === 2 && expressions.has("opposite of clean") && expressions.has("where dirty dishes pile up") && !expressions.has("game played with a cue ball") && !expressions.has("exercise in a pool"),
- highlight: "none"
- },
- {
- message: "Ok fine you got <span class='correct'>swim</span> instead<div class='small-break'></div>You still need to solve <span class='spotlight'>opposite of clean</span><div class='small-break'></div>Looking at the <strong>parent clue</strong> text can help...",
- condition: expressions => expressions.size === 2 && expressions.has("exercise in a pool") && expressions.has("game played with a cue ball") && !expressions.has("opposite of clean") && !expressions.has("where dirty dishes pile up"),
- highlight: "none"
- },
- {
- message: "Yee-haw <span class='correct'>swim</span> is right. Only one more <span class='spotlight'>clue</span> to go...<div class='small-break'></div>Try clicking it to reveal the first letter, that's called a <strong>\"peek\"</strong><div class='small-break'></div>Then go ahead and finish!",
- condition: expressions => expressions.size === 3 && expressions.has("exercise in a pool") && expressions.has("game played with a cue ball") && expressions.has("opposite of clean") && !expressions.has("where dirty dishes pile up") && this.state.solvedOrder[2] === "exercise in a pool",
- highlight: "none"
- },
- {
- message: "Yee-haw <span class='correct'>sink</span> is right. Only one more <span class='spotlight'>clue</span> to go...<div class='small-break'></div>Try clicking it to reveal the first letter, that's called a <strong>\"peek\"</strong><div class='small-break'></div>Then go ahead and finish!",
- condition: expressions => expressions.size === 3 && expressions.has("where dirty dishes pile up") && expressions.has("game played with a cue ball") && expressions.has("opposite of clean") && !expressions.has("exercise in a pool") && this.state.solvedOrder[2] === "where dirty dishes pile up",
- highlight: "none"
- },
- {
- message: "Yee-haw <span class='correct'>dirty</span> is right. Only one more <span class='spotlight'>clue</span> to go...<div class='small-break'></div>Try clicking it to reveal the first letter, that's called a <strong>\"peek\"</strong><div class='small-break'></div>Then go ahead and finish!",
- condition: expressions => expressions.size === 3 && expressions.has("opposite of clean") && expressions.has("game played with a cue ball") && expressions.has("exercise in a pool") && this.state.solvedOrder[2] === "opposite of clean",
- highlight: "none"
- },
- {
- message: "Yee-haw <span class='correct'>pool</span> is right. Only one more <span class='spotlight'>clue</span> to go...<div class='small-break'></div>Try clicking it to reveal the first letter, that's called a <strong>\"peek\"</strong><div class='small-break'></div>Then go ahead and finish!",
- condition: expressions => expressions.size === 3 && expressions.has("opposite of clean") && expressions.has("game played with a cue ball") && expressions.has("where dirty dishes pile up") && this.state.solvedOrder[2] === "game played with a cue ball",
- highlight: "none"
- },
- {
- message: "<strong>Just to recap:</strong><div class='small-break'></div>* you can solve any <span class='spotlight'>highlighted clue</span> \u{2013} just type your guess and hit enter<div class='small-break'></div>* click once on a clue to <strong>peek</strong> at the first letter, twice to <span class='correct'>reveal</span> the answer",
- condition: expressions => !this.state.displayState.includes('['),
- highlight: "none",
- isComplete: true
- }
- ];
-
- // Initialize the UI
- this.initializeGame();
- }
-
-
- /**
- * Initializes the tutorial game
- */
- initializeGame() {
- // Generate the initial HTML
- this.root.innerHTML = this.generateInitialHTML();
-
- // Setup input handlers
- this.setupInputHandlers();
-
- // Render the initial state
- this.render();
-
- // Setup tutorial guidance
- this.updateTutorialGuidance();
-
- // Setup clue click handlers for peeking
- this.setupClueClickHandlers();
- }
-
- /**
- * Generates the initial HTML for the tutorial
- */
- generateInitialHTML() {
- return `
- <div class="puzzle-container">
- ${this.generateHeaderHTML()}
- <div class="puzzle-content">
- ${this.generateTutorialGuidanceHTML()}
- ${this.generatePuzzleDisplayHTML()}
-
- ${this.generateInputContainerHTML()}
- </div>
- </div>
- `;
- }
-
- /**
- * Generates the header HTML
- */
- generateHeaderHTML() {
- return `
- <div class="puzzle-header">
- <h1>[Bracket Shitty]</h1>
- <button class="exit-button">X</button>
- <div class="nav-container">
- <div class="puzzle-date">How To Play</div>
- </div>
- </div>
- `;
- }
-
- /**
- * Generates the puzzle display HTML with fixed height
- */
- generatePuzzleDisplayHTML() {
- return `<div class="puzzle-display tutorial-puzzle-display"></div>`;
- }
-
- /**
- * Generates the tutorial guidance container HTML
- */
- generateTutorialGuidanceHTML() {
- return `
- <div class="tutorial-guidance">
- <div class="tutorial-message"></div>
- </div>
- `;
- }
-
- /**
- * Generates the input container HTML based on device type
- */
- generateInputContainerHTML() {
- if (this.isMobile) {
- return `
- <div class="input-container">
- <div class="message"></div>
- <div class="input-submit-wrapper" style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
- <div class="custom-input" style="flex-grow: 1; height: 44px; display: flex; align-items: center;">
- <span class="placeholder">type any answer...</span>
- </div>
- <button
- class="mobile-submit-button"
- style="height: 44px; background-color: #2563eb; color: white; padding: 0 1rem; border-radius: 6px; font-weight: 500; border: none;"
- >
- [submit]
- </button>
- </div>
- <div class="custom-keyboard">
- ${this.generateKeyboardButtonsHTML()}
- </div>
- </div>
- `;
- }
-
- return `
- <div class="input-container">
- <div class="input-submit-wrapper" style="display: flex; gap: 0.5rem;">
- <input
- type="text"
- placeholder="type any answer..."
- class="answer-input"
- autocomplete="off"
- autocapitalize="off"
- >
- <button
- class="submit-answer"
- >
- [submit]
- </button>
- </div>
- <div class="message"></div>
- </div>
- `;
- }
-
- /**
- * Generates mobile keyboard buttons HTML
- */
- generateKeyboardButtonsHTML() {
- const mainLayout = [
- { keys: 'qwertyuiop', columns: 10 },
- { keys: 'asdfghjkl', columns: 10 },
- { keys: [
- { key: '123', display: '123', small: true },
- 'z', 'x', 'c', 'v', 'b', 'n', 'm',
- { key: 'backspace', display: '\u{232B}', wide: true }
- ], columns: 10 }
- ];
-
- const altLayout = [
- { keys: '1234567890', columns: 10 },
- { keys: '-/:;()$&@"', columns: 10 },
- { keys: [
- { key: 'ABC', display: 'ABC', small: true },
- '.', ',', '?', '!', "'", "=", "+",
- { key: 'backspace', display: '\u{232B}', wide: true }
- ], columns: 10 }
- ];
-
- const rows = this.isAltKeyboard ? altLayout : mainLayout;
-
- let html = ``;
-
- rows.forEach((row, rowIndex) => {
- // Adding margin for middle row on default layout since it's only 9 keys vs 10
- const leftPadding = (rowIndex === 1 && !this.isAltKeyboard) ? 'margin-left: 5%' : '';
- html += `<div class="keyboard-row" style="${leftPadding}">`;
-
- const keys = Array.isArray(row.keys) ? row.keys : row.keys.split('');
- keys.forEach(keyItem => {
- if (typeof keyItem === 'string') {
- html += this.generateKeyboardKeyHTML(keyItem);
- } else {
- html += this.generateSpecialKeyHTML(keyItem);
- }
- });
-
- html += '</div>';
- });
-
- return html;
- }
-
- /**
- * Generates a special keyboard key HTML
- */
- generateSpecialKeyHTML(keyItem) {
- const fontSize = keyItem.small ? '0.875rem' : '1.125rem';
-
- return `
- <button class="keyboard-key" data-key="${keyItem.key}" style="
- grid-column: ${keyItem.wide ? 'span 2' : 'span 1'};
- font-size: ${fontSize};
- ">
- ${keyItem.display}
- </button>
- `;
- }
-
- /**
- * Generates a regular keyboard key HTML
- */
- generateKeyboardKeyHTML(key) {
- return `
- <button class="keyboard-key" data-key="${key}">
- ${key}
- </button>
- `;
- }
-
- /**
- * Sets up input handlers for desktop or mobile
- */
- setupInputHandlers() {
- if (this.isMobile) {
- this.setupMobileInput();
- } else {
- this.setupDesktopInput();
- }
-
- // Setup exit button (formerly help button)
- const exitButton = this.root.querySelector('.exit-button');
- if (exitButton) {
- exitButton.addEventListener('click', () => {
- if (confirm('Are you sure you want to exit the tutorial?')) {
- window.location.href = '/bs';
- }
- });
- }
- }
-
- /**
- * Sets up desktop input handling
- */
- setupDesktopInput() {
- this.answerInput = this.root.querySelector('.answer-input');
- const submitButton = this.root.querySelector('.submit-answer');
-
- if (!this.answerInput || !submitButton) {
- console.error('Required input elements not found');
- return;
- }
-
- // Handle submit button click
- submitButton.addEventListener('click', () => {
- this.handleSubmission();
- });
-
- // Handle Enter key with visual feedback
- this.answerInput.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
-
- // Apply visual feedback style directly to the button
- submitButton.style.transform = 'scale(0.95)';
- submitButton.style.backgroundColor = '#1e40af';
-
- // Process the submission
- this.handleSubmission();
-
- // Remove the styles after a short delay
- setTimeout(() => {
- submitButton.style.transform = '';
- submitButton.style.backgroundColor = '';
- }, 85); // Slightly longer for better visibility
- }
- });
-
- this.answerInput.addEventListener('keydown', (e) => {
- this.handleKeyDown(e);
- });
-
- this.answerInput.addEventListener('paste', (e) => {
- e.preventDefault();
- });
-
- this.answerInput.focus();
- }
-
- /**
- * Sets up mobile input handling
- */
- setupMobileInput() {
- this.customInputValue = '';
- this.customInputEl = this.root.querySelector('.custom-input');
-
- if (!this.customInputEl) {
- console.error('Custom input display element not found');
- return;
- }
-
- // Clean up old placeholder if it exists
- const oldPlaceholder = this.customInputEl.querySelector('.placeholder');
- if (oldPlaceholder) {
- oldPlaceholder.remove();
- }
-
- // Create fresh placeholder
- const placeholderEl = document.createElement('span');
- placeholderEl.className = 'placeholder';
- placeholderEl.style.color = '#9ca3af';
- placeholderEl.textContent = 'type any answer...';
- this.customInputEl.appendChild(placeholderEl);
-
- // Set up the mobile submit button
- const mobileSubmitButton = this.root.querySelector('.mobile-submit-button');
- if (mobileSubmitButton) {
- mobileSubmitButton.addEventListener('click', () => {
- this.handleSubmission();
- });
- }
-
- // Attach fresh click listeners to keyboard keys
- const keyboardKeys = this.root.querySelectorAll('.keyboard-key');
- keyboardKeys.forEach(keyEl => {
- keyEl.addEventListener('click', () => {
- const key = keyEl.getAttribute('data-key');
- if (key === 'backspace') {
- this.customInputValue = this.customInputValue.slice(0, -1);
- } else if (key === '123' || key === 'ABC') {
- this.isAltKeyboard = !this.isAltKeyboard;
- // Re-render the keyboard
- const keyboardContainer = this.root.querySelector('.custom-keyboard');
- if (keyboardContainer) {
- keyboardContainer.innerHTML = this.generateKeyboardButtonsHTML();
- }
- // Re-attach event listeners while preserving input
- this.setupMobileInput();
- return;
- } else {
- this.customInputValue += key;
- this.state.totalKeystrokes++;
- }
-
- this.updateCustomInputDisplay();
- });
-
- // Prevent dragging/swiping on the key
- keyEl.addEventListener('touchmove', (e) => {
- e.preventDefault();
- }, { passive: false });
- });
-
- // Enable :active states on iOS
- document.addEventListener('touchstart', () => {}, false);
-
- // Update display to show current input value
- this.updateCustomInputDisplay();
- }
-
- handleSubmission() {
- const input = this.isMobile ? this.customInputValue : this.answerInput.value;
- if (!input || !input.trim()) return;
-
- const normalizedInput = this.normalizeInput(input);
- const match = this.findMatchingExpression(normalizedInput);
-
- if (match) {
- this.solveExpression(match);
- }
-
- // Clear input regardless of match
- if (this.isMobile) {
- this.customInputValue = '';
- this.updateCustomInputDisplay();
- } else {
- this.answerInput.value = '';
- this.answerInput.focus();
- }
- }
- /**
- * Updates the custom input display for mobile
- */
- updateCustomInputDisplay() {
- if (!this.customInputEl) return;
-
- // Find or create placeholder
- let placeholderEl = this.customInputEl.querySelector('.placeholder');
- if (!placeholderEl) {
- placeholderEl = document.createElement('span');
- placeholderEl.className = 'placeholder';
- placeholderEl.style.color = '#9ca3af';
- placeholderEl.textContent = 'start typing answers...';
- }
-
- // Clear the input
- this.customInputEl.innerHTML = '';
-
- // Show either placeholder or input value
- if (this.customInputValue.length > 0) {
- this.customInputEl.textContent = this.customInputValue;
- } else {
- this.customInputEl.appendChild(placeholderEl);
- }
- }
-
- /**
- * Sets up clue click handlers for peeking functionality
- */
- setupClueClickHandlers() {
- const puzzleDisplay = this.root.querySelector('.puzzle-display');
- if (!puzzleDisplay) return;
-
- puzzleDisplay.addEventListener('click', (event) => {
- const clueElement = event.target.closest('.active-clue');
- if (!clueElement) return;
-
- // Find the clicked expression
- const cleanText = this.state.displayState.replace(/<\/?[^>]+(>|$)/g, '');
- const activeClues = this.findActiveClues(cleanText);
- const cluePosition = Array.from(puzzleDisplay.querySelectorAll('.active-clue')).indexOf(clueElement);
-
- if (cluePosition >= 0 && cluePosition < activeClues.length) {
- const expression = activeClues[cluePosition].expression.trim();
- this.toggleHintMode(expression);
-
- // Return focus to input on desktop
- if (!this.isMobile && this.answerInput) {
- this.answerInput.focus();
- }
- }
- });
- }
-
- /**
- * Shows the help dialog
- */
- showHelp() {
- const helpContent = `
- <div style="text-align: center; font-weight: bold; margin: 0 0 0.5em 0;">Welcome to the Bracket Shitty Tutorial</div>
- <div style="margin: 0.9em 0;">* In Bracket Shitty, you solve <mark style="background-color: #fff9c4;">[active clues]</mark> by typing the answers</div>
- <div style="margin: 0.9em 0;">* You can tap any <mark style="background-color: #fff9c4;">[active clue]</mark> to peek at its first letter, or tap again to reveal the full answer</div>
- <div style="margin: 0.9em 0;">* This tutorial will guide you through the basics</div>
- <div style="margin: 0.9em 0;">* Tap anywhere to continue</div>
- `;
-
- const puzzleDisplay = this.root.querySelector('.puzzle-display');
- if (!puzzleDisplay) return;
-
- // Store the current content
- this.previousDisplayContent = puzzleDisplay.innerHTML;
-
- // Show help content
- puzzleDisplay.innerHTML = helpContent;
-
- // Add click handler to close help
- const closeHandler = () => {
- puzzleDisplay.innerHTML = this.previousDisplayContent;
- puzzleDisplay.removeEventListener('click', closeHandler);
- // Return focus to input on desktop
- if (!this.isMobile && this.answerInput) {
- this.answerInput.focus();
- }
- };
-
- puzzleDisplay.addEventListener('click', closeHandler);
- }
-
- /**
- * Main render method to update the UI
- */
- render() {
- const puzzleDisplay = this.root.querySelector('.puzzle-display');
- if (!puzzleDisplay) return;
-
- // Apply highlighting to active clues
- const highlightedState = this.applyActiveClueHighlights(this.state.displayState);
- puzzleDisplay.innerHTML = highlightedState;
-
- // Check if puzzle is complete
- if (this.isPuzzleComplete()) {
- this.renderCompletionState();
- }
- }
-
- /**
- * Applies highlighting to active clues
- * @param {string} text - Current puzzle text
- * @returns {string} HTML with highlights applied
- */
- applyActiveClueHighlights(text) {
- const cleanText = text.replace(/<\/?span[^>]*(>|$)/g, '');
- const activeClues = this.findActiveClues(cleanText);
-
- // Sort clues by start position in reverse order
- activeClues.sort((a, b) => b.start - a.start);
-
- let highlightedText = cleanText;
-
- activeClues.forEach(clue => {
- const expressionText = clue.expression.trim();
- const before = highlightedText.slice(0, clue.start);
- let clueText = highlightedText.slice(clue.start, clue.end);
- const after = highlightedText.slice(clue.end);
-
- // Handle different clue states
- if (!clueText.includes('[') && !this.state.hintModeClues.has(expressionText)) {
- // Already solved clue
- highlightedText = before + clueText + after;
- } else {
- // Active or hint mode clue
- if (this.state.hintModeClues.has(expressionText)) {
- const solution = this.PUZZLE_DATA.solutions[expressionText];
- // Show just the first letter
- if (solution && !clueText.includes(`(`)) {
- // Get the first letter of the solution
- const firstLetter = solution.charAt(0).toUpperCase();
- // Remove closing bracket, add first letter hint, re-add closing bracket
- clueText = clueText.slice(0, -1) + ` (${firstLetter})]`;
- }
- }
-
- // Ensure we're only wrapping the exact clue text with brackets included
- highlightedText = before + `<span class="active-clue">${clueText}</span>` + after;
- }
- });
-
- return highlightedText;
- }
-
- /**
- * Renders the completion state while maintaining fixed height
- */
- renderCompletionState() {
- const inputContainer = this.root.querySelector('.input-container');
- const puzzleDisplay = this.root.querySelector('.puzzle-display');
-
- if (!inputContainer || !puzzleDisplay) return;
-
- // Hide input container
- inputContainer.style.display = 'none';
-
- // Add completion class but ensure height is preserved
- puzzleDisplay.classList.add('completed');
-
- // Add a tutorial completion message at the top
- const completionHTML = `
- <div class="tutorial-completion">
- <div class="tutorial-completion-title">🎉 Tutorial Complete! 🎉</div>
- <div class="tutorial-completion-text">Ready to play the full game?</div>
- <button id="playGameButton">Play Bracket Shitty</button>
- </div>
- `;
-
- // Get the puzzle content container
- const puzzleContent = this.root.querySelector('.puzzle-content');
-
- // Create a wrapper for the completion message
- const completionWrapper = document.createElement('div');
- completionWrapper.className = 'tutorial-completion-wrapper';
- completionWrapper.innerHTML = completionHTML;
-
- // Insert the completion message at the top of puzzle content
- if (puzzleContent) {
- puzzleContent.insertBefore(completionWrapper, puzzleContent.firstChild);
- }
-
- // Add event listener to the play button
- const playButton = this.root.querySelector('#playGameButton');
- if (playButton) {
- playButton.addEventListener('click', () => {
- window.location.href = '/bs';
- });
- }
- }
-
- /**
- * Updates the tutorial guidance based on the current state
- */
- updateTutorialGuidance() {
- const tutorialGuidance = this.root.querySelector('.tutorial-guidance');
- if (!tutorialGuidance) return;
-
- // Find the appropriate tutorial step
- let currentStep = this.tutorialSteps[0]; // Default to first step
-
- for (let i = this.tutorialSteps.length - 1; i >= 0; i--) {
- const step = this.tutorialSteps[i];
-
- // If this step has a condition and it's satisfied, use this step
- if (step.condition && step.condition(this.state.solvedExpressions)) {
- currentStep = step;
- break;
- }
- }
-
- // Clone and replace the guidance element to restart animation
- const newGuidance = tutorialGuidance.cloneNode(true);
- const messageElement = document.createElement('div');
- messageElement.className = 'tutorial-message';
- messageElement.innerHTML = currentStep.message;
-
- newGuidance.innerHTML = ''; // Clear existing content
- newGuidance.appendChild(messageElement);
- newGuidance.classList.add('emerge');
-
- tutorialGuidance.parentNode.replaceChild(newGuidance, tutorialGuidance);
-
- // Remove all highlight classes first
- const allClues = this.root.querySelectorAll('.active-clue');
- allClues.forEach(clue => {
- clue.classList.remove('tutorial-highlight', 'tap-hint');
- });
-
- // Add highlighting based on the current step
- if (currentStep.highlight === 'clue') {
- // Highlight active clues more prominently using CSS class
- const activeClues = this.root.querySelectorAll('.active-clue');
- activeClues.forEach(clue => {
- clue.classList.add('tutorial-highlight');
- });
- } else if (currentStep.highlight === 'hint') {
- // Add a subtle animation to suggest tapping a clue using CSS class
- const activeClues = this.root.querySelectorAll('.active-clue');
- if (activeClues.length > 0) {
- activeClues[0].classList.add('tap-hint');
- }
- }
- }
-
- /**
- * Determines if a keystroke should be counted
- * @param {KeyboardEvent} event - The keyboard event
- * @returns {boolean} Whether the keystroke should be counted
- */
- isCountableKeystroke(event) {
- // Only count single printable characters
- return event.key.length === 1 &&
- /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/? ]$/.test(event.key);
- }
-
- /**
- * Finds all active clues in the current puzzle state
- * @param {string} puzzleText - Current puzzle text
- * @returns {Array} Array of clue objects
- */
- findActiveClues(puzzleText) {
- const activeClues = [];
-
- function findClues(str, startOffset = 0) {
- let i = 0;
- while (i < str.length) {
- if (str[i] === '[') {
- const startIndex = i;
- let bracketCount = 1;
- let hasNestedBrackets = false;
- i++;
-
- let innerContent = '';
- while (i < str.length && bracketCount > 0) {
- if (str[i] === '[') {
- bracketCount++;
- hasNestedBrackets = true;
- } else if (str[i] === ']') {
- bracketCount--;
- }
- innerContent += str[i];
- i++;
- }
-
- innerContent = innerContent.slice(0, -1);
-
- if (!hasNestedBrackets) {
- // Clean expression text of HTML markup
- const cleanExpression = innerContent.replace(/<\/?[^>]+(>|$)/g, '');
- activeClues.push({
- start: startOffset + startIndex,
- end: startOffset + i,
- text: str.substring(startIndex, i),
- expression: cleanExpression
- });
- }
-
- if (hasNestedBrackets) {
- findClues(innerContent, startOffset + startIndex + 1);
- }
- } else {
- i++;
- }
- }
- }
-
- const cleanText = puzzleText.replace(/<\/?span[^>]*(>|$)/g, '');
- findClues(cleanText);
- return activeClues;
- }
-
- /**
- * Finds available expressions that can be solved
- * @param {string} text - Current puzzle text
- * @returns {Array} Array of expression objects
- */
- findAvailableExpressions(text) {
- const cleanText = text.replace(/<\/?span[^>]*(>|$)/g, '');
- const results = [];
- const regex = /\[([^\[\]]+?)\]/g;
- const matchedPositions = new Set();
- let match;
-
- while ((match = regex.exec(cleanText)) !== null) {
- const startIdx = match.index;
- const endIdx = startIdx + match[0].length;
-
- if (!matchedPositions.has(startIdx)) {
- matchedPositions.add(startIdx);
- results.push({
- expression: match[1],
- startIndex: startIdx,
- endIndex: endIdx
- });
- }
- }
-
- return results;
- }
-
- /**
- * Handles user input for solving expressions
- * @param {string} input - User input string
- */
- handleInput(input) {
- const normalizedInput = this.normalizeInput(input);
- if (!normalizedInput) {
- return;
- }
-
- const match = this.findMatchingExpression(normalizedInput);
- if (!match) {
- return;
- }
-
- this.solveExpression(match);
- }
-
- /**
- * Normalizes and validates user input
- * @param {string} input - Raw user input
- * @returns {string|null} Normalized input or null if invalid
- */
- normalizeInput(input) {
- if (!input?.trim()) {
- return null;
- }
- return input.trim().toLowerCase();
- }
-
- /**
- * Finds an unsolved expression matching the input
- * @param {string} normalizedInput - Normalized user input
- * @returns {Object|null} Matching expression info or null if not found
- */
- findMatchingExpression(normalizedInput) {
- const cleanState = this.state.displayState.replace(/<\/?[^>]+(>|$)/g, '');
- const availableExpressions = this.findAvailableExpressions(cleanState);
-
- for (const {expression, startIndex, endIndex} of availableExpressions) {
- const solution = this.PUZZLE_DATA.solutions[expression];
-
- if (solution?.toLowerCase() === normalizedInput &&
- !this.state.solvedExpressions.has(expression)) {
- return {
- expression,
- solution,
- startIndex,
- endIndex
- };
- }
- }
-
- return null;
- }
-
- /**
- * Processes a correct solution and updates game state
- * @param {Object} match - Expression match information
- */
- solveExpression(match) {
- const { expression, solution, startIndex, endIndex } = match;
-
- // Generate the new puzzle display state by replacing the solved expression
- const cleanState = this.state.displayState.replace(/<\/?[^>]+(>|$)/g, '');
- const escapedSolution = solution
- .replace(/"/g, '&quot;')
- .replace(/'/g, '&#39;');
-
- const newDisplayState =
- cleanState.slice(0, startIndex) +
- `<mark class="solved">${escapedSolution}</mark>` +
- cleanState.slice(endIndex);
-
- // Add to the solved expressions set
- const updatedExpressions = new Set([...this.state.solvedExpressions, expression]);
- this.state.solvedOrder.push(expression);
- // Update game state
- this.state.displayState = newDisplayState;
- this.state.solvedExpressions = updatedExpressions;
-
- // Clear the input based on device type
- if (this.isMobile) {
- this.customInputValue = '';
- this.updateCustomInputDisplay();
- } else {
- if (this.answerInput) {
- this.answerInput.value = '';
- }
- }
-
- // Re-render the puzzle display
- this.render();
-
- // Update tutorial guidance
- this.updateTutorialGuidance();
- }
-
- /**
- * Toggles hint mode for an expression
- * @param {string} expression - The expression to toggle hints for
- */
- toggleHintMode(expression) {
- // Early validation
- if (!expression || this.state.megaPeekedClues.has(expression)) {
- return;
- }
-
- // Handle mega peek (second click)
- if (this.state.hintModeClues.has(expression)) {
- this.handleMegaPeek(expression);
- return;
- }
-
- // Handle first peek
- this.handleFirstPeek(expression);
- }
-
- /**
- * Handles the first peek at an expression (shows first letter)
- * @param {string} expression - The expression to peek at
- */
- handleFirstPeek(expression) {
- this.state.hintModeClues.add(expression);
- this.state.peekedClues.add(expression);
-
- this.render();
- this.updateTutorialGuidance();
- }
-
- /**
- * Handles the mega peek action (reveals answer)
- * @param {string} expression - The expression to reveal
- */
- handleMegaPeek(expression) {
- const solution = this.PUZZLE_DATA.solutions[expression];
- if (!solution) {
- console.error('No solution found for expression:', expression);
- return;
- }
-
- const newDisplayState = this.processSolutionReveal(expression, solution);
- if (!newDisplayState) {
- console.error('Could not find expression in puzzle state');
- return;
- }
-
- // Update state
- this.state.displayState = newDisplayState;
- this.state.megaPeekedClues.add(expression);
- this.state.solvedExpressions.add(expression);
- this.state.hintModeClues.delete(expression);
-
- // Re-render
- this.render();
-
- // Update tutorial guidance
- this.updateTutorialGuidance();
- }
-
- /**
- * Processes a solution reveal for megapeek
- * @param {Object} expression - Expression to reveal
- * @param {string} solution - Solution text
- * @returns {string} New display state
- */
- processSolutionReveal(expression, solution) {
- // Clean the display state first
- const cleanState = this.state.displayState.replace(/<\/?[^>]+(>|$)/g, '');
- const availableExpressions = this.findAvailableExpressions(cleanState);
-
- for (const {startIndex, endIndex} of availableExpressions) {
- const currentExpression = cleanState.slice(startIndex + 1, endIndex - 1).trim();
-
- if (expression === currentExpression) {
- const escapedSolution = solution
- .replace(/"/g, '&quot;')
- .replace(/'/g, '&#39;');
-
- const newDisplayState =
- cleanState.slice(0, startIndex) +
- `<mark class="solved">${escapedSolution}</mark>` +
- cleanState.slice(endIndex);
-
- return newDisplayState;
- }
- }
- return null;
- }
-
- /**
- * Checks if the puzzle is complete
- * @returns {boolean} Whether the puzzle is complete
- */
- isPuzzleComplete() {
- return !this.state.displayState.includes('[');
- }
- }
-
- // Export the class for use in other files
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = { BracketCityTutorial };
- }