aboutsummaryrefslogtreecommitdiff
path: root/static/bs/tutorial
diff options
context:
space:
mode:
Diffstat (limited to 'static/bs/tutorial')
-rw-r--r--static/bs/tutorial/index.html22
-rw-r--r--static/bs/tutorial/tutorial-init.js22
-rw-r--r--static/bs/tutorial/tutorial.css945
-rw-r--r--static/bs/tutorial/tutorial.js1011
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, '&quot;')
- .replace(/'/g, '&#39;');
-
- const newDisplayState =
- cleanState.slice(0, startIndex) +
- `<mark class="solved">${escapedSolution}</mark>` +
- cleanState.slice(endIndex);
-
- // Add to the solved expressions set
- const updatedExpressions = new Set([...this.state.solvedExpressions, expression]);
- this.state.solvedOrder.push(expression);
- // Update game state
- this.state.displayState = newDisplayState;
- this.state.solvedExpressions = updatedExpressions;
-
- // Clear the input based on device type
- if (this.isMobile) {
- this.customInputValue = '';
- this.updateCustomInputDisplay();
- } else {
- if (this.answerInput) {
- this.answerInput.value = '';
- }
- }
-
- // Re-render the puzzle display
- this.render();
-
- // Update tutorial guidance
- this.updateTutorialGuidance();
- }
-
- /**
- * Toggles hint mode for an expression
- * @param {string} expression - The expression to toggle hints for
- */
- toggleHintMode(expression) {
- // Early validation
- if (!expression || this.state.megaPeekedClues.has(expression)) {
- return;
- }
-
- // Handle mega peek (second click)
- if (this.state.hintModeClues.has(expression)) {
- this.handleMegaPeek(expression);
- return;
- }
-
- // Handle first peek
- this.handleFirstPeek(expression);
- }
-
- /**
- * Handles the first peek at an expression (shows first letter)
- * @param {string} expression - The expression to peek at
- */
- handleFirstPeek(expression) {
- this.state.hintModeClues.add(expression);
- this.state.peekedClues.add(expression);
-
- this.render();
- this.updateTutorialGuidance();
- }
-
- /**
- * Handles the mega peek action (reveals answer)
- * @param {string} expression - The expression to reveal
- */
- handleMegaPeek(expression) {
- const solution = this.PUZZLE_DATA.solutions[expression];
- if (!solution) {
- console.error('No solution found for expression:', expression);
- return;
- }
-
- const newDisplayState = this.processSolutionReveal(expression, solution);
- if (!newDisplayState) {
- console.error('Could not find expression in puzzle state');
- return;
- }
-
- // Update state
- this.state.displayState = newDisplayState;
- this.state.megaPeekedClues.add(expression);
- this.state.solvedExpressions.add(expression);
- this.state.hintModeClues.delete(expression);
-
- // Re-render
- this.render();
-
- // Update tutorial guidance
- this.updateTutorialGuidance();
- }
-
- /**
- * Processes a solution reveal for megapeek
- * @param {Object} expression - Expression to reveal
- * @param {string} solution - Solution text
- * @returns {string} New display state
- */
- processSolutionReveal(expression, solution) {
- // Clean the display state first
- const cleanState = this.state.displayState.replace(/<\/?[^>]+(>|$)/g, '');
- const availableExpressions = this.findAvailableExpressions(cleanState);
-
- for (const {startIndex, endIndex} of availableExpressions) {
- const currentExpression = cleanState.slice(startIndex + 1, endIndex - 1).trim();
-
- if (expression === currentExpression) {
- const escapedSolution = solution
- .replace(/"/g, '&quot;')
- .replace(/'/g, '&#39;');
-
- const newDisplayState =
- cleanState.slice(0, startIndex) +
- `<mark class="solved">${escapedSolution}</mark>` +
- cleanState.slice(endIndex);
-
- return newDisplayState;
- }
- }
- return null;
- }
-
- /**
- * Checks if the puzzle is complete
- * @returns {boolean} Whether the puzzle is complete
- */
- isPuzzleComplete() {
- return !this.state.displayState.includes('[');
- }
- }
-
- // Export the class for use in other files
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = { BracketCityTutorial };
- }