diff options
Diffstat (limited to 'static/bs/tutorial')
-rw-r--r-- | static/bs/tutorial/index.html | 22 | ||||
-rw-r--r-- | static/bs/tutorial/tutorial-init.js | 22 | ||||
-rw-r--r-- | static/bs/tutorial/tutorial.css | 945 | ||||
-rw-r--r-- | static/bs/tutorial/tutorial.js | 1011 |
4 files changed, 0 insertions, 2000 deletions
diff --git a/static/bs/tutorial/index.html b/static/bs/tutorial/index.html deleted file mode 100644 index 11ffbed..0000000 --- a/static/bs/tutorial/index.html +++ /dev/null @@ -1,22 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <base href="/bs/tutorial/" /> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Bracket Shitty</title> - <link rel="icon" type="image/png" href="../bc-favicon.png"> - <!-- Tutorial CSS --> - <link rel="stylesheet" href="tutorial.css"> -</head> -<body> - <!-- Main container for the game --> - <div id="bracket-city-container"></div> - - - <!-- Tutorial mode scripts --> - <script src="tutorial.js"></script> - <script src="tutorial-init.js"></script> - -</body> -</html> diff --git a/static/bs/tutorial/tutorial-init.js b/static/bs/tutorial/tutorial-init.js deleted file mode 100644 index ade15a5..0000000 --- a/static/bs/tutorial/tutorial-init.js +++ /dev/null @@ -1,22 +0,0 @@ -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 Shitty tutorial'); - return null; - } - } diff --git a/static/bs/tutorial/tutorial.css b/static/bs/tutorial/tutorial.css deleted file mode 100644 index 4f2aff0..0000000 --- a/static/bs/tutorial/tutorial.css +++ /dev/null @@ -1,945 +0,0 @@ -/* =================== - 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/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, '"') - .replace(/'/g, '''); - - 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, '"') - .replace(/'/g, '''); - - 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 }; - } |