Lilli98 commited on
Commit
d429bec
·
verified ·
1 Parent(s): 8e687c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -59
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.13 - Classic UI Timing & Backlog Explanation)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -126,11 +126,8 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
126
  def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_after_arrivals: 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, as this is the decision point."""
129
-
130
  e_state = echelon_state_after_arrivals # Use the passed-in state for prompts
131
-
132
  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"
133
-
134
  if e_state['name'] == 'Factory':
135
  task_word = "production quantity"
136
  # Factory needs access to the global pipeline state
@@ -148,6 +145,7 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
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
  # Calculate Inventory Position based on state AFTER arrivals/orders
152
  if e_state['name'] == 'Factory':
153
  inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
@@ -155,21 +153,27 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
155
  else:
156
  inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Transit: {sum(e_state['incoming_shipments'])})"
157
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']))
 
158
  optimal_order = max(0, int(target_inventory_level - inventory_position))
 
159
  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."
160
 
161
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
162
  safety_stock = 4; anchor_demand = e_state['incoming_order']
 
163
  # Use state AFTER arrivals/orders for inventory correction calculation
164
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
 
165
  if e_state['name'] == 'Factory':
166
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
167
  supply_line_desc = "In Production"
168
  else:
169
  supply_line = sum(e_state['incoming_shipments'])
170
  supply_line_desc = "In Transit Shipments"
 
171
  calculated_order = anchor_demand + inventory_correction - supply_line
172
  rational_local_order = max(0, int(calculated_order))
 
173
  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 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."
174
 
175
  # --- Human-like ---
@@ -178,6 +182,7 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
178
  # Display other players' state AFTER arrivals/orders
179
  for name, other_e_state in all_echelons_state_after_arrivals.items():
180
  if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
 
181
  return f"""
182
  **You are a supply chain manager ({e_state['name']}) with full system visibility.**
183
  You can see everyone's current inventory and backlog before shipping, and the real customer demand.
@@ -198,22 +203,22 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
198
  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.
199
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
200
  """
201
- # ==============================================================================
202
 
 
203
 
204
  # =============== CORRECTED step_game FUNCTION ===============
205
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
206
  state = st.session_state.game_state
207
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
208
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
209
- echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
210
  llm_raw_responses = {}
211
 
212
  # Store state at the very beginning of the week for logging opening balances
213
  # These are the inventory/backlog values from the END of the previous week
214
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
215
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
216
- arrived_this_week = {name: 0 for name in roles} # Track arrivals for logging
217
 
218
  # --- Game Simulation Steps ---
219
 
@@ -228,7 +233,6 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
228
  else:
229
  inventory_after_production = factory_state['inventory']
230
 
231
-
232
  # Step 1b: Shipments arrive at downstream echelons
233
  inventory_after_arrival = {} # Store intermediate state
234
  for name in ["Retailer", "Wholesaler", "Distributor"]:
@@ -239,6 +243,7 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
239
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
240
  inventory_after_arrival["Factory"] = inventory_after_production # Add factory's state
241
 
 
242
  # Step 2: Orders arrive from downstream partners (using LAST week's placed order)
243
  total_backlog_before_shipping = {} # Store intermediate state
244
  for name in echelon_order:
@@ -267,7 +272,6 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
267
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
268
  }
269
 
270
-
271
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
272
  # Agents make decisions based on the decision_point_states
273
  current_week_orders = {}
@@ -280,6 +284,7 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
280
  else:
281
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
282
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
 
283
  llm_raw_responses[name] = raw_resp
284
  e['order_placed'] = max(0, order_amount) # Store the decision in the main state dict
285
  current_week_orders[name] = e['order_placed']
@@ -287,7 +292,6 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
287
  # Factory schedules production based on its 'order_placed' decision
288
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
289
 
290
-
291
  # --- Step 3: Fulfill orders (Ship Beer) ---
292
  # Now perform the shipping based on the inventory_after_arrival and total_backlog_before_shipping
293
  for name in echelon_order:
@@ -310,6 +314,7 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
310
 
311
  # --- Calculate Costs & Log (End of Week) ---
312
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
 
313
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
314
 
315
  for name in echelon_order:
@@ -349,14 +354,16 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
349
  state['week'] += 1
350
  state['decision_step'] = 'initial_order'
351
  state['last_week_orders'] = current_week_orders # Store current decisions for next week's Step 2
 
352
  if state['week'] > WEEKS: state['game_running'] = False
353
- # ==============================================================================
354
 
 
355
 
356
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
357
  # This function remains correct.
358
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
359
  fig.suptitle(title, fontsize=16)
 
360
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
361
  plot_data = []
362
  for _, row in df.iterrows():
@@ -366,24 +373,49 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
366
  'order_placed': row.get(f'{e}.order_placed', 0),
367
  'total_cost': row.get(f'{e}.total_cost', 0)})
368
  plot_df = pd.DataFrame(plot_data)
 
369
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
370
- 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)')
 
 
 
 
371
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
372
- 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)')
 
 
 
 
 
 
373
  total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
374
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
375
- total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
 
 
 
376
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
377
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
378
  human_df = df[human_df_cols].copy()
379
  human_df.rename(columns={
380
- f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
 
 
381
  }, inplace=True)
 
382
  if len(human_df.columns) > 1:
383
- 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')
 
 
 
 
384
  else:
385
- axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - No Data'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
386
- plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
 
 
 
 
387
 
388
  def save_logs_and_upload(state: dict):
389
  # This function remains correct.
@@ -391,16 +423,25 @@ def save_logs_and_upload(state: dict):
391
  participant_id = state['participant_id']
392
  df = pd.json_normalize(state['logs'])
393
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
 
394
  for col in df.columns:
395
  if df[col].dtype == 'object': df[col] = df[col].astype(str)
 
396
  df.to_csv(fname, index=False)
397
  st.success(f"Log successfully saved locally: `{fname}`")
398
  with open(fname, "rb") as f:
399
  st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
 
400
  if HF_TOKEN and HF_REPO_ID and hf_api:
401
  with st.spinner("Uploading log to Hugging Face Hub..."):
402
  try:
403
- 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)
 
 
 
 
 
 
404
  st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
405
  except Exception as e:
406
  st.error(f"Upload to Hugging Face failed: {e}")
@@ -458,7 +499,6 @@ else:
458
  * **Week 11 (System):** Your order arrives at the Factory (**{ORDER_PASSING_DELAY}w Order Delay**). Factory AI decides to produce 50.
459
  * **Week 12 (System):** Factory finishes producing 50 (**{FACTORY_LEAD_TIME}w Production Delay**) & ships it.
460
  * **Week 13 (System):** The 50 units arrive at your warehouse (**{FACTORY_SHIPPING_DELAY}w Shipping Delay**).
461
-
462
  **Conclusion:** Think 3 weeks ahead! Your order in Week 10 arrives at the start of Week 13.
463
  """)
464
 
@@ -478,13 +518,13 @@ else:
478
  """)
479
  # ====================================================================
480
 
481
- #st.subheader("5. The Bullwhip Effect (What to Avoid)")
482
- #st.markdown("""
483
- #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.
484
- #""")
485
 
486
  # =============== UPDATED: How Each Week Works & Dashboard Explanation ===============
487
- st.subheader("5. How Each Week Works & Understanding Your Dashboard")
488
  st.markdown(f"""
489
  Your main job is simple: place one order each week based on the dashboard presented to you.
490
 
@@ -495,10 +535,10 @@ else:
495
 
496
  **B) Your Dashboard (What You See for Your Turn):**
497
  The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
498
- * `Inventory`: Your stock **at the beginning of the week**. This is the inventory carried over from the end of last week.
499
- * `Backlog`: Unfilled orders **carried over from the end of last week**.
500
- * `Incoming Order`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2).
501
- * `Shipment Arriving (Next Week)`: The quantity scheduled to arrive at the start of the **next week**.
502
  * `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
503
  * `Cost Last Week`: The specific cost incurred just **last week**.
504
 
@@ -526,7 +566,10 @@ else:
526
  # --- Main Game Interface ---
527
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
528
  state = st.session_state.game_state
529
- week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
 
 
 
530
 
531
  st.header(f"Week {week} / {WEEKS}")
532
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
@@ -534,15 +577,18 @@ else:
534
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
535
  if info_sharing == 'full':
536
  cols = st.columns(4)
537
- for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
538
  with cols[i]:
539
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
540
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
541
  # Display the END OF LAST WEEK state (which is OPENING state for this week)
542
- st.metric("Inventory (Opening)", e['inventory']); st.metric("Backlog (Opening)", e['backlog'])
 
543
 
 
544
  if name == human_role:
545
  st.metric("Total Cost (Cumulative)", f"${e['total_cost']:,.2f}")
 
546
  last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
547
  st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
548
 
@@ -575,19 +621,35 @@ else:
575
 
576
  st.markdown("---")
577
  st.header("Your Decision (Step 4)")
578
- human_echelon_state_for_prompt = { # Construct the state needed for the prompt (AFTER arrivals/orders)
579
- 'name': human_role,
580
- 'inventory': echelons[human_role]['inventory'] + (list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0), # Opening + Arriving This Week (approx)
581
- 'backlog': echelons[human_role]['backlog'] + echelons[human_role]['incoming_order'], # Opening + Incoming Order This Week
582
- 'incoming_order': echelons[human_role]['incoming_order'],
583
- 'incoming_shipments': echelons[human_role]['incoming_shipments'] # Pass the queue
584
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
 
587
  if state['decision_step'] == 'initial_order':
588
  with st.form(key="initial_order_form"):
589
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
590
- default_initial = echelons[human_role]['incoming_order'] if echelons[human_role]['incoming_order'] > 0 else 4
 
591
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=default_initial)
592
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
593
  state['human_initial_order'] = int(initial_order)
@@ -596,25 +658,8 @@ else:
596
 
597
  elif state['decision_step'] == 'final_order':
598
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
599
- # Prepare all states AFTER arrivals/orders for the full info prompt context
600
- all_decision_point_states = {}
601
- for name in echelon_order:
602
- e_curr = echelons[name]
603
- arrived = 0
604
- if name == "Factory":
605
- if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0] # Peek, don't pop here
606
- else:
607
- if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0] # Peek
608
-
609
- all_decision_point_states[name] = {
610
- 'name': name,
611
- 'inventory': e_curr['inventory'] + arrived,
612
- 'backlog': e_curr['backlog'] + e_curr['incoming_order'],
613
- 'incoming_order': e_curr['incoming_order'],
614
- 'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
615
- }
616
-
617
- prompt_sugg = get_llm_prompt(all_decision_point_states[human_role], week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
618
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
619
 
620
  if 'final_order_input' not in st.session_state:
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.14 - Fixed NameError)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
126
  def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_after_arrivals: 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, as this is the decision point."""
 
129
  e_state = echelon_state_after_arrivals # Use the passed-in state for prompts
 
130
  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"
 
131
  if e_state['name'] == 'Factory':
132
  task_word = "production quantity"
133
  # Factory needs access to the global pipeline state
 
145
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
146
  safety_stock = 4
147
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
148
+
149
  # Calculate Inventory Position based on state AFTER arrivals/orders
150
  if e_state['name'] == 'Factory':
151
  inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
 
153
  else:
154
  inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Transit: {sum(e_state['incoming_shipments'])})"
155
  inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']))
156
+
157
  optimal_order = max(0, int(target_inventory_level - inventory_position))
158
+
159
  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."
160
 
161
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
162
  safety_stock = 4; anchor_demand = e_state['incoming_order']
163
+
164
  # Use state AFTER arrivals/orders for inventory correction calculation
165
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
166
+
167
  if e_state['name'] == 'Factory':
168
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
169
  supply_line_desc = "In Production"
170
  else:
171
  supply_line = sum(e_state['incoming_shipments'])
172
  supply_line_desc = "In Transit Shipments"
173
+
174
  calculated_order = anchor_demand + inventory_correction - supply_line
175
  rational_local_order = max(0, int(calculated_order))
176
+
177
  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 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."
178
 
179
  # --- Human-like ---
 
182
  # Display other players' state AFTER arrivals/orders
183
  for name, other_e_state in all_echelons_state_after_arrivals.items():
184
  if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
185
+
186
  return f"""
187
  **You are a supply chain manager ({e_state['name']}) with full system visibility.**
188
  You can see everyone's current inventory and backlog before shipping, and the real customer demand.
 
203
  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.
204
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
205
  """
 
206
 
207
+ # ==============================================================================
208
 
209
  # =============== CORRECTED step_game FUNCTION ===============
210
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
211
  state = st.session_state.game_state
212
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
213
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
214
+ echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Defined here
215
  llm_raw_responses = {}
216
 
217
  # Store state at the very beginning of the week for logging opening balances
218
  # These are the inventory/backlog values from the END of the previous week
219
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
220
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
221
+ arrived_this_week = {name: 0 for name in echelon_order} # Track arrivals for logging
222
 
223
  # --- Game Simulation Steps ---
224
 
 
233
  else:
234
  inventory_after_production = factory_state['inventory']
235
 
 
236
  # Step 1b: Shipments arrive at downstream echelons
237
  inventory_after_arrival = {} # Store intermediate state
238
  for name in ["Retailer", "Wholesaler", "Distributor"]:
 
243
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
244
  inventory_after_arrival["Factory"] = inventory_after_production # Add factory's state
245
 
246
+
247
  # Step 2: Orders arrive from downstream partners (using LAST week's placed order)
248
  total_backlog_before_shipping = {} # Store intermediate state
249
  for name in echelon_order:
 
272
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
273
  }
274
 
 
275
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
276
  # Agents make decisions based on the decision_point_states
277
  current_week_orders = {}
 
284
  else:
285
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
286
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
287
+
288
  llm_raw_responses[name] = raw_resp
289
  e['order_placed'] = max(0, order_amount) # Store the decision in the main state dict
290
  current_week_orders[name] = e['order_placed']
 
292
  # Factory schedules production based on its 'order_placed' decision
293
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
294
 
 
295
  # --- Step 3: Fulfill orders (Ship Beer) ---
296
  # Now perform the shipping based on the inventory_after_arrival and total_backlog_before_shipping
297
  for name in echelon_order:
 
314
 
315
  # --- Calculate Costs & Log (End of Week) ---
316
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
317
+ # These fields are nested in echelons, no need to log them top-level
318
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
319
 
320
  for name in echelon_order:
 
354
  state['week'] += 1
355
  state['decision_step'] = 'initial_order'
356
  state['last_week_orders'] = current_week_orders # Store current decisions for next week's Step 2
357
+
358
  if state['week'] > WEEKS: state['game_running'] = False
 
359
 
360
+ # ==============================================================================
361
 
362
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
363
  # This function remains correct.
364
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
365
  fig.suptitle(title, fontsize=16)
366
+
367
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
368
  plot_data = []
369
  for _, row in df.iterrows():
 
373
  'order_placed': row.get(f'{e}.order_placed', 0),
374
  'total_cost': row.get(f'{e}.total_cost', 0)})
375
  plot_df = pd.DataFrame(plot_data)
376
+
377
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
378
+ inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4)
379
+ axes[0].set_title('Inventory Levels (End of Week)')
380
+ axes[0].grid(True, linestyle='--')
381
+ axes[0].set_ylabel('Stock (Units)')
382
+
383
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
384
+ order_pivot.plot(ax=axes[1], style='--')
385
+ 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)
386
+ axes[1].set_title('Order Quantities / Production Decisions')
387
+ axes[1].grid(True, linestyle='--')
388
+ axes[1].legend()
389
+ axes[1].set_ylabel('Ordered/Produced (Units)')
390
+
391
  total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
392
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
393
+ total_costs.plot(kind='bar', ax=axes[2], rot=0)
394
+ axes[2].set_title('Total Cumulative Cost')
395
+ axes[2].set_ylabel('Cost ($)')
396
+
397
  human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
398
  human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
399
  human_df = df[human_df_cols].copy()
400
  human_df.rename(columns={
401
+ f'{human_role}.initial_order': 'Your Initial Order',
402
+ f'{human_role}.ai_suggestion': 'AI Suggestion',
403
+ f'{human_role}.order_placed': 'Your Final Order'
404
  }, inplace=True)
405
+
406
  if len(human_df.columns) > 1:
407
+ human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-')
408
+ axes[3].set_title(f'Analysis of Your ({human_role}) Decisions')
409
+ axes[3].set_ylabel('Order Quantity')
410
+ axes[3].grid(True, linestyle='--')
411
+ axes[3].set_xlabel('Week')
412
  else:
413
+ axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - No Data')
414
+ axes[3].grid(True, linestyle='--')
415
+ axes[3].set_xlabel('Week')
416
+
417
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
418
+ return fig
419
 
420
  def save_logs_and_upload(state: dict):
421
  # This function remains correct.
 
423
  participant_id = state['participant_id']
424
  df = pd.json_normalize(state['logs'])
425
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
426
+
427
  for col in df.columns:
428
  if df[col].dtype == 'object': df[col] = df[col].astype(str)
429
+
430
  df.to_csv(fname, index=False)
431
  st.success(f"Log successfully saved locally: `{fname}`")
432
  with open(fname, "rb") as f:
433
  st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
434
+
435
  if HF_TOKEN and HF_REPO_ID and hf_api:
436
  with st.spinner("Uploading log to Hugging Face Hub..."):
437
  try:
438
+ url = hf_api.upload_file(
439
+ path_or_fileobj=str(fname),
440
+ path_in_repo=f"logs/{fname.name}",
441
+ repo_id=HF_REPO_ID,
442
+ repo_type="dataset",
443
+ token=HF_TOKEN
444
+ )
445
  st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
446
  except Exception as e:
447
  st.error(f"Upload to Hugging Face failed: {e}")
 
499
  * **Week 11 (System):** Your order arrives at the Factory (**{ORDER_PASSING_DELAY}w Order Delay**). Factory AI decides to produce 50.
500
  * **Week 12 (System):** Factory finishes producing 50 (**{FACTORY_LEAD_TIME}w Production Delay**) & ships it.
501
  * **Week 13 (System):** The 50 units arrive at your warehouse (**{FACTORY_SHIPPING_DELAY}w Shipping Delay**).
 
502
  **Conclusion:** Think 3 weeks ahead! Your order in Week 10 arrives at the start of Week 13.
503
  """)
504
 
 
518
  """)
519
  # ====================================================================
520
 
521
+ st.subheader("5. The Bullwhip Effect (What to Avoid)")
522
+ st.markdown("""
523
+ 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.
524
+ """)
525
 
526
  # =============== UPDATED: How Each Week Works & Dashboard Explanation ===============
527
+ st.subheader("6. How Each Week Works & Understanding Your Dashboard")
528
  st.markdown(f"""
529
  Your main job is simple: place one order each week based on the dashboard presented to you.
530
 
 
535
 
536
  **B) Your Dashboard (What You See for Your Turn):**
537
  The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
538
+ * `Inventory (Opening)`: Your stock **at the beginning of the week**. This is the inventory carried over from the end of last week.
539
+ * `Backlog (Opening)`: Unfilled orders **carried over from the end of last week**.
540
+ * `Incoming Order (This Week)`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2). Use this for your planning.
541
+ * `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**. Use this for your planning.
542
  * `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
543
  * `Cost Last Week`: The specific cost incurred just **last week**.
544
 
 
566
  # --- Main Game Interface ---
567
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
568
  state = st.session_state.game_state
569
+ week, human_role, echelons, info_sharing = state['week'], state['echelons'], state['info_sharing']
570
+ # Define echelon order for display in the UI
571
+ echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
572
+
573
 
574
  st.header(f"Week {week} / {WEEKS}")
575
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
 
577
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
578
  if info_sharing == 'full':
579
  cols = st.columns(4)
580
+ for i, name in enumerate(echelon_order): # Use the defined echelon_order
581
  with cols[i]:
582
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
583
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
584
  # Display the END OF LAST WEEK state (which is OPENING state for this week)
585
+ st.metric("Inventory (Opening)", e['inventory'])
586
+ st.metric("Backlog (Opening)", e['backlog'])
587
 
588
+ # Display cumulative cost and last week's cost for the human player
589
  if name == human_role:
590
  st.metric("Total Cost (Cumulative)", f"${e['total_cost']:,.2f}")
591
+ # Display last week's cost if available (week > 1)
592
  last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
593
  st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
594
 
 
621
 
622
  st.markdown("---")
623
  st.header("Your Decision (Step 4)")
624
+
625
+ # Prepare the state snapshot for the AI prompt (State AFTER arrivals/orders, BEFORE shipping)
626
+ all_decision_point_states = {}
627
+ for name in echelon_order:
628
+ e_curr = echelons[name]
629
+ arrived = 0
630
+ # Peek at what *will* arrive this week (Step 1)
631
+ if name == "Factory":
632
+ # Peek at production pipeline
633
+ if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
634
+ else:
635
+ # Peek at incoming shipments
636
+ if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0]
637
+
638
+ all_decision_point_states[name] = {
639
+ 'name': name,
640
+ 'inventory': e_curr['inventory'] + arrived, # Opening Inv + Arriving This Week
641
+ 'backlog': e_curr['backlog'] + e_curr['incoming_order'], # Opening Backlog + Incoming Order This Week
642
+ 'incoming_order': e_curr['incoming_order'],
643
+ 'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
644
+ }
645
+ human_echelon_state_for_prompt = all_decision_point_states[human_role]
646
 
647
 
648
  if state['decision_step'] == 'initial_order':
649
  with st.form(key="initial_order_form"):
650
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
651
+ # Default initial order based on incoming order, minimum 4
652
+ default_initial = max(4, echelons[human_role]['incoming_order'])
653
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=default_initial)
654
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
655
  state['human_initial_order'] = int(initial_order)
 
658
 
659
  elif state['decision_step'] == 'final_order':
660
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
661
+ # Use the correctly timed state for the prompt
662
+ prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
664
 
665
  if 'final_order_input' not in st.session_state: