Lilli98 commited on
Commit
87169ce
·
verified ·
1 Parent(s): 5b0c6b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -78
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.20 - NO Introduction)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -15,14 +15,16 @@ import random
15
  import uuid
16
  from pathlib import Path
17
  from datetime import datetime
18
- from huggingface_hub import HfApi
 
 
 
19
 
20
  # -----------------------------------------------------------------------------
21
  # 0. Page Configuration (Must be the first Streamlit command)
22
  # -----------------------------------------------------------------------------
23
  st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide")
24
 
25
-
26
  # -----------------------------------------------------------------------------
27
  # 2. Game Parameters & API Configuration
28
  # -----------------------------------------------------------------------------
@@ -41,7 +43,8 @@ BACKLOG_COST = 1.0
41
  OPENAI_MODEL = "gpt-4o-mini"
42
  LOCAL_LOG_DIR = Path("logs")
43
  LOCAL_LOG_DIR.mkdir(exist_ok=True)
44
- IMAGE_PATH = "beer_game_diagram.png" # Path to your uploaded image
 
45
 
46
  # --- API & Secrets Configuration ---
47
  try:
@@ -55,7 +58,6 @@ except Exception as e:
55
  else:
56
  st.session_state.initialization_error = None
57
 
58
-
59
  # -----------------------------------------------------------------------------
60
  # 3. Core Game Logic Functions
61
  # -----------------------------------------------------------------------------
@@ -63,39 +65,36 @@ else:
63
  def get_customer_demand(week: int) -> int:
64
  return 4 if week <= 4 else 8
65
 
66
- # =============== CORRECTED Initialization (v4.17 logic) ===============
67
- def init_game_state(llm_personality: str, info_sharing: str):
68
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
69
  human_role = "Distributor" # Role is fixed
70
- participant_id = str(uuid.uuid4())[:8]
71
-
72
  st.session_state.game_state = {
73
- 'game_running': True, 'participant_id': participant_id, 'week': 1,
 
 
74
  'human_role': human_role, 'llm_personality': llm_personality,
75
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
76
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
77
  'decision_step': 'initial_order',
78
  'human_initial_order': None,
79
- # Initialize last week's orders to 0
80
  'last_week_orders': {name: 0 for name in roles}
81
  }
82
 
83
  for i, name in enumerate(roles):
84
  upstream = roles[i + 1] if i + 1 < len(roles) else None
85
  downstream = roles[i - 1] if i - 1 >= 0 else None
86
-
87
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
88
  elif name == "Factory": shipping_weeks = 0
89
  else: shipping_weeks = SHIPPING_DELAY
90
-
91
  st.session_state.game_state['echelons'][name] = {
92
- 'name': name,
93
- 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
94
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
95
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
96
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
97
  }
98
- st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
99
  # ==============================================================================
100
 
101
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
@@ -285,27 +284,191 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
285
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - Error Plotting Data'); axes[3].text(0.5, 0.5, f"Error: {plot_err}", ha='center', va='center'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
286
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  def save_logs_and_upload(state: dict):
289
- # This function remains correct.
290
- if not state.get('logs'): return
291
- participant_id = state['participant_id']
 
 
 
 
 
292
  try:
293
- df = pd.json_normalize(state['logs'])
294
- fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
295
- for col in df.select_dtypes(include=['object']).columns: df[col] = df[col].astype(str)
296
- df.to_csv(fname, index=False)
 
 
 
297
  st.success(f"Log successfully saved locally: `{fname}`")
298
  with open(fname, "rb") as f: st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
 
299
  if HF_TOKEN and HF_REPO_ID and hf_api:
300
- with st.spinner("Uploading log to Hugging Face Hub..."):
301
  try:
302
  url = hf_api.upload_file( path_or_fileobj=str(fname), path_in_repo=f"logs/{fname.name}", repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN)
303
- st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
304
  except Exception as e_upload: st.error(f"Upload to Hugging Face failed: {e_upload}")
305
- except Exception as e_save: st.error(f"Error processing or saving log data: {e_save}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  # -----------------------------------------------------------------------------
308
- # 4. Streamlit UI (Adjusted Dashboard Labels & Logic)
309
  # -----------------------------------------------------------------------------
310
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
311
 
@@ -315,25 +478,40 @@ else:
315
  # --- Game Setup & Instructions ---
316
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
317
 
318
- # =============== INTRODUCTION REMOVED ===============
319
- # st.markdown("---")
320
- # st.header("📖 Welcome to the Beer Game!")
321
- # ... (Introduction text) ...
322
- # st.subheader("6. How Each Week Works & Understanding Your Dashboard")
323
- # ... (Explanation text) ...
324
  # =======================================================
325
 
326
- st.markdown("---") # Add a separator
327
- st.header("⚙️ Game Configuration")
328
  c1, c2 = st.columns(2)
329
  with c1:
330
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
331
  with c2:
332
  info_sharing = st.selectbox("Information Sharing Level", ('local', 'full'), format_func=lambda x: x.title(), help="**Local:** You and the AI agents can only see your own inventory and incoming orders. **Full:** Everyone can see the entire supply chain's status and the true end-customer demand.")
333
 
 
334
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
335
- init_game_state(llm_personality, info_sharing)
336
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  # --- Main Game Interface ---
339
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
@@ -343,7 +521,8 @@ else:
343
 
344
 
345
  st.header(f"Week {week} / {WEEKS}")
346
- st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
 
347
  st.markdown("---")
348
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
349
 
@@ -354,23 +533,17 @@ else:
354
  e = echelons[name] # Get the echelon state
355
  icon = "👤" if name == human_role else "🤖"
356
 
357
- # =============== UI CHANGE: Highlight Player ===============
358
  if name == human_role:
359
- # Use markdown with HTML/CSS for highlighting
360
  st.markdown(f"##### **<span style='border: 1px solid #FF4B4B; padding: 2px 5px; border-radius: 3px;'>{icon} {name} (You)</span>**", unsafe_allow_html=True)
361
  else:
362
  st.markdown(f"##### {icon} {name}")
363
- # ========================================================
364
 
365
  st.metric("Inventory (Opening)", e['inventory'])
366
  st.metric("Backlog (Opening)", e['backlog'])
367
 
368
- # =============== UI CHANGE: Removed Costs ===============
369
- # Costs are no longer displayed on the main dashboard
370
- # =======================================================
371
-
372
- # Display info about THIS week's events / NEXT week's arrivals
373
- # Calculate the INCOMING order for THIS week
374
  current_incoming_order = 0
375
  if name == "Retailer":
376
  current_incoming_order = get_customer_demand(week)
@@ -378,40 +551,44 @@ else:
378
  downstream_name = e['downstream_name']
379
  if downstream_name:
380
  current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
381
-
382
- st.write(f"Incoming Order (This Week): **{current_incoming_order}**") # Display calculated order
383
 
384
  if name == "Factory":
385
- # Production completing NEXT week
386
- prod_completing_next = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
387
  st.write(f"Completing Next Week: **{prod_completing_next}**")
388
  else:
389
- # Shipment arriving NEXT week
390
- arriving_next = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
 
391
  st.write(f"Arriving Next Week: **{arriving_next}**")
 
392
  else: # Local Info Mode
393
  st.info("In Local Information mode, you can only see your own status dashboard.")
394
  e = echelons[human_role]
395
  st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
396
- col1, col2, col3, col4 = st.columns(4)
397
-
398
- # Display OPENING state
399
- col1.metric("Inventory (Opening)", e['inventory'])
400
- col2.metric("Backlog (Opening)", e['backlog'])
401
 
402
- # Display info about THIS week's events / NEXT week's arrivals
403
- # Calculate the INCOMING order for THIS week
404
- current_incoming_order = 0
405
- downstream_name = e['downstream_name'] # Wholesaler
406
- if downstream_name:
407
- current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
408
-
409
- col3.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}") # Display calculated order
410
- col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
411
-
412
- # =============== UI CHANGE: Removed Costs ===============
413
- # Costs are no longer displayed on the main dashboard
414
- # =======================================================
 
 
415
 
416
  st.markdown("---")
417
  st.header("Your Decision (Step 4)")
@@ -421,7 +598,6 @@ else:
421
  for name in echelon_order:
422
  e_curr = echelons[name] # This is END OF LAST WEEK state
423
  arrived = 0
424
- # Peek at what *will* arrive this week (Step 1) based on current queues
425
  if name == "Factory":
426
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
427
  else:
@@ -447,9 +623,7 @@ else:
447
  if state['decision_step'] == 'initial_order':
448
  with st.form(key="initial_order_form"):
449
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
450
- # =============== UI CHANGE: Removed Default Value ===============
451
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1) # No 'value'
452
- # ===============================================================
453
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
454
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
455
  state['decision_step'] = 'final_order'
@@ -457,16 +631,13 @@ else:
457
 
458
  elif state['decision_step'] == 'final_order':
459
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
460
- # Use the correctly timed state for the prompt
461
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
462
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
463
 
464
  with st.form(key="final_order_form"):
465
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
466
  st.markdown("Considering the AI's advice, submit your **final** order to end the week. (This order will arrive in 3 weeks).")
467
- # =============== UI CHANGE: Removed Default Value ===============
468
  st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input') # No 'value'
469
- # ===============================================================
470
 
471
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
472
  final_order_value = st.session_state.get('final_order_input', 0)
@@ -531,10 +702,14 @@ else:
531
  state['human_role']
532
  )
533
  st.pyplot(fig)
534
- save_logs_and_upload(state)
535
  except Exception as e:
536
  st.error(f"Error generating final report: {e}")
537
 
 
 
 
 
538
  if st.button("✨ Start a New Game"):
539
  del st.session_state.game_state
540
  st.rerun()
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.22 - Custom ID & Dynamic Leaderboard)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
15
  import uuid
16
  from pathlib import Path
17
  from datetime import datetime
18
+ from huggingface_hub import HfApi, hf_hub_download # 导入新工具
19
+ from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError # 导入错误处理
20
+ import json # 导入 JSON
21
+ import numpy as np # 导入 Numpy 用于 std
22
 
23
  # -----------------------------------------------------------------------------
24
  # 0. Page Configuration (Must be the first Streamlit command)
25
  # -----------------------------------------------------------------------------
26
  st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide")
27
 
 
28
  # -----------------------------------------------------------------------------
29
  # 2. Game Parameters & API Configuration
30
  # -----------------------------------------------------------------------------
 
43
  OPENAI_MODEL = "gpt-4o-mini"
44
  LOCAL_LOG_DIR = Path("logs")
45
  LOCAL_LOG_DIR.mkdir(exist_ok=True)
46
+ IMAGE_PATH = "beer_game_diagram.png"
47
+ LEADERBOARD_FILE = "leaderboard.json" # 新增:排行榜文件名
48
 
49
  # --- API & Secrets Configuration ---
50
  try:
 
58
  else:
59
  st.session_state.initialization_error = None
60
 
 
61
  # -----------------------------------------------------------------------------
62
  # 3. Core Game Logic Functions
63
  # -----------------------------------------------------------------------------
 
65
  def get_customer_demand(week: int) -> int:
66
  return 4 if week <= 4 else 8
67
 
68
+ # =============== MODIFIED Initialization (Accepts participant_id) ===============
69
+ def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
70
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
71
  human_role = "Distributor" # Role is fixed
72
+
 
73
  st.session_state.game_state = {
74
+ 'game_running': True,
75
+ 'participant_id': participant_id, # 使用传入的ID
76
+ 'week': 1,
77
  'human_role': human_role, 'llm_personality': llm_personality,
78
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
79
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
80
  'decision_step': 'initial_order',
81
  'human_initial_order': None,
 
82
  'last_week_orders': {name: 0 for name in roles}
83
  }
84
 
85
  for i, name in enumerate(roles):
86
  upstream = roles[i + 1] if i + 1 < len(roles) else None
87
  downstream = roles[i - 1] if i - 1 >= 0 else None
 
88
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
89
  elif name == "Factory": shipping_weeks = 0
90
  else: shipping_weeks = SHIPPING_DELAY
 
91
  st.session_state.game_state['echelons'][name] = {
92
+ 'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
 
93
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
94
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
95
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
96
  }
97
+ st.info(f"New game started for **{participant_id}**! AI Mode: **{llm_personality} / {info_sharing}**. You are the **{human_role}**.")
98
  # ==============================================================================
99
 
100
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
 
284
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - Error Plotting Data'); axes[3].text(0.5, 0.5, f"Error: {plot_err}", ha='center', va='center'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
285
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
286
 
287
+
288
+ # =============== NEW: Leaderboard Functions ===============
289
+
290
+ @st.cache_data(ttl=60) # 缓存1分钟
291
+ def load_leaderboard_data():
292
+ """从Hugging Face Hub下载并加载排行榜数据。"""
293
+ if not hf_api or not HF_REPO_ID:
294
+ return {} # 没有HF连接,返回空
295
+ try:
296
+ local_path = hf_hub_download(
297
+ repo_id=HF_REPO_ID,
298
+ repo_type="dataset",
299
+ filename=LEADERBOARD_FILE,
300
+ token=HF_TOKEN,
301
+ cache_dir=LOCAL_LOG_DIR / "hf_cache" # 明确指定缓存目录
302
+ )
303
+ with open(local_path, 'r', encoding='utf-8') as f:
304
+ return json.load(f)
305
+ except EntryNotFoundError:
306
+ st.sidebar.info("Leaderboard file not found. A new one will be created.")
307
+ return {}
308
+ except Exception as e:
309
+ st.sidebar.error(f"Could not load leaderboard: {e}")
310
+ return {}
311
+
312
+ def save_leaderboard_data(data):
313
+ """将更新后的排行榜数据保存到Hugging Face Hub。"""
314
+ if not hf_api or not HF_REPO_ID or not HF_TOKEN:
315
+ st.sidebar.warning("Cannot save leaderboard. HF credentials missing.")
316
+ return
317
+ try:
318
+ local_path = LOCAL_LOG_DIR / LEADERBOARD_FILE
319
+ with open(local_path, 'w', encoding='utf-8') as f:
320
+ json.dump(data, f, indent=2, ensure_ascii=False)
321
+
322
+ hf_api.upload_file(
323
+ path_or_fileobj=str(local_path),
324
+ path_in_repo=LEADERBOARD_FILE,
325
+ repo_id=HF_REPO_ID,
326
+ repo_type="dataset",
327
+ token=HF_TOKEN
328
+ )
329
+ st.sidebar.success("Leaderboard updated!")
330
+ st.cache_data.clear() # 清除缓存
331
+ except Exception as e:
332
+ st.sidebar.error(f"Failed to upload leaderboard: {e}")
333
+
334
+ def display_rankings(df, top_n=10):
335
+ """在UI上显示三个排名的辅助函数。"""
336
+ if df.empty:
337
+ st.info("No completed games for this category yet. Be the first!")
338
+ return
339
+
340
+ # 数据清洗:确保成本和标准差是数字
341
+ df['total_cost'] = pd.to_numeric(df['total_cost'], errors='coerce')
342
+ df['order_std_dev'] = pd.to_numeric(df['order_std_dev'], errors='coerce')
343
+ df = df.dropna(subset=['total_cost', 'order_std_dev'])
344
+ if df.empty:
345
+ st.info("No valid completed games for this category yet.")
346
+ return
347
+
348
+ c1, c2, c3 = st.columns(3)
349
+
350
+ with c1:
351
+ st.subheader("🏆 Supply Chain Champions")
352
+ st.caption(f"Top {top_n} - Lowest Total Cost")
353
+ champs_df = df.sort_values('total_cost', ascending=True).head(top_n).copy()
354
+ champs_df['total_cost'] = champs_df['total_cost'].map('${:,.2f}'.format)
355
+ champs_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
356
+ st.dataframe(champs_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
357
+
358
+ with c2:
359
+ st.subheader("👑 Bullwhip Kings")
360
+ st.caption(f"Top {top_n} - Highest Total Cost")
361
+ kings_df = df.sort_values('total_cost', ascending=False).head(top_n).copy()
362
+ kings_df['total_cost'] = kings_df['total_cost'].map('${:,.2f}'.format)
363
+ kings_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
364
+ st.dataframe(kings_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
365
+
366
+ with c3:
367
+ st.subheader("🧘 Mr. Smooth")
368
+ st.caption(f"Top {top_n} - Lowest Order Variation (Std. Dev.)")
369
+ smooth_df = df.sort_values('order_std_dev', ascending=True).head(top_n).copy()
370
+ smooth_df['order_std_dev'] = smooth_df['order_std_dev'].map('{:,.2f}'.format)
371
+ smooth_df.rename(columns={'id': 'Participant', 'order_std_dev': 'Order Std. Dev.'}, inplace=True)
372
+ st.dataframe(smooth_df[['Participant', 'Order Std. Dev.']], use_container_width=True, hide_index=True)
373
+
374
+ def show_leaderboard_ui():
375
+ """加载数据并显示完整的排行榜UI。"""
376
+ st.markdown("---")
377
+ st.header("📊 The Bullwhip Leaderboard")
378
+ st.caption("Leaderboard updates after you finish a game. Cached for 60 seconds.")
379
+
380
+ leaderboard_data = load_leaderboard_data()
381
+ if not leaderboard_data:
382
+ st.info("No leaderboard data yet. Be the first to finish a game!")
383
+ else:
384
+ try:
385
+ df = pd.DataFrame(leaderboard_data.values())
386
+ if 'id' not in df.columns: # 确保id列存在
387
+ df['id'] = list(leaderboard_data.keys())
388
+
389
+ if 'total_cost' not in df.columns or 'order_std_dev' not in df.columns or 'setting' not in df.columns:
390
+ st.error("Leaderboard data is corrupted or incomplete.")
391
+ return
392
+
393
+ groups = sorted(df.setting.unique())
394
+ tabs = st.tabs(["**Overall**"] + groups)
395
+
396
+ with tabs[0]: # Overall
397
+ display_rankings(df)
398
+
399
+ for i, group_name in enumerate(groups):
400
+ with tabs[i+1]:
401
+ df_group = df[df.setting == group_name].copy()
402
+ display_rankings(df_group)
403
+ except Exception as e:
404
+ st.error(f"Error displaying leaderboard: {e}")
405
+ st.dataframe(leaderboard_data) # 原始数据以供调试
406
+ # ==============================================================================
407
+
408
+
409
+ # =============== MODIFIED Function (Updates Leaderboard) ===============
410
  def save_logs_and_upload(state: dict):
411
+ if not state.get('logs'):
412
+ st.warning("No log data to save.")
413
+ return
414
+
415
+ participant_id = state['participant_id'] # 这是您输入的自定义ID
416
+ logs_df = None
417
+
418
+ # 1. Save individual log CSV
419
  try:
420
+ logs_df = pd.json_normalize(state['logs'])
421
+ # 确保文件名安全
422
+ safe_participant_id = re.sub(r'[^a-zA-Z0-9_-]', '_', participant_id)
423
+ fname = LOCAL_LOG_DIR / f"log_{safe_participant_id}_{int(time.time())}.csv"
424
+
425
+ for col in logs_df.select_dtypes(include=['object']).columns: logs_df[col] = logs_df[col].astype(str)
426
+ logs_df.to_csv(fname, index=False)
427
  st.success(f"Log successfully saved locally: `{fname}`")
428
  with open(fname, "rb") as f: st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
429
+
430
  if HF_TOKEN and HF_REPO_ID and hf_api:
431
+ with st.spinner("Uploading log CSV to Hugging Face Hub..."):
432
  try:
433
  url = hf_api.upload_file( path_or_fileobj=str(fname), path_in_repo=f"logs/{fname.name}", repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN)
434
+ st.success(f"✅ Log CSV successfully uploaded! [View File]({url})")
435
  except Exception as e_upload: st.error(f"Upload to Hugging Face failed: {e_upload}")
436
+ except Exception as e_save:
437
+ st.error(f"Error processing or saving log CSV: {e_save}")
438
+ return # Don't proceed to leaderboard if CSV failed
439
+
440
+ # 2. Update and upload leaderboard.json
441
+ if logs_df is None: return # Ensure logs_df was created
442
+
443
+ st.subheader("Updating Leaderboard...")
444
+ try:
445
+ human_role = state['human_role']
446
+
447
+ # Calculate metrics
448
+ total_cost = logs_df[f'{human_role}.total_cost'].iloc[-1]
449
+ order_std_dev = logs_df[f'{human_role}.order_placed'].std()
450
+ setting_name = f"{state['llm_personality']} / {state['info_sharing']}"
451
+
452
+ new_entry = {
453
+ 'id': participant_id, # 使用自定义ID
454
+ 'setting': setting_name,
455
+ 'total_cost': float(total_cost), # 确保是JSON兼容的float
456
+ 'order_std_dev': float(order_std_dev) # 确保是JSON兼容的float
457
+ }
458
+
459
+ # Load, update, save
460
+ leaderboard_data = load_leaderboard_data()
461
+ # 使用ID作为键来允许覆盖/更新 (同名/同组名的人会更新成绩)
462
+ leaderboard_data[participant_id] = new_entry
463
+ save_leaderboard_data(leaderboard_data)
464
+
465
+ except Exception as e_board:
466
+ st.error(f"Error calculating or saving leaderboard score: {e_board}")
467
+ # ==============================================================================
468
+
469
 
470
  # -----------------------------------------------------------------------------
471
+ # 4. Streamlit UI (Adjusted for Custom ID and Leaderboard)
472
  # -----------------------------------------------------------------------------
473
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
474
 
 
478
  # --- Game Setup & Instructions ---
479
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
480
 
481
+ st.markdown("---")
482
+ st.header("⚙️ Game Configuration")
483
+
484
+ # =============== NEW: Participant ID Input ===============
485
+ participant_id = st.text_input("Enter Your Name or Team ID:", key="participant_id_input", placeholder="e.g., Team A")
 
486
  # =======================================================
487
 
 
 
488
  c1, c2 = st.columns(2)
489
  with c1:
490
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
491
  with c2:
492
  info_sharing = st.selectbox("Information Sharing Level", ('local', 'full'), format_func=lambda x: x.title(), help="**Local:** You and the AI agents can only see your own inventory and incoming orders. **Full:** Everyone can see the entire supply chain's status and the true end-customer demand.")
493
 
494
+ # =============== MODIFIED: Start Game Button ===============
495
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
496
+ if not participant_id:
497
+ st.error("Please enter a Name or Team ID to start!")
498
+ elif participant_id in load_leaderboard_data(): # 检查ID是否已存在
499
+ st.warning(f"ID '{participant_id}' already exists! Your score will be overwritten. Click again to confirm.")
500
+ # 这是一个简单的检查,更复杂的可能需要一个确认按钮
501
+ # 为了课堂使用,我们先假设他们会自己协调
502
+ # 或者让他们加个后缀,比如 "Team A - 2"
503
+ # 为简单起见,我们允许覆盖
504
+ init_game_state(llm_personality, info_sharing, participant_id)
505
+ st.rerun()
506
+ else:
507
+ # Pass the participant_id to the init function
508
+ init_game_state(llm_personality, info_sharing, participant_id)
509
+ st.rerun()
510
+ # ===========================================================
511
+
512
+ # =============== NEW: Show Leaderboard on Start Page ===============
513
+ show_leaderboard_ui()
514
+ # =================================================================
515
 
516
  # --- Main Game Interface ---
517
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
 
521
 
522
 
523
  st.header(f"Week {week} / {WEEKS}")
524
+ # 显示自定义ID
525
+ st.subheader(f"Your Role: **{human_role}** ({state['participant_id']}) | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
526
  st.markdown("---")
527
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
528
 
 
533
  e = echelons[name] # Get the echelon state
534
  icon = "👤" if name == human_role else "🤖"
535
 
 
536
  if name == human_role:
 
537
  st.markdown(f"##### **<span style='border: 1px solid #FF4B4B; padding: 2px 5px; border-radius: 3px;'>{icon} {name} (You)</span>**", unsafe_allow_html=True)
538
  else:
539
  st.markdown(f"##### {icon} {name}")
 
540
 
541
  st.metric("Inventory (Opening)", e['inventory'])
542
  st.metric("Backlog (Opening)", e['backlog'])
543
 
544
+ # 移除成本显示
545
+
546
+ # --- NEW: Added Arriving This Week ---
 
 
 
547
  current_incoming_order = 0
548
  if name == "Retailer":
549
  current_incoming_order = get_customer_demand(week)
 
551
  downstream_name = e['downstream_name']
552
  if downstream_name:
553
  current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
554
+ st.write(f"Incoming Order (This Week): **{current_incoming_order}**")
 
555
 
556
  if name == "Factory":
557
+ arriving_this_week = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
558
+ st.write(f"Completing This Week: **{arriving_this_week}**")
559
+ prod_completing_next = list(state['factory_production_pipeline'])[1] if len(state['factory_production_pipeline']) > 1 else 0
560
  st.write(f"Completing Next Week: **{prod_completing_next}**")
561
  else:
562
+ arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
563
+ st.write(f"Arriving This Week: **{arriving_this_week}**")
564
+ arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) > 1 else (list(e['incoming_shipments'])[0] if name in ('Wholesaler', 'Retailer') and len(e['incoming_shipments']) > 0 else 0) # 修正 2-week delay 的显示
565
  st.write(f"Arriving Next Week: **{arriving_next}**")
566
+
567
  else: # Local Info Mode
568
  st.info("In Local Information mode, you can only see your own status dashboard.")
569
  e = echelons[human_role]
570
  st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
571
+
572
+ col1, col2, col3 = st.columns(3)
573
+ with col1:
574
+ st.metric("Inventory (Opening)", e['inventory'])
575
+ st.metric("Backlog (Opening)", e['backlog'])
576
 
577
+ with col2:
578
+ current_incoming_order = 0
579
+ downstream_name = e['downstream_name'] # Wholesaler
580
+ if downstream_name:
581
+ current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
582
+ st.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}")
583
+
584
+ with col3:
585
+ # Arriving THIS week (Step 1)
586
+ arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
587
+ st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
588
+
589
+ # Arriving NEXT week (Peek at the next item in the 1-week delay queue)
590
+ arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) > 1 else 0
591
+ st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
592
 
593
  st.markdown("---")
594
  st.header("Your Decision (Step 4)")
 
598
  for name in echelon_order:
599
  e_curr = echelons[name] # This is END OF LAST WEEK state
600
  arrived = 0
 
601
  if name == "Factory":
602
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
603
  else:
 
623
  if state['decision_step'] == 'initial_order':
624
  with st.form(key="initial_order_form"):
625
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
 
626
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1) # No 'value'
 
627
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
628
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
629
  state['decision_step'] = 'final_order'
 
631
 
632
  elif state['decision_step'] == 'final_order':
633
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
 
634
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
635
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
636
 
637
  with st.form(key="final_order_form"):
638
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
639
  st.markdown("Considering the AI's advice, submit your **final** order to end the week. (This order will arrive in 3 weeks).")
 
640
  st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input') # No 'value'
 
641
 
642
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
643
  final_order_value = st.session_state.get('final_order_input', 0)
 
702
  state['human_role']
703
  )
704
  st.pyplot(fig)
705
+ save_logs_and_upload(state) # This now also updates the leaderboard
706
  except Exception as e:
707
  st.error(f"Error generating final report: {e}")
708
 
709
+ # =============== NEW: Show Leaderboard on End Page ===============
710
+ show_leaderboard_ui()
711
+ # ===============================================================
712
+
713
  if st.button("✨ Start a New Game"):
714
  del st.session_state.game_state
715
  st.rerun()