Spaces:
Running
Running
# app.py - Ultimate Enhanced Version | |
import streamlit as st | |
import random | |
import re | |
import time | |
import base64 | |
import json | |
import requests | |
from PIL import Image | |
import io | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from groq import Groq | |
import pygame | |
import sys | |
from io import StringIO | |
import contextlib | |
import traceback | |
# Configure Streamlit page | |
st.set_page_config( | |
page_title="StoryCoder - Play & Learn Coding", | |
page_icon="🎮", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
# Modern gradient background with animation | |
st.markdown(""" | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&family=Comic+Neue:wght@700&display=swap'); | |
:root { | |
--primary: #6A67CE; | |
--secondary: #FF7C7C; | |
--accent: #FDD85D; | |
--dark: #2D3250; | |
--light: #F0F0F0; | |
--game-blue: #1A8CD8; | |
--game-purple: #9B5DE5; | |
--gradient-start: #1a2a6c; | |
--gradient-mid: #b21f1f; | |
--gradient-end: #1a2a6c; | |
} | |
body { | |
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-mid), var(--gradient-end)); | |
background-size: 400% 400%; | |
animation: gradientBG 15s ease infinite; | |
font-family: 'Comic Neue', cursive; | |
margin: 0; | |
padding: 0; | |
min-height: 100vh; | |
} | |
@keyframes gradientBG { | |
0% {background-position: 0% 50%;} | |
50% {background-position: 100% 50%;} | |
100% {background-position: 0% 50%;} | |
} | |
.stApp { | |
background: rgba(255, 255, 255, 0.05); | |
backdrop-filter: blur(12px); | |
-webkit-backdrop-filter: blur(12px); | |
} | |
.game-card { | |
background: rgba(255, 255, 255, 0.9); | |
border-radius: 20px; | |
padding: 25px; | |
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37); | |
border: 2px solid rgba(255, 255, 255, 0.18); | |
margin-bottom: 25px; | |
transition: all 0.3s; | |
} | |
.game-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 12px 24px rgba(31, 38, 135, 0.5); | |
} | |
.header { | |
color: white; | |
font-family: 'Fredoka One', cursive; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
} | |
.concept-card { | |
background: rgba(255, 255, 255, 0.95); | |
border-radius: 15px; | |
padding: 15px; | |
margin: 10px 0; | |
border-left: 5px solid var(--accent); | |
box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
} | |
.stButton>button { | |
background: linear-gradient(45deg, var(--primary), var(--game-blue)); | |
color: white; | |
border-radius: 50px; | |
padding: 12px 28px; | |
font-weight: bold; | |
font-size: 18px; | |
border: none; | |
transition: all 0.3s; | |
font-family: 'Fredoka One', cursive; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.stButton>button:hover { | |
transform: scale(1.05); | |
box-shadow: 0 8px 16px rgba(106, 103, 206, 0.4); | |
} | |
.stTextInput>div>div>input { | |
border-radius: 20px; | |
padding: 14px; | |
border: 3px solid var(--accent); | |
font-size: 18px; | |
background: rgba(255, 255, 255, 0.9); | |
} | |
.tabs { | |
display: flex; | |
gap: 10px; | |
margin-bottom: 20px; | |
overflow-x: auto; | |
background: rgba(255, 255, 255, 0.15); | |
padding: 10px; | |
border-radius: 20px; | |
backdrop-filter: blur(5px); | |
} | |
.tab { | |
padding: 12px 24px; | |
background: rgba(255, 255, 255, 0.25); | |
border-radius: 15px; | |
cursor: pointer; | |
font-weight: bold; | |
white-space: nowrap; | |
font-family: 'Fredoka One', cursive; | |
font-size: 16px; | |
transition: all 0.3s; | |
color: white; | |
} | |
.tab.active { | |
background: linear-gradient(45deg, var(--primary), var(--game-blue)); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.game-board { | |
display: grid; | |
grid-template-columns: repeat(8, 1fr); | |
grid-template-rows: repeat(8, 1fr); | |
gap: 4px; | |
width: 500px; | |
height: 500px; | |
margin: 0 auto; | |
background: rgba(106, 103, 206, 0.2); | |
padding: 10px; | |
border-radius: 15px; | |
} | |
.cell { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
border-radius: 8px; | |
font-size: 30px; | |
background: rgba(255, 255, 255, 0.85); | |
transition: all 0.2s; | |
cursor: pointer; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.cell:hover { | |
transform: scale(1.05); | |
background: rgba(255, 255, 255, 1); | |
} | |
.player { | |
background: linear-gradient(45deg, #6A67CE, #1A8CD8); | |
color: white; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.goal { | |
background: linear-gradient(45deg, #FDD85D, #FFB347); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.obstacle { | |
background: linear-gradient(45deg, #FF7C7C, #FF5252); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.star { | |
background: linear-gradient(45deg, #FFD700, #FFA500); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.portal { | |
background: linear-gradient(45deg, #9B5DE5, #6A0DAD); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
} | |
.game-controls { | |
display: flex; | |
justify-content: center; | |
gap: 15px; | |
margin: 25px 0; | |
} | |
.control-btn { | |
width: 70px; | |
height: 70px; | |
border-radius: 50%; | |
background: linear-gradient(45deg, var(--primary), var(--game-blue)); | |
color: white; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 30px; | |
cursor: pointer; | |
border: none; | |
box-shadow: 0 6px 12px rgba(0,0,0,0.2); | |
transition: all 0.2s; | |
} | |
.control-btn:hover { | |
transform: scale(1.1); | |
box-shadow: 0 8px 16px rgba(0,0,0,0.3); | |
} | |
.score-board { | |
background: rgba(255, 255, 255, 0.9); | |
border-radius: 15px; | |
padding: 15px; | |
text-align: center; | |
font-family: 'Fredoka One', cursive; | |
font-size: 24px; | |
margin: 20px auto; | |
width: 300px; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
} | |
.level-indicator { | |
background: rgba(255, 255, 255, 0.9); | |
border-radius: 15px; | |
padding: 10px 20px; | |
text-align: center; | |
font-family: 'Fredoka One', cursive; | |
font-size: 20px; | |
margin: 10px auto; | |
width: 200px; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
} | |
.concept-highlight { | |
background: rgba(253, 216, 93, 0.2); | |
border-radius: 10px; | |
padding: 15px; | |
margin: 10px 0; | |
border-left: 4px solid var(--accent); | |
} | |
.code-block { | |
background: #2b2d42; | |
color: #f8f9fa; | |
border-radius: 15px; | |
padding: 20px; | |
font-family: 'Courier New', monospace; | |
overflow-x: auto; | |
margin: 20px 0; | |
box-shadow: 0 8px 16px rgba(0,0,0,0.2); | |
} | |
.code-explanation { | |
background: rgba(255, 255, 255, 0.9); | |
border-radius: 15px; | |
padding: 15px; | |
margin: 15px 0; | |
} | |
.game-animation { | |
text-align: center; | |
margin: 20px 0; | |
} | |
.animated-char { | |
font-size: 80px; | |
display: inline-block; | |
animation: bounce 2s infinite; | |
} | |
@keyframes bounce { | |
0%, 100% {transform: translateY(0);} | |
50% {transform: translateY(-20px);} | |
} | |
@media (max-width: 768px) { | |
.tabs { | |
flex-wrap: wrap; | |
} | |
.game-board { | |
width: 90vw; | |
height: 90vw; | |
} | |
.control-btn { | |
width: 60px; | |
height: 60px; | |
font-size: 24px; | |
} | |
} | |
.typewriter h2 { | |
overflow: hidden; | |
border-right: .15em solid var(--accent); | |
white-space: nowrap; | |
margin: 0 auto; | |
letter-spacing: .15em; | |
animation: typing 3.5s steps(40, end), blink-caret .75s step-end infinite; | |
} | |
@keyframes typing { | |
from { width: 0 } | |
to { width: 100% } | |
} | |
@keyframes blink-caret { | |
from, to { border-color: transparent } | |
50% { border-color: var(--accent); } | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Initialize session state | |
def init_session_state(): | |
if 'story' not in st.session_state: | |
st.session_state.story = "" | |
if 'concepts' not in st.session_state: | |
st.session_state.concepts = [] | |
if 'game_scenario' not in st.session_state: | |
st.session_state.game_scenario = "" | |
if 'game_code' not in st.session_state: | |
st.session_state.game_code = "" | |
if 'game_explanation' not in st.session_state: | |
st.session_state.game_explanation = "" | |
if 'game_preview' not in st.session_state: | |
st.session_state.game_preview = None | |
if 'active_tab' not in st.session_state: | |
st.session_state.active_tab = "story" | |
if 'loading' not in st.session_state: | |
st.session_state.loading = False | |
if 'game_state' not in st.session_state: | |
reset_game_state() | |
if 'groq_api_key' not in st.session_state: | |
st.session_state.groq_api_key = "" | |
if 'player_char' not in st.session_state: | |
st.session_state.player_char = "🦸" | |
if 'goal_char' not in st.session_state: | |
st.session_state.goal_char = "🏁" | |
if 'obstacle_char' not in st.session_state: | |
st.session_state.obstacle_char = "🪨" | |
if 'current_level' not in st.session_state: | |
st.session_state.current_level = 1 | |
if 'total_levels' not in st.session_state: | |
st.session_state.total_levels = 3 | |
if 'show_concept_animation' not in st.session_state: | |
st.session_state.show_concept_animation = False | |
if 'concept_animation' not in st.session_state: | |
st.session_state.concept_animation = None | |
if 'pygame_running' not in st.session_state: | |
st.session_state.pygame_running = False | |
if 'pygame_output' not in st.session_state: | |
st.session_state.pygame_output = "" | |
# Concept database | |
CONCEPTS = { | |
"loop": { | |
"name": "Loop", | |
"emoji": "🔄", | |
"description": "Loops repeat actions multiple times", | |
"example": "for i in range(5):\n print('Hello!')", | |
"color": "#6A67CE", | |
"game_example": "Repeating jumps to cross a river", | |
"animation": "🔄➰➿" | |
}, | |
"conditional": { | |
"name": "Conditional", | |
"emoji": "❓", | |
"description": "Conditionals make decisions in code", | |
"example": "if sunny:\n go_outside()\nelse:\n stay_inside()", | |
"color": "#FF7C7C", | |
"game_example": "Choosing paths based on obstacles", | |
"animation": "❓👉👈" | |
}, | |
"function": { | |
"name": "Function", | |
"emoji": "✨", | |
"description": "Functions are reusable blocks of code", | |
"example": "def greet(name):\n print(f'Hello {name}!')", | |
"color": "#FDD85D", | |
"game_example": "Creating a jump function used multiple times", | |
"animation": "✨🌟💫" | |
}, | |
"variable": { | |
"name": "Variable", | |
"emoji": "📦", | |
"description": "Variables store information", | |
"example": "score = 10\nplayer = 'Alex'", | |
"color": "#1A8CD8", | |
"game_example": "Keeping track of collected stars", | |
"animation": "📊📈📉" | |
}, | |
"list": { | |
"name": "List", | |
"emoji": "📝", | |
"description": "Lists store collections of items", | |
"example": "fruits = ['apple', 'banana', 'orange']", | |
"color": "#4CAF50", | |
"game_example": "Storing collected treasures in a backpack", | |
"animation": "📋📑📄" | |
} | |
} | |
# Initialize Groq client | |
def get_groq_client(): | |
try: | |
if st.session_state.groq_api_key: | |
return Groq(api_key=st.session_state.groq_api_key) | |
return None | |
except: | |
return None | |
# Analyze story and extract concepts | |
def analyze_story(story): | |
"""Analyze story and identify programming concepts""" | |
story_lower = story.lower() | |
detected_concepts = [] | |
# Improved concept detection | |
# Check for loops | |
if any(word in story_lower for word in ["times", "repeat", "again", "multiple", "each", "every"]): | |
detected_concepts.append("loop") | |
# Check for conditionals | |
if any(word in story_lower for word in ["if", "when", "unless", "whether", "decide", "choice", "otherwise"]): | |
detected_concepts.append("conditional") | |
# Check for functions | |
if any(word in story_lower for word in ["make", "create", "do", "perform", "cast", "action", "use"]): | |
detected_concepts.append("function") | |
# Check for variables | |
if any(word in story_lower for word in ["is", "has", "set to", "value", "score", "count", "number"]): | |
detected_concepts.append("variable") | |
# Check for lists | |
if any(word in story_lower for word in ["and", "many", "several", "collection", "items", "group", "set of"]): | |
detected_concepts.append("list") | |
return list(set(detected_concepts)) if detected_concepts else ["variable", "function"] | |
# Generate game scenario using Groq | |
def generate_game_scenario(story, concepts): | |
"""Generate a game scenario based on the story and concepts""" | |
try: | |
client = get_groq_client() | |
if client: | |
concept_names = [CONCEPTS[c]['name'] for c in concepts] | |
concept_list = ", ".join(concept_names) | |
system_prompt = ( | |
"You are an expert in creating educational games for children aged 6-12. " | |
"Create a simple game scenario based on the child's story. " | |
"The game should teach programming concepts through gameplay. " | |
"Structure your response with these sections:\n" | |
"Game Title: ...\n" | |
"Game Objective: ...\n" | |
"Characters: ...\n" | |
"Game Mechanics: ...\n" | |
"Coding Concepts: Explain how these programming concepts are used: " + concept_list + "\n" | |
"Visual Description: Describe the game world visually\n" | |
"Keep it under 200 words and fun for kids." | |
) | |
response = client.chat.completions.create( | |
model="llama3-70b-8192", | |
messages=[ | |
{"role": "system", "content": system_prompt}, | |
{"role": "user", "content": story} | |
], | |
temperature=0.8, | |
max_tokens=500 | |
) | |
return response.choices[0].message.content | |
except Exception as e: | |
st.error(f"Game scenario generation error: {str(e)}") | |
# Fallback template | |
concept_names = [CONCEPTS[c]['name'] for c in concepts] | |
concept_list = ", ".join(concept_names) | |
return f""" | |
Game Title: {story[:15]} Adventure | |
Game Objective: Complete challenges based on your story: {story[:100]}... | |
Characters: | |
- Hero: The main character from your story | |
- Helper: A friendly guide who explains coding concepts | |
- Villain: A character that creates obstacles (if applicable) | |
Game Mechanics: | |
1. Move your character using arrow keys | |
2. Collect stars while avoiding obstacles | |
3. Reach the goal to win | |
4. Helper characters appear to teach {concept_list} | |
Coding Concepts: This game teaches {concept_list} through: | |
- Using loops to repeat actions | |
- Making decisions with conditionals | |
- Creating reusable functions | |
- Tracking progress with variables | |
- Managing collections with lists | |
Visual Description: Colorful game world with cartoon-style characters, vibrant landscapes, and magical effects. | |
""" | |
# Generate game code explanation | |
def generate_game_explanation(story, concepts, game_scenario): | |
"""Generate explanation of game code""" | |
try: | |
client = get_groq_client() | |
if client: | |
concept_names = [CONCEPTS[c]['name'] for c in concepts] | |
concept_list = ", ".join(concept_names) | |
system_prompt = ( | |
"Explain how the game code implements programming concepts in a way " | |
"a child aged 6-12 can understand. Use simple analogies and relate to the story. " | |
"Structure your response with:\n" | |
"Introduction: ...\n" | |
"Concept 1: ... (with example from the game)\n" | |
"Concept 2: ... (with example from the game)\n" | |
"Conclusion: ...\n" | |
"Keep it under 300 words and engaging for kids." | |
) | |
response = client.chat.completions.create( | |
model="llama3-70b-8192", | |
messages=[ | |
{"role": "system", "content": system_prompt}, | |
{"role": "user", "content": f"Story: {story}\nGame Scenario: {game_scenario}\nConcepts: {concept_list}"} | |
], | |
temperature=0.7, | |
max_tokens=600 | |
) | |
return response.choices[0].message.content | |
except Exception as e: | |
st.error(f"Explanation generation error: {str(e)}") | |
# Fallback explanation | |
concept_explanations = "\n".join( | |
[f"- {CONCEPTS[c]['name']}: {CONCEPTS[c]['game_example']}" for c in concepts] | |
) | |
return f""" | |
In this game based on your story "{story[:20]}...", we use programming concepts to make it work: | |
{concept_explanations} | |
As you play the game, think about: | |
1. How the game uses these concepts to create challenges | |
2. How you might change the code to make the game easier or harder | |
The code brings your story to life in a fun game world! | |
""" | |
# Generate game preview visualization | |
def generate_game_preview(story): | |
"""Generate a visual preview of the game""" | |
try: | |
# Extract keywords for theme | |
theme = "space" if "space" in story.lower() else "jungle" if "jungle" in story.lower() else "fantasy" | |
# Create a simple visualization | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
if theme == "space": | |
bg_color = '#0B0B2B' | |
player_color = '#6A67CE' | |
goal_color = '#FDD85D' | |
obstacle_color = '#FF7C7C' | |
title = "Space Adventure" | |
elif theme == "jungle": | |
bg_color = '#143D2C' | |
player_color = '#6A67CE' | |
goal_color = '#FDD85D' | |
obstacle_color = '#D35400' | |
title = "Jungle Adventure" | |
else: | |
bg_color = '#3A015C' | |
player_color = '#6A67CE' | |
goal_color = '#FDD85D' | |
obstacle_color = '#FF7C7C' | |
title = "Fantasy Quest" | |
ax.set_facecolor(bg_color) | |
ax.set_xlim(0, 10) | |
ax.set_ylim(0, 6) | |
# Draw game elements | |
ax.text(5, 5, title, fontsize=20, ha='center', color='white') | |
ax.plot([1, 9], [1, 1], 'w-', linewidth=2) # Ground | |
# Player character | |
ax.plot(2, 2, 'o', markersize=15, color=player_color) | |
ax.text(2, 2.7, 'You', ha='center', color='white', fontsize=12) | |
# Goal | |
ax.plot(8, 2, 'o', markersize=12, color=goal_color) | |
ax.text(8, 2.7, 'Goal', ha='center', color='white', fontsize=12) | |
# Obstacles | |
for i in range(3): | |
x = random.uniform(3, 7) | |
y = random.uniform(1.5, 2.5) | |
ax.plot(x, y, 's', markersize=15, color=obstacle_color) | |
ax.text(x, y+0.4, 'Obstacle', ha='center', color='white', fontsize=8) | |
# Stars | |
for i in range(3): | |
x = random.uniform(2.5, 7.5) | |
y = random.uniform(1.2, 2.8) | |
ax.plot(x, y, '*', markersize=15, color='yellow') | |
ax.text(x, y+0.4, 'Star', ha='center', color='white', fontsize=8) | |
# Path | |
ax.plot([2, 8], [2, 2], 'y--', linewidth=1, alpha=0.5) | |
ax.axis('off') | |
ax.set_title("Game Preview", fontsize=16, color='white') | |
# Save to bytes | |
buf = io.BytesIO() | |
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight', facecolor=bg_color) | |
buf.seek(0) | |
return buf | |
except Exception as e: | |
st.error(f"Preview generation error: {str(e)}") | |
return None | |
def reset_game_state(): | |
st.session_state.game_state = { | |
"player_pos": [0, 0], | |
"goal_pos": [7, 7], | |
"obstacles": [[2, 2], [3, 4], [5, 3], [4, 6], [6, 5]], | |
"stars": [[1, 1], [3, 3], [5, 5], [7, 1]], | |
"score": 0, | |
"moves": 0, | |
"game_over": False, | |
"portals": { | |
"A": {"in": [6, 6], "out": [1, 7]}, | |
"B": {"in": [0, 3], "out": [7, 3]} | |
} | |
} | |
# Handle player movement | |
def move_player(direction): | |
"""Update player position based on movement direction""" | |
state = st.session_state.game_state | |
player_pos = state["player_pos"] | |
goal_pos = state["goal_pos"] | |
obstacles = state["obstacles"] | |
stars = state["stars"] | |
portals = state["portals"] | |
new_pos = player_pos.copy() | |
if direction == "up" and player_pos[0] > 0: | |
new_pos[0] -= 1 | |
elif direction == "down" and player_pos[0] < 7: | |
new_pos[0] += 1 | |
elif direction == "left" and player_pos[1] > 0: | |
new_pos[1] -= 1 | |
elif direction == "right" and player_pos[1] < 7: | |
new_pos[1] += 1 | |
# Check if new position is valid | |
if new_pos != player_pos: | |
# Check for obstacle collision | |
if new_pos not in obstacles: | |
state["player_pos"] = new_pos | |
state["moves"] += 1 | |
# Check for portal | |
for portal, positions in portals.items(): | |
if new_pos == positions["in"]: | |
state["player_pos"] = positions["out"].copy() | |
state["moves"] += 1 # Count portal as a move | |
break | |
# Check for goal collision | |
if new_pos == goal_pos: | |
state["score"] += 20 | |
state["game_over"] = True | |
# Check for star collection | |
elif new_pos in stars: | |
state["score"] += 5 | |
state["stars"].remove(new_pos) | |
st.session_state.game_state = state | |
# Create a playable game in the browser | |
def create_playable_game(): | |
"""Create an interactive grid-based game""" | |
st.subheader("🎮 Play Your Game Now!") | |
state = st.session_state.game_state | |
player_pos = state["player_pos"] | |
goal_pos = state["goal_pos"] | |
obstacles = state["obstacles"] | |
stars = state["stars"] | |
portals = state["portals"] | |
# Level indicator | |
st.markdown(f""" | |
<div class="level-indicator"> | |
Level: {st.session_state.current_level}/{st.session_state.total_levels} | |
</div> | |
""", unsafe_allow_html=True) | |
# Score board | |
st.markdown(f""" | |
<div class="score-board"> | |
Stars Collected: {state["score"]} | Moves: {state["moves"]} | |
</div> | |
""", unsafe_allow_html=True) | |
# Game board | |
st.markdown("<div class='game-board'>", unsafe_allow_html=True) | |
# Create grid cells | |
for row in range(8): | |
cols = st.columns(8) | |
for col in range(8): | |
pos = [row, col] | |
cell_class = "cell" | |
cell_content = "" | |
if pos == player_pos: | |
cell_class += " player" | |
cell_content = st.session_state.player_char | |
elif pos == goal_pos: | |
cell_class += " goal" | |
cell_content = st.session_state.goal_char | |
elif pos in obstacles: | |
cell_class += " obstacle" | |
cell_content = st.session_state.obstacle_char | |
elif pos in stars: | |
cell_class += " star" | |
cell_content = "⭐" | |
else: | |
# Check for portals | |
for portal, positions in portals.items(): | |
if pos == positions["in"] or pos == positions["out"]: | |
cell_class += " portal" | |
cell_content = "🌀" | |
break | |
with cols[col]: | |
st.markdown(f"<div class='{cell_class}'>{cell_content}</div>", unsafe_allow_html=True) | |
st.markdown("</div>", unsafe_allow_html=True) | |
# Game controls | |
st.markdown("<div class='game-controls'>", unsafe_allow_html=True) | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
st.button("↑", key="up", on_click=move_player, args=("up",), use_container_width=True) | |
with col2: | |
st.button("←", key="left", on_click=move_player, args=("left",), use_container_width=True) | |
with col3: | |
st.button("↓", key="down", on_click=move_player, args=("down",), use_container_width=True) | |
with col4: | |
st.button("→", key="right", on_click=move_player, args=("right",), use_container_width=True) | |
st.markdown("</div>", unsafe_allow_html=True) | |
# Reset button | |
st.button("🔄 Reset Game", on_click=reset_game_state, use_container_width=True) | |
# Next level button | |
if state["game_over"]: | |
st.balloons() | |
st.success(f"🎉 You won! Final Score: {state['score']} in {state['moves']} moves!") | |
if st.session_state.current_level < st.session_state.total_levels: | |
if st.button("Next Level →", use_container_width=True): | |
st.session_state.current_level += 1 | |
reset_game_state() | |
st.rerun() | |
# Run PyGame code in the browser | |
def run_pygame_in_browser(code): | |
"""Simulate running PyGame code with output capture""" | |
st.subheader("🎮 Play the Full Game in Your Browser") | |
# Create a code runner | |
output = StringIO() | |
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output): | |
try: | |
# Create a fake pygame module for simulation | |
class FakePygame: | |
def __init__(self): | |
self.quit = lambda: None | |
self.init = lambda: None | |
self.display = type('', (), {'set_mode': lambda *a, **kw: None, 'set_caption': lambda *a: None, 'flip': lambda: None})() | |
self.event = type('', (), {'get': lambda: []})() | |
self.key = type('', (), {'get_pressed': lambda: [0]*300})() | |
self.Rect = lambda *a: None | |
self.draw = type('', (), {'rect': lambda *a, **kw: None, 'circle': lambda *a, **kw: None})() | |
self.font = type('', (), {'SysFont': lambda *a, **kw: type('', (), {'render': lambda *a, **kw: None})})() | |
self.time = type('', (), {'Clock': lambda: type('', (), {'tick': lambda *a: None})()})() | |
self.Surface = lambda *a: type('', (), {'blit': lambda *a, **kw: None})() | |
sys.modules['pygame'] = FakePygame() | |
# Execute the code | |
exec(code, {'__name__': '__main__'}) | |
st.session_state.pygame_output = "Game executed successfully!" | |
except Exception as e: | |
st.session_state.pygame_output = f"Error: {str(e)}\n{traceback.format_exc()}" | |
# Show output | |
st.code(st.session_state.pygame_output, language='python') | |
st.info("This is a simulation. To play the full game, download the code and run it on your computer!") | |
# Display concept animation | |
def show_concept_animation(concept): | |
"""Display animated concept visualization""" | |
if concept in CONCEPTS: | |
st.subheader(f"{CONCEPTS[concept]['emoji']} {CONCEPTS[concept]['name']} Concept") | |
# Animation | |
st.markdown(f"<div class='game-animation'><span class='animated-char'>{CONCEPTS[concept]['animation']}</span></div>", | |
unsafe_allow_html=True) | |
# Explanation | |
st.markdown(f""" | |
<div class='concept-highlight'> | |
<p><strong>How it works:</strong> {CONCEPTS[concept]['description']}</p> | |
<p><strong>In your game:</strong> {CONCEPTS[concept]['game_example']}</p> | |
</div> | |
""", unsafe_allow_html=True) | |
# Code example | |
st.code(CONCEPTS[concept]['example'], language='python') | |
# Main application function | |
def main(): | |
init_session_state() | |
# Sidebar for API key and settings | |
with st.sidebar: | |
st.header("⚙️ Settings") | |
st.session_state.groq_api_key = st.text_input( | |
"Enter Groq API Key (optional):", | |
type="password", | |
help="Get a free key from https://console.groq.com/keys" | |
) | |
st.caption("Using Groq API will create more creative and personalized games!") | |
st.divider() | |
st.header("🎮 Game Theme") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.session_state.player_char = st.selectbox( | |
"Player Character", | |
["🦸", "👨🚀", "🧙♂️", "🐱", "🐉", "🦊"], | |
index=0 | |
) | |
with col2: | |
st.session_state.goal_char = st.selectbox( | |
"Goal Character", | |
["🏁", "🏰", "🚩", "🎯", "🔑"], | |
index=0 | |
) | |
with col3: | |
st.session_state.obstacle_char = st.selectbox( | |
"Obstacle Character", | |
["🪨", "🌵", "🔥", "🌊", "🌳"], | |
index=0 | |
) | |
st.divider() | |
st.header("📚 Learning Level") | |
st.session_state.current_level = st.slider( | |
"Select Difficulty Level", | |
1, 5, st.session_state.current_level | |
) | |
st.divider() | |
st.caption("Made with ❤️ for kids learning to code") | |
st.caption("v4.0 | StoryCoder") | |
st.title("🎮 StoryCoder - Play & Learn Coding!") | |
st.markdown("<div class='typewriter'><h2>Turn stories into games, and games into coding skills!</h2></div>", | |
unsafe_allow_html=True) | |
# Create tabs | |
st.markdown('<div class="tabs">', unsafe_allow_html=True) | |
col1, col2, col3, col4, col5 = st.columns(5) | |
with col1: | |
if st.button("📖 Create Story", use_container_width=True): | |
st.session_state.active_tab = "story" | |
with col2: | |
if st.button("🎮 Play Game", use_container_width=True): | |
st.session_state.active_tab = "game" | |
with col3: | |
if st.button("🔍 Concepts", use_container_width=True): | |
st.session_state.active_tab = "concepts" | |
with col4: | |
if st.button("💻 Game Code", use_container_width=True): | |
st.session_state.active_tab = "code" | |
with col5: | |
if st.button("🚀 PyGame", use_container_width=True): | |
st.session_state.active_tab = "pygame" | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Story creation tab | |
if st.session_state.active_tab == "story": | |
with st.container(): | |
st.header("📖 Create Your Story") | |
st.write("Write a short story (2-5 sentences) and I'll turn it into a playable game!") | |
story = st.text_area( | |
"Your story:", | |
height=200, | |
placeholder="Once upon a time, a brave knight had to collect 5 magical stars in a castle...", | |
value=st.session_state.story, | |
key="story_input" | |
) | |
if st.button("✨ Create Game!", use_container_width=True): | |
if len(story) < 10: | |
st.error("Your story needs to be at least 10 characters long!") | |
else: | |
st.session_state.story = story | |
st.session_state.loading = True | |
with st.spinner("🧠 Analyzing your story for coding concepts..."): | |
st.session_state.concepts = analyze_story(story) | |
with st.spinner("🎮 Creating your game scenario..."): | |
st.session_state.game_scenario = generate_game_scenario( | |
story, st.session_state.concepts | |
) | |
with st.spinner("📚 Creating coding explanations..."): | |
st.session_state.game_explanation = generate_game_explanation( | |
story, st.session_state.concepts, st.session_state.game_scenario | |
) | |
with st.spinner("🖼️ Generating game preview..."): | |
st.session_state.game_preview = generate_game_preview(story) | |
reset_game_state() | |
st.session_state.active_tab = "game" | |
st.session_state.loading = False | |
st.rerun() | |
# Show examples | |
st.subheader("✨ Story Examples") | |
col1, col2, col3 = st.columns(3) | |
example_images = { | |
"Space Explorer": "https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=500", | |
"Jungle Adventure": "https://images.unsplash.com/photo-1546182990-dffeafbe841d?w=500", | |
"Dragon Quest": "https://images.unsplash.com/photo-1541414779316-9564c1f85944?w=500" | |
} | |
with col1: | |
st.caption("Space Explorer") | |
st.code('"An astronaut needs to collect 3 stars while avoiding asteroids"', language="text") | |
st.image(example_images["Space Explorer"], | |
use_container_width=True, | |
caption="Space Explorer Game") | |
with col2: | |
st.caption("Jungle Adventure") | |
st.code('"A monkey swings through trees to collect bananas before sunset"', language="text") | |
st.image(example_images["Jungle Adventure"], | |
use_container_width=True, | |
caption="Jungle Adventure Game") | |
with col3: | |
st.caption("Dragon Quest") | |
st.code('"A dragon flies through clouds to collect magic crystals"', language="text") | |
st.image(example_images["Dragon Quest"], | |
use_container_width=True, | |
caption="Dragon Quest Game") | |
# Game tab | |
elif st.session_state.active_tab == "game": | |
st.header("🎮 Your Story Game") | |
if not st.session_state.story: | |
st.warning("Please create a story first!") | |
st.session_state.active_tab = "story" | |
st.rerun() | |
# Display game scenario | |
st.subheader("🌟 Game Scenario") | |
st.markdown(f'<div class="game-card">{st.session_state.game_scenario}</div>', unsafe_allow_html=True) | |
# Display game preview | |
st.subheader("🖼️ Game Preview") | |
if st.session_state.game_preview: | |
st.image(st.session_state.game_preview, use_container_width=True) | |
else: | |
st.info("Game preview visualization") | |
st.image("https://images.unsplash.com/photo-1542751110-97427bbecf20?w=500", use_container_width=True) | |
# Playable game | |
create_playable_game() | |
# Game explanation | |
st.subheader("📚 How This Game Teaches Coding") | |
st.markdown(f'<div class="game-card">{st.session_state.game_explanation}</div>', unsafe_allow_html=True) | |
if st.button("Learn Coding Concepts", use_container_width=True): | |
st.session_state.active_tab = "concepts" | |
st.rerun() | |
# Concepts tab | |
elif st.session_state.active_tab == "concepts": | |
st.header("🔍 Coding Concepts in Your Game") | |
st.subheader("Your game teaches these programming concepts:") | |
if not st.session_state.concepts: | |
st.warning("No concepts detected in your story! Try adding words like '3 times', 'if', or 'collect'.") | |
else: | |
# Concept selector | |
selected_concept = st.selectbox( | |
"Select a concept to explore:", | |
st.session_state.concepts, | |
format_func=lambda x: CONCEPTS[x]["name"] | |
) | |
# Show animation and explanation for selected concept | |
if selected_concept: | |
show_concept_animation(selected_concept) | |
if st.button("See the Game Code", use_container_width=True): | |
st.session_state.active_tab = "code" | |
st.rerun() | |
# Code tab | |
elif st.session_state.active_tab == "code": | |
st.header("💻 Game Code") | |
st.write("Here's the Python code for your game. Download it and run on your computer!") | |
# Generate simple game code | |
if st.session_state.story and st.session_state.concepts: | |
# Extract keywords from story | |
keywords = re.findall(r'\b\w{4,}\b', st.session_state.story)[:3] | |
player_char = keywords[0].capitalize() if keywords else "Hero" | |
collect_item = keywords[1] if len(keywords) > 1 else "star" | |
obstacle = keywords[2] if len(keywords) > 2 else "rock" | |
# Get concept emojis | |
concept_emojis = "".join([CONCEPTS[c]['emoji'] for c in st.session_state.concepts]) | |
game_code = f""" | |
# {player_char}'s Adventure: {st.session_state.story[:20]}... | |
# Teaches: {concept_emojis} {", ".join([CONCEPTS[c]['name'] for c in st.session_state.concepts])} | |
import pygame | |
import random | |
import sys | |
# Initialize pygame | |
pygame.init() | |
# Game setup | |
WIDTH, HEIGHT = 800, 600 | |
screen = pygame.display.set_mode((WIDTH, HEIGHT)) | |
pygame.display.set_caption("{player_char}'s Adventure") | |
clock = pygame.time.Clock() | |
# Colors | |
BACKGROUND = (230, 240, 255) # Light blue | |
PLAYER_COLOR = (106, 103, 206) # Purple | |
GOAL_COLOR = (253, 216, 93) # Yellow | |
OBSTACLE_COLOR = (255, 124, 124) # Coral | |
TEXT_COLOR = (45, 50, 80) # Dark blue | |
STAR_COLOR = (255, 215, 0) # Gold | |
# Player setup | |
player_size = 40 | |
player_x = 100 | |
player_y = HEIGHT // 2 | |
player_speed = 5 | |
# Goal setup | |
goal_size = 30 | |
goal_x = WIDTH - 150 | |
goal_y = HEIGHT // 2 | |
# Variables concept: Tracking score | |
score = 0 | |
font = pygame.font.SysFont(None, 36) | |
# List concept: Creating obstacles | |
obstacles = [] | |
for i in range(5): | |
obstacles.append([ | |
random.randint(200, WIDTH - 100), | |
random.randint(50, HEIGHT - 100), | |
random.randint(30, 70), | |
random.randint(20, 50) | |
]) | |
# List concept: Creating stars | |
stars = [] | |
for i in range(5): | |
stars.append([ | |
random.randint(100, WIDTH - 100), | |
random.randint(50, HEIGHT - 100), | |
20 | |
]) | |
# Game loop | |
running = True | |
while running: | |
# Event handling | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
# Player movement | |
keys = pygame.key.get_pressed() | |
if keys[pygame.K_UP] or keys[pygame.K_w]: | |
player_y -= player_speed | |
if keys[pygame.K_DOWN] or keys[pygame.K_s]: | |
player_y += player_speed | |
if keys[pygame.K_LEFT] or keys[pygame.K_a]: | |
player_x -= player_speed | |
if keys[pygame.K_RIGHT] or keys[pygame.K_d]: | |
player_x += player_speed | |
# Boundary checking | |
player_x = max(0, min(WIDTH - player_size, player_x)) | |
player_y = max(0, min(HEIGHT - player_size, player_y)) | |
# Collision detection with goal | |
player_rect = pygame.Rect(player_x, player_y, player_size, player_size) | |
goal_rect = pygame.Rect(goal_x, goal_y, goal_size, goal_size) | |
# Conditional concept: Check for collision | |
if player_rect.colliderect(goal_rect): | |
# Function concept: Increase score | |
score += 10 | |
# Move goal to new position | |
goal_x = random.randint(100, WIDTH - 100) | |
goal_y = random.randint(50, HEIGHT - 100) | |
# Collision detection with stars | |
for star in stars[:]: | |
star_rect = pygame.Rect(star[0], star[1], star[2], star[2]) | |
if player_rect.colliderect(star_rect): | |
score += 5 | |
stars.remove(star) | |
# Drawing | |
screen.fill(BACKGROUND) | |
# Draw obstacles | |
for obstacle in obstacles: | |
pygame.draw.rect(screen, OBSTACLE_COLOR, | |
(obstacle[0], obstacle[1], obstacle[2], obstacle[3])) | |
# Draw stars | |
for star in stars: | |
pygame.draw.circle(screen, STAR_COLOR, | |
(star[0] + star[2]//2, star[1] + star[2]//2), star[2]//2) | |
# Draw player and goal | |
pygame.draw.rect(screen, PLAYER_COLOR, | |
(player_x, player_y, player_size, player_size)) | |
pygame.draw.circle(screen, GOAL_COLOR, | |
(goal_x + goal_size//2, goal_y + goal_size//2), goal_size//2) | |
# Display score | |
score_text = font.render(f"{collect_item.capitalize()}s: {{score}}", True, TEXT_COLOR) | |
screen.blit(score_text, (20, 20)) | |
# Display story title | |
title_text = font.render(f"{player_char}'s Adventure: {st.session_state.story[:20]}...", True, TEXT_COLOR) | |
screen.blit(title_text, (WIDTH // 2 - 150, 20)) | |
# Display concepts | |
concepts_text = font.render(f"Teaches: {', '.join([CONCEPTS[c]['name'] for c in st.session_state.concepts])}", True, TEXT_COLOR) | |
screen.blit(concepts_text, (20, HEIGHT - 40)) | |
# Display instructions | |
help_text = font.render("Arrow keys to move - Collect stars and reach the goal!", True, TEXT_COLOR) | |
screen.blit(help_text, (WIDTH // 2 - 200, HEIGHT - 80)) | |
# Update display | |
pygame.display.flip() | |
clock.tick(60) | |
pygame.quit() | |
sys.exit() | |
""" | |
# Display code with syntax highlighting | |
st.subheader("Your Game Code") | |
st.code(game_code, language="python") | |
# Download button | |
st.download_button( | |
label="📥 Download Game Code", | |
data=game_code, | |
file_name="story_game.py", | |
mime="text/python", | |
use_container_width=True | |
) | |
# Code explanation | |
st.subheader("🧠 Code Explanation") | |
st.markdown(""" | |
<div class="code-explanation"> | |
<p>This code creates a game based on your story:</p> | |
<ul> | |
<li><strong>Variables:</strong> Used to track score and positions</li> | |
<li><strong>Conditionals:</strong> Check collisions and game rules</li> | |
<li><strong>Functions:</strong> pygame functions create the game mechanics</li> | |
<li><strong>Loops:</strong> The game loop runs continuously</li> | |
<li><strong>Lists:</strong> Store obstacles and collectible stars</li> | |
</ul> | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.warning("No game code generated yet!") | |
# Try PyGame button | |
if st.session_state.story and st.session_state.concepts: | |
if st.button("▶️ Try PyGame in Browser", use_container_width=True): | |
st.session_state.active_tab = "pygame" | |
st.session_state.pygame_running = True | |
st.rerun() | |
if st.button("Create Another Story!", use_container_width=True): | |
st.session_state.active_tab = "story" | |
st.rerun() | |
# PyGame tab | |
elif st.session_state.active_tab == "pygame": | |
st.header("🎮 Play PyGame in Browser") | |
if st.session_state.story and st.session_state.concepts: | |
run_pygame_in_browser(st.session_state.game_code) | |
else: | |
st.warning("No game code generated yet!") | |
if st.button("Back to Game Code", use_container_width=True): | |
st.session_state.active_tab = "code" | |
st.rerun() | |
if __name__ == "__main__": | |
main() |