Spaces:
Sleeping
Sleeping
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) |