awacke1 commited on
Commit
f265ff5
·
verified ·
1 Parent(s): 2783bba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -198
app.py CHANGED
@@ -1,243 +1,211 @@
1
- # app.py
2
- import streamlit as st
3
- import streamlit.components.v1 as components
4
- import os
5
- import json
6
- import pandas as pd
7
- import uuid
8
- import math
9
- import time
10
-
11
- # Import our GameState class
12
- from gamestate import GameState
13
-
14
- # --- Page Config ---
15
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
16
 
17
- # --- Constants ---
18
  SAVE_DIR = "saved_worlds"
19
- PLOT_WIDTH = 50.0 # Width of each plot in 3D space
20
- PLOT_DEPTH = 50.0 # Depth of each plot
21
- CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
22
-
23
- # --- Ensure Save Directory Exists ---
 
 
 
 
24
  os.makedirs(SAVE_DIR, exist_ok=True)
25
 
26
- @st.cache_data(ttl=3600)
27
  def load_plot_metadata():
28
- """Scans SAVE_DIR for plot files and returns metadata."""
29
- plots = []
30
  try:
31
- plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
 
32
  except FileNotFoundError:
33
- st.error(f"Save directory '{SAVE_DIR}' not found.")
34
  return []
35
  except Exception as e:
36
- st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
37
  return []
38
 
39
- parsed_plots = []
40
- for filename in plot_files:
41
  try:
42
- parts = filename[:-4].split('_') # Remove .csv
43
- grid_x = int(parts[1][1:]) # After 'X'
44
- grid_z = int(parts[2][1:]) # After 'Z'
45
- plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
46
- parsed_plots.append({
47
- 'id': filename[:-4],
48
- 'filename': filename,
49
- 'grid_x': grid_x,
50
- 'grid_z': grid_z,
51
- 'name': plot_name,
52
- 'x_offset': grid_x * PLOT_WIDTH,
53
- 'z_offset': grid_z * PLOT_DEPTH
54
  })
55
- except (IndexError, ValueError):
56
- st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
57
- continue
 
58
 
59
- parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
60
- return parsed_plots
61
 
62
  def load_plot_objects(filename, x_offset, z_offset):
63
- """Loads objects from a CSV file and applies world offsets."""
64
- file_path = os.path.join(SAVE_DIR, filename)
65
- objects = []
66
  try:
67
- df = pd.read_csv(file_path)
68
- if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
69
- st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
 
70
  return []
71
- df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
72
- for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
 
 
73
  if col not in df.columns:
74
  df[col] = default
75
 
 
76
  for _, row in df.iterrows():
77
- obj_data = row.to_dict()
78
- obj_data['pos_x'] += x_offset
79
- obj_data['pos_z'] += z_offset
80
- objects.append(obj_data)
81
- return objects
82
  except FileNotFoundError:
83
- st.error(f"File not found during object load: {filename}")
84
  return []
85
  except pd.errors.EmptyDataError:
86
  return []
87
  except Exception as e:
88
- st.error(f"Error loading objects from {filename}: {e}")
89
  return []
90
 
91
- def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
92
- """Saves object data list to a CSV file, making positions relative to the plot origin."""
93
- file_path = os.path.join(SAVE_DIR, filename)
94
- relative_objects = []
95
- if not isinstance(objects_data_list, list):
96
- st.error("Invalid data format received for saving (expected a list).")
97
- return False
98
 
99
- for obj in objects_data_list:
100
- pos = obj.get('position', {})
101
- rot = obj.get('rotation', {})
102
- obj_type = obj.get('type', 'Unknown')
103
- obj_id = obj.get('obj_id', str(uuid.uuid4()))
 
104
 
105
- if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
106
- print(f"Skipping malformed object during save prep: {obj}")
 
 
 
 
 
 
107
  continue
108
-
109
- relative_obj = {
110
- 'obj_id': obj_id, 'type': obj_type,
111
- 'pos_x': pos.get('x', 0.0) - plot_x_offset,
112
- 'pos_y': pos.get('y', 0.0),
113
- 'pos_z': pos.get('z', 0.0) - plot_z_offset,
114
- 'rot_x': rot.get('_x', 0.0), 'rot_y': rot.get('_y', 0.0),
115
- 'rot_z': rot.get('_z', 0.0), 'rot_order': rot.get('_order', 'XYZ')
116
- }
117
- relative_objects.append(relative_obj)
118
-
119
  try:
120
- df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
121
- df.to_csv(file_path, index=False)
122
- st.success(f"Saved {len(relative_objects)} objects to {filename}")
123
  return True
124
  except Exception as e:
125
- st.error(f"Failed to save plot data to {filename}: {e}")
126
  return False
127
 
128
- # --- Initialize GameState Singleton ---
129
  @st.cache_resource
130
  def get_game_state():
131
- # This instance is shared across all sessions and reruns.
132
  return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
133
 
134
  game_state = get_game_state()
135
 
136
- # --- Session State Initialization ---
137
- if 'selected_object' not in st.session_state:
138
- st.session_state.selected_object = 'None'
139
- if 'new_plot_name' not in st.session_state:
140
- st.session_state.new_plot_name = ""
141
- if 'js_save_data_result' not in st.session_state:
142
- st.session_state.js_save_data_result = None
143
 
 
144
  plots_metadata = load_plot_metadata()
145
  all_initial_objects = []
146
- for plot in plots_metadata:
147
- all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
148
 
149
- # --- Sidebar ---
150
  with st.sidebar:
151
  st.title("🏗️ World Controls")
152
- st.header("Navigation (Plots)")
153
- st.caption("Click to teleport player to a plot.")
154
- max_cols = 2
155
- cols = st.columns(max_cols)
156
- col_idx = 0
157
- sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
158
- for plot in sorted_plots_for_nav:
159
- button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
160
- if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
161
- target_x = plot['x_offset']
162
- target_z = plot['z_offset']
163
  try:
164
- js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
165
  from streamlit_js_eval import streamlit_js_eval
166
- streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
 
167
  except Exception as e:
168
- st.error(f"Failed to send teleport command: {e}")
169
- col_idx = (col_idx + 1) % max_cols
170
 
171
  st.markdown("---")
172
- st.header("Place Objects")
173
- object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
174
- current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
175
- selected_object_type_widget = st.selectbox("Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
176
- if selected_object_type_widget != st.session_state.selected_object:
177
- st.session_state.selected_object = selected_object_type_widget
178
 
179
  st.markdown("---")
180
- st.header("Save Work")
181
- st.caption("Saves newly placed objects to the current plot. A new plot file is created for new areas.")
182
  if st.button("💾 Save Current Work", key="save_button"):
183
  from streamlit_js_eval import streamlit_js_eval
184
- js_get_data_code = "getSaveDataAndPosition();"
185
- streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
186
  st.rerun()
187
 
188
- # --- Process Save Data from JS ---
189
- save_data_from_js = st.session_state.get("js_save_processor", None)
190
- if save_data_from_js is not None:
191
- st.info("Received save data from client...")
192
- save_processed_successfully = False
193
  try:
194
- payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
195
- if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
196
- player_pos = payload['playerPosition']
197
- objects_to_save = payload['objectsToSave']
198
- if isinstance(objects_to_save, list):
199
- target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
200
- target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
201
- target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
202
- target_plot_x_offset = target_grid_x * PLOT_WIDTH
203
- target_plot_z_offset = target_grid_z * PLOT_DEPTH
204
- st.write(f"Attempting to save plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
205
- is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
206
- save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
207
- if save_ok:
208
- load_plot_metadata.clear() # Clear cache so metadata reloads
209
- try:
210
- from streamlit_js_eval import streamlit_js_eval
211
- streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
212
- except Exception as js_e:
213
- st.warning(f"Could not reset JS state after save: {js_e}")
214
- if is_new_plot_file:
215
- st.success(f"New plot created and saved: {target_filename}")
216
- else:
217
- st.success(f"Updated existing plot: {target_filename}")
218
- # Update shared game state with new objects from this session
219
- game_state.update_state(objects_to_save)
220
- save_processed_successfully = True
221
- else:
222
- st.error(f"Failed to save plot data to file: {target_filename}")
223
- else:
224
- st.error("Invalid 'objectsToSave' format received (expected list).")
225
- else:
226
- st.error("Invalid save payload structure received from client.")
227
- except json.JSONDecodeError:
228
- st.error("Failed to decode save data from client.")
229
  except Exception as e:
230
- st.error(f"Error processing save: {e}")
231
- st.session_state.js_save_processor = None
232
- if save_processed_successfully:
233
- st.rerun()
234
 
235
- # --- Main Area ---
236
- st.header("Infinite Shared 3D World")
237
- st.caption("Move to empty areas to expand the world. Use the sidebar 'Save' to store your work.")
238
 
239
- # Inject state into JS—including the shared GAME_STATE from our GameState singleton.
240
- injected_state = {
241
  "ALL_INITIAL_OBJECTS": all_initial_objects,
242
  "PLOTS_METADATA": plots_metadata,
243
  "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
@@ -246,33 +214,27 @@ injected_state = {
246
  "GAME_STATE": game_state.get_state()
247
  }
248
 
249
- html_file_path = 'index.html'
250
- html_content_with_state = None
251
  try:
252
- with open(html_file_path, 'r', encoding='utf-8') as f:
253
- html_template = f.read()
254
-
255
- js_injection_script = f"""
256
  <script>
257
- window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
258
- window.PLOTS_METADATA = {json.dumps(injected_state["PLOTS_METADATA"])};
259
- window.SELECTED_OBJECT_TYPE = {json.dumps(injected_state["SELECTED_OBJECT_TYPE"])};
260
- window.PLOT_WIDTH = {json.dumps(injected_state["PLOT_WIDTH"])};
261
- window.PLOT_DEPTH = {json.dumps(injected_state["PLOT_DEPTH"])};
262
- window.GAME_STATE = {json.dumps(injected_state["GAME_STATE"])};
263
- console.log("Streamlit State Injected:", {{
264
- selectedObject: window.SELECTED_OBJECT_TYPE,
265
- initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
266
- plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0,
267
- gameStateObjects: window.GAME_STATE ? window.GAME_STATE.length : 0
268
  }});
269
  </script>
270
  """
271
- html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
272
- components.html(html_content_with_state, height=750, scrolling=False)
273
  except FileNotFoundError:
274
- st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
275
- st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
276
  except Exception as e:
277
- st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
278
- st.exception(e)
 
1
+ import streamlit as st # 🌐 Streamlit magic
2
+ import streamlit.components.v1 as components # 🖼️ Embed custom HTML/JS
3
+ import os # 📂 File operations
4
+ import json # 🔄 JSON encoding/decoding
5
+ import pandas as pd # 📊 DataFrame handling
6
+ import uuid # 🆔 Unique IDs
7
+ import math # ➗ Math utils
8
+ import time # ⏳ Time utilities
9
+
10
+ from gamestate import GameState # 💼 Shared game-state singleton
11
+
12
+ # 🚀 Page setup
 
 
13
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
14
 
15
+ # 📏 Constants for world dimensions & CSV schema
16
  SAVE_DIR = "saved_worlds"
17
+ PLOT_WIDTH = 50.0 # ↔️ Plot width in world units
18
+ PLOT_DEPTH = 50.0 # ↕️ Plot depth in world units
19
+ CSV_COLUMNS = [
20
+ 'obj_id', 'type',
21
+ 'pos_x', 'pos_y', 'pos_z',
22
+ 'rot_x', 'rot_y', 'rot_z', 'rot_order'
23
+ ]
24
+
25
+ # 🗂️ Ensure directory for plots exists
26
  os.makedirs(SAVE_DIR, exist_ok=True)
27
 
28
+ @st.cache_data(ttl=3600) # 🕒 Cache for 1h
29
  def load_plot_metadata():
30
+ # 🔍 Scan SAVE_DIR for plot CSVs
 
31
  try:
32
+ plot_files = [f for f in os.listdir(SAVE_DIR)
33
+ if f.endswith(".csv") and f.startswith("plot_X")]
34
  except FileNotFoundError:
35
+ st.error(f"Folder '{SAVE_DIR}' missing! 🚨")
36
  return []
37
  except Exception as e:
38
+ st.error(f"Error reading '{SAVE_DIR}': {e}")
39
  return []
40
 
41
+ parsed = []
42
+ for fn in plot_files:
43
  try:
44
+ parts = fn[:-4].split('_') # strip .csv
45
+ gx = int(parts[1][1:]) # X index
46
+ gz = int(parts[2][1:]) # Z index
47
+ name = " ".join(parts[3:]) if len(parts)>3 else f"Plot({gx},{gz})"
48
+ parsed.append({
49
+ 'id': fn[:-4],
50
+ 'filename': fn,
51
+ 'grid_x': gx,
52
+ 'grid_z': gz,
53
+ 'name': name,
54
+ 'x_offset': gx * PLOT_WIDTH,
55
+ 'z_offset': gz * PLOT_DEPTH
56
  })
57
+ except Exception:
58
+ st.warning(f"Skip invalid file: {fn}")
59
+ parsed.sort(key=lambda p: (p['grid_x'], p['grid_z']))
60
+ return parsed
61
 
 
 
62
 
63
  def load_plot_objects(filename, x_offset, z_offset):
64
+ # 📥 Load objects from a plot CSV and shift by offsets
65
+ path = os.path.join(SAVE_DIR, filename)
 
66
  try:
67
+ df = pd.read_csv(path)
68
+ # 🛡️ Ensure essentials exist
69
+ if not all(c in df.columns for c in ['type','pos_x','pos_y','pos_z']):
70
+ st.warning(f"Missing cols in {filename} 🧐")
71
  return []
72
+ # 🆔 Guarantee obj_id
73
+ df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in df.index]))
74
+ # 🔄 Fill missing rotation
75
+ for col, default in [('rot_x',0.0),('rot_y',0.0),('rot_z',0.0),('rot_order','XYZ')]:
76
  if col not in df.columns:
77
  df[col] = default
78
 
79
+ objs = []
80
  for _, row in df.iterrows():
81
+ o = row.to_dict()
82
+ o['pos_x'] += x_offset
83
+ o['pos_z'] += z_offset
84
+ objs.append(o)
85
+ return objs
86
  except FileNotFoundError:
87
+ st.error(f"CSV not found: {filename}")
88
  return []
89
  except pd.errors.EmptyDataError:
90
  return []
91
  except Exception as e:
92
+ st.error(f"Load error {filename}: {e}")
93
  return []
94
 
 
 
 
 
 
 
 
95
 
96
+ def save_plot_data(filename, objects_list, px, pz):
97
+ # 💾 Save list of new objects relative to plot origin
98
+ path = os.path.join(SAVE_DIR, filename)
99
+ if not isinstance(objects_list, list):
100
+ st.error("👎 Invalid data format for save")
101
+ return False
102
 
103
+ rel = []
104
+ for o in objects_list:
105
+ pos = o.get('position', {})
106
+ rot = o.get('rotation', {})
107
+ typ = o.get('type','Unknown')
108
+ oid = o.get('obj_id', str(uuid.uuid4()))
109
+ # 🛑 Skip bad objects
110
+ if not all(k in pos for k in ['x','y','z']) or typ=='Unknown':
111
  continue
112
+ rel.append({
113
+ 'obj_id': oid, 'type': typ,
114
+ 'pos_x': pos['x']-px, 'pos_y': pos['y'], 'pos_z': pos['z']-pz,
115
+ 'rot_x': rot.get('_x',0.0), 'rot_y': rot.get('_y',0.0),
116
+ 'rot_z': rot.get('_z',0.0), 'rot_order': rot.get('_order','XYZ')
117
+ })
 
 
 
 
 
118
  try:
119
+ pd.DataFrame(rel, columns=CSV_COLUMNS).to_csv(path, index=False)
120
+ st.success(f"🎉 Saved {len(rel)} to {filename}")
 
121
  return True
122
  except Exception as e:
123
+ st.error(f"Save failed: {e}")
124
  return False
125
 
126
+ # 🔒 Singleton for global world state
127
  @st.cache_resource
128
  def get_game_state():
 
129
  return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
130
 
131
  game_state = get_game_state()
132
 
133
+ # 🧠 Session state defaults
134
+ st.session_state.setdefault('selected_object','None')
135
+ st.session_state.setdefault('new_plot_name','')
136
+ st.session_state.setdefault('js_save_data_result',None)
 
 
 
137
 
138
+ # 🔄 Load everything
139
  plots_metadata = load_plot_metadata()
140
  all_initial_objects = []
141
+ for p in plots_metadata:
142
+ all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset'])
143
 
144
+ # 🖥️ Sidebar UI
145
  with st.sidebar:
146
  st.title("🏗️ World Controls")
147
+ st.header("📍 Navigate Plots")
148
+ cols = st.columns(2)
149
+ i = 0
150
+ for p in sorted(plots_metadata, key=lambda x:(x['grid_x'],x['grid_z'])):
151
+ label = f"➡️ {p['name']} ({p['grid_x']},{p['grid_z']})"
152
+ if cols[i].button(label, key=f"nav_{p['id']}"):
 
 
 
 
 
153
  try:
 
154
  from streamlit_js_eval import streamlit_js_eval
155
+ js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});"
156
+ streamlit_js_eval(js_code=js, key=f"tp_{p['id']}")
157
  except Exception as e:
158
+ st.error(f"TP fail: {e}")
159
+ i = (i+1)%2
160
 
161
  st.markdown("---")
162
+ st.header("🌲 Place Objects")
163
+ opts = ["None","Simple House","Tree","Rock","Fence Post"]
164
+ idx = opts.index(st.session_state.selected_object) if st.session_state.selected_object in opts else 0
165
+ sel = st.selectbox("Select:", opts, index=idx, key="selected_object_widget")
166
+ if sel != st.session_state.selected_object:
167
+ st.session_state.selected_object = sel
168
 
169
  st.markdown("---")
170
+ st.header("💾 Save Work")
 
171
  if st.button("💾 Save Current Work", key="save_button"):
172
  from streamlit_js_eval import streamlit_js_eval
173
+ streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor")
 
174
  st.rerun()
175
 
176
+ # 📨 Handle incoming save data
177
+ raw = st.session_state.get("js_save_processor")
178
+ if raw:
179
+ st.info("📬 Got save data!")
180
+ ok=False
181
  try:
182
+ pay = json.loads(raw) if isinstance(raw,str) else raw
183
+ pos, objs = pay.get('playerPosition'), pay.get('objectsToSave')
184
+ if isinstance(objs,list) and pos:
185
+ gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH)
186
+ fn = f"plot_X{gx}_Z{gz}.csv"
187
+ if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH):
188
+ load_plot_metadata.clear()
189
+ try:
190
+ from streamlit_js_eval import streamlit_js_eval
191
+ streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js")
192
+ except:
193
+ pass
194
+ game_state.update_state(objs)
195
+ ok=True
196
+ if not ok:
197
+ st.error("❌ Save error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  except Exception as e:
199
+ st.error(f"Err: {e}")
200
+ st.session_state.js_save_processor=None
201
+ if ok: st.rerun()
 
202
 
203
+ # 🏠 Main view
204
+ st.header("🌍 Infinite Shared 3D World")
205
+ st.caption("➡️ Explore, click to build, 💾 to save!")
206
 
207
+ # 🔌 Inject state into JS
208
+ state = {
209
  "ALL_INITIAL_OBJECTS": all_initial_objects,
210
  "PLOTS_METADATA": plots_metadata,
211
  "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
 
214
  "GAME_STATE": game_state.get_state()
215
  }
216
 
 
 
217
  try:
218
+ with open('index.html','r',encoding='utf-8') as f:
219
+ html = f.read()
220
+ script = f"""
 
221
  <script>
222
+ window.ALL_INITIAL_OBJECTS = {json.dumps(state['ALL_INITIAL_OBJECTS'])};
223
+ window.PLOTS_METADATA = {json.dumps(state['PLOTS_METADATA'])};
224
+ window.SELECTED_OBJECT_TYPE = {json.dumps(state['SELECTED_OBJECT_TYPE'])};
225
+ window.PLOT_WIDTH = {json.dumps(state['PLOT_WIDTH'])};
226
+ window.PLOT_DEPTH = {json.dumps(state['PLOT_DEPTH'])};
227
+ window.GAME_STATE = {json.dumps(state['GAME_STATE'])};
228
+ console.log('👍 State injected!', {{
229
+ objs: window.ALL_INITIAL_OBJECTS.length,
230
+ plots: window.PLOTS_METADATA.length,
231
+ gs: window.GAME_STATE.length
 
232
  }});
233
  </script>
234
  """
235
+ html = html.replace('</head>', script + '\n</head>', 1)
236
+ components.html(html, height=750, scrolling=False)
237
  except FileNotFoundError:
238
+ st.error(" index.html missing!")
 
239
  except Exception as e:
240
+ st.error(f"😱 HTML inject failed: {e}")