Spaces:
Runtime error
Runtime error
| # app.py - SIMCIUDAD ARGENTINA FUNCIONAL COMPLETA | |
| import gradio as gr | |
| import time | |
| import math | |
| import random | |
| import os | |
| from PIL import Image, ImageDraw | |
| import json | |
| # Groq AI Integration (mejorada) | |
| try: | |
| from groq import Groq | |
| GROQ_AVAILABLE = True | |
| groq_client = Groq(api_key=os.getenv("GROQ_API_KEY")) if os.getenv("GROQ_API_KEY") else None | |
| except ImportError: | |
| GROQ_AVAILABLE = False | |
| groq_client = None | |
| class AIUrbanPlanner: | |
| """IA para planificación urbana argentina con Groq""" | |
| def __init__(self): | |
| self.enabled = GROQ_AVAILABLE and groq_client is not None | |
| def generate_city_advice(self, city_state): | |
| """Genera consejos de planificación urbana inteligentes""" | |
| if not self.enabled: | |
| fallback_advice = [ | |
| "Construí más plazas para aumentar la felicidad vecinal.", | |
| "Las parrillas mejoran la cultura y la moral del barrio.", | |
| "Balanceá viviendas con servicios públicos.", | |
| "Las canchas de fútbol son clave para la identidad argentina." | |
| ] | |
| return random.choice(fallback_advice) | |
| try: | |
| prompt = f""" | |
| Sos un urbanista argentino experto. Analizá esta ciudad y dá consejos breves: | |
| Población: {city_state.get('population', 0)} habitantes | |
| Felicidad: {city_state.get('happiness', 0)}% | |
| Presupuesto: ${city_state.get('budget', 0):,} | |
| Edificios: {city_state.get('buildings', 0)} | |
| Identidad Cultural: {city_state.get('culture', 80)}% | |
| Dá UN consejo específico y argentino en máximo 2 líneas. | |
| Usá jerga porteña cuando sea apropiado. | |
| """ | |
| response = groq_client.chat.completions.create( | |
| messages=[{"role": "user", "content": prompt}], | |
| model="llama-3.1-8b-instant", | |
| temperature=0.8, | |
| max_tokens=100 | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| print(f"Error Groq: {e}") | |
| return "La ciudad necesita más espacios verdes y culturales para prosperar." | |
| class ArgentineCityBuilder: | |
| """SimCity Argentino con mecánicas revolucionarias""" | |
| def __init__(self): | |
| self.width, self.height = 1000, 700 | |
| self.grid_size = 20 | |
| self.city_grid = {} # Coordenadas -> tipo de edificio | |
| self.population = 100 | |
| self.happiness = 75 | |
| self.economy = 1000 | |
| self.inflation = 1.0 | |
| self.cultural_identity = 90 | |
| # Sistemas dinámicos | |
| self.neighborhoods = {} | |
| self.transport_network = {} | |
| self.cultural_centers = {} | |
| self.businesses = {} | |
| # Recursos complejos (CORREGIDO - todos los recursos necesarios) | |
| self.resources = { | |
| 'presupuesto': 50000, | |
| 'energia': 100, | |
| 'agua': 100, | |
| 'seguridad': 70, | |
| 'educacion': 60, | |
| 'salud': 65, | |
| 'cultura': 80, | |
| 'empleo': 75, | |
| 'vivienda': 50, | |
| 'transport': 60, # AGREGADO | |
| 'pollution': 0 # AGREGADO | |
| } | |
| # Edificios disponibles (CORREGIDOS - todos los efectos) | |
| self.building_types = { | |
| 'casa': {'cost': 1000, 'pop': 4, 'happiness': 2, 'vivienda': 5}, | |
| 'villa': {'cost': 500, 'pop': 8, 'happiness': -2, 'solidarity': 15, 'vivienda': 8}, | |
| 'edificio': {'cost': 5000, 'pop': 20, 'happiness': 1, 'vivienda': 15}, | |
| 'escuela': {'cost': 8000, 'educacion': 20, 'happiness': 10}, | |
| 'hospital': {'cost': 15000, 'salud': 30, 'happiness': 15}, | |
| 'cancha': {'cost': 3000, 'happiness': 25, 'cultura': 10}, | |
| 'parrilla': {'cost': 2000, 'happiness': 20, 'cultura': 15}, | |
| 'centro_cultural': {'cost': 10000, 'cultura': 40, 'educacion': 10}, | |
| 'fabrica': {'cost': 12000, 'empleo': 30, 'pollution': -10, 'happiness': -5}, | |
| 'plaza': {'cost': 1500, 'happiness': 15, 'cultura': 5}, | |
| 'subte': {'cost': 20000, 'transport': 50, 'happiness': 10}, | |
| 'comisaria': {'cost': 7000, 'seguridad': 25, 'happiness': -5}, | |
| 'kiosco': {'cost': 800, 'happiness': 5, 'empleo': 2} | |
| } | |
| # Inicializar ciudad base | |
| self._initialize_base_city() | |
| def _initialize_base_city(self): | |
| """Crea la ciudad inicial con algunos edificios""" | |
| # Plaza central | |
| self.city_grid[(25, 17)] = 'plaza' | |
| # Algunas casas iniciales | |
| initial_houses = [(20, 15), (30, 15), (25, 12), (25, 22)] | |
| for pos in initial_houses: | |
| self.city_grid[pos] = 'casa' | |
| # Una escuela inicial | |
| self.city_grid[(15, 20)] = 'escuela' | |
| class AdvancedCityRenderer: | |
| """Renderizador avanzado con gráficos isométricos COMPLETO""" | |
| def __init__(self): | |
| self.animation_time = 0 | |
| self.tile_size = 20 | |
| def render_city_frame(self, city_builder) -> Image.Image: | |
| """Renderiza la ciudad con vista isométrica mejorada""" | |
| img = Image.new('RGB', (city_builder.width, city_builder.height), (20, 40, 20)) | |
| draw = ImageDraw.Draw(img) | |
| self.animation_time += 0.1 | |
| # Fondo: Cielo dinámico según felicidad | |
| self._draw_dynamic_sky(draw, city_builder) | |
| # Grid base de la ciudad | |
| self._draw_city_grid(draw, city_builder) | |
| # Edificios con perspectiva isométrica | |
| self._draw_isometric_buildings(draw, city_builder) | |
| # Sistemas dinámicos (tráfico, personas, etc.) | |
| self._draw_city_life(draw, city_builder) | |
| # UI avanzada | |
| self._draw_advanced_ui(draw, city_builder) | |
| # Efectos especiales | |
| self._draw_special_effects(draw, city_builder) | |
| return img | |
| def _draw_dynamic_sky(self, draw, city): | |
| """Cielo que cambia según el estado de la ciudad""" | |
| happiness = city.happiness | |
| # Color base del cielo | |
| if happiness > 80: | |
| sky_color = (135, 206, 250) # Azul cielo feliz | |
| elif happiness > 60: | |
| sky_color = (176, 196, 222) # Azul grisáceo | |
| elif happiness > 40: | |
| sky_color = (119, 136, 153) # Gris claro | |
| else: | |
| sky_color = (105, 105, 105) # Gris oscuro | |
| # Gradiente de cielo | |
| for y in range(200): | |
| alpha = 1.0 - (y / 400) | |
| color = tuple(int(c * alpha + 20 * (1-alpha)) for c in sky_color) | |
| draw.rectangle([0, y, city.width, y+2], fill=color) | |
| # Nubes dinámicas | |
| cloud_offset = int(self.animation_time * 20) % city.width | |
| for i in range(3): | |
| x = (i * 300 + cloud_offset) % (city.width + 100) | |
| y = 50 + i * 30 | |
| self._draw_cloud(draw, x, y, happiness) | |
| def _draw_cloud(self, draw, x, y, happiness): | |
| """Dibuja nube con forma según felicidad""" | |
| cloud_color = (255, 255, 255) if happiness > 50 else (200, 200, 200) | |
| # Forma de nube | |
| for i in range(5): | |
| offset_x = i * 15 + random.randint(-5, 5) | |
| offset_y = random.randint(-8, 8) | |
| size = 20 + random.randint(-5, 10) | |
| draw.ellipse([x + offset_x - size//2, y + offset_y - size//2, | |
| x + offset_x + size//2, y + offset_y + size//2], | |
| fill=cloud_color) | |
| def _draw_city_grid(self, draw, city): | |
| """Grid isométrico de la ciudad""" | |
| grid_color = (60, 80, 60) if city.cultural_identity > 50 else (80, 80, 80) | |
| # Grid horizontal | |
| for y in range(0, city.height, self.tile_size): | |
| draw.line([0, y, city.width, y], fill=grid_color, width=1) | |
| # Grid vertical | |
| for x in range(0, city.width, self.tile_size): | |
| draw.line([x, 0, x, city.height], fill=grid_color, width=1) | |
| def _draw_isometric_buildings(self, draw, city): | |
| """Edificios con perspectiva isométrica - TODOS LOS TIPOS""" | |
| for (grid_x, grid_y), building_type in city.city_grid.items(): | |
| screen_x = grid_x * self.tile_size | |
| screen_y = grid_y * self.tile_size | |
| # TODOS los tipos de edificios implementados | |
| if building_type == 'casa': | |
| self._draw_house_isometric(draw, screen_x, screen_y, city.happiness) | |
| elif building_type == 'villa': | |
| self._draw_villa_house(draw, screen_x, screen_y, city.cultural_identity) | |
| elif building_type == 'edificio': | |
| self._draw_apartment_building(draw, screen_x, screen_y, city.economy) | |
| elif building_type == 'escuela': | |
| self._draw_school(draw, screen_x, screen_y, city.resources['educacion']) | |
| elif building_type == 'hospital': | |
| self._draw_hospital(draw, screen_x, screen_y, city.resources['salud']) | |
| elif building_type == 'cancha': | |
| self._draw_soccer_field(draw, screen_x, screen_y, city.happiness) | |
| elif building_type == 'parrilla': | |
| self._draw_parrilla(draw, screen_x, screen_y, city.cultural_identity) | |
| elif building_type == 'plaza': | |
| self._draw_plaza(draw, screen_x, screen_y, city.happiness) | |
| elif building_type == 'fabrica': | |
| self._draw_factory(draw, screen_x, screen_y, city.resources['empleo']) | |
| elif building_type == 'centro_cultural': | |
| self._draw_cultural_center(draw, screen_x, screen_y, city.resources['cultura']) | |
| elif building_type == 'kiosco': | |
| self._draw_kiosco(draw, screen_x, screen_y, city.economy) | |
| elif building_type == 'subte': | |
| self._draw_subway(draw, screen_x, screen_y, city.resources['transport']) | |
| elif building_type == 'comisaria': | |
| self._draw_police_station(draw, screen_x, screen_y, city.resources['seguridad']) | |
| # TODOS los métodos de dibujo de edificios (implementados completamente) | |
| def _draw_house_isometric(self, draw, x, y, happiness): | |
| """Casa con perspectiva isométrica""" | |
| house_color = (139, 69, 19) if happiness > 60 else (101, 67, 33) | |
| # Paredes | |
| wall_points = [(x + 2, y + 15), (x + 18, y + 15), (x + 18, y + 2), (x + 2, y + 2)] | |
| draw.polygon(wall_points, fill=house_color, outline=(80, 40, 0), width=1) | |
| # Techo isométrico | |
| roof_color = (160, 82, 45) if happiness > 70 else (120, 60, 30) | |
| roof_points = [(x, y), (x + 10, y - 5), (x + 20, y), (x + 10, y + 5)] | |
| draw.polygon(roof_points, fill=roof_color, outline=(100, 50, 25)) | |
| # Puerta | |
| draw.rectangle([x + 8, y + 10, x + 12, y + 15], fill=(101, 67, 33)) | |
| # Ventanas con luz según felicidad | |
| window_color = (255, 255, 200) if happiness > 50 else (150, 150, 150) | |
| draw.rectangle([x + 4, y + 8, x + 7, y + 11], fill=window_color, outline=(0, 0, 0)) | |
| draw.rectangle([x + 13, y + 8, x + 16, y + 11], fill=window_color, outline=(0, 0, 0)) | |
| # Jardincito si hay alta felicidad | |
| if happiness > 80: | |
| for i in range(3): | |
| flower_x = x + 2 + i * 4 | |
| flower_y = y + 16 | |
| draw.ellipse([flower_x, flower_y, flower_x + 2, flower_y + 2], fill=(255, 100, 150)) | |
| def _draw_villa_house(self, draw, x, y, cultural_identity): | |
| """Casa de villa con colores vibrantes""" | |
| villa_colors = [(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100), (255, 100, 255)] | |
| color = random.choice(villa_colors) | |
| # Casa principal | |
| draw.rectangle([x, y + 5, x + 20, y + 18], fill=color, outline=(0, 0, 0), width=2) | |
| # Techo de chapa | |
| draw.polygon([(x-2, y+5), (x+10, y-2), (x+22, y+5)], fill=(150, 150, 150)) | |
| # Extensión autoconstruida | |
| if cultural_identity > 60: | |
| draw.rectangle([x + 16, y + 12, x + 25, y + 18], fill=color, outline=(0, 0, 0)) | |
| # Antena parabólica | |
| draw.ellipse([x + 15, y - 1, x + 18, y + 2], outline=(200, 200, 200), width=2) | |
| # Tendedero con ropa | |
| draw.line([(x + 2, y + 3), (x + 18, y + 3)], fill=(100, 100, 100), width=1) | |
| for i in range(3): | |
| cloth_x = x + 4 + i * 5 | |
| draw.rectangle([cloth_x, y + 3, cloth_x + 3, y + 8], | |
| fill=random.choice([(255, 0, 0), (0, 255, 0), (0, 0, 255)])) | |
| def _draw_apartment_building(self, draw, x, y, economy): | |
| """Edificio de departamentos""" | |
| building_color = (120, 120, 160) if economy > 5000 else (100, 100, 140) | |
| # Estructura principal más alta | |
| draw.rectangle([x + 1, y - 10, x + 23, y + 18], fill=building_color, outline=(0, 0, 0), width=1) | |
| # Ventanas en pisos | |
| for floor in range(4): | |
| floor_y = y - 5 + floor * 6 | |
| for window in range(3): | |
| window_x = x + 4 + window * 6 | |
| light_on = economy > 3000 and random.random() < 0.7 | |
| window_color = (255, 255, 200) if light_on else (100, 100, 120) | |
| draw.rectangle([window_x, floor_y, window_x + 3, floor_y + 3], | |
| fill=window_color, outline=(0, 0, 0)) | |
| def _draw_school(self, draw, x, y, education_level): | |
| """Escuela con bandera argentina""" | |
| school_color = (255, 255, 100) if education_level > 70 else (200, 200, 80) | |
| # Edificio principal | |
| draw.rectangle([x + 1, y + 8, x + 23, y + 18], fill=school_color, outline=(0, 0, 0), width=2) | |
| # Bandera argentina | |
| draw.rectangle([x + 20, y + 2, x + 22, y + 8], fill=(100, 150, 255)) # Azul | |
| draw.rectangle([x + 20, y + 4, x + 22, y + 6], fill=(255, 255, 255)) # Blanco | |
| # Asta | |
| draw.line([(x + 22, y + 8), (x + 22, y + 2)], fill=(139, 69, 19), width=2) | |
| # Patio escolar | |
| draw.rectangle([x + 5, y + 12, x + 19, y + 16], fill=(200, 200, 200), outline=(150, 150, 150)) | |
| # Niños en el patio si alta educación | |
| if education_level > 60: | |
| for i in range(3): | |
| child_x = x + 7 + i * 4 | |
| child_y = y + 13 | |
| draw.ellipse([child_x, child_y, child_x + 2, child_y + 3], fill=(180, 140, 100)) | |
| def _draw_hospital(self, draw, x, y, health_level): | |
| """Hospital con cruz roja""" | |
| hospital_color = (255, 100, 100) if health_level > 70 else (200, 80, 80) | |
| # Edificio | |
| draw.rectangle([x + 1, y + 5, x + 23, y + 18], fill=hospital_color, outline=(0, 0, 0), width=2) | |
| # Cruz roja | |
| draw.rectangle([x + 10, y + 8, x + 14, y + 15], fill=(255, 0, 0)) | |
| draw.rectangle([x + 7, y + 10, x + 17, y + 13], fill=(255, 0, 0)) | |
| # Ambulancia si hospital activo | |
| if health_level > 50: | |
| draw.rectangle([x + 2, y + 15, x + 10, y + 18], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| draw.ellipse([x + 3, y + 16, x + 5, y + 18], fill=(0, 0, 0)) | |
| draw.ellipse([x + 7, y + 16, x + 9, y + 18], fill=(0, 0, 0)) | |
| def _draw_soccer_field(self, draw, x, y, happiness): | |
| """Cancha de fútbol con actividad""" | |
| # Césped | |
| grass_color = (50, 150, 50) if happiness > 60 else (80, 120, 80) | |
| draw.rectangle([x, y, x + 40, y + 25], fill=grass_color, outline=(255, 255, 255), width=2) | |
| # Líneas de la cancha | |
| draw.line([(x + 20, y), (x + 20, y + 25)], fill=(255, 255, 255), width=2) | |
| draw.ellipse([x + 15, y + 10, x + 25, y + 15], outline=(255, 255, 255), width=2) | |
| # Arcos | |
| draw.rectangle([x, y + 8, x + 3, y + 17], outline=(255, 255, 255), width=2) | |
| draw.rectangle([x + 37, y + 8, x + 40, y + 17], outline=(255, 255, 255), width=2) | |
| # Jugadores animados | |
| if happiness > 70: | |
| for i in range(6): | |
| player_x = x + 5 + i * 6 + int(3 * math.sin(self.animation_time + i)) | |
| player_y = y + 8 + int(2 * math.cos(self.animation_time + i * 1.5)) | |
| draw.ellipse([player_x, player_y, player_x + 3, player_y + 5], fill=(180, 140, 100)) | |
| # Pelota animada | |
| ball_x = x + 20 + int(10 * math.sin(self.animation_time * 2)) | |
| ball_y = y + 12 + int(5 * math.cos(self.animation_time * 2)) | |
| draw.ellipse([ball_x, ball_y, ball_x + 3, ball_y + 3], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| def _draw_parrilla(self, draw, x, y, cultural_identity): | |
| """Parrilla argentina con humo""" | |
| # Base de la parrilla | |
| draw.rectangle([x + 5, y + 10, x + 15, y + 18], fill=(50, 50, 50), outline=(100, 100, 100)) | |
| # Parrilla | |
| for i in range(3): | |
| draw.line([(x + 6 + i * 3, y + 12), (x + 6 + i * 3, y + 16)], fill=(150, 150, 150), width=2) | |
| # Carne en la parrilla | |
| if cultural_identity > 70: | |
| for i in range(2): | |
| meat_x = x + 7 + i * 4 | |
| meat_y = y + 13 | |
| draw.ellipse([meat_x, meat_y, meat_x + 3, meat_y + 2], fill=(139, 69, 19)) | |
| # Humo animado | |
| for i in range(4): | |
| smoke_x = x + 10 + random.randint(-3, 3) | |
| smoke_y = y + 8 - i * 4 + int(2 * math.sin(self.animation_time * 2 + i)) | |
| size = 3 + i | |
| draw.ellipse([smoke_x - size//2, smoke_y - size//2, smoke_x + size//2, smoke_y + size//2], | |
| fill=(200, 200, 200)) | |
| # Gente alrededor | |
| if cultural_identity > 60: | |
| for i in range(3): | |
| person_x = x + 2 + i * 8 | |
| person_y = y + 15 | |
| draw.ellipse([person_x, person_y, person_x + 4, person_y + 8], fill=(180, 140, 100)) | |
| def _draw_plaza(self, draw, x, y, happiness): | |
| """Plaza con actividades""" | |
| # Base verde de la plaza | |
| plaza_color = (100, 180, 100) if happiness > 50 else (120, 140, 120) | |
| draw.rectangle([x, y, x + 30, y + 30], fill=plaza_color, outline=(80, 120, 80)) | |
| # Senderos | |
| draw.line([(x, y + 15), (x + 30, y + 15)], fill=(160, 140, 120), width=3) | |
| draw.line([(x + 15, y), (x + 15, y + 30)], fill=(160, 140, 120), width=3) | |
| # Monumento central | |
| draw.rectangle([x + 12, y + 12, x + 18, y + 18], fill=(180, 180, 180), outline=(120, 120, 120)) | |
| # Árboles | |
| tree_positions = [(x + 5, y + 5), (x + 25, y + 5), (x + 5, y + 25), (x + 25, y + 25)] | |
| for tree_x, tree_y in tree_positions: | |
| # Tronco | |
| draw.rectangle([tree_x, tree_y + 3, tree_x + 2, tree_y + 8], fill=(101, 67, 33)) | |
| # Copa | |
| draw.ellipse([tree_x - 2, tree_y, tree_x + 4, tree_y + 6], fill=(50, 150, 50)) | |
| # Actividades según felicidad | |
| if happiness > 70: | |
| # Ronda de mate | |
| for i in range(4): | |
| angle = i * (math.pi / 2) | |
| person_x = x + 15 + int(8 * math.cos(angle)) | |
| person_y = y + 15 + int(8 * math.sin(angle)) | |
| draw.ellipse([person_x, person_y, person_x + 3, person_y + 5], fill=(180, 140, 100)) | |
| def _draw_factory(self, draw, x, y, employment_level): | |
| """Fábrica con chimeneas""" | |
| factory_color = (150, 100, 100) if employment_level > 60 else (120, 80, 80) | |
| # Edificio principal | |
| draw.rectangle([x + 1, y + 8, x + 23, y + 18], fill=factory_color, outline=(0, 0, 0), width=2) | |
| # Chimeneas | |
| for chimney_x in [x + 6, x + 12, x + 18]: | |
| draw.rectangle([chimney_x, y + 2, chimney_x + 3, chimney_x + 8], fill=(80, 80, 80)) | |
| # Humo | |
| smoke_y = y + 2 - int(3 * math.sin(self.animation_time)) | |
| draw.ellipse([chimney_x, smoke_y - 2, chimney_x + 3, smoke_y + 1], fill=(150, 150, 150)) | |
| # Ventanas industriales | |
| for window_x in range(x + 4, x + 20, 4): | |
| draw.rectangle([window_x, y + 12, window_x + 2, window_x + 15], | |
| fill=(255, 200, 100), outline=(0, 0, 0)) | |
| def _draw_cultural_center(self, draw, x, y, culture_level): | |
| """Centro cultural""" | |
| center_color = (255, 140, 0) if culture_level > 70 else (200, 100, 0) | |
| # Edificio principal | |
| draw.rectangle([x + 1, y + 5, x + 23, y + 18], fill=center_color, outline=(0, 0, 0), width=2) | |
| # Cartelera cultural | |
| draw.rectangle([x + 5, y + 8, x + 19, y + 15], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| # Máscaras de teatro | |
| draw.ellipse([x + 7, y + 10, x + 11, y + 13], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| draw.ellipse([x + 13, y + 10, x + 17, y + 13], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| def _draw_kiosco(self, draw, x, y, economy): | |
| """Kiosco de barrio""" | |
| kiosco_color = (200, 150, 100) if economy > 2000 else (150, 100, 80) | |
| # Estructura pequeña | |
| draw.rectangle([x + 5, y + 10, x + 15, y + 18], fill=kiosco_color, outline=(0, 0, 0), width=2) | |
| # Cartel | |
| draw.rectangle([x + 7, y + 8, x + 13, y + 10], fill=(255, 0, 0)) | |
| # Productos en ventana | |
| for i in range(3): | |
| product_x = x + 6 + i * 3 | |
| draw.rectangle([product_x, y + 12, product_x + 2, y + 15], | |
| fill=random.choice([(255, 0, 0), (0, 255, 0), (0, 0, 255)])) | |
| def _draw_subway(self, draw, x, y, transport_level): | |
| """Estación de subte""" | |
| # Entrada subterránea | |
| draw.rectangle([x + 5, y + 10, x + 15, y + 18], fill=(100, 100, 100), outline=(0, 0, 0), width=2) | |
| # Cartel de subte | |
| draw.rectangle([x + 7, y + 5, x + 13, y + 8], fill=(0, 100, 200)) | |
| # Escaleras | |
| for i in range(3): | |
| step_y = y + 12 + i * 2 | |
| draw.line([(x + 6, step_y), (x + 14, step_y)], fill=(150, 150, 150), width=1) | |
| def _draw_police_station(self, draw, x, y, security_level): | |
| """Comisaría""" | |
| station_color = (100, 100, 150) if security_level > 70 else (80, 80, 120) | |
| # Edificio | |
| draw.rectangle([x + 1, y + 5, x + 23, y + 18], fill=station_color, outline=(0, 0, 0), width=2) | |
| # Escudo policial | |
| draw.ellipse([x + 10, y + 8, x + 14, y + 12], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| # Patrullero | |
| if security_level > 50: | |
| draw.rectangle([x + 16, y + 15, x + 22, y + 18], fill=(255, 255, 255), outline=(0, 0, 0)) | |
| draw.ellipse([x + 17, y + 17, x + 19, y + 19], fill=(0, 0, 0)) | |
| draw.ellipse([x + 20, y + 17, x + 22, y + 19], fill=(0, 0, 0)) | |
| def _draw_city_life(self, draw, city): | |
| """Sistemas dinámicos de vida urbana""" | |
| # Tráfico animado en calles principales | |
| if city.resources['transport'] > 30: | |
| self._draw_animated_traffic(draw, city) | |
| # Personas caminando | |
| if city.happiness > 40: | |
| self._draw_pedestrians(draw, city) | |
| # Efectos de contaminación | |
| if city.resources.get('pollution', 0) < -20: | |
| self._draw_pollution_effects(draw, city) | |
| def _draw_animated_traffic(self, draw, city): | |
| """Tráfico animado en las calles""" | |
| # Colectivos argentinos | |
| for i in range(3): | |
| bus_x = (100 + i * 300 + int(self.animation_time * 50)) % city.width | |
| bus_y = 200 + i * 100 | |
| # Colectivo | |
| draw.rectangle([bus_x, bus_y, bus_x + 25, bus_y + 12], | |
| fill=(255, 200, 0), outline=(0, 0, 0), width=2) | |
| # Ventanas | |
| for window in range(3): | |
| draw.rectangle([bus_x + 3 + window * 6, bus_y + 2, | |
| bus_x + 8 + window * 6, bus_y + 7], | |
| fill=(150, 200, 255)) | |
| # Ruedas | |
| draw.ellipse([bus_x + 2, bus_y + 10, bus_x + 6, bus_y + 14], fill=(50, 50, 50)) | |
| draw.ellipse([bus_x + 19, bus_y + 10, bus_x + 23, bus_y + 14], fill=(50, 50, 50)) | |
| def _draw_pedestrians(self, draw, city): | |
| """Peatones animados""" | |
| for i in range(8): | |
| person_x = (50 + i * 120 + int(self.animation_time * 30 + i * 50)) % city.width | |
| person_y = 300 + i * 40 + int(5 * math.sin(self.animation_time + i)) | |
| # Persona simple | |
| draw.ellipse([person_x, person_y, person_x + 4, person_y + 8], fill=(180, 140, 100)) | |
| # Mate en la mano si alta cultura | |
| if city.cultural_identity > 70 and i % 3 == 0: | |
| draw.ellipse([person_x + 3, person_y + 2, person_x + 6, person_y + 5], fill=(100, 50, 0)) | |
| def _draw_pollution_effects(self, draw, city): | |
| """Efectos de contaminación""" | |
| pollution_level = abs(city.resources.get('pollution', 0)) | |
| if pollution_level > 20: | |
| # Smog gris | |
| for i in range(int(pollution_level / 5)): | |
| smog_x = random.randint(0, city.width) | |
| smog_y = random.randint(100, 300) | |
| size = random.randint(10, 30) | |
| draw.ellipse([smog_x - size, smog_y - size, smog_x + size, smog_y + size], | |
| fill=(120, 120, 120)) | |
| def _draw_advanced_ui(self, draw, city): | |
| """UI avanzada con gráficos informativos""" | |
| # Panel principal con transparencia | |
| panel_width = 300 | |
| panel_height = 200 | |
| # Panel de fondo con gradiente | |
| for i in range(panel_height): | |
| alpha = int(180 - i) | |
| color = (0, 0, 0) if alpha > 0 else (20, 20, 20) | |
| draw.rectangle([10, 10 + i, 10 + panel_width, 11 + i], fill=color) | |
| # Borde elegante | |
| draw.rectangle([10, 10, 10 + panel_width, 10 + panel_height], | |
| outline=(100, 150, 255), width=3) | |
| # Información de la ciudad (texto simplificado por limitaciones de PIL) | |
| info_y = 25 | |
| city_info = [ | |
| f"Población: {city.population:,}", | |
| f"Felicidad: {city.happiness}%", | |
| f"Economía: ${city.economy:,}", | |
| f"Identidad: {city.cultural_identity}%", | |
| f"Inflación: {city.inflation:.1f}x" | |
| ] | |
| # Barras de recursos en el lado derecho | |
| self._draw_resource_bars(draw, city, city.width - 250, 20) | |
| # Mini mapa en esquina inferior derecha | |
| self._draw_mini_map(draw, city, city.width - 150, city.height - 120) | |
| def _draw_resource_bars(self, draw, city, start_x, start_y): | |
| """Barras de recursos con efectos visuales""" | |
| resources = city.resources | |
| bar_width = 200 | |
| bar_height = 15 | |
| resource_colors = { | |
| 'presupuesto': (255, 215, 0), 'energia': (255, 255, 0), | |
| 'agua': (0, 191, 255), 'seguridad': (255, 99, 71), | |
| 'educacion': (147, 112, 219), 'salud': (255, 20, 147), | |
| 'cultura': (255, 140, 0), 'empleo': (50, 205, 50), | |
| 'vivienda': (139, 69, 19), 'transport': (100, 149, 237), | |
| 'pollution': (120, 120, 120) | |
| } | |
| y_offset = start_y | |
| for resource, value in resources.items(): | |
| if resource == 'presupuesto': | |
| # Presupuesto se muestra diferente | |
| y_offset += 25 | |
| continue | |
| # Normalizar valor para barra (0-100) | |
| if resource == 'pollution': | |
| normalized_value = max(0, min(100, abs(value))) | |
| else: | |
| normalized_value = max(0, min(100, value)) if isinstance(value, (int, float)) else 50 | |
| # Barra de fondo | |
| draw.rectangle([start_x, y_offset, start_x + bar_width, y_offset + bar_height], | |
| fill=(40, 40, 40), outline=(100, 100, 100)) | |
| # Barra de valor con gradiente | |
| fill_width = int((normalized_value / 100) * (bar_width - 4)) | |
| color = resource_colors.get(resource, (150, 150, 150)) | |
| # Efecto de brillo | |
| for i in range(fill_width): | |
| brightness = 0.7 + 0.3 * math.sin(self.animation_time + i/10) | |
| bright_color = tuple(int(c * brightness) for c in color) | |
| draw.rectangle([start_x + 2 + i, y_offset + 2, | |
| start_x + 3 + i, y_offset + bar_height - 2], | |
| fill=bright_color) | |
| y_offset += 22 | |
| def _draw_mini_map(self, draw, city, start_x, start_y): | |
| """Mini mapa de la ciudad""" | |
| map_width, map_height = 120, 100 | |
| # Fondo del mini mapa | |
| draw.rectangle([start_x, start_y, start_x + map_width, start_y + map_height], | |
| fill=(20, 20, 20), outline=(150, 150, 150), width=2) | |
| # Edificios en el mini mapa | |
| scale = 2 | |
| for (grid_x, grid_y), building_type in city.city_grid.items(): | |
| mini_x = start_x + (grid_x * scale) % map_width | |
| mini_y = start_y + (grid_y * scale) % map_height | |
| building_colors = { | |
| 'casa': (100, 150, 100), 'villa': (255, 150, 100), | |
| 'edificio': (150, 150, 200), 'escuela': (200, 200, 100), | |
| 'hospital': (255, 100, 100), 'cancha': (100, 255, 100), | |
| 'plaza': (150, 255, 150), 'fabrica': (150, 100, 100), | |
| 'centro_cultural': (255, 140, 0), 'kiosco': (200, 150, 100), | |
| 'subte': (100, 149, 237), 'comisaria': (100, 100, 150), | |
| 'parrilla': (200, 100, 50) | |
| } | |
| color = building_colors.get(building_type, (100, 100, 100)) | |
| draw.rectangle([mini_x, mini_y, mini_x + scale, mini_y + scale], fill=color) | |
| def _draw_special_effects(self, draw, city): | |
| """Efectos especiales adicionales""" | |
| # Efectos de festivales si alta cultura | |
| if city.resources['cultura'] > 80: | |
| for i in range(5): | |
| firework_x = random.randint(100, city.width - 100) | |
| firework_y = random.randint(50, 150) | |
| size = random.randint(3, 8) | |
| color = random.choice([(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100)]) | |
| draw.ellipse([firework_x - size, firework_y - size, | |
| firework_x + size, firework_y + size], fill=color) | |
| class EnhancedCityGame: | |
| """Juego de ciudad argentino mejorado COMPLETO""" | |
| def __init__(self): | |
| self.city = ArgentineCityBuilder() | |
| self.renderer = AdvancedCityRenderer() | |
| self.ai_planner = AIUrbanPlanner() | |
| self.selected_building = None | |
| self.game_speed = 1 | |
| self.events_log = [] | |
| self.day = 1 | |
| def place_building(self, building_type, grid_x, grid_y): | |
| """Coloca un edificio en la ciudad - CORREGIDO""" | |
| try: | |
| grid_x, grid_y = int(grid_x), int(grid_y) | |
| if building_type not in self.city.building_types: | |
| return "Tipo de edificio inválido", self.get_city_status(), "Error: Tipo de edificio no válido" | |
| if (grid_x, grid_y) in self.city.city_grid: | |
| return "Ya hay un edificio ahí", self.get_city_status(), "Error: Posición ocupada" | |
| building_info = self.city.building_types[building_type] | |
| cost = building_info['cost'] | |
| if self.city.resources['presupuesto'] < cost: | |
| return f"No tenés suficiente presupuesto (${cost:,})", self.get_city_status(), "Error: Sin presupuesto" | |
| # Construir edificio | |
| self.city.resources['presupuesto'] -= cost | |
| self.city.city_grid[(grid_x, grid_y)] = building_type | |
| # Aplicar efectos CORREGIDOS | |
| self._apply_building_effects(building_type, building_info) | |
| # Actualizar estadísticas | |
| self._update_city_stats() | |
| # Generar consejo IA | |
| advice = self.ai_planner.generate_city_advice({ | |
| 'population': self.city.population, | |
| 'happiness': self.city.happiness, | |
| 'budget': self.city.resources['presupuesto'], | |
| 'buildings': len(self.city.city_grid), | |
| 'culture': self.city.cultural_identity | |
| }) | |
| frame = self.renderer.render_city_frame(self.city) | |
| status = self.get_city_status() | |
| message = f"✅ {building_type.replace('_', ' ').title()} construido por ${cost:,}" | |
| return frame, status, f"{message}\n\n🤖 IA Urbana: {advice}" | |
| except Exception as e: | |
| print(f"Error en construcción: {e}") | |
| return self.renderer.render_city_frame(self.city), self.get_city_status(), f"Error: {str(e)}" | |
| def _apply_building_effects(self, building_type, building_info): | |
| """Aplica los efectos de un edificio a la ciudad - CORREGIDO""" | |
| for effect, value in building_info.items(): | |
| if effect == 'cost': | |
| continue | |
| elif effect == 'pop': | |
| self.city.population += value | |
| elif effect == 'happiness': | |
| self.city.happiness = min(100, max(0, self.city.happiness + value)) | |
| elif effect == 'solidarity': | |
| self.city.cultural_identity = min(100, max(0, self.city.cultural_identity + value)) | |
| elif effect in self.city.resources: | |
| if effect == 'pollution': | |
| # Pollution es negativa, así que sumamos el valor negativo | |
| self.city.resources[effect] += value | |
| else: | |
| current_value = self.city.resources[effect] | |
| self.city.resources[effect] = min(100, max(0, current_value + value)) | |
| def _update_city_stats(self): | |
| """Actualiza estadísticas generales de la ciudad""" | |
| # Calcular economía basada en edificios | |
| economy_buildings = ['fabrica', 'kiosco', 'centro_cultural', 'subte'] | |
| economy_bonus = sum(5000 for pos, building in self.city.city_grid.items() | |
| if building in economy_buildings) | |
| self.city.economy = 1000 + economy_bonus + self.city.population * 30 | |
| # Inflación aumenta con el tiempo y población | |
| self.city.inflation += 0.005 + (len(self.city.city_grid) * 0.001) | |
| # Felicidad general basada en múltiples factores | |
| happiness_factors = [ | |
| self.city.resources.get('cultura', 50), | |
| self.city.resources.get('seguridad', 50), | |
| self.city.resources.get('empleo', 50), | |
| self.city.resources.get('salud', 50), | |
| self.city.resources.get('educacion', 50), | |
| self.city.cultural_identity | |
| ] | |
| self.city.happiness = sum(happiness_factors) // len(happiness_factors) | |
| # Limitar felicidad | |
| self.city.happiness = max(0, min(100, self.city.happiness)) | |
| def simulate_day(self): | |
| """Simula un día en la ciudad""" | |
| try: | |
| self.day += 1 | |
| # Ingresos diarios | |
| base_income = self.city.population * 12 | |
| economy_bonus = self.city.economy * 0.008 | |
| daily_income = int(base_income + economy_bonus) | |
| self.city.resources['presupuesto'] += daily_income | |
| # Gastos de mantenimiento | |
| maintenance_cost = len(self.city.city_grid) * 80 | |
| self.city.resources['presupuesto'] -= maintenance_cost | |
| # Efectos de la inflación (más realista) | |
| inflation_impact = int(self.city.resources['presupuesto'] * 0.01) | |
| self.city.resources['presupuesto'] -= inflation_impact | |
| # Eventos aleatorios | |
| event_message = "" | |
| if random.random() < 0.25: | |
| event_message = self._trigger_city_event() | |
| # Actualizar todo | |
| self._update_city_stats() | |
| # Generar análisis IA del día | |
| ai_analysis = self.ai_planner.generate_city_advice({ | |
| 'population': self.city.population, | |
| 'happiness': self.city.happiness, | |
| 'budget': self.city.resources['presupuesto'], | |
| 'buildings': len(self.city.city_grid), | |
| 'culture': self.city.cultural_identity | |
| }) | |
| frame = self.renderer.render_city_frame(self.city) | |
| status = self.get_city_status() | |
| message = f"📅 Día {self.day} completado\n💰 Ingresos: ${daily_income:,}\n💸 Gastos: ${maintenance_cost:,}" | |
| if event_message: | |
| message += f"\n📰 {event_message}" | |
| message += f"\n\n🤖 Análisis IA: {ai_analysis}" | |
| return frame, status, message | |
| except Exception as e: | |
| print(f"Error en simulación: {e}") | |
| return self.renderer.render_city_frame(self.city), self.get_city_status(), f"Error: {str(e)}" | |
| def _trigger_city_event(self): | |
| """Eventos aleatorios de la ciudad - MEJORADOS""" | |
| events = [ | |
| ("🎉 Festival de Tango en San Telmo", "cultura", 15, "happiness", 20), | |
| ("⚽ Superclásico River vs Boca", "happiness", 30, "cultura", 10), | |
| ("🏭 Inversión extranjera en fábrica", "empleo", 20, "presupuesto", 8000), | |
| ("🌧️ Lluvia beneficiosa para la ciudad", "agua", 25, "energia", -5), | |
| ("📚 Programa nacional de alfabetización", "educacion", 18, "cultura", 12), | |
| ("🏥 Campaña de vacunación masiva", "salud", 25, "happiness", 15), | |
| ("💸 Crisis económica menor", "presupuesto", -5000, "happiness", -8), | |
| ("🎭 Inauguración de centro cultural", "cultura", 30, "educacion", 15), | |
| ("🚌 Mejoras en transporte público", "transport", 20, "happiness", 12), | |
| ("🛡️ Operativo de seguridad exitoso", "seguridad", 15, "happiness", 8), | |
| ("🏠 Programa de vivienda social", "vivienda", 25, "happiness", 18), | |
| ("🔥 Festival del Asado Argentino", "cultura", 20, "happiness", 25) | |
| ] | |
| event_name, res1, val1, res2, val2 = random.choice(events) | |
| # Aplicar efectos | |
| if res1 == 'presupuesto': | |
| self.city.resources[res1] += val1 | |
| elif res1 == 'happiness': | |
| self.city.happiness = min(100, max(0, self.city.happiness + val1)) | |
| elif res1 in self.city.resources: | |
| self.city.resources[res1] = min(100, max(0, self.city.resources[res1] + val1)) | |
| if res2 == 'presupuesto': | |
| self.city.resources[res2] += val2 | |
| elif res2 == 'happiness': | |
| self.city.happiness = min(100, max(0, self.city.happiness + val2)) | |
| elif res2 in self.city.resources: | |
| self.city.resources[res2] = min(100, max(0, self.city.resources[res2] + val2)) | |
| self.events_log.append(event_name) | |
| return event_name | |
| def get_city_status(self): | |
| """Status completo de la ciudad""" | |
| try: | |
| building_count = len(self.city.city_grid) | |
| return { | |
| "🏙️ Población": f"{self.city.population:,} habitantes", | |
| "😊 Felicidad": f"{self.city.happiness}%", | |
| "💰 Presupuesto": f"${self.city.resources['presupuesto']:,}", | |
| "📊 Economía": f"${self.city.economy:,}", | |
| "🇦🇷 Identidad": f"{self.city.cultural_identity}%", | |
| "📈 Inflación": f"{self.city.inflation:.2f}x", | |
| "🏗️ Edificios": f"{building_count} construidos", | |
| "📅 Día": self.day, | |
| "⚡ Energía": f"{self.city.resources['energia']}%", | |
| "💧 Agua": f"{self.city.resources['agua']}%", | |
| "🎓 Educación": f"{self.city.resources['educacion']}%", | |
| "🏥 Salud": f"{self.city.resources['salud']}%", | |
| "🎭 Cultura": f"{self.city.resources['cultura']}%", | |
| "💼 Empleo": f"{self.city.resources['empleo']}%" | |
| } | |
| except Exception as e: | |
| return {"Error": str(e)} | |
| def create_simcity_interface(): | |
| """Interfaz del SimCity Argentino COMPLETA""" | |
| game = EnhancedCityGame() | |
| with gr.Blocks( | |
| title="🇦🇷 SIMCIUDAD ARGENTINA - Construcción Épica", | |
| theme=gr.themes.Glass(), | |
| css=""" | |
| .building-btn { font-size: 1.0em; padding: 8px; margin: 2px; } | |
| .city-header { background: linear-gradient(45deg, #74b9ff, #0984e3, #00b894); } | |
| .status-panel { background: #2d3436; border-radius: 10px; } | |
| """ | |
| ) as interface: | |
| gr.HTML(""" | |
| <div class="city-header" style="padding: 25px; text-align: center; border-radius: 15px; margin: 15px;"> | |
| <h1 style="color: white; font-size: 2.8em; margin: 0; text-shadow: 3px 3px 6px rgba(0,0,0,0.7);"> | |
| 🏙️ SIMCIUDAD ARGENTINA 🇦🇷 | |
| </h1> | |
| <h2 style="color: #ddd; font-size: 1.2em; margin: 10px 0;"> | |
| Construí la Ciudad Argentina de tus Sueños con IA | |
| </h2> | |
| <p style="color: #f1f2f6; font-size: 1.0em;"> | |
| Gráficos Isométricos • Mecánicas Realistas • Cultura Auténtica • Powered by Groq AI | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| city_display = gr.Image( | |
| label="🌆 Tu Ciudad Argentina", | |
| width=1000, | |
| height=700, | |
| value=game.renderer.render_city_frame(game.city) | |
| ) | |
| gr.Markdown("### 🏗️ HERRAMIENTAS DE CONSTRUCCIÓN") | |
| with gr.Row(): | |
| grid_x_input = gr.Number(label="Coordenada X", value=20, minimum=0, maximum=45) | |
| grid_y_input = gr.Number(label="Coordenada Y", value=20, minimum=0, maximum=30) | |
| # Edificios residenciales | |
| gr.Markdown("#### 🏠 RESIDENCIALES") | |
| with gr.Row(): | |
| house_btn = gr.Button("🏠 CASA ($1K)", variant="secondary", elem_classes="building-btn") | |
| villa_btn = gr.Button("🏘️ VILLA ($500)", variant="secondary", elem_classes="building-btn") | |
| building_btn = gr.Button("🏢 EDIFICIO ($5K)", variant="primary", elem_classes="building-btn") | |
| kiosco_btn = gr.Button("🏪 KIOSCO ($800)", variant="secondary", elem_classes="building-btn") | |
| # Servicios públicos | |
| gr.Markdown("#### 🏛️ SERVICIOS PÚBLICOS") | |
| with gr.Row(): | |
| school_btn = gr.Button("🏫 ESCUELA ($8K)", variant="primary", elem_classes="building-btn") | |
| hospital_btn = gr.Button("🏥 HOSPITAL ($15K)", variant="stop", elem_classes="building-btn") | |
| police_btn = gr.Button("🛡️ COMISARÍA ($7K)", variant="stop", elem_classes="building-btn") | |
| subway_btn = gr.Button("🚇 SUBTE ($20K)", variant="primary", elem_classes="building-btn") | |
| # Cultura y entretenimiento | |
| gr.Markdown("#### 🎭 CULTURA Y ENTRETENIMIENTO") | |
| with gr.Row(): | |
| field_btn = gr.Button("⚽ CANCHA ($3K)", variant="secondary", elem_classes="building-btn") | |
| grill_btn = gr.Button("🔥 PARRILLA ($2K)", variant="stop", elem_classes="building-btn") | |
| cultural_btn = gr.Button("🎭 C.CULTURAL ($10K)", variant="primary", elem_classes="building-btn") | |
| plaza_btn = gr.Button("🌳 PLAZA ($1.5K)", variant="secondary", elem_classes="building-btn") | |
| # Industria | |
| gr.Markdown("#### 🏭 INDUSTRIA") | |
| with gr.Row(): | |
| factory_btn = gr.Button("🏭 FÁBRICA ($12K)", variant="primary", elem_classes="building-btn") | |
| # Simulación | |
| with gr.Row(): | |
| simulate_btn = gr.Button("⏭️ SIMULAR DÍA", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📊 ESTADO DE LA CIUDAD") | |
| city_status = gr.JSON( | |
| label="📈 Estadísticas Completas", | |
| value=game.get_city_status() | |
| ) | |
| gr.Markdown("### 📰 NOTICIAS Y EVENTOS") | |
| events_display = gr.Textbox( | |
| value="🎮 ¡Bienvenido al SimCity Argentino con IA! Construí tu ciudad con identidad nacional. La IA te dará consejos de planificación urbana en tiempo real.", | |
| label="📺 Canal de Noticias Urbanas", | |
| lines=6, | |
| interactive=False | |
| ) | |
| gr.Markdown("### 💰 GUÍA DE CONSTRUCCIÓN") | |
| gr.HTML(""" | |
| <div style="background: #2d3436; color: white; padding: 15px; border-radius: 10px; font-size: 0.85em;"> | |
| <p><strong>💵 Precios y Efectos:</strong></p> | |
| <ul style="margin: 5px 0; padding-left: 15px;"> | |
| <li>🏠 Casa: $1,000 (+4 pop, +2 felicidad)</li> | |
| <li>🏘️ Villa: $500 (+8 pop, -2 felicidad, +15 solidaridad)</li> | |
| <li>🏢 Edificio: $5,000 (+20 pop, +1 felicidad)</li> | |
| <li>🏫 Escuela: $8,000 (+20 educación)</li> | |
| <li>🏥 Hospital: $15,000 (+30 salud)</li> | |
| <li>⚽ Cancha: $3,000 (+25 felicidad, +10 cultura)</li> | |
| <li>🔥 Parrilla: $2,000 (+20 felicidad, +15 cultura)</li> | |
| <li>🌳 Plaza: $1,500 (+15 felicidad, +5 cultura)</li> | |
| <li>🏭 Fábrica: $12,000 (+30 empleo, -10 pollution)</li> | |
| <li>🎭 C.Cultural: $10,000 (+40 cultura)</li> | |
| <li>🛡️ Comisaría: $7,000 (+25 seguridad)</li> | |
| <li>🚇 Subte: $20,000 (+50 transporte)</li> | |
| <li>🏪 Kiosco: $800 (+5 felicidad, +2 empleo)</li> | |
| </ul> | |
| </div> | |
| """) | |
| # Funciones de construcción mejoradas | |
| def build_structure(building_type, x, y): | |
| return game.place_building(building_type, x, y) | |
| # Conectar TODOS los botones | |
| house_btn.click(fn=lambda x, y: build_structure("casa", x, y), | |
| inputs=[grid_x_input, grid_y_input], | |
| ) |