From 293df027348307de896232f1b867dd220ae8caa3 Mon Sep 17 00:00:00 2001 From: Nat Lasseter Date: Tue, 1 Apr 2025 11:45:48 +0100 Subject: [bracket] rename --- static/bv/tutorial/index.html | 22 + static/bv/tutorial/tutorial-init.js | 22 + static/bv/tutorial/tutorial.css | 945 ++++++++++++++++++++++++++++++++ static/bv/tutorial/tutorial.js | 1011 +++++++++++++++++++++++++++++++++++ 4 files changed, 2000 insertions(+) create mode 100644 static/bv/tutorial/index.html create mode 100644 static/bv/tutorial/tutorial-init.js create mode 100644 static/bv/tutorial/tutorial.css create mode 100644 static/bv/tutorial/tutorial.js (limited to 'static/bv/tutorial') diff --git a/static/bv/tutorial/index.html b/static/bv/tutorial/index.html new file mode 100644 index 0000000..ac49d70 --- /dev/null +++ b/static/bv/tutorial/index.html @@ -0,0 +1,22 @@ + + + + + + + Bracket Village + + + + + + +
+ + + + + + + + diff --git a/static/bv/tutorial/tutorial-init.js b/static/bv/tutorial/tutorial-init.js new file mode 100644 index 0000000..ba0cc73 --- /dev/null +++ b/static/bv/tutorial/tutorial-init.js @@ -0,0 +1,22 @@ +startTutorialMode(); + + // Helper function to manually start the tutorial mode + function startTutorialMode() { + const container = document.getElementById('bracket-city-container'); + + if (container) { + // Clear any existing content + container.innerHTML = ''; + + // Initialize the tutorial + const tutorial = new BracketCityTutorial(container); + + // Store the tutorial instance in case we need to access it later + window.bracketCityTutorial = tutorial; + + return tutorial; + } else { + console.error('Could not find container element for Bracket Village tutorial'); + return null; + } + } diff --git a/static/bv/tutorial/tutorial.css b/static/bv/tutorial/tutorial.css new file mode 100644 index 0000000..4f2aff0 --- /dev/null +++ b/static/bv/tutorial/tutorial.css @@ -0,0 +1,945 @@ +/* =================== + Root Variables and Resets + =================== */ + :root { + --main-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + --highlight-color: #fefcbf; + } + + *, *::before, *::after { + box-sizing: border-box; + } + + /* =================== + Container Structure + =================== */ + .puzzle-container { + width: 100%; + margin: 0 auto; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + background: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + padding: 0; + display: flex; + flex-direction: column; + } + + @media (min-width: 1024px) { + .puzzle-container { + max-width: 42rem; + } + } + + /* =================== + Header Styles + =================== */ + /* Header Base Styles */ + .puzzle-header { + background: white; + border-bottom: 1px solid #e2e8f0; + box-sizing: border-box; + padding: 0.6rem; + position: relative; /* Desktop default */ + text-align: center; + z-index: 10; + } + + /* Mobile-specific header styles */ + @media (max-width: 640px) { + .puzzle-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + padding: max(0.4rem, env(safe-area-inset-top)); + } + } + + /* Header title */ + .puzzle-header h1 { + margin: 0 0 0.5rem 0; + text-align: center; + font-size: clamp(2rem, 8vw, 3rem); + cursor: pointer; + font-family: var(--main-font); + font-variant: small-caps; + color: #1a202c; + font-weight: 600; + } + + @media (max-width: 640px) { + .puzzle-header h1 { + margin: 0; + font-size: clamp(1.8rem, 7vw, 2.8rem); + } + } + + /* Help button */ + .puzzle-header .exit-button { + position: absolute; + top: 10px; + right: 20px; + background: none; + border: 2px solid #333; + border-radius: 50%; + width: 2.4rem; + height: 2.4rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-weight: bold; + font-size: 1.2rem; + color: #333; + } + + @media (max-width: 640px) { + .puzzle-header .exit-button { + right: 10px; + } + } + + /* Navigation container */ + .puzzle-header .nav-container { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + } + + /* Navigation buttons */ + .puzzle-header .nav-button { + padding: 4px 12px; + border: none; + border-radius: 4px; + background: none; + cursor: pointer; + font-weight: bold; + font-size: 1.5rem; + color: #333; + } + + .puzzle-header .nav-button:disabled { + opacity: 0.3; + cursor: default; + } + + /* Puzzle date */ + .puzzle-header .puzzle-date { + margin: 0; + font-size: 20px; + font-weight: 600; + font-family: var(--main-font); + font-variant: small-caps; + color: #1a202c; + } + + /* =================== + Main Content Area + =================== */ + /* New rules for loading state */ + + .loading { + text-align: center; + padding: 2rem; + } + + .puzzle-content { + padding: 0rem 1rem 1rem 1rem; + display: flex; + flex-direction: column; + gap: 0rem; + width: 100%; + } + + /* =================== + Puzzle Display + =================== */ + .puzzle-display { + width: 100%; + min-height: 40vh; + max-height: 60vh; + background: #f1f5f9; + border-radius: 0.5rem; + padding: 1.5rem; + font-family: var(--mono-font); + font-size: 24px; + line-height: 1.5; + overflow-y: auto; + word-break: keep-all; + overflow-wrap: break-word; + hyphens: none; + transition: all 0.3s ease; + } + + .puzzle-display.completed { + min-height: auto; + max-height: none; + height: auto; + } + + /* =================== + Active Clue & Mark Styles + =================== */ + .active-clue { + background-color: rgba(255, 255, 0, 0.2); + border-radius: 3px; + padding: 2px 4px; + margin: -2px -4px; + transition: background-color 0.3s ease; + cursor: pointer; + } + + .active-clue:hover { + background-color: rgba(255, 255, 0, 0.3); + } + + mark.solved { + background-color: rgba(0, 255, 0, 0.2); + border-radius: 3px; + padding: 2px 4px; + margin: -2px -4px; + } + + mark { + background-color: transparent; + color: inherit; + } + + /* =================== + Input Styles + =================== */ + .input-container { + width: 100%; + margin: 10px 0 0 0; + position: relative; + } + + .answer-input { + width: 100%; + padding: 16px; + font-size: 16px !important; + border: 1px solid #e2e8f0; + border-radius: 6px; + margin-bottom: 0px; + box-sizing: border-box; + font-family: var(--mono-font); + } + + /* =================== + Message Styles + =================== */ + .message { + padding: 0.5rem; + margin: 0.5rem 0; + border-radius: 0.375rem; + text-align: center; + font-family: var(--mono-font); + display: none; + } + + .message.success { + background-color: #c6f6d5; + color: #1a202c; + overflow-wrap: anywhere; + font-size: 1.2rem; + display: block; + padding: 20px; + line-height: 2rem; + } + + .message.success .completion-link { + color: #2563eb; + text-decoration: underline; + } + + .message.error { + background-color: #fee2e2; + color: #ef4444; + display: block; + } + + /* =================== + Completion Message + =================== */ + .completion-message { + margin: 0.5rem 0; + padding: 0rem; + border-radius: 0.375rem; + text-align: center; + font-family: var(--mono-font); + font-size: 1.2rem; + line-height: 1.5; + } + + /* =================== + Solved Expressions + =================== */ + .solved-expressions { + margin-top: 0rem; + } + + .solved-expressions h3 { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 0.75rem; + font-family: var(--main-font); + color: #1a202c; + } + + .expression-item { + background: #f1f5f9; + padding: 0.75rem; + border-radius: 0.375rem; + margin-bottom: 0.5rem; + font-size: 1.2rem; + font-family: var(--mono-font); + line-height: 1.5; + } + + .expression-item .solution { + font-weight: 600; + color: #1a202c; + } + + + /* =================== + Stats & Completion + =================== */ + .stats-content { + margin-top: 0rem; + } + + .stat-items { + margin: 0.5rem 0; + } + + .stat-item { + background: #f1f5f9; + padding: 0.75rem; + border-radius: 0.375rem; + margin-bottom: 0.5rem; + font-size: 1rem; + line-height: 1.5; + font-family: var(--main-font); + } + + /* =================== + Share Button Styles + =================== */ + .share-buttons { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .share-button { + width: 100%; + padding: 0.75rem; + font-size: 1rem; + font-family: var(--main-font); + background: #1a202c; + color: white; + border: none; + border-radius: 0.375rem; + cursor: pointer; + } + + .share-button.reset { + background-color: #ff4444; + } + + .share-button:hover { + background: #2d3748; + } + + .share-message { + font-family: var(--main-font); + color: #1a202c; + text-align: center; + padding: 0.5rem; + margin-top: 0.5rem; + background: var(--highlight-color); + border-radius: 0.375rem; + } + + /* =================== + Rank Display + =================== */ + .rank-display { + font-family: var(--mono-font); + position: relative; + overflow: hidden; + padding: 1rem; + text-align: center; + font-size: 1.2em; + font-weight: 500; + border-radius: 0.375rem; + margin: 0rem 0rem 1rem 0rem; + border: 1px solid rgba(255,255,255,0.4); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + cursor: pointer; + transition: transform 0.2s; + user-select: none; + } + + .rank-display:active { + transform: scale(0.95); + } + .rank-display .next-rank { + font-size: 0.8em; + font-weight: normal; + } + + .rank-display .share-hint { + font-size: 0.7em; + margin-top: 0.5em; + color: #666; + } + + /* Rank gradients */ + .rank-display[data-rank="Tourist"] { background: linear-gradient(135deg, #f0fdf4 0%, #6ee7b7 100%); } + .rank-display[data-rank="Commuter"] { background: linear-gradient(135deg, #fff7ed 0%, #fbd38d 100%); } + .rank-display[data-rank="Resident"] { background: linear-gradient(135deg, #f8fafc 0%, #cbd5e1 100%); } + .rank-display[data-rank="Council Member"] { background: linear-gradient(135deg, #fafaf9 0%, #d6d3d1 100%); } + .rank-display[data-rank="Chief of Police"] { background: linear-gradient(135deg, #f0f9ff 0%, #93c5fd 100%); } + .rank-display[data-rank="Mayor"] { background: linear-gradient(135deg, #fff1f2 0%, #fda4af 100%); } + .rank-display[data-rank="Power Broker"] { background: linear-gradient(135deg, #fefce8 0%, #92400e 100%); } + .rank-display[data-rank="Kingmaker"] { background: linear-gradient(135deg, #fffbeb 0%, #fcd34d 100%); } + .rank-display[data-rank="Puppet Master"] { background: linear-gradient(135deg, #faf5ff 0%, #d8b4fe 100%); } + + /* =================== + Shimmer Animation + =================== */ + @keyframes shimmer { + 0% { transform: translateX(-150%) rotate(45deg); } + 100% { transform: translateX(150%) rotate(45deg); } + } + + .rank-display::after { + content: ''; + position: absolute; + top: -50%; + left: -100%; + width: 300%; + height: 200%; + background: linear-gradient( + to bottom right, + rgba(255,255,255,0) 0%, + rgba(255,255,255,0.3) 50%, + rgba(255,255,255,0) 100% + ); + transform: rotate(45deg); + animation: shimmer 4s infinite linear; + pointer-events: none; + } + + /* =================== + Mobile Styles + =================== */ + @media (max-width: 640px) { + body { + margin: 0; + padding: 0; + height: 100vh; + font-size: 14px; + overflow: hidden; + } + + .message.success { + background-color: #c6f6d5; + color: #1a202c; + overflow-wrap: anywhere; + font-size: 1rem; + display: block; + padding: 10px; + line-height: 1.4rem; + margin-top: 0.2rem; + } + + /* Mobile container setup */ + .puzzle-container { + height: 100vh; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + } + + /* Mobile content area */ + .puzzle-content { + position: absolute; + top: 70px; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: 0; + gap: 0rem; + } + + @font-face { + font-family: 'CustomMono'; + /* Use ui-monospace for every character except underscore */ + src: local("ui-monospace"); + /* All Unicode except underscore (U+005F) */ + unicode-range: U+0000-005E, U+0060-10FFFF; + } + + @font-face { + font-family: 'CustomMono'; + /* Use Menlo for underscore only */ + src: local("Menlo"); + unicode-range: U+005F; + } + + .puzzle-display { + height: 155px; + padding: 0.5rem 1rem 1rem 1rem; + margin: 0; + font-size: 1.2rem; + background: #f1f5f9; + padding-bottom: 200px; + font-family: 'CustomMono', ui-monospace; + } + + .puzzle-display.completed { + padding-bottom: 1rem !important; + } + + + .rank-display { + margin: 0rem 0.5rem 1rem 0.5rem; + } + + + /* Mobile input container */ + .input-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + z-index: 10; + width: 100%; + border-top: 1px solid #e2e8f0; + box-shadow: 0 -2px 6px rgba(0,0,0,0.1); + padding: 0.5rem calc(0.5rem + env(safe-area-inset-left)) calc(0.5rem + env(safe-area-inset-bottom)) calc(0.5rem + env(safe-area-inset-right)); + box-sizing: border-box; + } + + .custom-input { + border: 1px solid #e2e8f0; + padding: 0.63rem; + border-radius: 6px; + margin-bottom: 0.125rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 1.2rem; + min-height: 2.4rem; + background: white; + width: 100%; + color: #000; + } + + .placeholder { + color: #9ca3af; + } + + /* Mobile keyboard */ + .custom-keyboard { + width: 100%; + padding: .25rem 0 0 0; + box-sizing: border-box; + background-color: white; + touch-action: none; + } + + .keyboard-row { + display: grid; + grid-template-columns: repeat(10, 1fr); + gap: 0.02rem; + margin-bottom: 0.125rem; + justify-content: center; + width: 100%; + } + + .keyboard-key { + height: 3.3rem; + min-height: 2.2rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0.2rem; + margin: 0.1rem 0.15rem 0.1rem 0.15rem; + cursor: pointer; + color: black; + background-color:rgb(233, 233, 233); + box-sizing: border-box; + touch-action: manipulation; + text-align: center; + font-size: 1.125rem; + font-weight: 600; + border: 1px solid rgb(200, 200, 200); + user-select: none; + -webkit-tap-highlight-color: transparent; + -webkit-appearance: none !important; + appearance: none !important; + border-radius: 4px !important; + outline: none !important; + -webkit-tap-highlight-color: transparent !important; + transition: background-color 0.1s ease-out, + transform 0.1s ease-out; + -webkit-user-select: none; + user-select: none; + -webkit-transform: translateZ(0); + transform: translateZ(0); + } + + .keyboard-key:active { + background-color: #94a3b8; + transform: translateY(2px); + } + + .stat-item { + background: #f1f5f9; + padding: 0.75rem; + border-radius: 0.375rem; + margin-bottom: 0.5rem; + font-size: 1rem; + line-height: 1.5; + } + + /* Hide desktop elements */ + .bottom-controls, + .reset-button, + .solved-expressions, + .keystroke-stats, + .share-button.copy { + display: none; + } + + /* Mobile completion state */ + .puzzle-container.completed .puzzle-content { + position: static; + height: auto; + overflow: visible; + margin-top: 85px; + padding: 1rem; + } + + .puzzle-container.completed { + position: static; + height: auto; + min-height: 100vh; + overflow-y: auto; + } + + .puzzle-display.completed .input-container { + display: none; + } + + .puzzle-container.completed .solved-expressions, + .puzzle-container.completed .keystroke-stats { + display: block; + margin: 1rem; + } + + .keystroke-stats { + margin-top: 0rem; + } + } + + /* =================== + iOS-specific Styles + =================== */ + @supports (-webkit-touch-callout: none) { + .keyboard-key:active { + background-color: #94a3b8 !important; + transform: translateY(2px) !important; + } + } + +/* Tutorial Mode Styles */ +.tutorial-container { + --tutorial-guidance-bg: #ffe8fa; + --tutorial-guidance-border: #4a6fa5; + --tutorial-highlight-border: #f6b73c; + --tutorial-completion-bg: #e9f7ef; + --tutorial-completion-border: #27ae60; + --tutorial-button-bg: #2980b9; + --tutorial-button-hover: #3498db; + } + + /* Tutorial Guidance */ + .tutorial-guidance { + background-color: #ffe8fa !important; + border-radius: 8px; + font-family: var(--main-font); + border-left: 4px solid #ab57a8; + border-right: 4px solid #ab57a8; + height: 130px !important; + overflow: hidden; + padding: 16px; + margin: 16px 0; + vertical-align: middle; + } + + .small-break { + min-height: 1rem; /* adjust as needed */ + } + + @media (max-width: 640px) { + .small-break { + min-height: 0.8rem; /* adjust as needed */ + } + } + + .tutorial-guidance.emerge { + animation: emerge 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + @keyframes emerge { + 0% { + opacity: 0; + transform: scale(0.95); + } + 100% { + opacity: 1; + transform: scale(1); + } + } + + .tutorial-guidance.highlight { + background-color: #fff8e5; + border-left-color: #f6b73c; + } + + @media (max-width: 640px) { + .tutorial-guidance { + margin: 0.625rem 0.625rem 0.625rem 0.625rem !important; /* 10px */ + /*padding: 1rem 0.3125rem !important; /* 16px 5px */ + font-size: 0.9rem; + height: 140px !important; + vertical-align: middle; + } + } + + /* Hide guidance when puzzle is completed */ + .tutorial-guidance.completed, + .puzzle-display.completed ~ .tutorial-guidance { + /*display: none;*/ + } + + /* Tutorial Message */ + .tutorial-message { + font-size: 16px; + } + + .tutorial-message strong { + font-weight: 600; + } + + .tutorial-message a { + text-decoration: none; + border-bottom: 1px solid; + padding-bottom: 1px; + transition: color 0.2s; + } + + .tutorial-message a:hover { + opacity: 0.8; + } + + .tutorial-message .emoji { + font-size: 1.2em; + } + + @media (max-width: 640px) { + .tutorial-message { + font-size: 14px; + } + } + + /* Tutorial Puzzle Display */ + .tutorial-puzzle-display { + height: 155px !important; + min-height: 155px !important; + max-height: 155px !important; + overflow-y: auto; + background: #f1f5f9; + border-radius: 0.5rem; + padding: 1rem; + font-family: var(--mono-font); + line-height: 1.7; + word-break: keep-all; + overflow-wrap: break-word; + transition: none; + } + + .tutorial-puzzle-display.completed { + height: 155px !important; + min-height: 155px !important; + max-height: 155px !important; + } + + @media (max-width: 640px) { + .tutorial-puzzle-display { + height: 155px !important; + min-height: 155px !important; + max-height: 155px !important; + padding: 0.8rem; + } + } + + /* Tutorial Highlights and Animations */ + .spotlight { + background-color: #fff9c4; + box-shadow: 0 0 0 2px #fff9c4; + border-radius: 4px; + } + + .correct { + background-color: rgba(0, 255, 0, 0.25); + box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); + border-radius: 4px; + } + + @keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); + transform: scale(1); + } + 50% { + box-shadow: 0 0 0 6px rgba(255, 193, 7, 0); + transform: scale(1.05); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); + transform: scale(1); + } + } + + @keyframes tap-hint { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } + } + + .active-clue.tutorial-highlight { + box-shadow: 0 0 0 2px #ffc107; + animation: pulse 2.5s infinite; + background-color: rgba(255, 255, 0, 0.2); + font-weight: 500; + } + + .active-clue.tap-hint { + animation: tap-hint 2s infinite; + } + + /* Tutorial Completion */ + .tutorial-completion { + background-color: #e9f7ef; + padding: 20px; + border-radius: 8px; + margin: 10px 10px 0px 10px; + text-align: center; + border: 2px solid #27ae60; + font-family: var(--mono-font); + animation: fade-in 0.5s ease-in-out; + } + + .tutorial-completion-title { + font-size: 20px; + margin-bottom: 10px; + } + + .tutorial-completion-text { + font-size: 16px; + } + + .tutorial-completion-wrapper { + margin: 0px; + } + + @keyframes fade-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Play Game Button */ + #playGameButton { + margin-top: 15px; + padding: 10px 20px; + background-color: #2980b9; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.2s, transform 0.1s; + } + + #playGameButton:hover { + background-color: #3498db; + } + + #playGameButton:active { + transform: scale(0.98); + } + + /* First Letter Hint */ + .first-letter-hint { + font-weight: bold; + color: #0c63e4; + } + + /* Submit button and input wrapper */ + .input-submit-wrapper { + display: flex; + gap: 0.5rem; + width: 100%; + } + + .submit-answer { + padding: 0 1.5rem; + height: 60px; + background-color: #2563eb; + color: white; + border: none; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 500; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + white-space: nowrap; + font-size: 1rem; + } + + .submit-answer:hover { + background-color: #1d4ed8; + } + + .submit-answer:active { + background-color: #1e40af; + transform: scale(0.98); + } + + .mobile-submit-button { + background-color: #2563eb !important; + color: white !important; + font-size: 1rem !important; + font-weight: 500 !important; + } diff --git a/static/bv/tutorial/tutorial.js b/static/bv/tutorial/tutorial.js new file mode 100644 index 0000000..33f9258 --- /dev/null +++ b/static/bv/tutorial/tutorial.js @@ -0,0 +1,1011 @@ +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 Village you can solve any clue just by submitting an answer
No need to click, just type the answer to any highlighted clue and hit enter!
Keep guessing until you get one!", + highlight: "clue" + }, + { + message: "Nice! pool is correct!
Clues are often nested within other clues
You'll need to solve opposite of clean to reveal its parent clue about dishes", + condition: expressions => expressions.size === 1 && expressions.has("game played with a cue ball"), + highlight: "none" + }, + { + message: "Nice! dirty is correct!
Clues are often nested within other clues
Now solve game played with a cue ball to reveal the parent clue about exercise", + condition: expressions => expressions.size === 1 && expressions.has("opposite of clean"), + highlight: "none" + }, + { + message: "Excellent! pool is correct!
Now both parent clues are revealed, so they are both highlighted and solvable
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! dirty is correct!
Now both parent clues are revealed, so they are both highlighted and solvable
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 sink instead
You still need to solve game played with a cue ball
Looking at the parent clue 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 swim instead
You still need to solve opposite of clean
Looking at the parent clue 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 swim is right. Only one more clue to go...
Try clicking it to reveal the first letter, that's called a \"peek\"
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 sink is right. Only one more clue to go...
Try clicking it to reveal the first letter, that's called a \"peek\"
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 dirty is right. Only one more clue to go...
Try clicking it to reveal the first letter, that's called a \"peek\"
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 pool is right. Only one more clue to go...
Try clicking it to reveal the first letter, that's called a \"peek\"
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: "Just to recap:
* you can solve any highlighted clue \u{2013} just type your guess and hit enter
* click once on a clue to peek at the first letter, twice to reveal 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 ` +
+ ${this.generateHeaderHTML()} +
+ ${this.generateTutorialGuidanceHTML()} + ${this.generatePuzzleDisplayHTML()} + + ${this.generateInputContainerHTML()} +
+
+ `; + } + + /** + * Generates the header HTML + */ + generateHeaderHTML() { + return ` +
+

[Bracket Village]

+ + +
+ `; + } + + /** + * Generates the puzzle display HTML with fixed height + */ + generatePuzzleDisplayHTML() { + return `
`; + } + + /** + * Generates the tutorial guidance container HTML + */ + generateTutorialGuidanceHTML() { + return ` +
+
+
+ `; + } + + /** + * Generates the input container HTML based on device type + */ + generateInputContainerHTML() { + if (this.isMobile) { + return ` +
+
+
+
+ type any answer... +
+ +
+
+ ${this.generateKeyboardButtonsHTML()} +
+
+ `; + } + + return ` +
+
+ + +
+
+
+ `; + } + + /** + * 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 += `
`; + + 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 += '
'; + }); + + return html; + } + + /** + * Generates a special keyboard key HTML + */ + generateSpecialKeyHTML(keyItem) { + const fontSize = keyItem.small ? '0.875rem' : '1.125rem'; + + return ` + + `; + } + + /** + * Generates a regular keyboard key HTML + */ + generateKeyboardKeyHTML(key) { + return ` + + `; + } + + /** + * 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 = '/bv'; + } + }); + } + } + + /** + * 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 = ` +
Welcome to the Bracket Village Tutorial
+
* In Bracket Village, you solve [active clues] by typing the answers
+
* You can tap any [active clue] to peek at its first letter, or tap again to reveal the full answer
+
* This tutorial will guide you through the basics
+
* Tap anywhere to continue
+ `; + + 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 + `${clueText}` + 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 = ` +
+
🎉 Tutorial Complete! 🎉
+
Ready to play the full game?
+ +
+ `; + + // 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 = '/bv'; + }); + } + } + + /** + * 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, '"') + .replace(/'/g, '''); + + const newDisplayState = + cleanState.slice(0, startIndex) + + `${escapedSolution}` + + 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, '"') + .replace(/'/g, '''); + + const newDisplayState = + cleanState.slice(0, startIndex) + + `${escapedSolution}` + + 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 }; + } -- cgit v1.2.3