import streamlit as st
import gymnasium as gym
from stable_baselines3 import DQN
import imageio
import time
import os
import base64
# --- CONFIGURATION ---
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
st.set_page_config(page_title="Lunar Lander AI", page_icon="🚀", layout="wide")
# --- CSS FOR STABILITY ---
# Enforce scrollbar visibility to prevent layout thrashing/jittering
st.markdown("""
""", unsafe_allow_html=True)
# --- SESSION STATE ---
if "simulation_active" not in st.session_state:
st.session_state.simulation_active = False
def start_simulation():
st.session_state.simulation_active = True
# --- HTML DOWNLOAD BUTTON ---
def get_binary_file_downloader_html(bin_file, file_label='File'):
try:
with open(bin_file, 'rb') as f:
data = f.read()
bin_str = base64.b64encode(data).decode()
href = f'''
📥 DOWNLOAD {file_label}
'''
return href
except FileNotFoundError:
return None
# --- LOAD MODEL ---
@st.cache_resource
def load_model():
path = "lunar_lander_model.zip"
if os.path.exists(path):
return DQN.load("lunar_lander_model")
return None
# --- UI LAYOUT ---
st.title("🌕 Lunar Lander AI")
st.markdown("Demonstration of the **Deep Q-Network (DQN)** algorithm.")
col_game, col_download = st.columns([2, 1])
# ==========================================
# 1. RIGHT SIDE (FIXED)
# ==========================================
with col_download:
container = st.container(border=True)
with container:
st.info("🎮 **Try it yourself!**")
st.write("The AI has learned it... but can you do it? Download the game to your computer!")
zip_filename = "lunar_lander_challenge.zip"
download_btn_html = get_binary_file_downloader_html(zip_filename, "GAME PACKAGE")
if download_btn_html:
st.markdown(download_btn_html, unsafe_allow_html=True)
st.caption("Windows .zip | Includes: Python code + Auto launcher")
else:
st.warning("⚠️ The game package (zip) is currently unavailable.")
# ==========================================
# 2. LEFT SIDE (SIMULATION)
# ==========================================
with col_game:
model = load_model()
if model is None:
st.error("⚠️ The 'lunar_lander_model.zip' file is missing!")
else:
st.subheader("🤖 AI Replay")
# Placeholders
controls_placeholder = st.empty()
status_placeholder = st.empty()
video_placeholder = st.empty()
result_placeholder = st.empty()
if not st.session_state.simulation_active:
# INITIAL STATE
controls_placeholder.button("▶️ Start Simulation", type="primary", on_click=start_simulation, use_container_width=True)
else:
# RUNNING
status_placeholder.info("🔄 Calculating simulation in the background... Please wait.")
env = gym.make("LunarLander-v3", render_mode="rgb_array")
obs, info = env.reset()
done = False
truncated = False
total_reward = 0
frames = []
while not done and not truncated:
action, _states = model.predict(obs, deterministic=True)
obs, reward, done, truncated, info = env.step(action)
total_reward += reward
frames.append(env.render())
env.close()
# GIF
gif_path = "replay.gif"
fps = 30
imageio.mimsave(gif_path, frames, fps=fps)
video_duration = len(frames) / fps
# PLAYBACK
status_placeholder.info(f"🎬 Replay in progress... ({video_duration:.1f} s)")
# --- FIX HERE: ---
# Removed 'use_container_width=True' to maintain fixed size/stability.
video_placeholder.image(gif_path, caption="AI Landing")
time.sleep(video_duration)
# RESULT
status_placeholder.empty()
if total_reward >= 200:
msg = f"✅ **SUCCESS!** Professional work.\n\nScore: **{total_reward:.2f}**"
result_placeholder.success(msg)
st.balloons()
elif total_reward >= 0:
msg = f"⚠️ **OK.** Safe, but not perfect.\n\nScore: **{total_reward:.2f}**"
result_placeholder.warning(msg)
else:
msg = f"💥 **CRASH!**\n\nScore: **{total_reward:.2f}**"
result_placeholder.error(msg)
# RESET
st.session_state.simulation_active = False
# Restore button
controls_placeholder.button("🔄 New Simulation", type="primary", on_click=start_simulation, use_container_width=True)