gameforrhw / app.py
pranit144's picture
Upload 8 files
a88a491 verified
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/<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 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)