Lilli98 commited on
Commit
f8c9614
·
verified ·
1 Parent(s): 9f02592

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -85
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.10 - Added Arrival Log Column - Complete Code)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -30,7 +30,7 @@ st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide"
30
  WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
33
- ORDER_PASSING_DELAY = 1
34
  SHIPPING_DELAY = 2 # General shipping delay
35
  FACTORY_LEAD_TIME = 1
36
  FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
@@ -75,20 +75,20 @@ def init_game_state(llm_personality: str, info_sharing: str):
75
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
76
  'decision_step': 'initial_order',
77
  'human_initial_order': None,
 
78
  }
79
 
80
  for i, name in enumerate(roles):
81
  upstream = roles[i + 1] if i + 1 < len(roles) else None
82
  downstream = roles[i - 1] if i - 1 >= 0 else None
83
 
84
- # Determine shipping delay for incoming goods for this role
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, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
91
- 'order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
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,
@@ -96,6 +96,7 @@ def init_game_state(llm_personality: str, info_sharing: str):
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
  if not client: return 8, "NO_API_KEY_DEFAULT"
100
  with st.spinner(f"Getting AI decision for {echelon_name}..."):
101
  try:
@@ -125,7 +126,10 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
125
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
126
  else:
127
  task_word = "order quantity"
128
- base_info += f"- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}\n- Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}"
 
 
 
129
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
130
  stable_demand = 8
131
  if echelon_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
@@ -137,10 +141,12 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
137
  inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
138
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
139
  else:
140
- inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Transit: {sum(echelon_state['incoming_shipments'])} + In_Pipeline: {sum(echelon_state['order_pipeline'])})"
141
- inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline']))
 
142
  optimal_order = max(0, int(target_inventory_level - inventory_position))
143
  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."
 
144
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
145
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
146
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
@@ -148,11 +154,13 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
148
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
149
  supply_line_desc = "In Production"
150
  else:
151
- supply_line = sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline'])
152
- supply_line_desc = "Supply Line (In Transit + In Pipeline)"
 
153
  calculated_order = anchor_demand + inventory_correction - supply_line
154
  rational_local_order = max(0, int(calculated_order))
155
  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 is {echelon_state['inventory'] - echelon_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 order.\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."
 
156
  elif llm_personality == 'human_like' and info_sharing == 'full':
157
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
158
  for name, e_state in all_echelons_state.items():
@@ -167,6 +175,7 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
167
  You are still human and might get anxious about your own stock levels.
168
  What {task_word} should you decide on this week? Respond with a single integer.
169
  """
 
170
  elif llm_personality == 'human_like' and info_sharing == 'local':
171
  return f"""
172
  **You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.
@@ -184,14 +193,14 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
184
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
185
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
186
  llm_raw_responses = {}
187
-
188
  # Store pre-step state for logging
189
  pre_step_inventory = echelons[human_role]['inventory']
190
  pre_step_backlog = echelons[human_role]['backlog']
191
- # Store arriving shipment amount *before* it's added to inventory
192
  arriving_shipment_this_week = list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0
193
 
194
- # --- Game Simulation Steps ---
 
195
  # Step 1a: Factory Production completes and adds to Factory inventory
196
  factory_state = echelons["Factory"]
197
  produced_units = 0
@@ -206,16 +215,19 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
206
  arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop shipment for current week
207
  echelons[name]['inventory'] += arrived_shipment
208
 
209
- # Step 2: Orders arrive from downstream partners
210
  for name in echelon_order:
211
  if name == "Retailer":
212
  echelons[name]['incoming_order'] = get_customer_demand(week)
213
  else:
214
- downstream = echelons[name]['downstream_name']
215
- order_from_downstream = 0
216
- if downstream and echelons[downstream]['order_pipeline']:
217
- order_from_downstream = echelons[downstream]['order_pipeline'].popleft()
218
- echelons[name]['incoming_order'] = order_from_downstream
 
 
 
219
 
220
  # Step 3: Fulfill orders (Ship Beer)
221
  for name in echelon_order:
@@ -230,9 +242,11 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
230
  sender = echelons[sender_name]
231
  receiver_name = sender['downstream_name']
232
  if receiver_name:
 
233
  echelons[receiver_name]['incoming_shipments'].append(sender['shipment_sent'])
234
 
235
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
 
236
  for name in echelon_order:
237
  e = echelons[name]
238
  if name == human_role:
@@ -242,95 +256,75 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
242
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
243
  llm_raw_responses[name] = raw_resp
244
  e['order_placed'] = max(0, order_amount) # This is the order/production decision for the week
245
-
246
- # Place the order into the *end* of the current player's own order pipeline (for upstream player to receive later)
247
- if name != "Factory":
248
- e['order_pipeline'].append(e['order_placed'])
249
 
250
  # Factory schedules production based on its 'order_placed' decision
251
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
252
 
253
  # --- Logging (End of Week) ---
254
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
255
- # Remove complex objects before logging
256
- del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
257
 
258
  for name in echelon_order:
259
  e = echelons[name]
260
- # Calculate costs based on inventory/backlog AFTER shipping step
261
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
262
-
263
- # Log core metrics (state at the END of the week)
264
  for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
265
  log_entry[f'{name}.{key}'] = e[key]
266
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
267
-
268
- # *** Explicitly log the value for 'Arriving Next Week' ***
269
- # This reads the state of the queues *after* all steps for the week are done.
270
  if name != 'Factory':
271
- # The next item in incoming_shipments is what arrives at the start of next week
272
  log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
273
  else:
274
- # For factory, log what completes production next week
275
  log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
276
 
277
- # Log human-specific metrics recorded DURING the week
278
  log_entry[f'{human_role}.opening_inventory'] = pre_step_inventory
279
  log_entry[f'{human_role}.opening_backlog'] = pre_step_backlog
280
- log_entry[f'{human_role}.arrived_this_week'] = arriving_shipment_this_week # Log the shipment that arrived in Step 1
281
- log_entry[f'{human_role}.initial_order'] = human_initial_order # Log Step 4a decision
282
- log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion # Log Step 4b suggestion
283
 
284
  state['logs'].append(log_entry)
285
 
286
  # --- Advance Week ---
287
  state['week'] += 1
288
  state['decision_step'] = 'initial_order'
 
289
  if state['week'] > WEEKS: state['game_running'] = False
290
  # ==============================================================================
291
 
292
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
 
293
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
294
  fig.suptitle(title, fontsize=16)
295
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
296
-
297
  plot_data = []
298
  for _, row in df.iterrows():
299
  for e in echelons:
300
- # Safely access keys, provide default if missing (e.g., first few weeks)
301
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
302
  'inventory': row.get(f'{e}.inventory', 0),
303
  'order_placed': row.get(f'{e}.order_placed', 0),
304
  'total_cost': row.get(f'{e}.total_cost', 0)})
305
  plot_df = pd.DataFrame(plot_data)
306
-
307
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
308
  inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
309
-
310
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
311
  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 (The Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered (Units)')
312
-
313
- # Ensure total_cost calculation handles potential missing data gracefully
314
- total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()] # Get row with max week for each echelon
315
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
316
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
317
-
318
- # Safely access human decision columns
319
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
320
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
321
  human_df = df[human_df_cols].copy()
322
  human_df.rename(columns={
323
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
324
  }, inplace=True)
325
- if len(human_df.columns) > 1: # Check if there's data to plot
326
  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')
327
  else:
328
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - No Data'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
329
-
330
-
331
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
332
 
333
  def save_logs_and_upload(state: dict):
 
334
  if not state.get('logs'): return
335
  participant_id = state['participant_id']
336
  df = pd.json_normalize(state['logs'])
@@ -348,7 +342,7 @@ def save_logs_and_upload(state: dict):
348
  st.error(f"Upload to Hugging Face failed: {e}")
349
 
350
  # -----------------------------------------------------------------------------
351
- # 4. Streamlit UI
352
  # -----------------------------------------------------------------------------
353
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
354
 
@@ -369,7 +363,7 @@ else:
369
  - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.
370
  - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.
371
  """)
372
-
373
  st.subheader("2. Your Role: The Distributor")
374
  st.markdown("""
375
  You will always play as the **Distributor**. The other 3 roles are played by AI.
@@ -392,13 +386,13 @@ else:
392
  * **Week 13 (System):** The 50 units *arrive* at your warehouse. (This is the **{FACTORY_SHIPPING_DELAY} week Shipping Delay**). You can now use this inventory.
393
  **Conclusion:** You must always think 3 weeks ahead. The order you place in Week 10 will not help you until Week 13.
394
  """)
395
-
396
  st.subheader("4. How Each Week Works (Your Task)")
397
  st.markdown(f"""
398
  Your main job is simple: place one order each week.
399
  **A) At the start of every week, the system automatically does 3 things:**
400
  * **(Step 1) Your Shipments Arrive:** The beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives and is added to your `Current Inventory`.
401
- * **(Step 2) New Orders Arrive:** You receive a new `Incoming Order` from the Wholesaler.
402
  * **(Step 3) You Ship Beer:** The system automatically ships as much beer as possible from your inventory to fulfill the Wholesaler's order (plus any old `Backlog`).
403
  **B) After this, you will see your new dashboard and must make your 2-part decision:**
404
  * **Step 4a (Initial Order):** Based on your new status, submit your **initial order** to the Factory.
@@ -459,7 +453,9 @@ else:
459
  if state['decision_step'] == 'initial_order':
460
  with st.form(key="initial_order_form"):
461
  st.markdown("#### **Step 4a:** Based on the information available, submit your **initial** order.")
462
- initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=human_echelon_state['incoming_order'])
 
 
463
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
464
  state['human_initial_order'] = int(initial_order)
465
  state['decision_step'] = 'final_order'
@@ -484,57 +480,36 @@ else:
484
  st.rerun()
485
 
486
  st.markdown("---")
487
- # =============== CORRECTED LOG DISPLAY BLOCK ===============
488
  with st.expander("📖 Your Weekly Decision Log", expanded=False):
489
- if not state.get('logs'): # Use .get() for safety
490
  st.write("Your weekly history will be displayed here after you complete the first week.")
491
  else:
492
- try: # Add error handling for data processing
493
  history_df = pd.json_normalize(state['logs'])
494
-
495
- # Define all desired columns and their display names
496
  human_cols = {
497
- 'week': 'Week',
498
- f'{human_role}.opening_inventory': 'Opening Inv.',
499
- f'{human_role}.opening_backlog': 'Opening Backlog',
500
- f'{human_role}.arrived_this_week': 'Arrived This Week', # Shipment that arrived at Step 1
501
- f'{human_role}.incoming_order': 'Incoming Order', # Order received at Step 2
502
- f'{human_role}.initial_order': 'Your Initial Order', # Step 4a
503
- f'{human_role}.ai_suggestion': 'AI Suggestion', # Step 4b
504
- f'{human_role}.order_placed': 'Your Final Order', # Step 4b (Order for Week+3)
505
- f'{human_role}.arriving_next_week': 'Arriving Next Week', # What will arrive in Step 1 of NEXT week
506
- f'{human_role}.weekly_cost': 'Weekly Cost', # Calculated at end of week
507
  }
508
-
509
- # Define the desired order of columns
510
  ordered_display_cols_keys = [
511
  'week', f'{human_role}.opening_inventory', f'{human_role}.opening_backlog',
512
  f'{human_role}.arrived_this_week', f'{human_role}.incoming_order',
513
  f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed',
514
  f'{human_role}.arriving_next_week', f'{human_role}.weekly_cost'
515
  ]
516
-
517
- # Filter the desired columns based on what actually exists in the log data
518
  final_cols_to_display = [col for col in ordered_display_cols_keys if col in history_df.columns]
519
 
520
  if not final_cols_to_display:
521
  st.write("No data columns available to display.")
522
  else:
523
- # Select and rename the columns that exist
524
  display_df = history_df[final_cols_to_display].rename(columns=human_cols)
525
-
526
- # Format the cost column
527
  if 'Weekly Cost' in display_df.columns:
528
- # Apply formatting safely, handling potential non-numeric data
529
  display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}" if isinstance(x, (int, float)) else "")
530
-
531
- # Display the dataframe
532
  st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
533
-
534
  except Exception as e:
535
  st.error(f"Error displaying weekly log: {e}")
536
- st.write("Log data structure might be inconsistent.")
537
- # =======================================================
538
 
539
  try: st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
540
  except FileNotFoundError: st.sidebar.warning("Image file not found.")
@@ -550,7 +525,7 @@ else:
550
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
551
  st.header("🎉 Game Over!")
552
  state = st.session_state.game_state
553
- try: # Add error handling for final plot/save
554
  logs_df = pd.json_normalize(state['logs'])
555
  fig = plot_results(
556
  logs_df,
@@ -561,7 +536,6 @@ else:
561
  save_logs_and_upload(state)
562
  except Exception as e:
563
  st.error(f"Error generating final report: {e}")
564
- st.write("Log data might be corrupted or incomplete.")
565
 
566
  if st.button("✨ Start a New Game"):
567
  del st.session_state.game_state
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.11 - Corrected Order Passing Logic)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
30
  WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
33
+ ORDER_PASSING_DELAY = 1 # *** CRITICAL: This is now handled directly in step_game ***
34
  SHIPPING_DELAY = 2 # General shipping delay
35
  FACTORY_LEAD_TIME = 1
36
  FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
 
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} # Store previous week's final orders
79
  }
80
 
81
  for i, name in enumerate(roles):
82
  upstream = roles[i + 1] if i + 1 < len(roles) else None
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, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
91
+ # 'order_pipeline' is no longer needed for passing delay = 1
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,
 
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.
100
  if not client: return 8, "NO_API_KEY_DEFAULT"
101
  with st.spinner(f"Getting AI decision for {echelon_name}..."):
102
  try:
 
126
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
127
  else:
128
  task_word = "order quantity"
129
+ # Base info now correctly reflects only incoming shipments for non-factory roles
130
+ base_info += f"- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}"
131
+ # We don't need 'Orders in pipeline' in the prompt anymore as delay=1 is handled directly
132
+
133
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
134
  stable_demand = 8
135
  if echelon_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
 
141
  inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
142
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
143
  else:
144
+ # Simplified inventory position for delay=1
145
+ inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Transit: {sum(echelon_state['incoming_shipments'])})"
146
+ inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']))
147
  optimal_order = max(0, int(target_inventory_level - inventory_position))
148
  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."
149
+
150
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
151
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
152
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
 
154
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
155
  supply_line_desc = "In Production"
156
  else:
157
+ # Simplified supply line for delay=1
158
+ supply_line = sum(echelon_state['incoming_shipments'])
159
+ supply_line_desc = "In Transit Shipments"
160
  calculated_order = anchor_demand + inventory_correction - supply_line
161
  rational_local_order = max(0, int(calculated_order))
162
  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 is {echelon_state['inventory'] - echelon_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 order.\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."
163
+
164
  elif llm_personality == 'human_like' and info_sharing == 'full':
165
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
166
  for name, e_state in all_echelons_state.items():
 
175
  You are still human and might get anxious about your own stock levels.
176
  What {task_word} should you decide on this week? Respond with a single integer.
177
  """
178
+
179
  elif llm_personality == 'human_like' and info_sharing == 'local':
180
  return f"""
181
  **You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.
 
193
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
194
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
195
  llm_raw_responses = {}
196
+
197
  # Store pre-step state for logging
198
  pre_step_inventory = echelons[human_role]['inventory']
199
  pre_step_backlog = echelons[human_role]['backlog']
 
200
  arriving_shipment_this_week = list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0
201
 
202
+ # --- Game Simulation Steps (Corrected Logic for Order Passing Delay = 1) ---
203
+
204
  # Step 1a: Factory Production completes and adds to Factory inventory
205
  factory_state = echelons["Factory"]
206
  produced_units = 0
 
215
  arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop shipment for current week
216
  echelons[name]['inventory'] += arrived_shipment
217
 
218
+ # Step 2: Orders arrive from downstream partners (using LAST week's placed order)
219
  for name in echelon_order:
220
  if name == "Retailer":
221
  echelons[name]['incoming_order'] = get_customer_demand(week)
222
  else:
223
+ # Get the downstream partner's name
224
+ downstream_name = echelons[name]['downstream_name']
225
+ if downstream_name:
226
+ # Retrieve the order placed by the downstream partner LAST week
227
+ order_from_downstream = state['last_week_orders'].get(downstream_name, 0)
228
+ echelons[name]['incoming_order'] = order_from_downstream
229
+ else: # Should not happen except maybe week 1 if not handled
230
+ echelons[name]['incoming_order'] = 0
231
 
232
  # Step 3: Fulfill orders (Ship Beer)
233
  for name in echelon_order:
 
242
  sender = echelons[sender_name]
243
  receiver_name = sender['downstream_name']
244
  if receiver_name:
245
+ # Append the shipment SENT this week to the receiver's incoming queue for the FUTURE
246
  echelons[receiver_name]['incoming_shipments'].append(sender['shipment_sent'])
247
 
248
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
249
+ current_week_orders = {} # Store this week's decisions
250
  for name in echelon_order:
251
  e = echelons[name]
252
  if name == human_role:
 
256
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
257
  llm_raw_responses[name] = raw_resp
258
  e['order_placed'] = max(0, order_amount) # This is the order/production decision for the week
259
+ current_week_orders[name] = e['order_placed'] # Store decision for next week's Step 2
 
 
 
260
 
261
  # Factory schedules production based on its 'order_placed' decision
262
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
263
 
264
  # --- Logging (End of Week) ---
265
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
266
+ del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
 
267
 
268
  for name in echelon_order:
269
  e = echelons[name]
 
270
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
 
 
271
  for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
272
  log_entry[f'{name}.{key}'] = e[key]
273
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
274
  if name != 'Factory':
 
275
  log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
276
  else:
 
277
  log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
278
 
 
279
  log_entry[f'{human_role}.opening_inventory'] = pre_step_inventory
280
  log_entry[f'{human_role}.opening_backlog'] = pre_step_backlog
281
+ log_entry[f'{human_role}.arrived_this_week'] = arriving_shipment_this_week
282
+ log_entry[f'{human_role}.initial_order'] = human_initial_order
283
+ log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
284
 
285
  state['logs'].append(log_entry)
286
 
287
  # --- Advance Week ---
288
  state['week'] += 1
289
  state['decision_step'] = 'initial_order'
290
+ state['last_week_orders'] = current_week_orders # Store current decisions for next week
291
  if state['week'] > WEEKS: state['game_running'] = False
292
  # ==============================================================================
293
 
294
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
295
+ # This function remains correct.
296
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
297
  fig.suptitle(title, fontsize=16)
298
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
 
299
  plot_data = []
300
  for _, row in df.iterrows():
301
  for e in echelons:
 
302
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
303
  'inventory': row.get(f'{e}.inventory', 0),
304
  'order_placed': row.get(f'{e}.order_placed', 0),
305
  'total_cost': row.get(f'{e}.total_cost', 0)})
306
  plot_df = pd.DataFrame(plot_data)
 
307
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
308
  inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
 
309
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
310
  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 (The Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered (Units)')
311
+ total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
 
 
312
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
313
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
 
 
314
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
315
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
316
  human_df = df[human_df_cols].copy()
317
  human_df.rename(columns={
318
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
319
  }, inplace=True)
320
+ if len(human_df.columns) > 1:
321
  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')
322
  else:
323
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - No Data'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
 
 
324
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
325
 
326
  def save_logs_and_upload(state: dict):
327
+ # This function remains correct.
328
  if not state.get('logs'): return
329
  participant_id = state['participant_id']
330
  df = pd.json_normalize(state['logs'])
 
342
  st.error(f"Upload to Hugging Face failed: {e}")
343
 
344
  # -----------------------------------------------------------------------------
345
+ # 4. Streamlit UI (No changes needed in this section)
346
  # -----------------------------------------------------------------------------
347
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
348
 
 
363
  - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.
364
  - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.
365
  """)
366
+
367
  st.subheader("2. Your Role: The Distributor")
368
  st.markdown("""
369
  You will always play as the **Distributor**. The other 3 roles are played by AI.
 
386
  * **Week 13 (System):** The 50 units *arrive* at your warehouse. (This is the **{FACTORY_SHIPPING_DELAY} week Shipping Delay**). You can now use this inventory.
387
  **Conclusion:** You must always think 3 weeks ahead. The order you place in Week 10 will not help you until Week 13.
388
  """)
389
+
390
  st.subheader("4. How Each Week Works (Your Task)")
391
  st.markdown(f"""
392
  Your main job is simple: place one order each week.
393
  **A) At the start of every week, the system automatically does 3 things:**
394
  * **(Step 1) Your Shipments Arrive:** The beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives and is added to your `Current Inventory`.
395
+ * **(Step 2) New Orders Arrive:** You receive a new `Incoming Order` from the Wholesaler (This was their order from *last* week).
396
  * **(Step 3) You Ship Beer:** The system automatically ships as much beer as possible from your inventory to fulfill the Wholesaler's order (plus any old `Backlog`).
397
  **B) After this, you will see your new dashboard and must make your 2-part decision:**
398
  * **Step 4a (Initial Order):** Based on your new status, submit your **initial order** to the Factory.
 
453
  if state['decision_step'] == 'initial_order':
454
  with st.form(key="initial_order_form"):
455
  st.markdown("#### **Step 4a:** Based on the information available, submit your **initial** order.")
456
+ # Default initial order to incoming order, or 4 if incoming is 0 initially
457
+ default_initial = human_echelon_state['incoming_order'] if human_echelon_state['incoming_order'] > 0 else 4
458
+ initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=default_initial)
459
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
460
  state['human_initial_order'] = int(initial_order)
461
  state['decision_step'] = 'final_order'
 
480
  st.rerun()
481
 
482
  st.markdown("---")
 
483
  with st.expander("📖 Your Weekly Decision Log", expanded=False):
484
+ if not state.get('logs'):
485
  st.write("Your weekly history will be displayed here after you complete the first week.")
486
  else:
487
+ try:
488
  history_df = pd.json_normalize(state['logs'])
 
 
489
  human_cols = {
490
+ 'week': 'Week', f'{human_role}.opening_inventory': 'Opening Inv.',
491
+ f'{human_role}.opening_backlog': 'Opening Backlog', f'{human_role}.arrived_this_week': 'Arrived This Week',
492
+ f'{human_role}.incoming_order': 'Incoming Order', f'{human_role}.initial_order': 'Your Initial Order',
493
+ f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order',
494
+ f'{human_role}.arriving_next_week': 'Arriving Next Week', f'{human_role}.weekly_cost': 'Weekly Cost',
 
 
 
 
 
495
  }
 
 
496
  ordered_display_cols_keys = [
497
  'week', f'{human_role}.opening_inventory', f'{human_role}.opening_backlog',
498
  f'{human_role}.arrived_this_week', f'{human_role}.incoming_order',
499
  f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed',
500
  f'{human_role}.arriving_next_week', f'{human_role}.weekly_cost'
501
  ]
 
 
502
  final_cols_to_display = [col for col in ordered_display_cols_keys if col in history_df.columns]
503
 
504
  if not final_cols_to_display:
505
  st.write("No data columns available to display.")
506
  else:
 
507
  display_df = history_df[final_cols_to_display].rename(columns=human_cols)
 
 
508
  if 'Weekly Cost' in display_df.columns:
 
509
  display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}" if isinstance(x, (int, float)) else "")
 
 
510
  st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
 
511
  except Exception as e:
512
  st.error(f"Error displaying weekly log: {e}")
 
 
513
 
514
  try: st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
515
  except FileNotFoundError: st.sidebar.warning("Image file not found.")
 
525
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
526
  st.header("🎉 Game Over!")
527
  state = st.session_state.game_state
528
+ try:
529
  logs_df = pd.json_normalize(state['logs'])
530
  fig = plot_results(
531
  logs_df,
 
536
  save_logs_and_upload(state)
537
  except Exception as e:
538
  st.error(f"Error generating final report: {e}")
 
539
 
540
  if st.button("✨ Start a New Game"):
541
  del st.session_state.game_state