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/72CVryFbv5I?si=oKWZ5Ep1Z3kcLI-g", "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/bbYJvTj8uiw?si=nClbPzN9L2buoilz", "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', 'reason' 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, "reason": "Rainwater collection significantly reduces the need for municipal or well water, directly lowering utility costs."}, {"question": "What is a common component of a basic rainwater system?", "options": ["Solar panels", "Downspout", "Microwave", "Treadmill"], "correct_answer_index": 1, "points": 20, "reason": "A downspout is crucial for directing water from the roof (collection surface) to the storage system."}, {"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, "reason": "Rainwater is a naturally replenished resource, making its use sustainable as it doesn't deplete finite resources."}, ], "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, "reason": "The initial step is always to collect water, typically from a roof or other suitable surface."}, {"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, "reason": "Advanced purification techniques are usually covered in more in-depth or specialized modules, not basic introductions."}, {"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, "reason": "Toilet flushing is a common and excellent use for untreated or minimally treated rainwater, reducing potable water consumption."}, ], "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, "reason": "A first flush diverter prevents the initial, most contaminated rainwater (carrying debris from the roof) from entering the main storage tank."}, {"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, "reason": "The 'first flush' refers to the first few minutes of a rainfall event, when the most impurities are washed off collection surfaces."}, {"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, "reason": "Diverting the first flush significantly improves the overall quality of the water collected in the storage tank."}, ], "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, "reason": "For drinking, rainwater requires thorough filtration to remove particulates and purification (like boiling or UV) to eliminate pathogens."}, {"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, "reason": "Raw rainwater can pick up various contaminants from the atmosphere and collection surfaces, including organic matter, dust, and animal waste."}, {"question": "Which method is commonly used for purifying drinking water from rainwater?", "options": ["Boiling", "Freezing", "Microwaving", "Shaking vigorously"], "correct_answer_index": 0, "points": 20, "reason": "Boiling water for at least one minute is a reliable method to kill most bacteria and viruses, making it safe for consumption."}, ], "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, "reason": "PVC (Polyvinyl Chloride) pipes are commonly used due to their durability, cost-effectiveness, and ease of installation for water systems."}, {"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, "reason": "It's installed in the downspout system before the water enters the main storage, allowing the initial flow to be diverted."}, {"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, "reason": "Safety precautions like using correct tools and fall protection are essential, especially when working on roofs or with heavy equipment."}, ], "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, "reason": "DIY tours focus on practical, often budget-friendly systems built and managed by homeowners themselves."}, {"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, "reason": "The storage tank is a central and visually significant component of any rainwater harvesting system, often a key focus in tours."}, {"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, "reason": "DIY tours offer insights into practical design considerations, modifications, and solutions to common challenges faced by homeowners."}, ], "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, "reason": "Upgrading often means expanding capacity to collect more water or increasing storage to meet higher demand."}, {"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, "reason": "Upgrades are typically motivated by a need for more water, better performance, or adapting to changing water demands."}, {"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, "reason": "Effective planning requires assessing financial resources, water needs, and how new components will fit with existing infrastructure."}, ], "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, "reason": "Deserts are characterized by scarce rainfall and high temperatures, leading to significant water loss through evaporation, making collection challenging."}, {"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, "reason": "In arid regions, it's vital to capture as much rainfall as possible from large surfaces and store it effectively to last through dry periods."}, {"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, "reason": "Water is extremely limited in deserts, so every drop must be managed wisely to ensure survival and sustainability."}, ], "v9": [ {"question": "What material are many water storage tanks made from?", "options": ["Glass", "Wood", "Polyethylene", "Cardboard"], "correct_answer_index": 2, "points": 20, "reason": "Polyethylene is a common material for water tanks due to its durability, resistance to corrosion, and food-grade safety for water storage."}, {"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, "reason": "Plastic welding or using a heat gun with filler material is the appropriate method for repairing cracks in polyethylene tanks."}, {"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, "reason": "A clean, dry surface is essential for proper adhesion and a long-lasting repair when working with adhesives or plastic welding."}, ], "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, "reason": "Pumps are used to move water from the storage tank to where it's needed, often at a specific pressure for household use."}, {"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, "reason": "While solar pumps exist, most common rainwater systems utilize electric pumps for consistent power and performance."}, {"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, "reason": "The pump must be chosen based on the desired water flow (how much water per minute) and pressure (how strong the water comes out) for the intended application."}, ], } 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/') 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/') 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/', 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 quiz already taken, and it's a GET request, redirect to dashboard # The user can't retake a quiz, so no need to show old results on GET if video_id in user_progress['video_quiz_scores'] and request.method == 'GET': 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 quiz_feedback = [] # To store feedback for each question for i, question_data in enumerate(quiz_questions): user_answer_index_str = request.form.get(f'question_{i}') user_answer_index = None is_correct = False points_for_this_question = 0 user_selected_option_text = "No answer" # Default if nothing selected if user_answer_index_str: try: user_answer_index = int(user_answer_index_str) user_selected_option_text = question_data['options'][user_answer_index] if user_answer_index == question_data['correct_answer_index']: is_correct = True points_for_this_question = question_data['points'] user_score += points_for_this_question correct_answers_count += 1 except ValueError: pass # user_answer_index remains None, user_selected_option_text is "No answer" correct_option_text = question_data['options'][question_data['correct_answer_index']] reason_text = question_data.get('reason', 'No specific reason provided for this question.') # Get reason, provide default quiz_feedback.append({ 'question': question_data['question'], 'user_answer_index': user_answer_index, 'user_selected_option': user_selected_option_text, 'correct_answer_index': question_data['correct_answer_index'], 'correct_option': correct_option_text, 'is_correct': is_correct, 'points_earned': points_for_this_question, 'reason': reason_text }) 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() # Render the quiz page again, but this time with feedback return render_template('quiz.html', video=video, quiz_questions=quiz_questions, quiz_feedback=quiz_feedback, user_score=user_score, total_quiz_points=sum(q['points'] for q in quiz_questions), enumerate=enumerate, quiz_submitted=True) # For GET request (displaying quiz for the first time) return render_template('quiz.html', video=video, quiz_questions=quiz_questions, enumerate=enumerate, quiz_submitted=False) @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)