<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krish Panchani</title>
    <description>The latest articles on DEV Community by Krish Panchani (@krishpanchani).</description>
    <link>https://dev.to/krishpanchani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1189653%2F63ba82e8-548f-4d2b-9470-2dbb71ce7f63.jpeg</url>
      <title>DEV Community: Krish Panchani</title>
      <link>https://dev.to/krishpanchani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krishpanchani"/>
    <language>en</language>
    <item>
      <title>Reviving My AI-Powered Draw and Learn Game - What If an AI Could Look at Your Drawing and Actually Respond?</title>
      <dc:creator>Krish Panchani</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:58:06 +0000</pubDate>
      <link>https://dev.to/krishpanchani/reviving-my-ai-powered-draw-and-learn-game-what-if-an-ai-could-look-at-your-drawing-and-actually-b20</link>
      <guid>https://dev.to/krishpanchani/reviving-my-ai-powered-draw-and-learn-game-what-if-an-ai-could-look-at-your-drawing-and-actually-b20</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI Playground: Draw, Create, and Explore&lt;/strong&gt; is an AI-powered draw and learn game that runs entirely in the browser. You draw on a canvas, and Google's Gemini AI - one of the most capable vision models available today - looks at your drawing and responds: it scores your art, guesses what you drew, or writes the next chapter of a story based on what you created.&lt;/p&gt;

&lt;p&gt;The core idea is simple: drawing is fun, and AI feedback makes it more interesting. Whether you're an artist or someone who draws stick figures and calls them dragons, the AI reacts to &lt;em&gt;your specific drawing&lt;/em&gt;, not a generic response.&lt;/p&gt;

&lt;p&gt;There are three game modes, each offering a completely different experience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creative Quest - The Main Learning Game&lt;/strong&gt;&lt;br&gt;
This is the heart of AI Playground. Each session gives you 10 drawing missions spread across 5 themed fantasy worlds (Forest, Crystal Caves, Ancient Ruins, Sky Realm, Dragon Peak). Before each chapter, Gemini AI generates a unique mission - something like "Draw a magical door hidden in an ancient forest" - and gives you a timer. You draw it, hit Submit, and Gemini actually &lt;em&gt;looks at your drawing&lt;/em&gt; using its vision model and scores it on three dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy (50%)&lt;/strong&gt; - How well does the drawing match the mission?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creativity (30%)&lt;/strong&gt; - Did you bring something original to it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effort (20%)&lt;/strong&gt; - Did you put real work into the drawing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three scores combine into a final score, which translates into XP. Complete all 10 chapters and you've finished an adventure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Artful Guesswork - Can the AI Guess What You Drew?&lt;/strong&gt;&lt;br&gt;
Draw literally anything - a cat, a spaceship, your lunch. Gemini tries to identify it and gives you a confidence score (0-100%). You then rate whether it was right or wrong. No timer, no pressure. It's a surprisingly fun way to see how AI "sees" drawings. Every submission gives you 8 XP just for playing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Artful Stories - Draw the Next Chapter&lt;/strong&gt;&lt;br&gt;
Your drawing becomes part of a story. You draw something, Gemini generates a short story chapter (max 150 words) narrating what happens based on what you drew, then you draw again for the next chapter. The whole illustrated story gets saved. It's creative writing + drawing in a loop. Each chapter earns 15 XP.&lt;/p&gt;

&lt;p&gt;Wrapped around all three modes is a full product layer that makes the game feel complete: Google Sign-In, a player profile with your XP and level, a full adventure history log, a community art gallery showing everyone's drawings, and a live leaderboard ranking the top 20 players.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19, Vite, Tailwind CSS, Fabric.js (canvas), Framer Motion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State &amp;amp; UX&lt;/td&gt;
&lt;td&gt;Zustand, React Context, react-hot-toast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Node.js, Express, Zod validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;MongoDB (Atlas) via Mongoose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Google Gemini 2.5 Flash - vision for scoring, text for missions and stories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Google OAuth 2.0 → server-verified JWT (7-day expiry)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/Krish-Panchani/ai-playground" rel="noopener noreferrer"&gt;github.com/Krish-Panchani/ai-playground&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://ai-playground-krish.vercel.app" rel="noopener noreferrer"&gt;ai-playground-krish.vercel.app&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Home - Choose Your Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3j9ie71qawecb7amm50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3j9ie71qawecb7amm50.png" alt="AI Playground home screen showing three animated game mode cards: Creative Quest, Artful Guesswork, and Artful Stories, plus a profile link" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Once signed in, this is where every session starts. Three animated mode cards, each with an illustration and a one-line pitch. No tutorials to sit through, no settings to configure - just pick a mode and start drawing.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Skill Level Selection - Before a Creative Quest&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcaf8hntvvatjsqklru8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcaf8hntvvatjsqklru8y.png" alt="Skill level selection screen showing four options: Beginner, Intermediate, Advanced, and Expert, with timer information for each level" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before starting a Creative Quest adventure, you choose your skill level. This isn't just a label - it directly controls how much time you get per mission. Beginners get 4 minutes per chapter; Expert players get 2.5 minutes. The AI also uses this context when generating missions, so a Beginner won't get asked to draw something highly detailed in a short window.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Creative Quest - Mission in Progress&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5dqtvv3eh57nfjkxx4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5dqtvv3eh57nfjkxx4b.png" alt="Creative Quest game screen with a countdown timer at the top, a mission prompt from Gemini, the Fabric.js drawing canvas with tool options, and a chapter progress bar below" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa6qnjdnp4xn1plfx2xc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa6qnjdnp4xn1plfx2xc.png" alt="Loading drawing mission" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93wc0pf2rlh9vfy6wfjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93wc0pf2rlh9vfy6wfjm.png" alt="Drawing canvas" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the core loop. The AI-generated mission sits at the top - something like "Draw a phoenix rising from golden flames." The countdown timer is running. The drawing canvas is in the center with a full toolbar: pen, marker, spray, shapes, eraser, color picker, brush size, and opacity slider. At the bottom a progress bar shows which chapter you're on out of 10. The whole thing is one focused screen - no distractions.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Mission Results - AI Scores Your Drawing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1lv67xo3y10obpgjx49k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1lv67xo3y10obpgjx49k.png" alt="Gemini mission result card showing accuracy: 78, creativity: 85, effort: 70, a short text feedback from Gemini, the final score, and XP earned for this chapter" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After submitting, Gemini's vision model analyzes the actual pixel content of your drawing against the mission prompt. You get three individual scores - accuracy, creativity, and effort - plus a short written comment that Gemini generates specifically about what you drew. The weighted final score and XP earned appear below. Then the next mission loads automatically.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Artful Stories - Drawing Becomes a Story&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftn2lpw4057tckcv5szj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftn2lpw4057tckcv5szj2.png" alt="Artful Stories screen showing a user's drawing on the left and a Gemini-generated story paragraph on the right, with a " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In Stories mode, each drawing you make becomes the next chapter in an illustrated narrative. You draw something, Gemini writes a short story continuation based on what it sees in your drawing, and the full text appears alongside your art. Then you draw the next chapter. The complete illustrated story - all drawings and all Gemini-written text - is saved to your profile and visible in the gallery.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Player Profile&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8y0r2t9pm782vyik6nh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8y0r2t9pm782vyik6nh.png" alt="Player profile page showing username, level number, XP progress bar toward next level, rank position on the leaderboard, stat cards for total drawings and adventures completed, and a recent activity section" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhywnb5vmc2rq1yj96h3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhywnb5vmc2rq1yj96h3e.png" alt="Player profile" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your profile aggregates everything: total XP, current level (XP ÷ 100), global rank on the leaderboard, total drawings submitted, and adventures completed across all three modes. The XP bar shows exactly how far you are to the next level. Below that, a recent activity section shows your last few sessions at a glance.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Art Gallery - Community Drawings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblci604chly0vlz59f89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblci604chly0vlz59f89.png" alt="Art gallery showing a grid of drawings submitted by multiple players, each with the player name, mode played, and AI score underneath" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All drawings from all players - Creative Quest missions, Guesswork submissions, and Story illustrations - show up here in one feed. It turns a solo game into a shared experience. Seeing someone else's interpretation of the same mission prompt you just struggled with is consistently entertaining, occasionally impressive, and sometimes very funny.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Leaderboard - Top Players by XP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibe2q62rjwglizypj9mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibe2q62rjwglizypj9mk.png" alt="Leaderboard showing top 20 players ranked by XP with their username, level, and total XP displayed in a styled table" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The top 20 players ranked by total XP. Since XP accumulates from all three modes, it rewards players who explore the full game - not just the one who grinds a single mode. In the original version this was a hardcoded array in the component. Now every row is a real player from MongoDB.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Where this project started
&lt;/h3&gt;

&lt;p&gt;This began at a hackathon. The idea was straightforward: what if Gemini could score your drawings and make the experience feel like a game? I had a weekend, a Gemini API key, some coffee, and an idea I thought was worth building.&lt;/p&gt;

&lt;p&gt;The hackathon demo worked. You could draw something, submit it, and Gemini would respond. People who watched it laughed, engaged with it, thought it was fun. Then the weekend ended, I pushed the final commit, and the repo sat there untouched for months.&lt;/p&gt;

&lt;p&gt;It wasn't that the idea was bad - I still thought it was interesting. It was that the code had never been meant to last beyond the demo. I knew it. And every time I thought about picking it back up, opening the codebase felt like opening a drawer full of things you've been meaning to deal with.&lt;/p&gt;

&lt;p&gt;The GitHub Finish-Up-A-Thon gave me the framing I needed: don't build something new, just &lt;em&gt;finish what you started&lt;/em&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  What the original version actually looked like
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch the original version here:&lt;/strong&gt; &lt;a href="https://youtu.be/A24KG36sfW4?si=e_r0pHcT07A_7lPg" rel="noopener noreferrer"&gt;youtu.be/A24KG36sfW4&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The video shows the before state better than I can describe it, but here's the technical picture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini API keys were exposed in the browser.&lt;/strong&gt; All AI calls happened inside React helper files (&lt;code&gt;src/helpers/&lt;/code&gt;). The API key lived in a &lt;code&gt;.env&lt;/code&gt; variable that Vite bakes into the client JavaScript bundle. Anyone who opened the browser's DevTools and looked at the network requests - or just read the compiled JS - would have had full access to the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase was wired directly on the client.&lt;/strong&gt; Auth, Firestore reads, Storage uploads - all happening in React components with the Firebase client SDK. There was an architecture document that listed backend API routes, but every single one was marked &lt;code&gt;501 Not Implemented&lt;/code&gt;. The server layer existed only on paper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creative Quest was a single AI call.&lt;/strong&gt; You'd draw something, it would fire one Gemini prompt, return a response, and that was it. Reload the page and everything was gone - no session, no saved score, no progress. The progress bar was always at 100% because there was no actual progress to track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The leaderboard was hardcoded data.&lt;/strong&gt; Not connected to a database, not real players. Just an array I wrote into the component to make the demo look more complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors crashed the experience.&lt;/strong&gt; When Gemini returned a 503 "model overloaded" error - which happens during peak hours - the user would see a popup containing raw JSON: &lt;code&gt;{"error":{"code":503,"status":"UNAVAILABLE","message":"The model is overloaded. Please try again later."}}&lt;/code&gt;. There was no retry, no fallback, no user-friendly message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mission timers were whatever Gemini decided.&lt;/strong&gt; Sometimes Gemini returned a 45-second timer. You cannot finish a drawing of anything in 45 seconds. The timer wasn't normalized or validated on the server - it went straight to the countdown component.&lt;/p&gt;

&lt;p&gt;The idea behind the project was solid. The implementation was a hackathon draft that had never moved past "it works in the demo."&lt;/p&gt;


&lt;h3&gt;
  
  
  What I fixed, refactored, and built from scratch
&lt;/h3&gt;

&lt;p&gt;This is the full list of what changed during the Finish-Up-A-Thon sprint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restructured the entire project into a client-server monorepo.&lt;/strong&gt;&lt;br&gt;
The frontend (React) and backend (Express + MongoDB) now live in the same repo but are fully separate: the React app in the root, the API server in &lt;code&gt;/server&lt;/code&gt;. The Vite dev server proxies &lt;code&gt;/api&lt;/code&gt; requests to &lt;code&gt;localhost:8080&lt;/code&gt; so local development feels seamless, but in production they're distinct services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moved all Gemini API calls to the server.&lt;/strong&gt;&lt;br&gt;
Zero AI calls happen in the browser anymore. The frontend sends drawing data to the Express API, and the server calls Gemini, handles retries, and returns results. The API key never reaches the client. This also made it possible to add retries and fallbacks - neither of which is viable when the AI is called from the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replaced Firebase client auth with Google OAuth → JWT.&lt;/strong&gt;&lt;br&gt;
A player clicks "Sign in with Google," the Google OAuth popup opens, and the resulting ID token is sent to the server. The server verifies the token using &lt;code&gt;google-auth-library&lt;/code&gt;, upserts the user in MongoDB, and returns a 7-day JWT. The client stores that JWT in localStorage and attaches it as a &lt;code&gt;Bearer&lt;/code&gt; header on every API request. Nothing sensitive runs on the client side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebuilt Creative Quest as a proper game loop with saved state.&lt;/strong&gt;&lt;br&gt;
Each playthrough creates a &lt;code&gt;GameSession&lt;/code&gt; document in MongoDB. That document tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which world you're in (5 worlds, 10 chapters total)&lt;/li&gt;
&lt;li&gt;Your current chapter number&lt;/li&gt;
&lt;li&gt;The AI-generated mission for each chapter&lt;/li&gt;
&lt;li&gt;Your score and XP for each completed chapter&lt;/li&gt;
&lt;li&gt;Session status: active, completed, or abandoned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Missions are generated per-chapter, and the previous mission prompts from that session are passed back to Gemini each time - so it won't repeat a prompt you've already seen in the same run. That duplicate-mission prevention was a small detail that made a big difference in playtesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed mission timers with server-side normalization.&lt;/strong&gt;&lt;br&gt;
Before a timer reaches the client, it goes through a normalization function that accounts for your chosen skill level and which chapter you're on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beginner: 240 seconds base&lt;/li&gt;
&lt;li&gt;Intermediate: 210 seconds&lt;/li&gt;
&lt;li&gt;Advanced: 180 seconds&lt;/li&gt;
&lt;li&gt;Expert: 150 seconds&lt;/li&gt;
&lt;li&gt;Later chapters get a small time bonus (up to 60s extra)&lt;/li&gt;
&lt;li&gt;Hard clamp: no mission can be shorter than 2 minutes or longer than 6 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more 45-second missions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Added retry logic and fallback missions for when Gemini is overloaded.&lt;/strong&gt;&lt;br&gt;
When Gemini returns a 503 or rate limit error, the server retries automatically - up to 4 times, with increasing delays (700ms → 1.4s → 2.8s → 5.6s). If all four attempts fail, the game doesn't crash. Instead, it pulls from a pool of 20 hand-curated fallback missions so the game loop continues uninterrupted. The player sees a small toast notification explaining the situation in plain language, not a JSON error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewrote the XP and scoring system server-side.&lt;/strong&gt;&lt;br&gt;
XP is no longer a client-side variable that disappears on refresh. Every score is computed on the server using this formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Final Score = (Accuracy × 0.5) + (Creativity × 0.3) + (Effort × 0.2)
XP Earned = max(10, round(Final Score ÷ 2))
Level = floor(Total XP ÷ 100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After submitting a drawing, the frontend calls the profile endpoint to refresh the user's XP, level, and rank - so the numbers you see on your profile always reflect what's actually stored in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replaced every alert() and raw JSON error with human-readable toast messages.&lt;/strong&gt;&lt;br&gt;
There is a &lt;code&gt;formatError.js&lt;/code&gt; utility on the frontend that pattern-matches server error responses to player-friendly strings. Gemini overloaded? → "Our AI helper is busy right now. Please wait a minute and try again." Network failure? → "Couldn't connect to the server." Expired session? → "Please sign in again to continue." These are surfaced as dismissible toast notifications, never as popups or raw JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built all four product pages that were missing entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile&lt;/strong&gt; - aggregates XP, level, global rank, total drawings submitted, adventure history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adventure Log&lt;/strong&gt; - browse all past game sessions with their status and chapter-by-chapter results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Art Gallery&lt;/strong&gt; - community feed of every drawing submitted across all three modes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboard&lt;/strong&gt; - top 20 players by XP, updated in real time (actual database queries, not hardcoded)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Before vs. After - Side by Side
&lt;/h2&gt;

&lt;p&gt;The biggest change was Creative Quest going from a one-shot demo to a real game loop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Original Version&lt;/th&gt;
&lt;th&gt;Finished Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Missions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same static prompt each run&lt;/td&gt;
&lt;td&gt;AI-generated, unique per chapter, no repeats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whatever Gemini returned (45s possible)&lt;/td&gt;
&lt;td&gt;Server-normalized by skill level (120–360s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client-side, no persistence&lt;/td&gt;
&lt;td&gt;Server-computed, saved to MongoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;XP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reset on page reload&lt;/td&gt;
&lt;td&gt;Persistent, synced to player profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Progress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Progress bar stuck at 100%&lt;/td&gt;
&lt;td&gt;Real chapter count (2/10, 5/10, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hard crash, raw JSON popup&lt;/td&gt;
&lt;td&gt;Retry → fallback mission → friendly toast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase client SDK&lt;/td&gt;
&lt;td&gt;Google OAuth + server-verified JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Leaderboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded array&lt;/td&gt;
&lt;td&gt;Live database query, real players&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Didn't exist&lt;/td&gt;
&lt;td&gt;Full stats, history, rank, XP bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exposed in browser bundle&lt;/td&gt;
&lt;td&gt;Server-side only, never sent to client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error UX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;alert()&lt;/code&gt; popups&lt;/td&gt;
&lt;td&gt;react-hot-toast with plain-language messages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h3&gt;
  
  
  Key Code Changes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scoring formula - moved from client to server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/src/services/progression.service.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;computeCreativeQuestScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;creativity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effort&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creativity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xpEarned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalScore&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;finalScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalScore&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;xpEarned&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;levelFromXp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mission timer normalization - so no mission is ever unfair:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/src/utils/mission-timer.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKILL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Beginner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Intermediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Expert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizeMissionTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;skillLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Beginner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chapterNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;skillBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SKILL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;skillLevel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chapterBonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chapterNumber&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;skillBase&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chapterBonus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gemini retry - because the AI is occasionally busy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/src/utils/gemini-retry.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isGeminiRetryableError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high demand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
         &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource_exhausted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gemini SDK surfaces errors in different shapes depending on whether the failure came from the HTTP layer, the SDK's internal parsing, or the response body. Checking all three forms means the retry logic actually catches all the cases - not just the obvious one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Player-friendly error messages - because users don't speak HTTP:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/formatError.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high demand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;503&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource_exhausted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Our AI helper is busy right now. Please wait a minute and try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed to fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Couldn't connect to the server. Check your connection and try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;401&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authentication&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please sign in again to continue.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;I want to give an honest account of this because the Finish-Up-A-Thon specifically asks about it, and "Copilot wrote my whole project" is both untrue and uninteresting.&lt;/p&gt;

&lt;p&gt;Here's the honest version: &lt;strong&gt;Copilot handled the work that usually kills side projects - the tedious, repetitive, unglamorous 30% - so I could spend my energy on the parts that actually required judgment.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The real places where Copilot made a difference
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Building out the backend API layer.&lt;/strong&gt;&lt;br&gt;
Once I set up the first authenticated Express route - &lt;code&gt;requireAuth&lt;/code&gt; middleware, controller function, Mongoose query, standardized JSON response - Copilot saw the pattern and replicated it consistently for every subsequent route I needed to add. Profile aggregation, gallery queries, adventure log, leaderboard, game session endpoints - the structural scaffolding was right on the first suggestion most of the time. I wasn't typing the same boilerplate eight times; I was reviewing and adjusting what Copilot generated.&lt;/p&gt;

&lt;p&gt;This alone probably saved three or four hours. Route scaffolding isn't hard, but doing it eight times in a row while trying to maintain consistency across middleware, error handling, and response shapes is exactly the kind of work that makes you lose momentum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrating dozens of Firebase calls to the new API client.&lt;/strong&gt;&lt;br&gt;
The migration from Firebase client SDK calls to a centralized &lt;code&gt;src/lib/api.js&lt;/code&gt; with JWT headers touched almost every page and component in the frontend. Every component that previously called Firebase directly needed to be updated to use the new API client pattern instead.&lt;/p&gt;

&lt;p&gt;Copilot saw the pattern in each file - the old Firebase call, the new &lt;code&gt;api.get()&lt;/code&gt; or &lt;code&gt;api.post()&lt;/code&gt; shape - and predicted the correct replacement. What would have been a full evening of tedious find-and-replace work took a couple of hours, and the replacements were consistent in a way that's easy to miss when you're doing it manually and getting tired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing the error-handling layer.&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;formatError.js&lt;/code&gt; utility and all the &lt;code&gt;react-hot-toast&lt;/code&gt; wrappers are the kind of work that's easy to skip because it's not a feature - it just makes the app &lt;em&gt;not feel broken&lt;/em&gt;. Copilot drafted the initial pattern-match structure and the toast helper function signatures. I adjusted the tone of each message to fit the game's friendly voice, but the structure and the enumeration of error cases were Copilot's first suggestion.&lt;/p&gt;

&lt;p&gt;Without Copilot, this section would probably have been "good enough" error handling - maybe one or two cases covered, everything else falling through to a generic message. With it, every meaningful failure scenario has a specific, useful, plain-language response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping the UI components visually consistent.&lt;/strong&gt;&lt;br&gt;
AI Playground has a dark-themed game UI with gradient headings, card components, stat blocks, progress bars, animated overlays, and a drawing HUD. All of these needed to look like they belonged together - same border radius, same color tokens, same responsive breakpoints. Copilot kept the Tailwind class composition consistent across &lt;code&gt;MissionHUD&lt;/code&gt;, &lt;code&gt;GameLoadingOverlay&lt;/code&gt;, profile stat cards, and the drawing toolbar as I built each piece. Small thing, but the visual coherence of the final UI is partly a result of Copilot staying consistent when I didn't have time to constantly cross-reference.&lt;/p&gt;


&lt;h3&gt;
  
  
  The moment Copilot actually surprised me
&lt;/h3&gt;

&lt;p&gt;The most valuable thing Copilot did wasn't fast typing - it was catching an edge case I would have missed.&lt;/p&gt;

&lt;p&gt;I was debugging the Gemini retry logic. My first version just checked &lt;code&gt;if (error.status === 503)&lt;/code&gt;, which felt like the right thing to do. The Gemini API returns 503 when overloaded, so catch that status and retry.&lt;/p&gt;

&lt;p&gt;When I described the problem to Copilot - "missions are silently failing when Gemini is overloaded, but my catch block doesn't seem to be catching everything" - its response wasn't just "add more status codes." It explained that the Gemini SDK surfaces failures in at least three different shapes depending on where in the call stack the error originates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes you get &lt;code&gt;error.status&lt;/code&gt; (HTTP layer error)&lt;/li&gt;
&lt;li&gt;Sometimes you get &lt;code&gt;error.error.status&lt;/code&gt; (SDK wrapper error)&lt;/li&gt;
&lt;li&gt;Sometimes neither field is set and the only signal is in the serialized error body as keywords like &lt;code&gt;"resource_exhausted"&lt;/code&gt; or &lt;code&gt;"overloaded"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the final retry check looks at all three forms. Without that, I would have had a retry policy that &lt;em&gt;looked&lt;/em&gt; correct in code review and silently missed half of the actual failure cases. I would have found it eventually - probably during a playtest at midnight - but I didn't have to.&lt;/p&gt;


&lt;h3&gt;
  
  
  Where I made the decisions myself
&lt;/h3&gt;

&lt;p&gt;Copilot does not know your game. It doesn't know when a failed drawing should trigger a retry versus abort the mission. It doesn't know what XP formula feels fair to a casual player. It doesn't know that passing previous mission prompts back to Gemini is how you prevent repeated missions. It doesn't know when "the AI is overloaded" should mean "try again" versus "load a fallback and keep playing."&lt;/p&gt;

&lt;p&gt;These are judgment calls - about user experience, game design, player frustration curves, and what a good session arc feels like. I made all of those.&lt;/p&gt;

&lt;p&gt;The auth security decisions were also entirely manual. What runs server-side versus client-side, how the JWT payload is structured, what &lt;code&gt;requireAuth&lt;/code&gt; must reject and why - none of that was delegated. Getting that wrong has real consequences.&lt;/p&gt;

&lt;p&gt;The most productive workflow I found with Copilot was giving it &lt;strong&gt;file-level tasks with enough context to be specific&lt;/strong&gt;: "Add a &lt;code&gt;GET /users/me/profile&lt;/code&gt; endpoint that aggregates the user document with drawing count, session history, and leaderboard rank from these three MongoDB models." That's specific enough for Copilot to generate something structurally useful. Vague prompts return vague code.&lt;/p&gt;


&lt;h3&gt;
  
  
  Copilot as a finishing partner, not a code generator
&lt;/h3&gt;

&lt;p&gt;The Finish-Up-A-Thon challenge is about reviving things you started. The biggest obstacle to reviving a stalled project usually isn't lack of ideas - it's the activation energy required to push through the unglamorous work before you get to the part that's fun again.&lt;/p&gt;

&lt;p&gt;Copilot lowered that activation energy. It handled the boilerplate, kept the patterns consistent, and caught at least one real bug I would have shipped. I handled the architecture, the game design, the security decisions, and the user experience. Both parts were necessary to finish the project.&lt;/p&gt;


&lt;h2&gt;
  
  
  What This Sprint Taught Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One complete loop beats ten half-finished features.&lt;/strong&gt; The thing that made AI Playground feel done was having a complete, working arc in Creative Quest: sign in → generate mission → draw → submit → Gemini scores → XP saves → profile updates. Every page I built after that felt like bonus. A complete loop is what separates "cool demo" from "actual game."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move AI to the server early.&lt;/strong&gt; The moment Gemini left the React layer, everything else became unlocked - retries, fallbacks, auth gating, rate limiting, security. None of that is possible when the AI runs client-side. This was the single structural change that made the most things possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good error messages are part of the product, not an afterthought.&lt;/strong&gt; Players don't read HTTP status codes. Someone who spent two minutes carefully drawing a dragon should not be told &lt;code&gt;{"error":{"code":503,"status":"UNAVAILABLE"}}&lt;/code&gt;. "Our AI helper is busy right now, we loaded a backup mission for you" is a completely different experience. The underlying failure is identical. The player's perception of the app is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "before and after" framing actually works.&lt;/strong&gt; The Finish-Up-A-Thon mandate - show us the arc - forced me to close loops instead of opening new ones. Every time I wanted to add a new feature, I asked whether it contributed to a clear completion story. If not, it went on a list for later. That constraint made the sprint productive in a way that open-ended "work on your project" never would have been.&lt;/p&gt;


&lt;h2&gt;
  
  
  Run It Locally
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Krish-Panchani/ai-playground.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-playground

&lt;span class="c"&gt;# Install frontend and backend dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nt"&gt;--prefix&lt;/span&gt; server &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Set up environment files&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env-example .env
&lt;span class="nb"&gt;cp &lt;/span&gt;server/.env-example server/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Frontend &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; (project root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_GOOGLE_CLIENT_ID=your-google-oauth-web-client-id.apps.googleusercontent.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend &lt;code&gt;server/.env&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=8080
NODE_ENV=development
CLIENT_ORIGIN=http://localhost:5173
MONGODB_URI=mongodb+srv://&amp;lt;user&amp;gt;:&amp;lt;pass&amp;gt;@&amp;lt;cluster&amp;gt;/&amp;lt;db&amp;gt;?retryWrites=true&amp;amp;w=majority
MONGODB_DB_NAME=ai_playground
GEMINI_API_KEY=your-gemini-api-key
GEMINI_VISION_MODEL=gemini-2.5-flash
GOOGLE_CLIENT_ID=your-google-oauth-web-client-id.apps.googleusercontent.com
JWT_SECRET=any-long-random-string-at-least-32-characters
JWT_EXPIRES_IN=7d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start backend (terminal 1)&lt;/span&gt;
npm run server:dev    &lt;span class="c"&gt;# runs on http://localhost:8080&lt;/span&gt;

&lt;span class="c"&gt;# Start frontend (terminal 2)&lt;/span&gt;
npm run dev           &lt;span class="c"&gt;# runs on http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run without a &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; - the server will return mock AI responses so you can test the full game loop, profile, leaderboard, and gallery without needing a Google Cloud account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI Playground started as a weekend hackathon idea that actually resonated with people - an AI that looks at your drawing and reacts to it specifically, not generically. The problem was that the prototype was never meant to survive past the demo. API keys in the browser, a hardcoded leaderboard, a game mode that was really just a single API call, errors surfacing as raw JSON popups. It worked in the moment and fell apart the second the moment ended.&lt;/p&gt;

&lt;p&gt;The GitHub Finish-Up-A-Thon gave me a reason to go back and do it properly. A real backend, a real auth flow, a real game loop with persistent state, proper error handling, and all four product pages that were missing. The core idea - drawing + AI feedback = fun - turned out to hold up. It just needed the infrastructure around it to make it feel like a product instead of a proof of concept.&lt;/p&gt;

&lt;p&gt;GitHub Copilot was the right tool for this specific challenge. Finishing a stalled project isn't about having more ideas - it's about grinding through the repetitive work until the interesting parts are reachable again. Copilot handled the repetitive parts (route scaffolding, migration boilerplate, UI consistency, error utilities) and caught at least one real edge case in the Gemini retry logic that I would have shipped as a silent bug. That freed me up to focus on the parts that actually required judgment: the game design, the auth security, the user experience, and the AI integration details.&lt;/p&gt;

&lt;p&gt;If you've got a project sitting in a GitHub tab that you keep meaning to come back to - this challenge is worth trying. The "before and after" framing forces you to actually finish something, and finishing something that already has momentum is a very different experience than starting from scratch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with GitHub Copilot for the GitHub Finish-Up-A-Thon Challenge.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Repository: &lt;a href="https://github.com/Krish-Panchani/ai-playground" rel="noopener noreferrer"&gt;github.com/Krish-Panchani/ai-playground&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Growing as a Developer Through Hacktoberfest 2023</title>
      <dc:creator>Krish Panchani</dc:creator>
      <pubDate>Fri, 27 Oct 2023 05:30:49 +0000</pubDate>
      <link>https://dev.to/krishpanchani/growing-as-a-developer-through-hacktoberfest-2023-ipb</link>
      <guid>https://dev.to/krishpanchani/growing-as-a-developer-through-hacktoberfest-2023-ipb</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Intro&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My name is &lt;strong&gt;Krish Panchani&lt;/strong&gt; and I am a &lt;strong&gt;IT Student&lt;/strong&gt;. This was my first Hacktoberfest, and I am so glad I participated! I learned so much and made some great contributions to open source projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Highs and Lows&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of my biggest accomplishments this month was completing a pull request for a complex issue in a popular open source project. I had to learn a lot about the project's codebase and architecture, but I was able to figure it out and make a meaningful contribution.&lt;/p&gt;

&lt;p&gt;Another highlight was meeting new people and collaborating with other contributors. I joined the project's Discord server and was able to get help from other developers when I got stuck. I also made some new friends who I hope to continue working with on open source projects in the future.&lt;/p&gt;

&lt;p&gt;One of the biggest challenges I faced was debugging a bug in the project's codebase. I spent a lot of time trying to figure it out, but I couldn't solve it on my own. Eventually, I I reached out to other contributors for help and we were able to track down the bug and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Growth&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before Hacktoberfest, I had some experience with Git and GitHub, but I wasn't very familiar with open source development. Through participating in Hacktoberfest, I learned a lot about how to find open source projects to contribute to, how to write pull requests, and how to collaborate with other developers.&lt;/p&gt;

&lt;p&gt;I also improved my coding skills by working on different types of issues. For example, I worked on fixing bugs, adding new features, and improving documentation.&lt;/p&gt;

&lt;p&gt;My participation in &lt;strong&gt;Hacktoberfest&lt;/strong&gt; has inspired me to continue contributing to open source projects. I am also more interested in pursuing a career in open source development.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Hacktoberfest was a great experience for me. I learned a lot, made some great contributions to open source projects, and met some new friends. I highly recommend participating in Hacktoberfest to anyone who is interested in learning more about open source development or contributing to open source projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips for future contributors&lt;/strong&gt;&lt;br&gt;
Here are a few tips for future Hacktoberfest contributors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start early. Don't wait until the last minute to start working on contributions. This will give you more time to learn about the project's codebase and architecture, and to get help from other contributors if you need it.&lt;/li&gt;
&lt;li&gt;Choose projects that interest you. This will make the experience more enjoyable and motivating.&lt;/li&gt;
&lt;li&gt;Don't be afraid to ask for help. There are many experienced contributors who are willing to help new contributors.&lt;/li&gt;
&lt;li&gt;Be patient. It may take some time to find the right project to contribute to and to learn about the project's codebase. Don't get discouraged if you don't get your first pull request accepted right away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this blog post has been helpful. Good luck with your Hacktoberfest journey!&lt;/p&gt;

</description>
      <category>hack23contributor</category>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
