Lilli98 commited on
Commit
ff7a527
·
verified ·
1 Parent(s): 947e84f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -294
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.18 - UI Adjustments)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -22,6 +22,7 @@ from huggingface_hub import HfApi
22
  # -----------------------------------------------------------------------------
23
  st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide")
24
 
 
25
  # -----------------------------------------------------------------------------
26
  # 2. Game Parameters & API Configuration
27
  # -----------------------------------------------------------------------------
@@ -54,14 +55,14 @@ except Exception as e:
54
  else:
55
  st.session_state.initialization_error = None
56
 
 
57
  # -----------------------------------------------------------------------------
58
- # 3. Core Game Logic Functions
59
  # -----------------------------------------------------------------------------
60
 
61
  def get_customer_demand(week: int) -> int:
62
  return 4 if week <= 4 else 8
63
 
64
- # =============== CORRECTED Initialization ===============
65
  def init_game_state(llm_personality: str, info_sharing: str):
66
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
67
  human_role = "Distributor" # Role is fixed
@@ -74,7 +75,6 @@ def init_game_state(llm_personality: str, info_sharing: str):
74
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
75
  'decision_step': 'initial_order',
76
  'human_initial_order': None,
77
- # Initialize last week's orders to 0, representing the state before week 1
78
  'last_week_orders': {name: 0 for name in roles}
79
  }
80
 
@@ -83,21 +83,17 @@ def init_game_state(llm_personality: str, info_sharing: str):
83
  downstream = roles[i - 1] if i - 1 >= 0 else None
84
 
85
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
86
- elif name == "Factory": shipping_weeks = 0 # Factory produces, doesn't receive shipments
87
- else: shipping_weeks = SHIPPING_DELAY # Retailer/Wholesaler use general delay
88
 
89
  st.session_state.game_state['echelons'][name] = {
90
  'name': name,
91
- 'inventory': INITIAL_INVENTORY, # End-of-week state
92
- 'backlog': INITIAL_BACKLOG, # End-of-week state
93
- 'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks), # Represents only SHIPPING delay
94
- 'incoming_order': 0, # Order received THIS week
95
- 'order_placed': 0, # Order placed THIS week
96
- 'shipment_sent': 0, # Shipment sent THIS week
97
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
98
  }
99
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
100
- # ==============================================================================
101
 
102
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
103
  # This function remains correct.
@@ -117,78 +113,54 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
117
  match = re.search(r'\d+', raw_text)
118
  if match: return int(match.group(0)), raw_text
119
  st.warning(f"LLM for {echelon_name} did not return a valid number. Defaulting to 4. Raw Response: '{raw_text}'")
120
- return 4, raw_text # Default to 4 if no number found
121
  except Exception as e:
122
- st.error(f"API call failed for {echelons[human_role]}: {e}. Defaulting to 4.") # Fixed typo: echelon_name -> echelons[human_role]
123
  return 4, f"API_ERROR: {e}"
124
 
125
- # =============== MODIFIED FUNCTION (Prompt uses simplified state) ===============
126
  def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
127
- """Generates the prompt for the LLM based on the game scenario.
128
- Uses the state AFTER arrivals and new orders are processed (decision point)."""
129
-
130
  e_state = echelon_state_decision_point
131
-
132
- # Base Info reflects state before shipping
133
  base_info = f"Your Current Status at the **{e_state['name']}** for **Week {week}** (Before Shipping):\n- On-hand inventory: {e_state['inventory']} units.\n- Backlog (total unfilled orders): {e_state['backlog']} units.\n- Incoming order this week (just received): {e_state['incoming_order']} units.\n"
134
-
135
  if e_state['name'] == 'Factory':
136
  task_word = "production quantity"
137
  base_info += f"- Your Production Pipeline (completing next week onwards): {list(st.session_state.game_state['factory_production_pipeline'])}"
138
  else:
139
  task_word = "order quantity"
140
- base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}" # This queue length matches shipping delay
141
-
142
- # --- Perfect Rational ---
143
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
144
  stable_demand = 8
145
- # Lead time calculation remains the same for target inventory
146
  if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
147
  elif e_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
148
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
149
  safety_stock = 4
150
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
151
-
152
- # Calculate Inventory Position based on decision point state + relevant pipelines
153
  if e_state['name'] == 'Factory':
154
- # IP = Inv - Backlog + In Production
155
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
156
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={sum(st.session_state.game_state['factory_production_pipeline'])})"
157
  else:
158
- # IP = Inv - Backlog + In Transit Shipments + Order Placed Last Week (in transit to supplier)
159
  order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
160
-
161
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']) + order_in_transit_to_supplier)
162
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + OrderToSupplier={order_in_transit_to_supplier})"
163
-
164
  optimal_order = max(0, int(target_inventory_level - inventory_position))
165
-
166
  return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
167
-
168
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
169
  safety_stock = 4; anchor_demand = e_state['incoming_order']
170
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
171
-
172
  if e_state['name'] == 'Factory':
173
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
174
  supply_line_desc = "In Production"
175
  else:
176
- # Supply line includes shipments AND the order placed last week
177
  order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
178
  supply_line = sum(e_state['incoming_shipments']) + order_in_transit_to_supplier
179
  supply_line_desc = "Supply Line (In Transit Shipments + Order To Supplier)"
180
-
181
  calculated_order = anchor_demand + inventory_correction - supply_line
182
  rational_local_order = max(0, int(calculated_order))
183
-
184
  return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock (before shipping) is {e_state['inventory'] - e_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed. These should be subtracted from your new decision.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
185
-
186
- # --- Human-like ---
187
  elif llm_personality == 'human_like' and info_sharing == 'full':
188
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
189
  for name, other_e_state in all_echelons_state_decision_point.items():
190
  if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
191
-
192
  return f"""
193
  **You are a supply chain manager ({e_state['name']}) with full system visibility.**
194
  You can see everyone's current inventory and backlog before shipping, and the real customer demand.
@@ -199,7 +171,6 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
199
  You are still human and might get anxious about your own stock levels.
200
  What {task_word} should you decide on this week? Respond with a single integer.
201
  """
202
-
203
  elif llm_personality == 'human_like' and info_sharing == 'local':
204
  return f"""
205
  **You are a reactive supply chain manager for the {e_state['name']}.** You have a limited view and tend to over-correct based on fear.
@@ -209,225 +180,107 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
209
  Your gut instinct is to panic and {task_word.split(' ')[0]} enough to ensure you are never caught with a backlog again, considering your current inventory.
210
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
211
  """
212
- # ==============================================================================
213
 
214
- # =============== CORRECTED step_game FUNCTION (Simplified Order Logic) ===============
215
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
216
  state = st.session_state.game_state
217
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
218
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
219
- echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Defined here
220
  llm_raw_responses = {}
221
-
222
- # Store state at the very beginning of the week (End of last week)
223
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
224
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
225
- arrived_this_week = {name: 0 for name in echelon_order} # Track arrivals for logging
226
-
227
- # --- Game Simulation Steps ---
228
-
229
- # Step 1a & 1b: Shipments/Production Arrive & Update Temp Inventory
230
- inventory_after_arrival = {} # Store intermediate inventory state
231
- # Factory production completion
232
  factory_state = echelons["Factory"]
233
  produced_units = 0
234
  if state['factory_production_pipeline']:
235
- produced_units = state['factory_production_pipeline'].popleft() # Pop completed production
236
  arrived_this_week["Factory"] = produced_units
237
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
238
-
239
- # Downstream shipment arrivals
240
  for name in ["Retailer", "Wholesaler", "Distributor"]:
241
  arrived_shipment = 0
242
  if echelons[name]['incoming_shipments']:
243
- arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop arrived shipment
244
  arrived_this_week[name] = arrived_shipment
245
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
246
-
247
- # Step 2: Orders Arrive from Downstream & Update Temp Backlog
248
- # Orders arrive based on LAST WEEK's placed order (Delay = 1)
249
- total_backlog_before_shipping = {} # Store intermediate backlog state
250
  for name in echelon_order:
251
  incoming_order_for_this_week = 0
252
- if name == "Retailer":
253
- incoming_order_for_this_week = get_customer_demand(week)
254
  else:
255
  downstream_name = echelons[name]['downstream_name']
256
- if downstream_name:
257
- # Use the order placed by the downstream partner LAST week
258
- incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
259
-
260
- echelons[name]['incoming_order'] = incoming_order_for_this_week # Store for logging/UI this week
261
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
262
-
263
- # --- Create State Snapshot for AI/Human Decision Point ---
264
- # This reflects the state AFTER arrivals and new orders, BEFORE shipping
265
  decision_point_states = {}
266
  for name in echelon_order:
267
- # Need a copy, including DEQUEUES for prompt generation
268
  decision_point_states[name] = {
269
- 'name': name,
270
- 'inventory': inventory_after_arrival[name], # Inventory available
271
- 'backlog': total_backlog_before_shipping[name], # Total demand to meet
272
- 'incoming_order': echelons[name]['incoming_order'], # Order received this week
273
- # Pass the current state of queues for prompt generation
274
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
275
  }
276
-
277
- # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
278
- # Agents make decisions based on the decision_point_states
279
  current_week_orders = {}
280
  for name in echelon_order:
281
- e = echelons[name] # Get the main state dict to store results
282
- prompt_state = decision_point_states[name] # Use the snapshot for the prompt
283
-
284
- if name == human_role:
285
- order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
286
  else:
287
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
288
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
289
-
290
- llm_raw_responses[name] = raw_resp
291
- e['order_placed'] = max(0, order_amount) # Store the decision in the main state dict
292
- current_week_orders[name] = e['order_placed'] # Store for NEXT week's Step 2
293
-
294
- # Factory schedules production based on its 'order_placed' decision
295
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
296
-
297
-
298
- # --- Step 3: Fulfill orders (Ship Beer) ---
299
- # Uses inventory_after_arrival and total_backlog_before_shipping
300
  units_shipped = {name: 0 for name in echelon_order}
301
  for name in echelon_order:
302
- e = echelons[name]
303
- demand_to_meet = total_backlog_before_shipping[name]
304
- available_inv = inventory_after_arrival[name]
305
-
306
- e['shipment_sent'] = min(available_inv, demand_to_meet)
307
- units_shipped[name] = e['shipment_sent'] # Store temporarily
308
-
309
- # Update the main state dict's inventory and backlog to reflect END OF WEEK state
310
- e['inventory'] = available_inv - e['shipment_sent']
311
- e['backlog'] = demand_to_meet - e['shipment_sent']
312
-
313
- # Step 3b: Place shipped items into the *end* of the downstream partner's incoming shipment queue
314
- # Factory -> Distributor (uses FACTORY_SHIPPING_DELAY)
315
- if units_shipped["Factory"] > 0:
316
- echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
317
- # Distributor -> Wholesaler (uses SHIPPING_DELAY)
318
- if units_shipped['Distributor'] > 0:
319
- echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
320
- # Wholesaler -> Retailer (uses SHIPPING_DELAY)
321
- if units_shipped['Wholesaler'] > 0:
322
- echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
323
-
324
-
325
- # --- Calculate Costs & Log (End of Week) ---
326
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
327
- # Clean up fields not suitable for direct logging
328
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
329
-
330
  for name in echelon_order:
331
- e = echelons[name]
332
- # Costs are based on the END OF WEEK state
333
- e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
334
- e['total_cost'] += e['weekly_cost']
335
-
336
- # Log end-of-week internal state and decisions/events of the week
337
- log_entry[f'{name}.inventory'] = e['inventory']; log_entry[f'{name}.backlog'] = e['backlog']
338
- log_entry[f'{name}.incoming_order'] = e['incoming_order']; log_entry[f'{name}.order_placed'] = e['order_placed']
339
- log_entry[f'{name}.shipment_sent'] = e['shipment_sent']; log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
340
- log_entry[f'{name}.total_cost'] = e['total_cost']; log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
341
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
342
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
343
-
344
- # Log prediction for next week's arrival/completion (based on queues AFTER this week's processing)
345
- if name != 'Factory':
346
- log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
347
- else:
348
- log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
349
-
350
- # Log human-specific decisions
351
- log_entry[f'{human_role}.initial_order'] = human_initial_order
352
- log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
353
-
354
  state['logs'].append(log_entry)
355
-
356
- # --- Advance Week ---
357
- state['week'] += 1
358
- state['decision_step'] = 'initial_order'
359
- state['last_week_orders'] = current_week_orders # Store current decisions for next week's Step 2
360
-
361
  if state['week'] > WEEKS: state['game_running'] = False
362
- # ==============================================================================
363
-
364
 
365
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
366
  # This function remains correct.
367
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
368
  fig.suptitle(title, fontsize=16)
369
-
370
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
371
  plot_data = []
372
  for _, row in df.iterrows():
373
  for e in echelons:
374
- # Safely access keys, provide default if missing (e.g., first few weeks)
375
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
376
- 'inventory': row.get(f'{e}.inventory', 0), # Use end-of-week inventory for plots
377
- 'order_placed': row.get(f'{e}.order_placed', 0),
378
  'total_cost': row.get(f'{e}.total_cost', 0)})
379
  plot_df = pd.DataFrame(plot_data)
380
-
381
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
382
- inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4)
383
- axes[0].set_title('Inventory Levels (End of Week)')
384
- axes[0].grid(True, linestyle='--')
385
- axes[0].set_ylabel('Stock (Units)')
386
-
387
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
388
- order_pivot.plot(ax=axes[1], style='--')
389
- axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5)
390
- axes[1].set_title('Order Quantities / Production Decisions')
391
- axes[1].grid(True, linestyle='--')
392
- axes[1].legend()
393
- axes[1].set_ylabel('Ordered/Produced (Units)')
394
-
395
- # Ensure total_cost calculation handles potential missing data gracefully
396
- total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()] # Get row with max week for each echelon
397
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
398
- total_costs.plot(kind='bar', ax=axes[2], rot=0)
399
- axes[2].set_title('Total Cumulative Cost')
400
- axes[2].set_ylabel('Cost ($)')
401
-
402
- # Safely access human decision columns
403
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
404
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
405
-
406
- # Add try-except for robust plotting if columns are missing
407
  try:
408
  human_df = df[human_df_cols].copy()
409
- human_df.rename(columns={
410
- f'{human_role}.initial_order': 'Your Initial Order',
411
- f'{human_role}.ai_suggestion': 'AI Suggestion',
412
- f'{human_role}.order_placed': 'Your Final Order'
413
- }, inplace=True)
414
-
415
- if len(human_df.columns) > 1: # Check if there's data to plot
416
- human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-')
417
- axes[3].set_title(f'Analysis of Your ({human_role}) Decisions')
418
- axes[3].set_ylabel('Order Quantity')
419
- axes[3].grid(True, linestyle='--')
420
- axes[3].set_xlabel('Week')
421
  else: raise ValueError("No human decision data columns found.")
422
  except (KeyError, ValueError) as plot_err:
423
- axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - Error Plotting Data')
424
- axes[3].text(0.5, 0.5, f"Error: {plot_err}", ha='center', va='center')
425
- axes[3].grid(True, linestyle='--')
426
- axes[3].set_xlabel('Week')
427
-
428
-
429
- plt.tight_layout(rect=[0, 0, 1, 0.96])
430
- return fig
431
 
432
  def save_logs_and_upload(state: dict):
433
  # This function remains correct.
@@ -436,35 +289,20 @@ def save_logs_and_upload(state: dict):
436
  try:
437
  df = pd.json_normalize(state['logs'])
438
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
439
-
440
- # Convert potential object columns safely before saving
441
- for col in df.select_dtypes(include=['object']).columns:
442
- df[col] = df[col].astype(str)
443
-
444
  df.to_csv(fname, index=False)
445
  st.success(f"Log successfully saved locally: `{fname}`")
446
- with open(fname, "rb") as f:
447
- st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
448
-
449
  if HF_TOKEN and HF_REPO_ID and hf_api:
450
  with st.spinner("Uploading log to Hugging Face Hub..."):
451
  try:
452
- url = hf_api.upload_file(
453
- path_or_fileobj=str(fname),
454
- path_in_repo=f"logs/{fname.name}",
455
- repo_id=HF_REPO_ID,
456
- repo_type="dataset",
457
- token=HF_TOKEN
458
- )
459
  st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
460
- except Exception as e_upload:
461
- st.error(f"Upload to Hugging Face failed: {e_upload}")
462
- except Exception as e_save:
463
- st.error(f"Error processing or saving log data: {e_save}")
464
-
465
 
466
  # -----------------------------------------------------------------------------
467
- # 4. Streamlit UI (Adjusted Dashboard Labels)
468
  # -----------------------------------------------------------------------------
469
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
470
 
@@ -474,89 +312,32 @@ else:
474
  # --- Game Setup & Instructions ---
475
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
476
 
 
477
  st.markdown("---")
478
  st.header("📖 Welcome to the Beer Game!")
479
  st.markdown("This is a simulation of a supply chain. You will play against 3 AI agents. **You do not need any prior knowledge to play.** Please read these instructions carefully.")
480
-
481
  st.subheader("1. Your Goal: Minimize Costs")
482
  st.success("**Your single, most important goal is to: Minimize the total cost for your position in the supply chain.**")
483
  st.markdown("You get costs from two things every week:")
484
- st.markdown(f"""
485
- - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.** (Cost applies to inventory left *after* shipping)
486
- - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.** (Cost applies to orders you couldn't fill *after* shipping)
487
- """)
488
  with st.expander("Click to see a cost calculation example"):
489
- st.markdown(f"""
490
- Imagine at the **end** of Week 5, *after* you shipped beer to the Wholesaler, your final state is:
491
- - Inventory: 10 units
492
- - Backlog: 0 units
493
- Your cost for Week 5 would be calculated *at this point*:
494
- - `(10 units of Inventory * ${HOLDING_COST:,.2f})` = $5.00
495
- - `(0 units of Backlog * ${BACKLOG_COST:,.2f})` = $0.00
496
- - **Total Weekly Cost:** = **$5.00**
497
- This cost is added to your cumulative total.
498
- """)
499
-
500
  st.subheader("2. Your Role: The Distributor")
501
- st.markdown("""
502
- You will always play as the **Distributor**. The other 3 roles are played by AI.
503
- - **Retailer (AI):** Sells to the final customer.
504
- - **Wholesaler (AI):** Sells to the Retailer.
505
- - **Distributor (You):** You sell to the Wholesaler.
506
- - **Factory (AI):** You order from the Factory.
507
- """)
508
  try: st.image(IMAGE_PATH, caption="You are the Distributor. You get orders from the Wholesaler and place orders to the Factory.")
509
  except FileNotFoundError: st.warning("Image file not found.")
510
-
511
  st.subheader("3. The Core Challenge: Delays!")
512
  st.warning(f"It takes **{ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks** for an order you place to arrive in your inventory.")
513
  with st.expander("Click to see a detailed example of the 3-week delay"):
514
- st.markdown(f"""
515
- * **Week 10 (You):** You place an order for **50**.
516
- * **Week 11 (System):** Your order arrives at the Factory (**{ORDER_PASSING_DELAY}w Order Delay**). Factory AI decides to produce 50.
517
- * **Week 12 (System):** Factory finishes producing 50 (**{FACTORY_LEAD_TIME}w Production Delay**) & ships it.
518
- * **Week 13 (System):** The 50 units arrive at your warehouse (**{FACTORY_SHIPPING_DELAY}w Shipping Delay**).
519
- **Conclusion:** Think 3 weeks ahead! Your order in Week 10 arrives at the start of Week 13.
520
- """)
521
-
522
  st.subheader("4. Understanding Inventory & Backlog")
523
- st.markdown("""
524
- Managing your inventory and backlog is key to minimizing costs. Here's how they work:
525
- * **Effective "Orders to Fill":** Each week, the total demand you need to satisfy is your `Incoming Order` for the week PLUS any `Backlog` carried over from the previous week.
526
- * **If you DON'T have enough inventory:**
527
- * You ship **all** the inventory you have (after receiving any arrivals for the week).
528
- * The remaining unfilled "Orders to Fill" becomes your **new Backlog** for next week.
529
- * **Backlog is cumulative!** If you start Week 10 with a backlog of 5, get an order for 8 (total needed = 13), receive 10 units, and ship those 10 units, your new backlog for Week 11 is `13 - 10 = 3`.
530
- * **If you DO have enough inventory:**
531
- * You ship all the "Orders to Fill".
532
- * Your Backlog becomes 0.
533
- * The remaining inventory is carried over to next week (and incurs holding costs).
534
- """)
535
-
536
  st.subheader("5. The Bullwhip Effect (What to Avoid)")
537
- st.markdown("""
538
- The "Bullwhip Effect" happens when small changes in customer demand cause **amplified**, chaotic swings in orders further up the supply chain (like you and the Factory). This often leads to cycles of **panic ordering** (ordering too much when out of stock) followed by **massive inventory pile-ups** (when late orders arrive). This cycle is very expensive. Try to order smoothly.
539
- """)
540
-
541
  st.subheader("6. How Each Week Works & Understanding Your Dashboard")
542
- st.markdown(f"""
543
- Your main job is simple: place one order each week based on the dashboard presented to you.
544
-
545
- **A) At the start of every week, BEFORE your turn:**
546
- * **(Step 1) Shipments Arrive:** Beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives.
547
- * **(Step 2) New Orders Arrive:** You receive a new order from the Wholesaler (their order from *last* week).
548
- * **(Step 3) You Ship Beer (Automatically):** The system ships beer *immediately* based on your inventory *after* Step 1 and the total demand *after* Step 2.
549
-
550
- **B) Your Dashboard (What You See for Your Turn):**
551
- The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
552
- * `Inventory (Opening)`: Your stock **at the beginning of the week**.
553
- * `Backlog (Opening)`: Unfilled orders **carried over from the end of last week**.
554
- * `Incoming Order (This Week)`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2). Use this for your planning.
555
- * `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**. Use this for your planning.
556
- * `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
557
- * `Cost Last Week`: The specific cost incurred just **last week**.
558
- """)
559
 
 
560
  st.markdown("---")
561
  st.header("⚙️ Game Configuration")
562
  c1, c2 = st.columns(2)
@@ -572,7 +353,7 @@ else:
572
  # --- Main Game Interface ---
573
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
574
  state = st.session_state.game_state
575
- week, human_role, echelons, info_sharing = state['week'], state['echelons'], state['human_role'], state['info_sharing']
576
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
577
 
578
 
@@ -580,25 +361,32 @@ else:
580
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
581
  st.markdown("---")
582
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
 
583
  if info_sharing == 'full':
584
  cols = st.columns(4)
585
  for i, name in enumerate(echelon_order): # Use the defined echelon_order
586
  with cols[i]:
587
- e, icon = echelons[name], "👤" if name == human_role else "🤖"
 
 
 
588
  if name == human_role:
589
- # Highlight Distributor for full info mode
590
- st.markdown(f"##### **<span style='color:#FF4B4B;'>{icon} {name} (You)</span>**", unsafe_allow_html=True) # Highlight
591
  else:
592
  st.markdown(f"##### {icon} {name}")
 
 
593
  # Display the END OF LAST WEEK state (which is OPENING state for this week)
594
  st.metric("Inventory (Opening)", e['inventory'])
595
  st.metric("Backlog (Opening)", e['backlog'])
596
 
597
- # 移除成本显示
598
  # if name == human_role:
599
  # st.metric("Total Cost (Cumulative)", f"${e['total_cost']:,.2f}")
600
  # last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
601
  # st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
 
602
 
603
  # Display info about THIS week's events / NEXT week's arrivals
604
  st.write(f"Incoming Order (This Week): **{e['incoming_order']}**") # Order arriving in Step 2
@@ -610,10 +398,13 @@ else:
610
  # Shipment arriving NEXT week
611
  arriving_next = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
612
  st.write(f"Arriving Next Week: **{arriving_next}**")
613
- else:
614
  st.info("In Local Information mode, you can only see your own status dashboard.")
615
  e = echelons[human_role]
 
616
  st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
 
 
617
  col1, col2, col3, col4 = st.columns(4)
618
  # Display OPENING state
619
  col1.metric("Inventory (Opening)", e['inventory'])
@@ -622,10 +413,11 @@ else:
622
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
623
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
624
 
625
- # 移除成本显示
626
  # st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
627
  # last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
628
  # st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
 
629
 
630
 
631
  st.markdown("---")
@@ -638,10 +430,8 @@ else:
638
  arrived = 0
639
  # Peek at what *will* arrive this week (Step 1) based on current queues
640
  if name == "Factory":
641
- # Peek at production pipeline
642
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
643
  else:
644
- # Peek at incoming shipments
645
  if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0]
646
 
647
  # Calculate the state AFTER arrivals and incoming orders for the prompt
@@ -653,7 +443,6 @@ else:
653
  'inventory': inv_after_arrival, # State for decision making
654
  'backlog': backlog_after_new_order, # State for decision making
655
  'incoming_order': e_curr['incoming_order'], # Info for decision making
656
- # Pass queue state as it is at start of week for prompt context
657
  'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
658
  }
659
  human_echelon_state_for_prompt = all_decision_point_states[human_role]
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.19 - Fixed TypeError, Removed Costs, Highlighted Player)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
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
  # -----------------------------------------------------------------------------
 
55
  else:
56
  st.session_state.initialization_error = None
57
 
58
+
59
  # -----------------------------------------------------------------------------
60
+ # 3. Core Game Logic Functions (Using stable v4.17 logic)
61
  # -----------------------------------------------------------------------------
62
 
63
  def get_customer_demand(week: int) -> int:
64
  return 4 if week <= 4 else 8
65
 
 
66
  def init_game_state(llm_personality: str, info_sharing: str):
67
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
68
  human_role = "Distributor" # Role is fixed
 
75
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
76
  'decision_step': 'initial_order',
77
  'human_initial_order': None,
 
78
  'last_week_orders': {name: 0 for name in roles}
79
  }
80
 
 
83
  downstream = roles[i - 1] if i - 1 >= 0 else None
84
 
85
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
86
+ elif name == "Factory": shipping_weeks = 0
87
+ else: shipping_weeks = SHIPPING_DELAY
88
 
89
  st.session_state.game_state['echelons'][name] = {
90
  'name': name,
91
+ 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
92
+ 'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
93
+ 'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
 
 
 
94
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
95
  }
96
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
 
97
 
98
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
99
  # This function remains correct.
 
113
  match = re.search(r'\d+', raw_text)
114
  if match: return int(match.group(0)), raw_text
115
  st.warning(f"LLM for {echelon_name} did not return a valid number. Defaulting to 4. Raw Response: '{raw_text}'")
116
+ return 4, raw_text # Default to 4
117
  except Exception as e:
118
+ st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 4.")
119
  return 4, f"API_ERROR: {e}"
120
 
 
121
  def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
122
+ # This function's logic remains correct (from v4.17).
 
 
123
  e_state = echelon_state_decision_point
 
 
124
  base_info = f"Your Current Status at the **{e_state['name']}** for **Week {week}** (Before Shipping):\n- On-hand inventory: {e_state['inventory']} units.\n- Backlog (total unfilled orders): {e_state['backlog']} units.\n- Incoming order this week (just received): {e_state['incoming_order']} units.\n"
 
125
  if e_state['name'] == 'Factory':
126
  task_word = "production quantity"
127
  base_info += f"- Your Production Pipeline (completing next week onwards): {list(st.session_state.game_state['factory_production_pipeline'])}"
128
  else:
129
  task_word = "order quantity"
130
+ base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}"
 
 
131
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
132
  stable_demand = 8
 
133
  if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
134
  elif e_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
135
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
136
  safety_stock = 4
137
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
 
 
138
  if e_state['name'] == 'Factory':
 
139
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
140
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={sum(st.session_state.game_state['factory_production_pipeline'])})"
141
  else:
 
142
  order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
 
143
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']) + order_in_transit_to_supplier)
144
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + OrderToSupplier={order_in_transit_to_supplier})"
 
145
  optimal_order = max(0, int(target_inventory_level - inventory_position))
 
146
  return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
 
147
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
148
  safety_stock = 4; anchor_demand = e_state['incoming_order']
149
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
 
150
  if e_state['name'] == 'Factory':
151
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
152
  supply_line_desc = "In Production"
153
  else:
 
154
  order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
155
  supply_line = sum(e_state['incoming_shipments']) + order_in_transit_to_supplier
156
  supply_line_desc = "Supply Line (In Transit Shipments + Order To Supplier)"
 
157
  calculated_order = anchor_demand + inventory_correction - supply_line
158
  rational_local_order = max(0, int(calculated_order))
 
159
  return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock (before shipping) is {e_state['inventory'] - e_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed. These should be subtracted from your new decision.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
 
 
160
  elif llm_personality == 'human_like' and info_sharing == 'full':
161
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
162
  for name, other_e_state in all_echelons_state_decision_point.items():
163
  if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
 
164
  return f"""
165
  **You are a supply chain manager ({e_state['name']}) with full system visibility.**
166
  You can see everyone's current inventory and backlog before shipping, and the real customer demand.
 
171
  You are still human and might get anxious about your own stock levels.
172
  What {task_word} should you decide on this week? Respond with a single integer.
173
  """
 
174
  elif llm_personality == 'human_like' and info_sharing == 'local':
175
  return f"""
176
  **You are a reactive supply chain manager for the {e_state['name']}.** You have a limited view and tend to over-correct based on fear.
 
180
  Your gut instinct is to panic and {task_word.split(' ')[0]} enough to ensure you are never caught with a backlog again, considering your current inventory.
181
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
182
  """
 
183
 
 
184
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
185
+ # This function's logic remains correct (from v4.17).
186
  state = st.session_state.game_state
187
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
188
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
189
+ echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
190
  llm_raw_responses = {}
 
 
191
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
192
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
193
+ arrived_this_week = {name: 0 for name in echelon_order}
194
+ inventory_after_arrival = {}
 
 
 
 
 
195
  factory_state = echelons["Factory"]
196
  produced_units = 0
197
  if state['factory_production_pipeline']:
198
+ produced_units = state['factory_production_pipeline'].popleft()
199
  arrived_this_week["Factory"] = produced_units
200
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
 
 
201
  for name in ["Retailer", "Wholesaler", "Distributor"]:
202
  arrived_shipment = 0
203
  if echelons[name]['incoming_shipments']:
204
+ arrived_shipment = echelons[name]['incoming_shipments'].popleft()
205
  arrived_this_week[name] = arrived_shipment
206
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
207
+ total_backlog_before_shipping = {}
 
 
 
208
  for name in echelon_order:
209
  incoming_order_for_this_week = 0
210
+ if name == "Retailer": incoming_order_for_this_week = get_customer_demand(week)
 
211
  else:
212
  downstream_name = echelons[name]['downstream_name']
213
+ if downstream_name: incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
214
+ echelons[name]['incoming_order'] = incoming_order_for_this_week
 
 
 
215
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
 
 
 
216
  decision_point_states = {}
217
  for name in echelon_order:
 
218
  decision_point_states[name] = {
219
+ 'name': name, 'inventory': inventory_after_arrival[name],
220
+ 'backlog': total_backlog_before_shipping[name], 'incoming_order': echelons[name]['incoming_order'],
 
 
 
221
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
222
  }
 
 
 
223
  current_week_orders = {}
224
  for name in echelon_order:
225
+ e = echelons[name]; prompt_state = decision_point_states[name]
226
+ if name == human_role: order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
 
 
 
227
  else:
228
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
229
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
230
+ llm_raw_responses[name] = raw_resp; e['order_placed'] = max(0, order_amount); current_week_orders[name] = e['order_placed']
 
 
 
 
 
231
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
 
 
 
 
232
  units_shipped = {name: 0 for name in echelon_order}
233
  for name in echelon_order:
234
+ e = echelons[name]; demand_to_meet = total_backlog_before_shipping[name]; available_inv = inventory_after_arrival[name]
235
+ e['shipment_sent'] = min(available_inv, demand_to_meet); units_shipped[name] = e['shipment_sent']
236
+ e['inventory'] = available_inv - e['shipment_sent']; e['backlog'] = demand_to_meet - e['shipment_sent']
237
+ if units_shipped["Factory"] > 0: echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
238
+ if units_shipped['Distributor'] > 0: echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
239
+ if units_shipped['Wholesaler'] > 0: echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
 
241
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
 
242
  for name in echelon_order:
243
+ e = echelons[name]; e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
244
+ for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']: log_entry[f'{name}.{key}'] = e[key]
245
+ log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
 
 
246
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
247
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
248
+ if name != 'Factory': log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
249
+ else: log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
250
+ log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
 
 
 
 
 
 
 
 
251
  state['logs'].append(log_entry)
252
+ state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
 
 
 
 
 
253
  if state['week'] > WEEKS: state['game_running'] = False
 
 
254
 
255
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
256
  # This function remains correct.
257
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
258
  fig.suptitle(title, fontsize=16)
 
259
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
260
  plot_data = []
261
  for _, row in df.iterrows():
262
  for e in echelons:
 
263
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
264
+ 'inventory': row.get(f'{e}.inventory', 0), 'order_placed': row.get(f'{e}.order_placed', 0),
 
265
  'total_cost': row.get(f'{e}.total_cost', 0)})
266
  plot_df = pd.DataFrame(plot_data)
 
267
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
268
+ inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels (End of Week)'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
 
 
 
 
269
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
270
+ order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities / Production Decisions'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered/Produced (Units)')
271
+ total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
 
 
 
 
 
 
 
272
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
273
+ total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
 
 
 
 
274
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
275
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
 
 
276
  try:
277
  human_df = df[human_df_cols].copy()
278
+ human_df.rename(columns={ f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'}, inplace=True)
279
+ if len(human_df.columns) > 1: human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-'); axes[3].set_title(f'Analysis of Your ({human_role}) Decisions'); axes[3].set_ylabel('Order Quantity'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
 
 
 
 
 
 
 
 
 
 
280
  else: raise ValueError("No human decision data columns found.")
281
  except (KeyError, ValueError) as plot_err:
282
+ 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')
283
+ plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
 
 
 
 
 
 
284
 
285
  def save_logs_and_upload(state: dict):
286
  # This function remains correct.
 
289
  try:
290
  df = pd.json_normalize(state['logs'])
291
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
292
+ for col in df.select_dtypes(include=['object']).columns: df[col] = df[col].astype(str)
 
 
 
 
293
  df.to_csv(fname, index=False)
294
  st.success(f"Log successfully saved locally: `{fname}`")
295
+ with open(fname, "rb") as f: st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
 
 
296
  if HF_TOKEN and HF_REPO_ID and hf_api:
297
  with st.spinner("Uploading log to Hugging Face Hub..."):
298
  try:
299
+ 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)
 
 
 
 
 
 
300
  st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
301
+ except Exception as e_upload: st.error(f"Upload to Hugging Face failed: {e_upload}")
302
+ except Exception as e_save: st.error(f"Error processing or saving log data: {e_save}")
 
 
 
303
 
304
  # -----------------------------------------------------------------------------
305
+ # 4. Streamlit UI (Removed Costs, Highlighted Player)
306
  # -----------------------------------------------------------------------------
307
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
308
 
 
312
  # --- Game Setup & Instructions ---
313
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
314
 
315
+ # --- Introduction Section (Remains Correct) ---
316
  st.markdown("---")
317
  st.header("📖 Welcome to the Beer Game!")
318
  st.markdown("This is a simulation of a supply chain. You will play against 3 AI agents. **You do not need any prior knowledge to play.** Please read these instructions carefully.")
 
319
  st.subheader("1. Your Goal: Minimize Costs")
320
  st.success("**Your single, most important goal is to: Minimize the total cost for your position in the supply chain.**")
321
  st.markdown("You get costs from two things every week:")
322
+ st.markdown(f"- **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.** (Cost applies to inventory left *after* shipping)\n- **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.** (Cost applies to orders you couldn't fill *after* shipping)")
 
 
 
323
  with st.expander("Click to see a cost calculation example"):
324
+ st.markdown(f"Imagine at the **end** of Week 5, *after* you shipped beer to the Wholesaler, your final state is:\n- Inventory: 10 units\n- Backlog: 0 units\nYour cost for Week 5 would be calculated *at this point*:\n- `(10 units of Inventory * ${HOLDING_COST:,.2f})` = $5.00\n- `(0 units of Backlog * ${BACKLOG_COST:,.2f})` = $0.00\n- **Total Weekly Cost:** = **$5.00**\nThis cost is added to your cumulative total.")
 
 
 
 
 
 
 
 
 
 
325
  st.subheader("2. Your Role: The Distributor")
326
+ st.markdown("You will always play as the **Distributor**. The other 3 roles are played by AI.\n- **Retailer (AI):** Sells to the final customer.\n- **Wholesaler (AI):** Sells to the Retailer.\n- **Distributor (You):** You sell to the Wholesaler.\n- **Factory (AI):** You order from the Factory.")
 
 
 
 
 
 
327
  try: st.image(IMAGE_PATH, caption="You are the Distributor. You get orders from the Wholesaler and place orders to the Factory.")
328
  except FileNotFoundError: st.warning("Image file not found.")
 
329
  st.subheader("3. The Core Challenge: Delays!")
330
  st.warning(f"It takes **{ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks** for an order you place to arrive in your inventory.")
331
  with st.expander("Click to see a detailed example of the 3-week delay"):
332
+ st.markdown(f"* **Week 10 (You):** You place an order for **50**.\n* **Week 11 (System):** Your order arrives at the Factory (**{ORDER_PASSING_DELAY}w Order Delay**). Factory AI decides to produce 50.\n* **Week 12 (System):** Factory finishes producing 50 (**{FACTORY_LEAD_TIME}w Production Delay**) & ships it.\n* **Week 13 (System):** The 50 units arrive at your warehouse (**{FACTORY_SHIPPING_DELAY}w Shipping Delay**).\n**Conclusion:** Think 3 weeks ahead! Your order in Week 10 arrives at the start of Week 13.")
 
 
 
 
 
 
 
333
  st.subheader("4. Understanding Inventory & Backlog")
334
+ st.markdown("Managing your inventory and backlog is key to minimizing costs. Here's how they work:\n* **Effective \"Orders to Fill\":** Each week, the total demand you need to satisfy is your `Incoming Order` for the week PLUS any `Backlog` carried over from the previous week.\n* **If you DON'T have enough inventory:**\n * You ship **all** the inventory you have (after receiving any arrivals for the week).\n * The remaining unfilled \"Orders to Fill\" becomes your **new Backlog** for next week.\n * **Backlog is cumulative!** If you start Week 10 with a backlog of 5, get an order for 8 (total needed = 13), receive 10 units, and ship those 10 units, your new backlog for Week 11 is `13 - 10 = 3`.\n* **If you DO have enough inventory:**\n * You ship all the \"Orders to Fill\".\n * Your Backlog becomes 0.\n * The remaining inventory is carried over to next week (and incurs holding costs).")
 
 
 
 
 
 
 
 
 
 
 
 
335
  st.subheader("5. The Bullwhip Effect (What to Avoid)")
336
+ st.markdown("The \"Bullwhip Effect\" happens when small changes in customer demand cause **amplified**, chaotic swings in orders further up the supply chain (like you and the Factory). This often leads to cycles of **panic ordering** (ordering too much when out of stock) followed by **massive inventory pile-ups** (when late orders arrive). This cycle is very expensive. Try to order smoothly.")
 
 
 
337
  st.subheader("6. How Each Week Works & Understanding Your Dashboard")
338
+ st.markdown(f"Your main job is simple: place one order each week based on the dashboard presented to you.\n\n**A) At the start of every week, BEFORE your turn:**\n* **(Step 1) Shipments Arrive:** Beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives.\n* **(Step 2) New Orders Arrive:** You receive a new order from the Wholesaler (their order from *last* week).\n* **(Step 3) You Ship Beer (Automatically):** The system ships beer *immediately* based on your inventory *after* Step 1 and the total demand *after* Step 2.\n\n**B) Your Dashboard (What You See for Your Turn):**\nThe dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:\n* `Inventory (Opening)`: Your stock **at the beginning of the week**.\n* `Backlog (Opening)`: Unfilled orders **carried over from the end of last week**.\n* `Incoming Order (This Week)`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2). Use this for your planning.\n* `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**. Use this for your planning.\n\n**C) Your Decision (Step 4 - Two Parts):**\nNow, looking at the dashboard (showing the start-of-week state) and considering the incoming order and future arrivals, you decide how much to order:\n* **(Step 4a - Initial Order):** Submit your first estimate.\n* **(Step 4b - Final Order):** See the AI's suggestion, then submit your final decision. This order will arrive in 3 weeks.\n\nSubmitting your final order ends the week. The system then calculates your `Weekly Cost` based on your inventory/backlog *after* Step 3 shipping, logs everything, and advances to the next week.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ # --- Game Configuration ---
341
  st.markdown("---")
342
  st.header("⚙️ Game Configuration")
343
  c1, c2 = st.columns(2)
 
353
  # --- Main Game Interface ---
354
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
355
  state = st.session_state.game_state
356
+ week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
357
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
358
 
359
 
 
361
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
362
  st.markdown("---")
363
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
364
+
365
  if info_sharing == 'full':
366
  cols = st.columns(4)
367
  for i, name in enumerate(echelon_order): # Use the defined echelon_order
368
  with cols[i]:
369
+ e = echelons[name] # Get the echelon state
370
+ icon = "👤" if name == human_role else "🤖"
371
+
372
+ # =============== UI CHANGE: Highlight Player ===============
373
  if name == human_role:
374
+ # Use markdown with HTML/CSS for highlighting
375
+ st.markdown(f"##### **<span style='color:#FF4B4B; border: 1px solid #FF4B4B; padding: 2px 5px; border-radius: 3px;'>{icon} {name} (You)</span>**", unsafe_allow_html=True)
376
  else:
377
  st.markdown(f"##### {icon} {name}")
378
+ # ========================================================
379
+
380
  # Display the END OF LAST WEEK state (which is OPENING state for this week)
381
  st.metric("Inventory (Opening)", e['inventory'])
382
  st.metric("Backlog (Opening)", e['backlog'])
383
 
384
+ # =============== UI CHANGE: Removed Costs ===============
385
  # if name == human_role:
386
  # st.metric("Total Cost (Cumulative)", f"${e['total_cost']:,.2f}")
387
  # last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
388
  # st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
389
+ # =======================================================
390
 
391
  # Display info about THIS week's events / NEXT week's arrivals
392
  st.write(f"Incoming Order (This Week): **{e['incoming_order']}**") # Order arriving in Step 2
 
398
  # Shipment arriving NEXT week
399
  arriving_next = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
400
  st.write(f"Arriving Next Week: **{arriving_next}**")
401
+ else: # Local Info Mode
402
  st.info("In Local Information mode, you can only see your own status dashboard.")
403
  e = echelons[human_role]
404
+ # =============== UI CHANGE: Highlight Player ===============
405
  st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
406
+ # ========================================================
407
+
408
  col1, col2, col3, col4 = st.columns(4)
409
  # Display OPENING state
410
  col1.metric("Inventory (Opening)", e['inventory'])
 
413
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
414
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
415
 
416
+ # =============== UI CHANGE: Removed Costs ===============
417
  # st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
418
  # last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
419
  # st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
420
+ # =======================================================
421
 
422
 
423
  st.markdown("---")
 
430
  arrived = 0
431
  # Peek at what *will* arrive this week (Step 1) based on current queues
432
  if name == "Factory":
 
433
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
434
  else:
 
435
  if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0]
436
 
437
  # Calculate the state AFTER arrivals and incoming orders for the prompt
 
443
  'inventory': inv_after_arrival, # State for decision making
444
  'backlog': backlog_after_new_order, # State for decision making
445
  'incoming_order': e_curr['incoming_order'], # Info for decision making
 
446
  'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
447
  }
448
  human_echelon_state_for_prompt = all_decision_point_states[human_role]