Lilli98 commited on
Commit
1224338
·
verified ·
1 Parent(s): 448bd42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -141
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.21 - Corrected 3-Week Lead Time Logic&UI)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -15,7 +15,10 @@ 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)
@@ -41,7 +44,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:
@@ -63,38 +67,37 @@ 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, '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! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
98
  # ==============================================================================
99
 
100
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
@@ -183,140 +186,78 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
183
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
184
  """
185
 
186
- # =============== CORRECTED step_game FUNCTION (Fixed Lead Time Logic) ===============
187
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
188
  state = st.session_state.game_state
189
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
190
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
191
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
192
  llm_raw_responses = {}
193
-
194
- # Store state at the very beginning of the week (End of last week)
195
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
196
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
197
  arrived_this_week = {name: 0 for name in echelon_order}
198
-
199
- # --- Game Simulation Steps ---
200
-
201
- # Step 1a: Factory Production completes
202
  factory_state = echelons["Factory"]
203
  produced_units = 0
204
  if state['factory_production_pipeline']:
205
- produced_units = state['factory_production_pipeline'].popleft() # Pop completed production
206
  arrived_this_week["Factory"] = produced_units
207
- inventory_after_arrival = {} # Store intermediate inventory state
208
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
209
-
210
- # Step 1b: Shipments arrive at downstream echelons
211
  for name in ["Retailer", "Wholesaler", "Distributor"]:
212
  arrived_shipment = 0
213
  if echelons[name]['incoming_shipments']:
214
- arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop arrived shipment
215
  arrived_this_week[name] = arrived_shipment
216
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
217
-
218
- # Step 2: Orders Arrive from Downstream
219
- total_backlog_before_shipping = {} # Store intermediate backlog state
220
  for name in echelon_order:
221
  incoming_order_for_this_week = 0
222
- if name == "Retailer":
223
- incoming_order_for_this_week = get_customer_demand(week)
224
  else:
225
  downstream_name = echelons[name]['downstream_name']
226
- if downstream_name:
227
- incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
228
-
229
- echelons[name]['incoming_order'] = incoming_order_for_this_week # Store for logging/UI this week
230
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
231
-
232
- # --- Create State Snapshot for AI/Human Decision Point ---
233
  decision_point_states = {}
234
  for name in echelon_order:
235
  decision_point_states[name] = {
236
- 'name': name,
237
- 'inventory': inventory_after_arrival[name], # Inventory available
238
- 'backlog': total_backlog_before_shipping[name], # Total demand to meet
239
- 'incoming_order': echelons[name]['incoming_order'], # Order received this week
240
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
241
  }
242
-
243
- # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
244
- current_week_orders = {} # Store THIS week's decisions
245
  for name in echelon_order:
246
- e = echelons[name]
247
- prompt_state = decision_point_states[name]
248
-
249
- if name == human_role:
250
- order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
251
  else:
252
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
253
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
254
-
255
- llm_raw_responses[name] = raw_resp
256
- e['order_placed'] = max(0, order_amount)
257
- current_week_orders[name] = e['order_placed'] # Store for NEXT week's Step 2
258
-
259
- # --- Step 3 (Logic Moved): Fulfill orders (Ship Beer) ---
260
- # This MUST happen BEFORE Step 5 (Pipelines Advance)
261
  units_shipped = {name: 0 for name in echelon_order}
262
  for name in echelon_order:
263
- e = echelons[name]
264
- demand_to_meet = total_backlog_before_shipping[name]
265
- available_inv = inventory_after_arrival[name]
266
-
267
- e['shipment_sent'] = min(available_inv, demand_to_meet)
268
- units_shipped[name] = e['shipment_sent'] # Store temporarily
269
-
270
- # Update the main state dict's inventory and backlog to reflect END OF WEEK state
271
- e['inventory'] = available_inv - e['shipment_sent']
272
- e['backlog'] = demand_to_meet - e['shipment_sent']
273
-
274
- # --- Step 5: Advance Pipelines (New Logic) ---
275
- # Factory's decision ('order_placed') from this week enters the production pipeline
276
- # This simulates the FACTORY_LEAD_TIME
277
- state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
278
-
279
- # Items shipped in Step 3 now enter their respective shipping pipelines
280
- # Factory -> Distributor (uses FACTORY_SHIPPING_DELAY)
281
- if units_shipped["Factory"] > 0:
282
- echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
283
- # Distributor -> Wholesaler (uses SHIPPING_DELAY)
284
- if units_shipped['Distributor'] > 0:
285
- echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
286
- # Wholesaler -> Retailer (uses SHIPPING_DELAY)
287
- if units_shipped['Wholesaler'] > 0:
288
- echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
289
-
290
- # --- Calculate Costs & Log (End of Week) ---
291
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
292
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
293
- if 'current_ai_suggestion' in log_entry: del log_entry['current_ai_suggestion']
294
-
295
  for name in echelon_order:
296
- e = echelons[name]
297
- e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
298
- e['total_cost'] += e['weekly_cost']
299
-
300
- log_entry[f'{name}.inventory'] = e['inventory']; log_entry[f'{name}.backlog'] = e['backlog']
301
- log_entry[f'{name}.incoming_order'] = e['incoming_order']; log_entry[f'{name}.order_placed'] = e['order_placed']
302
- log_entry[f'{name}.shipment_sent'] = e['shipment_sent']; log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
303
- log_entry[f'{name}.total_cost'] = e['total_cost']; log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
304
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
305
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
306
-
307
- if name != 'Factory':
308
- log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
309
- else:
310
- log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
311
-
312
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
313
  state['logs'].append(log_entry)
314
-
315
- # --- Advance Week ---
316
  state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
317
  state['current_ai_suggestion'] = None # Clean up
318
  if state['week'] > WEEKS: state['game_running'] = False
319
- # ==============================================================================
320
 
321
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
322
  # This function remains correct.
@@ -353,10 +294,19 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
353
 
354
  @st.cache_data(ttl=60) # 缓存1分钟
355
  def load_leaderboard_data():
356
- if not hf_api or not HF_REPO_ID: return {}
 
 
357
  try:
358
- local_path = hf_hub_download(repo_id=HF_REPO_ID, repo_type="dataset", filename=LEADERBOARD_FILE, token=HF_TOKEN, cache_dir=LOCAL_LOG_DIR / "hf_cache")
359
- with open(local_path, 'r', encoding='utf-8') as f: return json.load(f)
 
 
 
 
 
 
 
360
  except EntryNotFoundError:
361
  st.sidebar.info("Leaderboard file not found. A new one will be created.")
362
  return {}
@@ -365,27 +315,44 @@ def load_leaderboard_data():
365
  return {}
366
 
367
  def save_leaderboard_data(data):
368
- if not hf_api or not HF_REPO_ID or not HF_TOKEN: return
 
 
 
369
  try:
370
  local_path = LOCAL_LOG_DIR / LEADERBOARD_FILE
371
- with open(local_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False)
372
- hf_api.upload_file(path_or_fileobj=str(local_path), path_in_repo=LEADERBOARD_FILE, repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN, commit_message="Update leaderboard")
 
 
 
 
 
 
 
 
 
373
  st.sidebar.success("Leaderboard updated!")
374
- st.cache_data.clear()
375
  except Exception as e:
376
  st.sidebar.error(f"Failed to upload leaderboard: {e}")
377
 
378
  def display_rankings(df, top_n=10):
 
379
  if df.empty:
380
  st.info("No completed games for this category yet. Be the first!")
381
  return
 
 
382
  df['total_cost'] = pd.to_numeric(df['total_cost'], errors='coerce')
383
  df['order_std_dev'] = pd.to_numeric(df['order_std_dev'], errors='coerce')
384
  df = df.dropna(subset=['total_cost', 'order_std_dev'])
385
  if df.empty:
386
  st.info("No valid completed games for this category yet.")
387
  return
 
388
  c1, c2, c3 = st.columns(3)
 
389
  with c1:
390
  st.subheader("🏆 Supply Chain Champions")
391
  st.caption(f"Top {top_n} - Lowest Total Cost")
@@ -393,6 +360,7 @@ def display_rankings(df, top_n=10):
393
  champs_df['total_cost'] = champs_df['total_cost'].map('${:,.2f}'.format)
394
  champs_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
395
  st.dataframe(champs_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
 
396
  with c2:
397
  st.subheader("👑 Bullwhip Kings")
398
  st.caption(f"Top {top_n} - Highest Total Cost")
@@ -400,6 +368,7 @@ def display_rankings(df, top_n=10):
400
  kings_df['total_cost'] = kings_df['total_cost'].map('${:,.2f}'.format)
401
  kings_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
402
  st.dataframe(kings_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
 
403
  with c3:
404
  st.subheader("🧘 Mr. Smooth")
405
  st.caption(f"Top {top_n} - Lowest Order Variation (Std. Dev.)")
@@ -409,46 +378,62 @@ def display_rankings(df, top_n=10):
409
  st.dataframe(smooth_df[['Participant', 'Order Std. Dev.']], use_container_width=True, hide_index=True)
410
 
411
  def show_leaderboard_ui():
 
412
  st.markdown("---")
413
  st.header("📊 The Bullwhip Leaderboard")
414
  st.caption("Leaderboard updates after you finish a game. Cached for 60 seconds.")
 
415
  leaderboard_data = load_leaderboard_data()
416
  if not leaderboard_data:
417
  st.info("No leaderboard data yet. Be the first to finish a game!")
418
  else:
419
  try:
420
  df = pd.DataFrame(leaderboard_data.values())
421
- if 'id' not in df.columns and not df.empty: df['id'] = list(leaderboard_data.keys())
 
 
 
422
  if 'total_cost' not in df.columns or 'order_std_dev' not in df.columns or 'setting' not in df.columns:
423
  st.error("Leaderboard data is corrupted or incomplete.")
424
  return
 
425
  groups = sorted(df.setting.unique())
426
  tabs = st.tabs(["**Overall**"] + groups)
427
- with tabs[0]: display_rankings(df)
 
 
 
428
  for i, group_name in enumerate(groups):
429
  with tabs[i+1]:
430
  df_group = df[df.setting == group_name].copy()
431
  display_rankings(df_group)
432
  except Exception as e:
433
  st.error(f"Error displaying leaderboard: {e}")
434
- st.dataframe(leaderboard_data)
435
  # ==============================================================================
436
 
 
 
437
  def save_logs_and_upload(state: dict):
438
- # This function is now responsible for CSV *and* leaderboard updates.
439
  if not state.get('logs'):
440
  st.warning("No log data to save.")
441
  return
442
- participant_id = state['participant_id']
 
443
  logs_df = None
 
 
444
  try:
445
  logs_df = pd.json_normalize(state['logs'])
 
446
  safe_participant_id = re.sub(r'[^a-zA-Z0-9_-]', '_', participant_id)
447
  fname = LOCAL_LOG_DIR / f"log_{safe_participant_id}_{int(time.time())}.csv"
 
448
  for col in logs_df.select_dtypes(include=['object']).columns: logs_df[col] = logs_df[col].astype(str)
449
  logs_df.to_csv(fname, index=False)
450
  st.success(f"Log successfully saved locally: `{fname}`")
451
  with open(fname, "rb") as f: st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
 
452
  if HF_TOKEN and HF_REPO_ID and hf_api:
453
  with st.spinner("Uploading log CSV to Hugging Face Hub..."):
454
  try:
@@ -457,28 +442,40 @@ def save_logs_and_upload(state: dict):
457
  except Exception as e_upload: st.error(f"Upload to Hugging Face failed: {e_upload}")
458
  except Exception as e_save:
459
  st.error(f"Error processing or saving log CSV: {e_save}")
460
- return
461
- if logs_df is None: return
 
 
 
462
  st.subheader("Updating Leaderboard...")
463
  try:
464
  human_role = state['human_role']
 
 
465
  total_cost = logs_df[f'{human_role}.total_cost'].iloc[-1]
466
  order_std_dev = logs_df[f'{human_role}.order_placed'].std()
467
  setting_name = f"{state['llm_personality']} / {state['info_sharing']}"
 
468
  new_entry = {
469
- 'id': participant_id, 'setting': setting_name,
470
- 'total_cost': float(total_cost),
471
- 'order_std_dev': float(order_std_dev) if pd.notna(order_std_dev) else 0.0
 
472
  }
 
 
473
  leaderboard_data = load_leaderboard_data()
 
474
  leaderboard_data[participant_id] = new_entry
475
  save_leaderboard_data(leaderboard_data)
 
476
  except Exception as e_board:
477
  st.error(f"Error calculating or saving leaderboard score: {e_board}")
478
  # ==============================================================================
479
 
 
480
  # -----------------------------------------------------------------------------
481
- # 4. Streamlit UI (Adjusted for Custom ID, Leaderboard, and UI Fixes)
482
  # -----------------------------------------------------------------------------
483
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
484
 
@@ -509,19 +506,24 @@ else:
509
  # 检查ID是否已存在
510
  existing_data = load_leaderboard_data()
511
  if participant_id in existing_data:
512
- # 如果ID已存在,添加一个session_state标志,要求再次点击
513
- if st.session_state.get('last_id_warning') == participant_id:
514
- # 这是第二次点击确认覆盖
515
- st.session_state.pop('last_id_warning', None)
 
 
 
 
 
 
516
  init_game_state(llm_personality, info_sharing, participant_id)
 
517
  st.rerun()
518
  else:
519
- st.session_state['last_id_warning'] = participant_id
520
- st.warning(f"ID '{participant_id}' already exists! Your score will be overwritten. Click 'Start Game' again to confirm.")
521
  else:
522
- # 新ID,直接开始
523
- if 'last_id_warning' in st.session_state:
524
- del st.session_state['last_id_warning']
525
  init_game_state(llm_personality, info_sharing, participant_id)
526
  st.rerun()
527
  # ===========================================================
@@ -538,15 +540,16 @@ else:
538
 
539
 
540
  st.header(f"Week {week} / {WEEKS}")
 
541
  st.subheader(f"Your Role: **{human_role}** ({state['participant_id']}) | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
542
  st.markdown("---")
543
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
544
 
545
  if info_sharing == 'full':
546
  cols = st.columns(4)
547
- for i, name in enumerate(echelon_order):
548
  with cols[i]:
549
- e = echelons[name]
550
  icon = "👤" if name == human_role else "🤖"
551
 
552
  if name == human_role:
@@ -556,10 +559,10 @@ else:
556
 
557
  st.metric("Inventory (Opening)", e['inventory'])
558
  st.metric("Backlog (Opening)", e['backlog'])
559
-
560
  # 移除成本显示
561
 
562
- # --- UI FIX: Display "Arrived This Week" ---
563
  current_incoming_order = 0
564
  if name == "Retailer":
565
  current_incoming_order = get_customer_demand(week)
@@ -583,18 +586,14 @@ else:
583
  arriving_next = list(e['incoming_shipments'])[1]
584
  # 修正 2-week delay (R/W) 的显示
585
  elif name in ('Wholesaler', 'Retailer') and len(e['incoming_shipments']) > 0 and e['incoming_shipments'].maxlen == 2:
586
- # 如果队列长度为1但最大长度为2,说明下一个是0
587
- if len(e['incoming_shipments']) == 1:
588
- arriving_next = 0
589
- else: # 这种情况不应该发生,但作为保险
590
- arriving_next = list(e['incoming_shipments'])[0]
591
 
592
  st.write(f"Arriving Next Week: **{arriving_next}**")
593
 
594
  else: # Local Info Mode
595
  st.info("In Local Information mode, you can only see your own status dashboard.")
596
  e = echelons[human_role]
597
- st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True)
598
 
599
  col1, col2, col3 = st.columns(3)
600
  with col1:
@@ -614,9 +613,7 @@ else:
614
  st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
615
 
616
  # Arriving NEXT week (Peek at the next item in the 1-week delay queue)
617
- arriving_next = 0
618
- if len(e['incoming_shipments']) > 1: # 仅当队列中有多于1个元素时,才显示 [1]
619
- arriving_next = list(e['incoming_shipments'])[1]
620
  st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
621
 
622
  st.markdown("---")
@@ -684,6 +681,7 @@ else:
684
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
685
 
686
  if 'final_order_input' in st.session_state: del st.session_state.final_order_input
 
687
  st.rerun()
688
 
689
  st.markdown("---")
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.23 - Fixed Final Decision State Bug)
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
21
+ import numpy as np
22
 
23
  # -----------------------------------------------------------------------------
24
  # 0. Page Configuration (Must be the first Streamlit command)
 
44
  OPENAI_MODEL = "gpt-4o-mini"
45
  LOCAL_LOG_DIR = Path("logs")
46
  LOCAL_LOG_DIR.mkdir(exist_ok=True)
47
+ IMAGE_PATH = "beer_game_diagram.png"
48
+ LEADERBOARD_FILE = "leaderboard.json"
49
 
50
  # --- API & Secrets Configuration ---
51
  try:
 
67
  def get_customer_demand(week: int) -> int:
68
  return 4 if week <= 4 else 8
69
 
70
+ # =============== MODIFIED Initialization (Added current_ai_suggestion) ===============
71
+ def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
72
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
73
  human_role = "Distributor" # Role is fixed
74
+
 
75
  st.session_state.game_state = {
76
+ 'game_running': True,
77
+ 'participant_id': participant_id,
78
+ 'week': 1,
79
  'human_role': human_role, 'llm_personality': llm_personality,
80
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
81
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
82
  'decision_step': 'initial_order',
83
  'human_initial_order': None,
84
+ 'current_ai_suggestion': None, # 新增:用于存储AI建议
85
  'last_week_orders': {name: 0 for name in roles}
86
  }
87
 
88
  for i, name in enumerate(roles):
89
  upstream = roles[i + 1] if i + 1 < len(roles) else None
90
  downstream = roles[i - 1] if i - 1 >= 0 else None
 
91
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
92
  elif name == "Factory": shipping_weeks = 0
93
  else: shipping_weeks = SHIPPING_DELAY
 
94
  st.session_state.game_state['echelons'][name] = {
95
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
96
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
97
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
98
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
99
  }
100
+ st.info(f"New game started for **{participant_id}**! AI Mode: **{llm_personality} / {info_sharing}**. You are the **{human_role}**.")
101
  # ==============================================================================
102
 
103
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
 
186
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
187
  """
188
 
 
189
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
190
+ # This function's logic remains correct (from v4.17).
191
  state = st.session_state.game_state
192
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
193
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
194
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
195
  llm_raw_responses = {}
 
 
196
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
197
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
198
  arrived_this_week = {name: 0 for name in echelon_order}
199
+ inventory_after_arrival = {}
 
 
 
200
  factory_state = echelons["Factory"]
201
  produced_units = 0
202
  if state['factory_production_pipeline']:
203
+ produced_units = state['factory_production_pipeline'].popleft()
204
  arrived_this_week["Factory"] = produced_units
 
205
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
 
 
206
  for name in ["Retailer", "Wholesaler", "Distributor"]:
207
  arrived_shipment = 0
208
  if echelons[name]['incoming_shipments']:
209
+ arrived_shipment = echelons[name]['incoming_shipments'].popleft()
210
  arrived_this_week[name] = arrived_shipment
211
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
212
+ total_backlog_before_shipping = {}
 
 
213
  for name in echelon_order:
214
  incoming_order_for_this_week = 0
215
+ if name == "Retailer": incoming_order_for_this_week = get_customer_demand(week)
 
216
  else:
217
  downstream_name = echelons[name]['downstream_name']
218
+ if downstream_name: incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
219
+ echelons[name]['incoming_order'] = incoming_order_for_this_week
 
 
220
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
 
 
221
  decision_point_states = {}
222
  for name in echelon_order:
223
  decision_point_states[name] = {
224
+ 'name': name, 'inventory': inventory_after_arrival[name],
225
+ 'backlog': total_backlog_before_shipping[name], 'incoming_order': echelons[name]['incoming_order'],
 
 
226
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
227
  }
228
+ current_week_orders = {}
 
 
229
  for name in echelon_order:
230
+ e = echelons[name]; prompt_state = decision_point_states[name]
231
+ if name == human_role: order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
 
 
 
232
  else:
233
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
234
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
235
+ llm_raw_responses[name] = raw_resp; e['order_placed'] = max(0, order_amount); current_week_orders[name] = e['order_placed']
236
+ state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
 
 
 
 
 
237
  units_shipped = {name: 0 for name in echelon_order}
238
  for name in echelon_order:
239
+ e = echelons[name]; demand_to_meet = total_backlog_before_shipping[name]; available_inv = inventory_after_arrival[name]
240
+ e['shipment_sent'] = min(available_inv, demand_to_meet); units_shipped[name] = e['shipment_sent']
241
+ e['inventory'] = available_inv - e['shipment_sent']; e['backlog'] = demand_to_meet - e['shipment_sent']
242
+ if units_shipped["Factory"] > 0: echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
243
+ if units_shipped['Distributor'] > 0: echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
244
+ if units_shipped['Wholesaler'] > 0: echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
246
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
247
+ if 'current_ai_suggestion' in log_entry: del log_entry['current_ai_suggestion'] # Clean up
 
248
  for name in echelon_order:
249
+ e = echelons[name]; e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
250
+ for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']: log_entry[f'{name}.{key}'] = e[key]
251
+ log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
252
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
253
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
254
+ if name != 'Factory': log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
255
+ else: log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
 
 
 
256
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
257
  state['logs'].append(log_entry)
 
 
258
  state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
259
  state['current_ai_suggestion'] = None # Clean up
260
  if state['week'] > WEEKS: state['game_running'] = False
 
261
 
262
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
263
  # This function remains correct.
 
294
 
295
  @st.cache_data(ttl=60) # 缓存1分钟
296
  def load_leaderboard_data():
297
+ """从Hugging Face Hub下载并加载排行榜数据。"""
298
+ if not hf_api or not HF_REPO_ID:
299
+ return {} # 没有HF连接,返回空
300
  try:
301
+ local_path = hf_hub_download(
302
+ repo_id=HF_REPO_ID,
303
+ repo_type="dataset",
304
+ filename=LEADERBOARD_FILE,
305
+ token=HF_TOKEN,
306
+ cache_dir=LOCAL_LOG_DIR / "hf_cache" # 明确指定缓存目录
307
+ )
308
+ with open(local_path, 'r', encoding='utf-8') as f:
309
+ return json.load(f)
310
  except EntryNotFoundError:
311
  st.sidebar.info("Leaderboard file not found. A new one will be created.")
312
  return {}
 
315
  return {}
316
 
317
  def save_leaderboard_data(data):
318
+ """将更新后的排行榜数据保存到Hugging Face Hub。"""
319
+ if not hf_api or not HF_REPO_ID or not HF_TOKEN:
320
+ st.sidebar.warning("Cannot save leaderboard. HF credentials missing.")
321
+ return
322
  try:
323
  local_path = LOCAL_LOG_DIR / LEADERBOARD_FILE
324
+ with open(local_path, 'w', encoding='utf-8') as f:
325
+ json.dump(data, f, indent=2, ensure_ascii=False)
326
+
327
+ hf_api.upload_file(
328
+ path_or_fileobj=str(local_path),
329
+ path_in_repo=LEADERBOARD_FILE,
330
+ repo_id=HF_REPO_ID,
331
+ repo_type="dataset",
332
+ token=HF_TOKEN,
333
+ commit_message="Update leaderboard"
334
+ )
335
  st.sidebar.success("Leaderboard updated!")
336
+ st.cache_data.clear() # 清除缓存
337
  except Exception as e:
338
  st.sidebar.error(f"Failed to upload leaderboard: {e}")
339
 
340
  def display_rankings(df, top_n=10):
341
+ """在UI上显示三个排名的辅助函数。"""
342
  if df.empty:
343
  st.info("No completed games for this category yet. Be the first!")
344
  return
345
+
346
+ # 数据清洗:确保成本和标准差是数字
347
  df['total_cost'] = pd.to_numeric(df['total_cost'], errors='coerce')
348
  df['order_std_dev'] = pd.to_numeric(df['order_std_dev'], errors='coerce')
349
  df = df.dropna(subset=['total_cost', 'order_std_dev'])
350
  if df.empty:
351
  st.info("No valid completed games for this category yet.")
352
  return
353
+
354
  c1, c2, c3 = st.columns(3)
355
+
356
  with c1:
357
  st.subheader("🏆 Supply Chain Champions")
358
  st.caption(f"Top {top_n} - Lowest Total Cost")
 
360
  champs_df['total_cost'] = champs_df['total_cost'].map('${:,.2f}'.format)
361
  champs_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
362
  st.dataframe(champs_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
363
+
364
  with c2:
365
  st.subheader("👑 Bullwhip Kings")
366
  st.caption(f"Top {top_n} - Highest Total Cost")
 
368
  kings_df['total_cost'] = kings_df['total_cost'].map('${:,.2f}'.format)
369
  kings_df.rename(columns={'id': 'Participant', 'total_cost': 'Total Cost'}, inplace=True)
370
  st.dataframe(kings_df[['Participant', 'Total Cost']], use_container_width=True, hide_index=True)
371
+
372
  with c3:
373
  st.subheader("🧘 Mr. Smooth")
374
  st.caption(f"Top {top_n} - Lowest Order Variation (Std. Dev.)")
 
378
  st.dataframe(smooth_df[['Participant', 'Order Std. Dev.']], use_container_width=True, hide_index=True)
379
 
380
  def show_leaderboard_ui():
381
+ """加载数据并显示完整的排行榜UI。"""
382
  st.markdown("---")
383
  st.header("📊 The Bullwhip Leaderboard")
384
  st.caption("Leaderboard updates after you finish a game. Cached for 60 seconds.")
385
+
386
  leaderboard_data = load_leaderboard_data()
387
  if not leaderboard_data:
388
  st.info("No leaderboard data yet. Be the first to finish a game!")
389
  else:
390
  try:
391
  df = pd.DataFrame(leaderboard_data.values())
392
+ # 确保id列存在
393
+ if 'id' not in df.columns and not df.empty:
394
+ df['id'] = list(leaderboard_data.keys())
395
+
396
  if 'total_cost' not in df.columns or 'order_std_dev' not in df.columns or 'setting' not in df.columns:
397
  st.error("Leaderboard data is corrupted or incomplete.")
398
  return
399
+
400
  groups = sorted(df.setting.unique())
401
  tabs = st.tabs(["**Overall**"] + groups)
402
+
403
+ with tabs[0]: # Overall
404
+ display_rankings(df)
405
+
406
  for i, group_name in enumerate(groups):
407
  with tabs[i+1]:
408
  df_group = df[df.setting == group_name].copy()
409
  display_rankings(df_group)
410
  except Exception as e:
411
  st.error(f"Error displaying leaderboard: {e}")
412
+ st.dataframe(leaderboard_data) # 原始数据以供调试
413
  # ==============================================================================
414
 
415
+
416
+ # =============== MODIFIED Function (Updates Leaderboard) ===============
417
  def save_logs_and_upload(state: dict):
 
418
  if not state.get('logs'):
419
  st.warning("No log data to save.")
420
  return
421
+
422
+ participant_id = state['participant_id'] # 这是您输入的自定义ID
423
  logs_df = None
424
+
425
+ # 1. Save individual log CSV
426
  try:
427
  logs_df = pd.json_normalize(state['logs'])
428
+ # 确保文件名安全
429
  safe_participant_id = re.sub(r'[^a-zA-Z0-9_-]', '_', participant_id)
430
  fname = LOCAL_LOG_DIR / f"log_{safe_participant_id}_{int(time.time())}.csv"
431
+
432
  for col in logs_df.select_dtypes(include=['object']).columns: logs_df[col] = logs_df[col].astype(str)
433
  logs_df.to_csv(fname, index=False)
434
  st.success(f"Log successfully saved locally: `{fname}`")
435
  with open(fname, "rb") as f: st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
436
+
437
  if HF_TOKEN and HF_REPO_ID and hf_api:
438
  with st.spinner("Uploading log CSV to Hugging Face Hub..."):
439
  try:
 
442
  except Exception as e_upload: st.error(f"Upload to Hugging Face failed: {e_upload}")
443
  except Exception as e_save:
444
  st.error(f"Error processing or saving log CSV: {e_save}")
445
+ return # Don't proceed to leaderboard if CSV failed
446
+
447
+ # 2. Update and upload leaderboard.json
448
+ if logs_df is None: return # Ensure logs_df was created
449
+
450
  st.subheader("Updating Leaderboard...")
451
  try:
452
  human_role = state['human_role']
453
+
454
+ # Calculate metrics
455
  total_cost = logs_df[f'{human_role}.total_cost'].iloc[-1]
456
  order_std_dev = logs_df[f'{human_role}.order_placed'].std()
457
  setting_name = f"{state['llm_personality']} / {state['info_sharing']}"
458
+
459
  new_entry = {
460
+ 'id': participant_id, # 使用自定义ID
461
+ 'setting': setting_name,
462
+ 'total_cost': float(total_cost), # 确保是JSON兼容的float
463
+ 'order_std_dev': float(order_std_dev) if pd.notna(order_std_dev) else 0.0 # 处理NaN
464
  }
465
+
466
+ # Load, update, save
467
  leaderboard_data = load_leaderboard_data()
468
+ # 使用ID作为键来允许覆盖/更新 (同名/同组名的人会更新成绩)
469
  leaderboard_data[participant_id] = new_entry
470
  save_leaderboard_data(leaderboard_data)
471
+
472
  except Exception as e_board:
473
  st.error(f"Error calculating or saving leaderboard score: {e_board}")
474
  # ==============================================================================
475
 
476
+
477
  # -----------------------------------------------------------------------------
478
+ # 4. Streamlit UI (Adjusted for Custom ID and Leaderboard)
479
  # -----------------------------------------------------------------------------
480
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
481
 
 
506
  # 检查ID是否已存在
507
  existing_data = load_leaderboard_data()
508
  if participant_id in existing_data:
509
+ st.warning(f"ID '{participant_id}' already exists! Your score will be overwritten. Click 'Start Game' again to confirm.")
510
+ # 简单地要求再次点击,或者可以添加一个复选框
511
+ # 为了课堂使用我们先假设他们会自己协调
512
+ # 或者让他们加个后缀,比如 "Team A - 2"
513
+ # 我们允许覆盖,但给出警告
514
+ if "confirm_overwrite" not in st.session_state:
515
+ st.session_state.confirm_overwrite = False
516
+
517
+ if st.session_state.get(f"last_clicked_id") == participant_id:
518
+ # 如果他们再次点击(ID没变)
519
  init_game_state(llm_personality, info_sharing, participant_id)
520
+ st.session_state.pop("last_clicked_id", None)
521
  st.rerun()
522
  else:
523
+ st.session_state[f"last_clicked_id"] = participant_id
524
+
525
  else:
526
+ # Pass the participant_id to the init function
 
 
527
  init_game_state(llm_personality, info_sharing, participant_id)
528
  st.rerun()
529
  # ===========================================================
 
540
 
541
 
542
  st.header(f"Week {week} / {WEEKS}")
543
+ # 显示自定义ID
544
  st.subheader(f"Your Role: **{human_role}** ({state['participant_id']}) | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
545
  st.markdown("---")
546
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
547
 
548
  if info_sharing == 'full':
549
  cols = st.columns(4)
550
+ for i, name in enumerate(echelon_order): # Use the defined echelon_order
551
  with cols[i]:
552
+ e = echelons[name] # Get the echelon state
553
  icon = "👤" if name == human_role else "🤖"
554
 
555
  if name == human_role:
 
559
 
560
  st.metric("Inventory (Opening)", e['inventory'])
561
  st.metric("Backlog (Opening)", e['backlog'])
562
+
563
  # 移除成本显示
564
 
565
+ # --- NEW: Added Arriving This Week ---
566
  current_incoming_order = 0
567
  if name == "Retailer":
568
  current_incoming_order = get_customer_demand(week)
 
586
  arriving_next = list(e['incoming_shipments'])[1]
587
  # 修正 2-week delay (R/W) 的显示
588
  elif name in ('Wholesaler', 'Retailer') and len(e['incoming_shipments']) > 0 and e['incoming_shipments'].maxlen == 2:
589
+ arriving_next = 0 # Peek at index 1 is correct, if it's not there, it's 0
 
 
 
 
590
 
591
  st.write(f"Arriving Next Week: **{arriving_next}**")
592
 
593
  else: # Local Info Mode
594
  st.info("In Local Information mode, you can only see your own status dashboard.")
595
  e = echelons[human_role]
596
+ st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
597
 
598
  col1, col2, col3 = st.columns(3)
599
  with col1:
 
613
  st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
614
 
615
  # Arriving NEXT week (Peek at the next item in the 1-week delay queue)
616
+ arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) > 1 else 0
 
 
617
  st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
618
 
619
  st.markdown("---")
 
681
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
682
 
683
  if 'final_order_input' in st.session_state: del st.session_state.final_order_input
684
+ if 'current_ai_suggestion' in state: del state['current_ai_suggestion'] # Clean up
685
  st.rerun()
686
 
687
  st.markdown("---")