From 9f277d38121c476b194629fa73ad0e2a738794be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 15:38:33 +0000 Subject: [PATCH] feat(iter-6): onboarding overlay, levels 64-65 (NTILE + running balance) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI/UX: 3-step maritime onboarding overlay (OnboardingOverlay.tsx) with framer-motion spring animations, gold step-indicator pills, SVG icons, and "Set Sail! ⚓" CTA. Shown to first-time visitors; hasSeenOnboarding persisted via Zustand. Game: Level 64 — NTILE(4) loan-book risk tranching (Basel III pattern); Level 65 — SUM() OVER (ROWS UNBOUNDED PRECEDING) monthly running net balance (treasury ledger pattern). Both use existing tables, no new seed. LevelUpModal hint map extended for levels 64 and 65; nextHintKey sentinel updated to include 64, 65. Tests: 286/286 pass; tsc --noEmit clean; eslint clean. --- evolution_log.md | 29 ++++ src/components/GameProvider.tsx | 2 + src/components/LevelUpModal.tsx | 4 +- src/components/OnboardingOverlay.tsx | 212 +++++++++++++++++++++++++++ src/data/levels/expert.ts | 113 ++++++++++++++ src/store/useGameStore.ts | 5 + 6 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 src/components/OnboardingOverlay.tsx diff --git a/evolution_log.md b/evolution_log.md index a34a142..13c950a 100644 --- a/evolution_log.md +++ b/evolution_log.md @@ -4,6 +4,35 @@ Each entry records one autonomous improvement iteration. --- +## Iteration 6 — 2026-06-11 + +### [UI/UX Improvements] + +- **First-time Onboarding Overlay** (`OnboardingOverlay.tsx`, `useGameStore.ts`, `GameProvider.tsx`): + A 3-step maritime-themed welcome overlay shown to new players on their first visit (tracked via `hasSeenOnboarding` persisted in the Zustand store). + - **Step 1 — Welcome**: LCB brand introduction with an anchor SVG icon, contextualising the player as an LCB data analyst navigating maritime data. + - **Step 2 — SQL + XP mechanics**: Editor usage (⌘↵ to run), epoch progression (Foundational → Expert), XP earning explained. + - **Step 3 — Navigation guide**: Schema panel, level map strip, hint system. + - **Visual design**: Gold top-bar gradient, step indicator dots (active dot expands to 18 px pill), circular icon container with gold border, `framer-motion` spring entrance + key-swapped exit animations per step, backdrop blur at 82% black opacity. "Skip" link for returning users who reset data. "Set Sail! ⚓" CTA on final step. + - **Persistence**: `hasSeenOnboarding: boolean` added to `GameState` interface, store default, and `partialize` list — survives page reloads; new players always see the overlay. + - Overlay renders above the level-up modal (z-index 60) and appears 600 ms after DB ready state to avoid collision with the loading screen. + +### [Game Design Tweaks] + +- **Level 64** *(Expert, difficulty 4)* — "Loan Book Risk Tranching (NTILE)": + Teaches `NTILE(4) OVER (ORDER BY principal_amount DESC)` — the equal-count bucketing function used for Basel III capital adequacy reporting, VaR tranching, and ABS/CDO structuring at sovereign wealth funds and investment banks. CTE assigns each of 15 loans to one of 4 tranches (4-4-4-3 distribution). Outer query aggregates per tranche: loan_count, avg_principal, avg_rate_pct, total_exposure, default_count. Result reveals tranche 1 = four large grade-A home loans ($1.6M total, 0 defaults); tranche 3 = the only Default (loan 8, grade D); tranche 4 = three small high-rate personal loans. No new data needed; uses existing `loans` table. + +- **Level 65** *(Expert, difficulty 4)* — "Monthly Portfolio Cash Flow & Running Balance": + Teaches `SUM(net_flow) OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)` — the universal running-total pattern underpinning every treasury ledger, account statement, and real-time P&L feed. CTE aggregates 150 transactions by month (Jan–Jun 2024) into total_credits, total_debits, net_flow. Outer query adds the cumulative running_net_balance. The explicit `ROWS UNBOUNDED PRECEDING` frame is pedagogically important: without it, the default RANGE frame can give non-deterministic results on ties. The result shows 6 rows with the cumulative net position across the portfolio. No new data needed; uses existing `transactions` table. + +### [Database & Code Optimizations] + +- **`hasSeenOnboarding` state field** added to `GameState`, initialized `false`, persisted in `lcb-analytics-storage` localStorage key — zero-cost addition to existing Zustand `persist` middleware config. +- **`nextHintKey` sentinel array** in `LevelUpModal.tsx` extended from `[..., 63]` to `[..., 63, 64, 65]`; default fallback updated from `63` → `65`; `EPOCH_NEXT_HINT` map extended with entries for levels 64 and 65. +- **No new DB tables or seed rows** — both new levels run cleanly against the existing `loans` and `transactions` tables. + +--- + ## Iteration 5 — 2026-06-08 ### [UI/UX Improvements] diff --git a/src/components/GameProvider.tsx b/src/components/GameProvider.tsx index 223d358..31e725d 100644 --- a/src/components/GameProvider.tsx +++ b/src/components/GameProvider.tsx @@ -13,6 +13,7 @@ import { initDatabase } from '@/lib/db'; import { EPOCH_RANGES, MAX_LEVEL } from '@/lib/progression'; import SchemaViewer from './SchemaViewer'; import LevelProgressMap from './LevelProgressMap'; +import OnboardingOverlay from './OnboardingOverlay'; const HarbourCanvas = dynamic(() => import('./FlockCanvas'), { ssr: false, @@ -270,6 +271,7 @@ export default function GameProvider() { setShowLevelNavigator(false)} /> + ); } diff --git a/src/components/LevelUpModal.tsx b/src/components/LevelUpModal.tsx index 5ff16f9..147deb7 100644 --- a/src/components/LevelUpModal.tsx +++ b/src/components/LevelUpModal.tsx @@ -18,6 +18,8 @@ const EPOCH_NEXT_HINT: Record = { 59: 'LEAD function: next-event lookahead and gap analysis for vessel scheduling', 61: 'Conditional aggregation pivot: COUNT(CASE WHEN ...) — the standard SQL pivot pattern for any dialect', 63: 'Rolling volatility: SQRT(AVG(x²) − AVG(x)²) — population std dev via window functions', + 64: 'NTILE bucketing: equal-count portfolio tranching for Basel III capital adequacy reporting', + 65: 'Running balance: SUM() OVER (ROWS UNBOUNDED PRECEDING) — the universal treasury ledger pattern', }; export default function LevelUpModal() { @@ -48,7 +50,7 @@ export default function LevelUpModal() { return () => window.removeEventListener('keydown', onKey); }, [showLevelUp, handleClose]); - const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63].find((k) => currentLevel < k) ?? 63; + const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63, 64, 65].find((k) => currentLevel < k) ?? 65; const xpEarned = xpFor(currentLevel); return ( diff --git a/src/components/OnboardingOverlay.tsx b/src/components/OnboardingOverlay.tsx new file mode 100644 index 0000000..520cf17 --- /dev/null +++ b/src/components/OnboardingOverlay.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useGameStore } from '@/store/useGameStore'; + +const STEPS = [ + { + icon: ( + + + + + + ), + title: 'Welcome to Lion City Bank', + body: 'You are a data analyst at LCB, Singapore\'s foremost merchant shipping bank. Your SQL skills will navigate the fleet\'s data — from basic queries to senior data science patterns used at FAANG and sovereign institutions.', + }, + { + icon: ( + + + + + + + + ), + title: 'Write SQL, Earn XP', + body: 'Each challenge presents a real-world banking problem. Write your SQL query in the editor, press ⌘↵ or click Run to execute. Correct solutions earn XP and advance you through 4 epochs: Foundational → Intermediate → Advanced → Expert.', + }, + { + icon: ( + + + + + + + + ), + title: 'Navigate the Harbour', + body: 'Use the Schema panel to explore tables. The level map strip shows your voyage across 65 challenges. Stuck? Click the hint button after your first attempt. The harbour master is always watching.', + }, +]; + +export default function OnboardingOverlay() { + const { hasSeenOnboarding, setHasSeenOnboarding } = useGameStore(); + const [step, setStep] = useState(0); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (!hasSeenOnboarding) { + // Small delay so the DB loading screen fully fades before overlay appears + const t = setTimeout(() => setVisible(true), 600); + return () => clearTimeout(t); + } + }, [hasSeenOnboarding]); + + function handleNext() { + if (step < STEPS.length - 1) { + setStep((s) => s + 1); + } else { + dismiss(); + } + } + + function dismiss() { + setVisible(false); + setHasSeenOnboarding(); + } + + const current = STEPS[step]; + + return ( + + {visible && ( +
+ {/* Backdrop */} + + + {/* Card */} + + {/* Gold top bar */} +
+ + {/* Step indicator */} +
+ {STEPS.map((_, i) => ( +
+ ))} +
+ + {/* Icon */} +
+
+ {current.icon} +
+
+ + {/* Text content */} +
+

+ {current.title} +

+

+ {current.body} +

+
+ + {/* Actions */} +
+ + + +
+ + {/* Step count */} +

+ {step + 1} / {STEPS.length} +

+ +
+ )} + + ); +} diff --git a/src/data/levels/expert.ts b/src/data/levels/expert.ts index 1941a85..b8831cc 100644 --- a/src/data/levels/expert.ts +++ b/src/data/levels/expert.ts @@ -842,4 +842,117 @@ SELECT month, epoch: 'Expert', difficulty: 4, }, + + // ============================================================ + // SENIOR DS PATTERNS (Levels 64–65) + // NTILE portfolio tranching; running cumulative cash-flow balance + // ============================================================ + + { + id: 64, + title: 'Loan Book Risk Tranching (NTILE)', + description: `LCB's Credit Risk team must divide the loan portfolio into four equal-sized tranches by principal exposure — a regulatory requirement for capital adequacy reporting (Basel III / MAS Notice 637). + +Use \`NTILE(4) OVER (ORDER BY principal_amount DESC)\` in a CTE to assign each loan to a tranche (1 = largest exposure, 4 = smallest). In the outer query, aggregate per tranche: + +- \`loan_count\` — number of loans in the tranche +- \`avg_principal\` — average principal, rounded to the nearest dollar +- \`avg_rate_pct\` — average interest rate, rounded to 2 dp +- \`total_exposure\` — sum of principal, rounded to the nearest dollar +- \`default_count\` — number of loans with status \`'Default'\` + +Order by \`tranche\`. + +This NTILE bucketing pattern is the standard approach for portfolio segmentation at sovereign wealth funds, investment banks, and insurance firms — used whenever you need equal-count bins rather than equal-width intervals.`, + hint: "CTE: SELECT loan_id, principal_amount, interest_rate, status, NTILE(4) OVER (ORDER BY principal_amount DESC) AS tranche FROM loans. Outer: GROUP BY tranche, use ROUND(AVG(...), 0) for principal, ROUND(AVG(...), 2) for rate, SUM(CASE WHEN status='Default' THEN 1 ELSE 0 END) for default_count.", + seedQuery: `WITH tranches AS ( + SELECT loan_id, + principal_amount, + interest_rate, + status, + NTILE( ) OVER (ORDER BY principal_amount DESC) AS tranche + FROM loans +) +SELECT tranche, + COUNT(*) AS loan_count, + ROUND(AVG(principal_amount), 0) AS avg_principal, + ROUND(AVG(interest_rate), 2) AS avg_rate_pct, + ROUND(SUM(principal_amount), 0) AS total_exposure, + SUM(CASE WHEN status = THEN 1 ELSE 0 END) AS default_count + FROM tranches + GROUP BY + ORDER BY tranche`, + solutionQuery: `WITH tranches AS ( + SELECT loan_id, + principal_amount, + interest_rate, + status, + NTILE(4) OVER (ORDER BY principal_amount DESC) AS tranche + FROM loans +) +SELECT tranche, + COUNT(*) AS loan_count, + ROUND(AVG(principal_amount), 0) AS avg_principal, + ROUND(AVG(interest_rate), 2) AS avg_rate_pct, + ROUND(SUM(principal_amount), 0) AS total_exposure, + SUM(CASE WHEN status = 'Default' THEN 1 ELSE 0 END) AS default_count + FROM tranches + GROUP BY tranche + ORDER BY tranche`, + epoch: 'Expert', + difficulty: 4, + }, + + { + id: 65, + title: 'Monthly Portfolio Cash Flow & Running Balance', + description: `The Treasury desk needs a daily liquidity dashboard. As a first step, produce a month-by-month cash-flow summary across all accounts, plus a **running net balance** — the cumulative sum of monthly net flows from January through June 2024. + +Use a CTE to aggregate per month: +- \`total_credits\` — sum of all Credit transactions (2 dp) +- \`total_debits\` — sum of all Debit transactions (2 dp) +- \`net_flow\` — credits minus debits (2 dp) + +In the outer query, add: +- \`running_net_balance\` — cumulative \`net_flow\` from the start of the period to the current month, using \`SUM(net_flow) OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)\`, rounded to 2 dp + +Return all six months ordered by \`month\`. + +Running totals with \`SUM() OVER (ROWS UNBOUNDED PRECEDING)\` underpin every treasury ledger, account statement, and real-time P&L feed at banks, fintechs, and trading desks. The explicit frame is required for deterministic ordering in any dialect.`, + hint: "CTE: GROUP BY STRFTIME('%Y-%m', transaction_date), SUM CASE WHEN transaction_type = 'Credit' / 'Debit'. Outer: ROUND(SUM(net_flow) OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) AS running_net_balance.", + seedQuery: `WITH monthly_flows AS ( + SELECT STRFTIME('%Y-%m', transaction_date) AS month, + ROUND(SUM(CASE WHEN transaction_type = THEN amount ELSE 0 END), 2) AS total_credits, + ROUND(SUM(CASE WHEN transaction_type = THEN amount ELSE 0 END), 2) AS total_debits, + ROUND(SUM(CASE WHEN transaction_type = 'Credit' THEN amount ELSE -amount END), 2) AS net_flow + FROM transactions + GROUP BY month +) +SELECT month, + total_credits, + total_debits, + net_flow, + ROUND(SUM(net_flow) OVER (ORDER BY month ROWS BETWEEN AND CURRENT ROW), 2) + AS running_net_balance + FROM monthly_flows + ORDER BY month`, + solutionQuery: `WITH monthly_flows AS ( + SELECT STRFTIME('%Y-%m', transaction_date) AS month, + ROUND(SUM(CASE WHEN transaction_type = 'Credit' THEN amount ELSE 0 END), 2) AS total_credits, + ROUND(SUM(CASE WHEN transaction_type = 'Debit' THEN amount ELSE 0 END), 2) AS total_debits, + ROUND(SUM(CASE WHEN transaction_type = 'Credit' THEN amount ELSE -amount END), 2) AS net_flow + FROM transactions + GROUP BY month +) +SELECT month, + total_credits, + total_debits, + net_flow, + ROUND(SUM(net_flow) OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) + AS running_net_balance + FROM monthly_flows + ORDER BY month`, + epoch: 'Expert', + difficulty: 4, + }, ]; diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts index 5be1bbe..b4089d8 100644 --- a/src/store/useGameStore.ts +++ b/src/store/useGameStore.ts @@ -26,6 +26,7 @@ interface GameState { // UI state showLevelUp: boolean; lastSpawnedBird: { x: number; y: number } | null; + hasSeenOnboarding: boolean; // User userName: string | null; @@ -41,6 +42,7 @@ interface GameState { setHasAttemptedCurrent: (hasAttempted: boolean) => void; setShowLevelUp: (show: boolean) => void; setLastSpawnedBird: (pos: { x: number; y: number } | null) => void; + setHasSeenOnboarding: () => void; setUserName: (name: string | null) => void; resetGame: () => void; goToPreviousLevel: () => void; @@ -62,6 +64,7 @@ export const useGameStore = create()( hasAttemptedCurrent: false, showLevelUp: false, lastSpawnedBird: null, + hasSeenOnboarding: false, userName: null, setCurrentLevel: (level) => @@ -112,6 +115,7 @@ export const useGameStore = create()( setHasAttemptedCurrent: (hasAttempted) => set({ hasAttemptedCurrent: hasAttempted }), setShowLevelUp: (show) => set({ showLevelUp: show }), setLastSpawnedBird: (pos) => set({ lastSpawnedBird: pos }), + setHasSeenOnboarding: () => set({ hasSeenOnboarding: true }), setUserName: (name) => set({ userName: name }), resetGame: () => @@ -153,6 +157,7 @@ export const useGameStore = create()( userName: state.userName, totalXp: state.totalXp, currentStreak: state.currentStreak, + hasSeenOnboarding: state.hasSeenOnboarding, }), } )