Repository: susumutomita/wizarding-code-school
Pull request #46: Add gamification reward system
Author: login: devin-ai-integration[bot]
Created: 2025-05-07T22:32:54Z | Merged: 2025-05-07T22:57:24Z

# Add Gamification Reward System

This PR implements a gamification reward system for the Wizarding Code School application as requested in issue #33.

## Changes

- Added achievement tracking to the `ChapterProgress` interface in `useProgress.ts`
- Updated the `completeChapter` function to calculate and store achievement data
- Modified the `RunControls` component to track hint usage and attempts
- Updated the `ChapterSuccess` component to display achievement stars and details
- Added achievement stars display to the `ChapterSelector` component
- Created CSS styles for achievement displays

## Features

- **Star Rating System**: Users can earn up to 3 stars per chapter
  - 1 star for completing the chapter
  - 1 star for completing without using hints
  - 1 star for completing in 3 or fewer attempts
- **Achievement Display**: Stars and achievement details are shown on the success screen
- **Chapter List**: Achievement stars are displayed in the chapter selector

## Testing

- Verified that achievements are properly tracked and stored
- Confirmed that stars display correctly on the success screen
- Ensured that hint usage tracking works as expected
- Validated that the chapter selector shows achievement stars

## Link to Devin run
https://app.devin.ai/sessions/4a226323cc284127b36b341272ded02f

Requested by: bull (oyster880@gmail.com)



Diff:
diff --git a/src/App.tsx b/src/App.tsx
index 74a2b62..a057ebb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -151,9 +151,10 @@ ${currentChapter.allowedCommands[0]}();`
   // Handle successful chapter completion
   const handleChapterSuccess = (
     code: string,
-    validationResult?: { allRequirementsMet: boolean; missingCommands: string[] }
+    validationResult?: { allRequirementsMet: boolean; missingCommands: string[] },
+    achievementData?: { noHints: boolean; attemptsCount: number }
   ): void => {
-    completeChapter(currentChapter.id, code);
+    completeChapter(currentChapter.id, code, achievementData);
     setCurrentValidationResult(validationResult);
     setCurrentScreen(AppScreen.SUCCESS);
   };
@@ -196,6 +197,7 @@ ${currentChapter.allowedCommands[0]}();`
             <ChapterSelector
               currentChapterId={currentChapter.id}
               completedChapters={completedChapterIds}
+              progress={progress}
               onSelectChapter={(id: string): void => {
                 // チャプター切り替え前に現在のコードを保存
                 if (currentChapter) {
@@ -282,6 +284,7 @@ ${currentChapter.allowedCommands[0]}();`
             chapter={currentChapter}
             onContinue={handleContinue}
             validationResult={currentValidationResult}
+            achievementData={progress.chapters[currentChapter.id]?.achievements}
           />
         )}
       </main>
diff --git a/src/components/ChapterSelector.tsx b/src/components/ChapterSelector.tsx
index ec9ef8b..4b56b77 100644
--- a/src/components/ChapterSelector.tsx
+++ b/src/components/ChapterSelector.tsx
@@ -1,16 +1,20 @@
 import React from 'react';
 import { getAllChapters } from '../data/chapters';
+import { Progress } from '../hooks/useProgress';
+import './achievements.css';
 
 interface ChapterSelectorProps {
   currentChapterId: string;
   completedChapters: string[];
   onSelectChapter: (chapterId: string) => void;
+  progress?: Progress;
 }
 
 export const ChapterSelector: React.FC<ChapterSelectorProps> = ({
   currentChapterId,
   completedChapters,
   onSelectChapter,
+  progress,
 }) => {
   const chapters = getAllChapters();
 
@@ -34,6 +38,22 @@ export const ChapterSelector: React.FC<ChapterSelectorProps> = ({
               <div className="chapter-info">
                 <div className="chapter-title">{chapter.title}</div>
                 <div className="chapter-desc">{chapter.description}</div>
+                {isCompleted && (
+                  <div className="chapter-achievements">
+                    {[...Array(3)].map((_, i) => {
+                      const chapterAchievements = progress?.chapters[chapter.id]?.achievements;
+                      const stars = chapterAchievements?.stars || 0;
+                      return (
+                        <span
+                          key={i}
+                          className={`achievement-star-small ${i < stars ? 'earned' : 'unearned'}`}
+                        >
+                          {i < stars ? '⭐' : '☆'}
+                        </span>
+                      );
+                    })}
+                  </div>
+                )}
               </div>
               {!isAvailable && <div className="chapter-lock">🔒</div>}
             </div>
diff --git a/src/components/ChapterSuccess.tsx b/src/components/ChapterSuccess.tsx
index c632b59..0f38724 100644
--- a/src/components/ChapterSuccess.tsx
+++ b/src/components/ChapterSuccess.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import { Chapter, getChapter } from '../data/chapters';
+import './achievements.css';
 
 interface ChapterSuccessProps {
   chapter: Chapter;
@@ -8,12 +9,18 @@ interface ChapterSuccessProps {
     allRequirementsMet: boolean;
     missingCommands: string[];
   };
+  achievementData?: {
+    noHints?: boolean;
+    attemptsCount?: number;
+    stars?: number;
+  };
 }
 
 export const ChapterSuccess: React.FC<ChapterSuccessProps> = ({
   chapter,
   onContinue,
   validationResult,
+  achievementData,
 }) => {
   // Check if there's a next chapter
   const hasNextChapter = !!chapter.nextChapterId;
@@ -33,6 +40,52 @@ export const ChapterSuccess: React.FC<ChapterSuccessProps> = ({
       <div className="success-content">
         <p className="success-message">{chapter.successMessage}</p>
 
+        {/* Display achievement stars */}
+        {achievementData && (
+          <div className="achievement-stars">
+            <h3>
+              <span role="img" aria-label="trophy">
+                🏆
+              </span>{' '}
+              Your Achievements
+            </h3>
+            <div className="stars-container">
+              {[...Array(3)].map((_, i) => (
+                <span
+                  key={i}
+                  className={`achievement-star ${i < (achievementData.stars || 0) ? 'earned' : 'unearned'}`}
+                  role="img"
+                  aria-label={i < (achievementData.stars || 0) ? 'earned star' : 'unearned star'}
+                >
+                  {i < (achievementData.stars || 0) ? '⭐' : '☆'}
+                </span>
+              ))}
+            </div>
+            <div className="achievement-details">
+              <p>
+                <span
+                  className={achievementData.noHints ? 'achievement-earned' : 'achievement-missed'}
+                >
+                  {achievementData.noHints ? '✅' : '❌'} Completed without hints
+                </span>
+              </p>
+              <p>
+                <span
+                  className={
+                    (achievementData.attemptsCount || 0) <= 3
+                      ? 'achievement-earned'
+                      : 'achievement-missed'
+                  }
+                >
+                  {(achievementData.attemptsCount || 0) <= 3 ? '✅' : '❌'} Completed in{' '}
+                  {achievementData.attemptsCount || 0} attempts{' '}
+                  {(achievementData.attemptsCount || 0) <= 3 ? '(3 or fewer)' : '(more than 3)'}
+                </span>
+              </p>
+            </div>
+          </div>
+        )}
+
         {/* Show feedback about missing required commands */}
         {validationResult && !validationResult.allRequirementsMet && (
           <div className="concept-feedback-warning">
diff --git a/src/components/RunControls.tsx b/src/components/RunControls.tsx
index 5af58cc..c63b799 100644
--- a/src/components/RunControls.tsx
+++ b/src/components/RunControls.tsx
@@ -19,7 +19,8 @@ interface RunControlsProps {
   /** Optional callback to save progress */
   onSuccess?: (
     code: string,
-    validationResult?: { allRequirementsMet: boolean; missingCommands: string[] }
+    validationResult?: { allRequirementsMet: boolean; missingCommands: string[] },
+    achievementData?: { noHints: boolean; attemptsCount: number }
   ) => void;
   /** Optional callback for showing a hint */
   onShowHint?: () => void;
@@ -53,6 +54,7 @@ export const RunControls: React.FC<RunControlsProps> = ({
   const [stopRunner, setStopRunner] = useState<(() => void) | null>(null);
   const [errorMessage, setErrorMessage] = useState<string | null>(null);
   const [attempts, setAttempts] = useState(0);
+  const [usedHint, setUsedHint] = useState(false);
   const [localTorches, setLocalTorches] = useState<TorchState[]>(torches);
 
   // Sync localTorches state with the torches prop when it changes
@@ -84,6 +86,13 @@ export const RunControls: React.FC<RunControlsProps> = ({
     }
   };
 
+  const handleHintButtonClick = (): void => {
+    if (onShowHint) {
+      setUsedHint(true);
+      onShowHint();
+    }
+  };
+
   const handleRun = (): void => {
     try {
       setIsRunning(true);
@@ -145,7 +154,10 @@ export const RunControls: React.FC<RunControlsProps> = ({
             // Wait for the animation to play before showing success dialog
             setTimeout(() => {
               if (onSuccess) {
-                onSuccess(code, validationResult);
+                onSuccess(code, validationResult, {
+                  noHints: !usedHint,
+                  attemptsCount: attempts,
+                });
               }
             }, 1500);
           } else if (runResult === 'fail') {
@@ -246,7 +258,7 @@ export const RunControls: React.FC<RunControlsProps> = ({
           </button>
         )}
         {onShowHint && (
-          <button onClick={onShowHint} className="hint-button" disabled={isRunning}>
+          <button onClick={handleHintButtonClick} className="hint-button" disabled={isRunning}>
             Get Hint
           </button>
         )}
diff --git a/src/components/achievements.css b/src/components/achievements.css
new file mode 100644
index 0000000..dfdca44
--- /dev/null
+++ b/src/components/achievements.css
@@ -0,0 +1,66 @@
+/* Achievement Stars Styling */
+.achievement-stars {
+  margin-top: 20px;
+  padding: 15px;
+  background-color: rgba(255, 255, 255, 0.1);
+  border-radius: 8px;
+  text-align: center;
+}
+
+.stars-container {
+  display: flex;
+  justify-content: center;
+  margin: 15px 0;
+  gap: 10px;
+}
+
+.achievement-star {
+  font-size: 32px;
+  transition: transform 0.3s ease;
+}
+
+.achievement-star.earned {
+  color: gold;
+  text-shadow: 0 0 10px rgba(255, 215, 0, 0.7);
+  transform: scale(1.1);
+}
+
+.achievement-star.unearned {
+  color: #888;
+  opacity: 0.6;
+}
+
+.achievement-details {
+  text-align: left;
+  margin-top: 10px;
+}
+
+.achievement-earned {
+  color: #4caf50;
+  font-weight: bold;
+}
+
+.achievement-missed {
+  color: #ff9800;
+}
+
+/* Small stars for chapter selector */
+.chapter-achievements {
+  display: flex;
+  margin-top: 5px;
+  gap: 5px;
+}
+
+.achievement-star-small {
+  font-size: 14px;
+}
+
+.achievement-star-small.earned {
+  color: gold;
+  text-shadow: 0 0 5px rgba(255, 215, 0, 0.5);
+}
+
+.achievement-star-small.unearned {
+  color: #888;
+  opacity: 0.5;
+}
diff --git a/src/hooks/useProgress.ts b/src/hooks/useProgress.ts
index 39f4741..ff71ae4 100644
--- a/src/hooks/useProgress.ts
+++ b/src/hooks/useProgress.ts
@@ -4,9 +4,14 @@ import { getFirstChapter, getChapter, Chapter } from '../data/chapters';
 interface ChapterProgress {
   completed: boolean;
   code?: string;
+  achievements?: {
+    noHints?: boolean;
+    attemptsCount?: number;
+    stars?: number;
+  };
 }
 
-interface Progress {
+export interface Progress {
   currentChapterId: string;
   chapters: Record<string, ChapterProgress>;
 }
@@ -21,7 +26,11 @@ export const useProgress = (
   userId?: string
 ): {
   currentChapter: Chapter;
-  completeChapter: (chapterId: string, code?: string) => void;
+  completeChapter: (
+    chapterId: string,
+    code?: string,
+    achievementData?: { noHints: boolean; attemptsCount: number }
+  ) => void;
   navigateToChapter: (chapterId: string) => void;
   isChapterCompleted: (chapterId: string) => boolean;
   getSavedCode: (chapterId: string) => string | undefined;
@@ -116,10 +125,27 @@ export const useProgress = (
   };
 
   /**
-   * Mark a chapter as completed and optionally save the solution code
+   * Mark a chapter as completed and optionally save the solution code and achievements
    */
-  const completeChapter = (chapterId: string, code?: string): void => {
+  const completeChapter = (
+    chapterId: string,
+    code?: string,
+    achievementData?: {
+      noHints: boolean;
+      attemptsCount: number;
+    }
+  ): void => {
+    const calculateStars = (achievementData?: { noHints: boolean; attemptsCount: number }): number => {
+      let stars = 1; // At least 1 star for completion
+      if (achievementData) {
+        if (achievementData.noHints) stars++;
+        if (achievementData.attemptsCount <= 3) stars++;
+      }
+      return stars;
+    };
+
     setProgress(prev => {
+      const stars = calculateStars(achievementData);
       const updatedProgress = {
         ...prev,
         chapters: {
@@ -127,6 +153,14 @@ export const useProgress = (
           [chapterId]: {
             completed: true,
             code: code || prev.chapters[chapterId]?.code,
+            achievements: {
+              ...prev.chapters[chapterId]?.achievements,
+              ...(achievementData && {
+                noHints: achievementData.noHints,
+                attemptsCount: achievementData.attemptsCount,
+                stars,
+              }),
+            },
           },
         },
       };


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements a gamification reward system for the Wizarding Code School application by tracking and displaying achievement data when chapters are completed. Key changes include:
- Updating progress tracking (in useProgress.ts) to compute achievement stars based on hint usage and attempt count.
- Modifying UI components (ChapterSuccess, ChapterSelector, RunControls) to display achievement stars and details.
- Adding new CSS styles for achievement displays.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                             | Description                                                                   |
| -------------------------------- | ----------------------------------------------------------------------------- |
| src/hooks/useProgress.ts         | Added achievement tracking and star calculation logic in the completeChapter function. |
| src/components/achievements.css  | Created CSS styles for achievement stars and details.                        |
| src/components/RunControls.tsx     | Updated hint usage tracking and integrated achievement data in onSuccess handling.   |
| src/components/ChapterSuccess.tsx  | Modified component to display achievement stars and details on chapter success.        |
| src/components/ChapterSelector.tsx | Updated component to show achievement stars for completed chapters.          |
| src/App.tsx                      | Passed achievement data to components and updated completeChapter invocation.     |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T22:49:00Z

Review comments:
diff_hunk: @@ -116,17 +125,38 @@ export const useProgress = (
   };
 
   /**
-   * Mark a chapter as completed and optionally save the solution code
+   * Mark a chapter as completed and optionally save the solution code and achievements
    */
-  const completeChapter = (chapterId: string, code?: string): void => {
+  const completeChapter = (
+    chapterId: string,
+    code?: string,
+    achievementData?: {
+      noHints: boolean;
+      attemptsCount: number;
+    }
+  ): void => {
     setProgress(prev => {
+      let stars = 1; // At least 1 star for completion
+      if (achievementData) {
+        if (achievementData.noHints) stars++;
+        if (achievementData.attemptsCount <= 3) stars++;
+      }
+
path: src/hooks/useProgress.ts
user: login: Copilot
body: [nitpick] Consider extracting the star calculation logic into a separate helper function to improve readability and ease future modifications.
```suggestion
    const calculateStars = (achievementData?: { noHints: boolean; attemptsCount: number }): number => {
      let stars = 1; // At least 1 star for completion
      if (achievementData) {
        if (achievementData.noHints) stars++;
        if (achievementData.attemptsCount <= 3) stars++;
      }
      return stars;
    };

    setProgress(prev => {
      const stars = calculateStars(achievementData);
```
created_at: 2025-05-07T22:48:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +38,22 @@ export const ChapterSelector: React.FC<ChapterSelectorProps> = ({
               <div className="chapter-info">
                 <div className="chapter-title">{chapter.title}</div>
                 <div className="chapter-desc">{chapter.description}</div>
+                {isCompleted && completedChapters.includes(chapter.id) && (
path: src/components/ChapterSelector.tsx
user: login: Copilot
body: The condition redundantly checks if the chapter is completed; since 'isCompleted' already represents completedChapters.includes(chapter.id), removing the duplicate condition would simplify the logic.
```suggestion
                {isCompleted && (
```
created_at: 2025-05-07T22:49:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: devin-ai-integration[bot]
created_at: 2025-05-07T22:32:59Z
body: ### 🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:
- Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
- Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:
- [ ] Disable automatic comment and CI monitoring

author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-07T22:33:02Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Bot user detected.
> 
> To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=susumutomita/wizarding-code-school&utm_content=46):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Join our [Discord community](https://discord.gg/coderabbit) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE