File size: 22,739 Bytes
05dff9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
import math
from flask import Flask, render_template, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = 'supersecretkey_for_session_management' # Required for flash messages and session

# --- In-Memory Content Definitions ---

# Videos (ID, Title, YouTube URL, Length, Base Points for Completion)
# Renamed to RAW as we'll augment it with position and path data
VIDEOS_RAW = [
    {"id": "v1", "title": "A Beginner's Guide to Rainwater Collection", "url": "https://www.youtube.com/embed/uPS3GT1DC_Q?si=OYkUG-tuJeZquIkv", "length": "10:51", "base_points": 50},
    {"id": "v2", "title": "Introduction to Rainwater Harvesting", "url": "https://www.youtube.com/embed/uPS3GT1DC_Q?si=OYkUG-tuJeZquIkv", "length": "6:44", "base_points": 40},
    {"id": "v3", "title": "First Flush Diverter Explained", "url": "https://www.youtube.com/embed/zO09yau1lCo?si=g-EJqsLU3PNiWjRD", "length": "8:16", "base_points": 60},
    {"id": "v4", "title": "Is Rainwater Safe to Drink?", "url": "https://www.youtube.com/embed/nLphSEiHroU?si=PJ49EHuKVxeYRt_H", "length": "7:00", "base_points": 70}, # Adjusted length for the new video
    {"id": "v5", "title": "First Flush Installation", "url": "https://www.youtube.com/embed/gxguzuyDicQ?si=m8tMaKPh366Ox89a", "length": "9:34", "base_points": 65},
    {"id": "v6", "title": "DIY Home System Tour", "url": "https://www.youtube.com/embed/J2t_ZZGmBoo?si=LpSyztIH8xtF_Z_3", "length": "10:27", "base_points": 80},
    {"id": "v7", "title": "Upgrading a Large System", "url": "https://www.youtube.com/embed/-NJ8aS4_WDA?si=2C0vCgr2O2hiMJx6", "length": "9:01", "base_points": 75},
    {"id": "v8", "title": "Living on Rainwater in a Desert Environment", "url": "https://www.youtube.com/embed/rxmspcSjjKM?si=TerHbg_Ny_ZjbEy3", "length": "10:43", "base_points": 90},
    {"id": "v9", "title": "Polyethylene Tank Repair", "url": "https://www.youtube.com/embed/R1CCQXHUvNE?si=XEn2Jwdza5u5x_2o", "length": "11:00", "base_points": 55},
    {"id": "v10", "title": "Pump System Overview", "url": "https://www.youtube.com/embed/-yhWEkXqVR0?si=eWeutKwzNdpJGIOy", "length": "7:30", "base_points": 85},
]
# NOTE: The YouTube URLs have been updated with the actual video embed URLs.

# Fixed node positions (percentage from left/top of map-area)
# These are floats for easier calculation in Python.
NODE_POSITIONS = {
    'v1': {'top': 90, 'left': 20},
    'v2': {'top': 80, 'left': 35},
    'v3': {'top': 70, 'left': 20},
    'v4': {'top': 60, 'left': 45},
    'v5': {'top': 50, 'left': 25},
    'v6': {'top': 40, 'left': 50},
    'v7': {'top': 30, 'left': 30},
    'v8': {'top': 20, 'left': 60},
    'v9': {'top': 10, 'left': 40},
    'v10': {'top': 5, 'left': 75}
}

# --- Augment VIDEOS_RAW with positions and path data ---
VIDEOS = []
prev_node_pos_data = None # Stores {'top': float, 'left': float} for the previous node

# Assuming a hypothetical map_area pixel dimension for path calculations
# These are used to convert % to px for path length calculation.
# In a real responsive app, for truly accurate paths, these dimensions might need to be dynamic (e.e.g, from JS on client).
# For now, these fixed values provide a consistent visual ratio.
MAP_AREA_WIDTH_PX = 800 # Represents 100% width of the map-area
MAP_AREA_HEIGHT_PX = 700 # Represents 100% height of the map-area

for i, video_data in enumerate(VIDEOS_RAW):
    video_id = video_data['id']
    node_pos_percent = NODE_POSITIONS.get(video_id)
    if not node_pos_percent:
        continue # Skip if no position defined for this video

    # Add position data (as percentages) to the video dictionary
    video_data['pos_top'] = node_pos_percent['top']
    video_data['pos_left'] = node_pos_percent['left']

    # Calculate path data leading TO this current node FROM the previous node
    if prev_node_pos_data:
        # Convert percentages to approximate pixels for calculation
        x1_px = prev_node_pos_data['left'] / 100 * MAP_AREA_WIDTH_PX
        y1_px = prev_node_pos_data['top'] / 100 * MAP_AREA_HEIGHT_PX
        x2_px = video_data['pos_left'] / 100 * MAP_AREA_WIDTH_PX
        y2_px = video_data['pos_top'] / 100 * MAP_AREA_HEIGHT_PX

        dx = x2_px - x1_px
        dy = y2_px - y1_px

        distance = math.sqrt(dx**2 + dy**2)
        angle_rad = math.atan2(dy, dx) # Angle in radians

        # Store path data in the *current* video, which represents the segment *before* it
        video_data['path_data'] = {
            'top': f"{prev_node_pos_data['top']}%", # Path starts at previous node's top
            'left': f"{prev_node_pos_data['left']}%", # Path starts at previous node's left
            'width': f"{distance}px", # Width in pixels
            'transform': f"rotate({math.degrees(angle_rad)}deg)" # CSS rotate expects degrees
        }
    else:
        video_data['path_data'] = None # No path for the very first node

    VIDEOS.append(video_data)
    # Update prev_node_pos_data for the next iteration
    prev_node_pos_data = {'top': video_data['pos_top'], 'left': video_data['pos_left']}


# Quizzes (Video ID -> List of Questions)
# Each question: 'question', 'options' (list), 'correct_answer_index' (0-indexed), 'points'
QUIZZES = {
    "v1": [
        {"question": "What is the primary benefit of rainwater collection?", "options": ["Reduced water bills", "Increased garden pests", "More cloudy days", "Better internet speed"], "correct_answer_index": 0, "points": 20},
        {"question": "What is a common component of a basic rainwater system?", "options": ["Solar panels", "Downspout", "Microwave", "Treadmill"], "correct_answer_index": 1, "points": 20},
        {"question": "Why is rainwater harvesting considered sustainable?", "options": ["It uses more energy", "It depletes groundwater", "It utilizes a renewable resource", "It requires constant human intervention"], "correct_answer_index": 2, "points": 20},
    ],
    "v2": [
        {"question": "What is the first step in harvesting rainwater?", "options": ["Filtering", "Collecting from a surface", "Storing in a tank", "Drinking it"], "correct_answer_index": 1, "points": 20},
        {"question": "Which of these is NOT typically part of an introduction to rainwater harvesting?", "options": ["System sizing", "Components overview", "Basic benefits", "Advanced purification techniques"], "correct_answer_index": 3, "points": 20},
        {"question": "What is a common use for harvested rainwater in a household?", "options": ["Cooking", "Showering", "Toilet flushing", "Drinking without treatment"], "correct_answer_index": 2, "points": 20},
    ],
    "v3": [
        {"question": "What is the main purpose of a first flush diverter?", "options": ["To increase water pressure", "To divert initial dirty rainwater", "To cool the water", "To attract insects"], "correct_answer_index": 1, "points": 20},
        {"question": "When does the first flush occur?", "options": ["During the middle of a storm", "After a prolonged dry spell", "At the very beginning of a rain event", "When the tank is full"], "correct_answer_index": 2, "points": 20},
        {"question": "Why is it important to divert the first flush?", "options": ["To save space in the tank", "To improve water quality", "To reduce the amount of collected water", "To make the system more complex"], "correct_answer_index": 1, "points": 20},
    ],
     "v4": [
        {"question": "What is crucial for making rainwater safe to drink?", "options": ["No filtration at all", "Only basic screening", "Proper filtration and purification", "Adding sugar"], "correct_answer_index": 2, "points": 20},
        {"question": "What contaminants might be found in raw rainwater?", "options": ["Pure oxygen", "Roof debris, dust, bird droppings", "Salt", "Distilled water"], "correct_answer_index": 1, "points": 20},
        {"question": "Which method is commonly used for purifying drinking water from rainwater?", "options": ["Boiling", "Freezing", "Microwaving", "Shaking vigorously"], "correct_answer_index": 0, "points": 20},
    ],
    "v5": [
        {"question": "What type of pipe is often used for first flush diverters?", "options": ["Electrical conduit", "PVC pipe", "Garden hose", "Metal rebar"], "correct_answer_index": 1, "points": 20},
        {"question": "Where is a first flush diverter typically installed?", "options": ["Inside the storage tank", "At the very top of the roof", "Between the gutter downspout and the storage tank", "Underground, far from the house"], "correct_answer_index": 2, "points": 20},
        {"question": "What is a common safety measure during installation?", "options": ["Working alone", "Wearing a blindfold", "Using proper tools and fall protection", "Installing during a thunderstorm"], "correct_answer_index": 2, "points": 20},
    ],
    "v6": [
        {"question": "What does a 'DIY Home System Tour' typically showcase?", "options": ["Professional installation only", "Pre-built commercial systems", "Custom-built, homeowner-installed systems", "Fantasy systems"], "correct_answer_index": 2, "points": 20},
        {"question": "What is a common component highlighted in a home tour?", "options": ["An electric car charger", "A storage tank", "A swimming pool heater", "A satellite dish"], "correct_answer_index": 1, "points": 20},
        {"question": "What can be learned from a DIY system tour?", "options": ["How to break the system", "Specific design choices and challenges", "The history of plumbing", "Advanced quantum physics"], "correct_answer_index": 1, "points": 20},
    ],
    "v7": [
        {"question": "What might an upgrade to a large system involve?", "options": ["Reducing tank capacity", "Adding more collection surfaces or storage", "Removing all filters", "Switching to bottled water"], "correct_answer_index": 1, "points": 20},
        {"question": "Why would someone upgrade a large rainwater harvesting system?", "options": ["To collect less water", "To improve efficiency or meet higher demand", "To make it less reliable", "To attract more birds"], "correct_answer_index": 1, "points": 20},
        {"question": "What should be considered when planning a system upgrade?", "options": ["Ignoring existing infrastructure", "Only focusing on aesthetics", "Budget, demand, and integration with current setup", "Completely random additions"], "correct_answer_index": 2, "points": 20},
    ],
    "v8": [
        {"question": "What is a primary challenge of living on rainwater in a desert?", "options": ["Too much rain", "Limited rainfall and high evaporation", "No need for water", "Excessive humidity"], "correct_answer_index": 1, "points": 20},
        {"question": "What strategy is crucial for rainwater harvesting in arid climates?", "options": ["Using tiny collection areas", "Maximizing collection surfaces and storage", "Wasting water frequently", "Ignoring forecasts"], "correct_answer_index": 1, "points": 20},
        {"question": "Why is careful management important in a desert environment?", "options": ["Water is abundant", "Water is a scarce and precious resource", "It's a fun hobby", "To prove a point"], "correct_answer_index": 1, "points": 20},
    ],
    "v9": [
        {"question": "What material are many water storage tanks made from?", "options": ["Glass", "Wood", "Polyethylene", "Cardboard"], "correct_answer_index": 2, "points": 20},
        {"question": "What tool might be used to prepare a cracked polyethylene tank for repair?", "options": ["A hammer", "A heat gun or plastic welder", "A spoon", "A paperclip"], "correct_answer_index": 1, "points": 20},
        {"question": "What is a key step in ensuring a successful tank repair?", "options": ["Leaving the crack dirty", "Cleaning and drying the area thoroughly", "Applying repair material blindly", "Ignoring manufacturer instructions"], "correct_answer_index": 1, "points": 20},
    ],
    "v10": [
        {"question": "What is the main function of a pump in a rainwater system?", "options": ["To filter the water", "To add chemicals to the water", "To pressurize and distribute water", "To cool the water"], "correct_answer_index": 2, "points": 20},
        {"question": "What kind of power source do most rainwater pumps use?", "options": ["Manual hand crank", "Electricity", "Solar power only", "Wind power"], "correct_answer_index": 1, "points": 20},
        {"question": "What needs to be considered when selecting a pump?", "options": ["Its color", "Flow rate and pressure requirements", "The brand of your car", "Its weight"], "correct_answer_index": 1, "points": 20},
    ],
}

BADGES = [
    {"id": "b1", "name": "Raindrop Rookie", "description": "Completed your first video.", "unlock_criteria": {"type": "video_completion", "video_id": "v1"}},
    {"id": "b2", "name": "First Flush Friend", "description": "Understood first flush diverters (v3 & v5).", "unlock_criteria": {"type": "multiple_video_completion", "video_ids": ["v3", "v5"]}},
    {"id": "b3", "name": "Clean Water Champion", "description": "Mastered making rainwater safe to drink (v4 quiz 100%).", "unlock_criteria": {"type": "quiz_score_100", "video_id": "v4"}},
    {"id": "b4", "name": "System Surveyor", "description": "Explored home and large system upgrades (v6 & v7).", "unlock_criteria": {"type": "multiple_video_completion", "video_ids": ["v6", "v7"]}},
    {"id": "b5", "name": "Desert Harvester", "description": "Learned about harvesting in arid climates (v8).", "unlock_criteria": {"type": "video_completion", "video_id": "v8"}},
    {"id": "b6", "name": "HydroHero Apprentice", "description": "Reached 500 total points.", "unlock_criteria": {"type": "total_points_threshold", "threshold": 500}},
    {"id": "b7", "name": "Ultimate HydroHero", "description": "Completed all videos and quizzes.", "unlock_criteria": {"type": "all_videos_completed_and_quizzed"}},
]

# --- In-Memory User Progress State (Global for single-user session) ---
# Using session for user_progress to make it somewhat persistent per user session
# Initialize if not present in session
def get_user_progress():
    if 'user_progress' not in session:
        session['user_progress'] = {
            "points": 0,
            "completed_videos": [],
            "video_quiz_scores": {},
            "unlocked_badges": []
        }
    return session['user_progress']

# --- Helper Functions ---

def get_video_by_id(video_id):
    """Retrieves video details by ID from the processed VIDEOS list."""
    return next((video for video in VIDEOS if video["id"] == video_id), None)

def get_quiz_by_video_id(video_id):
    """Retrieves quiz questions by video ID."""
    return QUIZZES.get(video_id)

def evaluate_badges():
    """Checks user progress against badge criteria and unlocks new badges."""
    user_progress = get_user_progress()
    current_unlocked_badge_ids = {badge['id'] for badge in user_progress['unlocked_badges']}

    for badge in BADGES:
        if badge['id'] not in current_unlocked_badge_ids:
            unlocked = False
            criteria = badge['unlock_criteria']

            if criteria['type'] == 'video_completion':
                if criteria['video_id'] in user_progress['completed_videos']:
                    unlocked = True
            elif criteria['type'] == 'multiple_video_completion':
                if all(vid in user_progress['completed_videos'] for vid in criteria['video_ids']):
                    unlocked = True
            elif criteria['type'] == 'quiz_score_100':
                video_id = criteria['video_id']
                if video_id in user_progress['video_quiz_scores']:
                    quiz_questions = get_quiz_by_video_id(video_id)
                    if quiz_questions:
                        max_score = sum(q['points'] for q in quiz_questions)
                        if user_progress['video_quiz_scores'][video_id] == max_score:
                            unlocked = True
            elif criteria['type'] == 'total_points_threshold':
                if user_progress['points'] >= criteria['threshold']:
                    unlocked = True
            elif criteria['type'] == 'all_videos_completed_and_quizzed':
                if len(user_progress['completed_videos']) == len(VIDEOS) and \
                   len(user_progress['video_quiz_scores']) == len(VIDEOS) and \
                   all(score is not None for score in user_progress['video_quiz_scores'].values()):
                    unlocked = True

            if unlocked:
                user_progress['unlocked_badges'].append(badge)
                flash(f"🎉 New Badge Unlocked: {badge['name']}!", 'success')
                session['user_progress'] = user_progress # Update session after modification

# --- Flask Routes ---

@app.route('/')
def landing():
    """Renders the game's landing page."""
    return render_template('landing.html')

@app.route('/dashboard')
def dashboard():
    """Renders the main dashboard showing videos, progress, and badges."""
    user_progress = get_user_progress()
    all_videos_and_quizzes_completed = False
    if 'b7' in {badge['id'] for badge in user_progress['unlocked_badges']}:
        all_videos_and_quizzes_completed = True

    return render_template('dashboard.html',
                           videos=VIDEOS, # Pass the augmented VIDEOS list
                           user_progress=user_progress,
                           total_videos=len(VIDEOS),
                           badges=BADGES,
                           all_videos_and_quizzes_completed=all_videos_and_quizzes_completed,
                           username=session.get('username', ''))

@app.route('/start_level/<video_id>')
def start_level(video_id):
    """Displays the video for the user to watch."""
    user_progress = get_user_progress()
    video = get_video_by_id(video_id)

    if not video:
        flash("Video not found!", 'error')
        return redirect(url_for('dashboard'))

    # Check if the previous video is completed for sequential access (unless it's the first video)
    video_index = next((i for i, v in enumerate(VIDEOS) if v['id'] == video_id), -1)
    if video_index > 0 and VIDEOS[video_index - 1]['id'] not in user_progress['completed_videos']:
        flash("Please complete the previous video first!", 'warning')
        return redirect(url_for('dashboard'))

    return render_template('start_level.html', video=video)


@app.route('/complete_video/<video_id>')
def complete_video(video_id):
    """Marks a video as complete and redirects to its quiz."""
    user_progress = get_user_progress()
    video = get_video_by_id(video_id)

    if not video:
        flash("Video not found!", 'error')
        return redirect(url_for('dashboard'))

    if video_id not in user_progress['completed_videos']:
        user_progress['completed_videos'].append(video_id)
        user_progress['points'] += video['base_points']
        flash(f"Video '{video['title']}' completed! +{video['base_points']} points.", 'info')
        session['user_progress'] = user_progress # Update session after modification
        evaluate_badges()

    return redirect(url_for('quiz', video_id=video_id))

@app.route('/quiz/<video_id>', methods=['GET', 'POST'])
def quiz(video_id):
    """Displays and processes quizzes for completed videos."""
    user_progress = get_user_progress()
    video = get_video_by_id(video_id)
    quiz_questions = get_quiz_by_video_id(video_id)

    if not video or not quiz_questions:
        flash("Quiz or video not found!", 'error')
        return redirect(url_for('dashboard'))

    if video_id not in user_progress['completed_videos']:
        flash("Please complete the video before taking the quiz.", 'warning')
        return redirect(url_for('dashboard'))

    if video_id in user_progress['video_quiz_scores']:
        flash(f"You already completed the quiz for '{video['title']}'. Your score: {user_progress['video_quiz_scores'][video_id]} points.", 'info')
        return redirect(url_for('dashboard'))

    if request.method == 'POST':
        user_score = 0
        correct_answers_count = 0
        for i, question_data in enumerate(quiz_questions):
            user_answer_index = request.form.get(f'question_{i}')
            if user_answer_index:
                try:
                    user_answer_index = int(user_answer_index)
                    if user_answer_index == question_data['correct_answer_index']:
                        user_score += question_data['points']
                        correct_answers_count += 1
                except ValueError:
                    pass

        user_progress['points'] += user_score
        user_progress['video_quiz_scores'][video_id] = user_score
        flash(f"Quiz completed for '{video['title']}': You scored {user_score} points ({correct_answers_count}/{len(quiz_questions)} correct)!", 'success')
        session['user_progress'] = user_progress # Update session after modification
        evaluate_badges()
        return redirect(url_for('dashboard'))

    return render_template('quiz.html', video=video, quiz_questions=quiz_questions, enumerate=enumerate)

@app.route('/set_username', methods=['POST'])
def set_username():
    """Sets the username in the session."""
    if request.method == 'POST':
        username = request.form.get('username')
        if username:
            session['username'] = username
            flash(f"Your name has been set to {username}!", 'info')
        else:
            flash("Please enter a valid name.", 'warning')
    return redirect(url_for('dashboard'))

@app.route('/certificate')
def certificate():
    """Generates and displays the certificate of completion."""
    user_progress = get_user_progress()
    # Check if the 'Ultimate HydroHero' badge (b7) is unlocked
    all_completed = 'b7' in {badge['id'] for badge in user_progress['unlocked_badges']}

    if not all_completed:
        flash("You must complete all videos and quizzes to earn a certificate!", 'error')
        return redirect(url_for('dashboard'))

    username = session.get('username', 'Rainwater Hero') # Default name if not set
    total_points = user_progress['points']

    # Custom date filter for Jinja2
    from datetime import datetime
    app.jinja_env.filters['date'] = lambda date_str, fmt: datetime.now().strftime(fmt)

    return render_template('certificate.html', username=username, total_points=total_points)

# --- Run the App ---
if __name__ == '__main__':
    app.run(debug=True)