Lilli98 commited on
Commit
e8bd11c
·
verified ·
1 Parent(s): 38b222d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -96
app.py CHANGED
@@ -1,5 +1,6 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.21 - Removed Introduction)
 
3
  # -----------------------------------------------------------------------------
4
  # 1. Import Libraries
5
  # -----------------------------------------------------------------------------
@@ -21,6 +22,7 @@ from huggingface_hub import HfApi
21
  # -----------------------------------------------------------------------------
22
  st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide")
23
 
 
24
  # -----------------------------------------------------------------------------
25
  # 2. Game Parameters & API Configuration
26
  # -----------------------------------------------------------------------------
@@ -53,16 +55,20 @@ except Exception as e:
53
  else:
54
  st.session_state.initialization_error = None
55
 
 
56
  # -----------------------------------------------------------------------------
57
  # 3. Core Game Logic Functions
58
  # -----------------------------------------------------------------------------
 
59
  def get_customer_demand(week: int) -> int:
60
  return 4 if week <= 4 else 8
61
 
 
62
  def init_game_state(llm_personality: str, info_sharing: str):
63
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
64
  human_role = "Distributor" # Role is fixed
65
  participant_id = str(uuid.uuid4())[:8]
 
66
  st.session_state.game_state = {
67
  'game_running': True, 'participant_id': participant_id, 'week': 1,
68
  'human_role': human_role, 'llm_personality': llm_personality,
@@ -70,14 +76,18 @@ def init_game_state(llm_personality: str, info_sharing: str):
70
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
71
  'decision_step': 'initial_order',
72
  'human_initial_order': None,
 
73
  'last_week_orders': {name: 0 for name in roles}
74
  }
 
75
  for i, name in enumerate(roles):
76
  upstream = roles[i + 1] if i + 1 < len(roles) else None
77
  downstream = roles[i - 1] if i - 1 >= 0 else None
 
78
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
79
  elif name == "Factory": shipping_weeks = 0
80
  else: shipping_weeks = SHIPPING_DELAY
 
81
  st.session_state.game_state['echelons'][name] = {
82
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
83
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
@@ -85,6 +95,7 @@ def init_game_state(llm_personality: str, info_sharing: str):
85
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
86
  }
87
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
 
88
 
89
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
90
  # This function remains correct.
@@ -119,7 +130,6 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
119
  else:
120
  task_word = "order quantity"
121
  base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}"
122
-
123
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
124
  stable_demand = 8
125
  if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
@@ -136,7 +146,6 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
136
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + OrderToSupplier={order_in_transit_to_supplier})"
137
  optimal_order = max(0, int(target_inventory_level - inventory_position))
138
  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."
139
-
140
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
141
  safety_stock = 4; anchor_demand = e_state['incoming_order']
142
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
@@ -150,7 +159,6 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
150
  calculated_order = anchor_demand + inventory_correction - supply_line
151
  rational_local_order = max(0, int(calculated_order))
152
  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."
153
-
154
  elif llm_personality == 'human_like' and info_sharing == 'full':
155
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
156
  for name, other_e_state in all_echelons_state_decision_point.items():
@@ -165,7 +173,6 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
165
  You are still human and might get anxious about your own stock levels.
166
  What {task_word} should you decide on this week? Respond with a single integer.
167
  """
168
-
169
  elif llm_personality == 'human_like' and info_sharing == 'local':
170
  return f"""
171
  **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.
@@ -176,88 +183,139 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
176
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
177
  """
178
 
 
179
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
180
- # This function's logic remains correct (from v4.17).
181
  state = st.session_state.game_state
182
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
183
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
184
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
185
  llm_raw_responses = {}
186
 
 
187
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
188
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
189
-
190
  arrived_this_week = {name: 0 for name in echelon_order}
191
- inventory_after_arrival = {}
192
 
 
 
 
193
  factory_state = echelons["Factory"]
194
  produced_units = 0
195
  if state['factory_production_pipeline']:
196
- produced_units = state['factory_production_pipeline'].popleft()
197
  arrived_this_week["Factory"] = produced_units
 
198
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
199
 
 
200
  for name in ["Retailer", "Wholesaler", "Distributor"]:
201
  arrived_shipment = 0
202
  if echelons[name]['incoming_shipments']:
203
- arrived_shipment = echelons[name]['incoming_shipments'].popleft()
204
  arrived_this_week[name] = arrived_shipment
205
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
206
 
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
 
 
217
  decision_point_states = {}
218
  for name in echelon_order:
219
  decision_point_states[name] = {
220
- 'name': name, 'inventory': inventory_after_arrival[name],
221
- 'backlog': total_backlog_before_shipping[name], 'incoming_order': echelons[name]['incoming_order'],
 
 
222
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
223
  }
224
 
225
- current_week_orders = {}
 
226
  for name in echelon_order:
227
- e = echelons[name]; prompt_state = decision_point_states[name]
228
- if name == human_role: order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
 
 
 
229
  else:
230
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
231
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
232
- llm_raw_responses[name] = raw_resp; e['order_placed'] = max(0, order_amount); current_week_orders[name] = e['order_placed']
233
 
234
- state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
 
 
235
 
 
236
  units_shipped = {name: 0 for name in echelon_order}
237
  for name in echelon_order:
238
- e = echelons[name]; demand_to_meet = total_backlog_before_shipping[name]; available_inv = inventory_after_arrival[name]
239
- e['shipment_sent'] = min(available_inv, demand_to_meet); units_shipped[name] = e['shipment_sent']
240
- e['inventory'] = available_inv - e['shipment_sent']; e['backlog'] = demand_to_meet - e['shipment_sent']
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- if units_shipped["Factory"] > 0: echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
243
- if units_shipped['Distributor'] > 0: echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
244
- if units_shipped['Wholesaler'] > 0: echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
 
 
 
 
 
 
 
245
 
 
 
246
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
247
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
 
248
  for name in echelon_order:
249
- e = echelons[name]; e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
250
- for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']: log_entry[f'{name}.{key}'] = e[key]
251
- log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
252
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
253
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
254
- if name != 'Factory': log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
255
- else: log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
 
 
 
256
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
257
  state['logs'].append(log_entry)
258
 
 
259
  state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
260
  if state['week'] > WEEKS: state['game_running'] = False
 
 
261
 
262
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
263
  # This function remains correct.
@@ -318,16 +376,41 @@ if st.session_state.get('initialization_error'):
318
  else:
319
  # --- Game Setup & Instructions ---
320
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
321
-
322
- # --- Introduction Section Removed as Requested ---
323
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  st.header("⚙️ Game Configuration")
325
  c1, c2 = st.columns(2)
326
  with c1:
327
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
328
  with c2:
329
  info_sharing = st.selectbox("Information Sharing Level", ('local', 'full'), format_func=lambda x: x.title(), help="**Local:** You and the AI agents can only see your own inventory and incoming orders. **Full:** Everyone can see the entire supply chain's status and the true end-customer demand.")
330
-
331
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
332
  init_game_state(llm_personality, info_sharing)
333
  st.rerun()
@@ -337,38 +420,30 @@ else:
337
  state = st.session_state.game_state
338
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
339
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
340
-
341
  st.header(f"Week {week} / {WEEKS}")
342
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
343
  st.markdown("---")
344
-
345
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
346
-
 
347
  if info_sharing == 'full':
348
  cols = st.columns(4)
349
- for i, name in enumerate(echelon_order): # Use the defined echelon_order
350
  with cols[i]:
351
- e = echelons[name] # Get the echelon state
352
  icon = "👤" if name == human_role else "🤖"
353
-
354
- # =============== UI CHANGE: Highlight Player ===============
355
  if name == human_role:
356
- # Use markdown with HTML/CSS for highlighting
357
  st.markdown(f"##### **<span style='border: 1px solid #FF4B4B; padding: 2px 5px; border-radius: 3px;'>{icon} {name} (You)</span>**", unsafe_allow_html=True)
358
  else:
359
  st.markdown(f"##### {icon} {name}")
360
- # ========================================================
361
 
362
- # Display the END OF LAST WEEK state (which is OPENING state for this week)
363
  st.metric("Inventory (Opening)", e['inventory'])
364
  st.metric("Backlog (Opening)", e['backlog'])
365
-
366
- # =============== UI CHANGE: Removed Costs ===============
367
- # Costs are no longer displayed on the main dashboard
368
- # =======================================================
369
-
370
- # Display info about THIS week's events / NEXT week's arrivals
371
- # Calculate the INCOMING order for THIS week
372
  current_incoming_order = 0
373
  if name == "Retailer":
374
  current_incoming_order = get_customer_demand(week)
@@ -376,40 +451,51 @@ else:
376
  downstream_name = e['downstream_name']
377
  if downstream_name:
378
  current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
379
-
380
- st.write(f"Incoming Order (This Week): **{current_incoming_order}**") # Display calculated order
381
-
382
- # Display prediction for NEXT week's arrivals
383
  if name == "Factory":
384
- prod_completing_next = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
 
 
 
385
  st.write(f"Completing Next Week: **{prod_completing_next}**")
386
  else:
387
- arriving_next = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
 
 
 
 
388
  st.write(f"Arriving Next Week: **{arriving_next}**")
 
389
  else: # Local Info Mode
390
  st.info("In Local Information mode, you can only see your own status dashboard.")
391
  e = echelons[human_role]
392
- st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True) # Highlight self
393
- col1, col2, col3, col4 = st.columns(4)
394
-
395
- # Display OPENING state
396
- col1.metric("Inventory (Opening)", e['inventory'])
397
- col2.metric("Backlog (Opening)", e['backlog'])
398
-
399
- # Display info about THIS week's events / NEXT week's arrivals
400
- # Calculate the INCOMING order for THIS week
401
- current_incoming_order = 0
402
- downstream_name = e['downstream_name'] # Wholesaler
403
- if downstream_name:
404
- current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
405
-
406
- col3.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}") # Display calculated order
407
- col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
408
 
409
- # =============== UI CHANGE: Removed Costs ===============
410
- # Costs are no longer displayed on the main dashboard
411
- # =======================================================
 
 
 
 
 
 
 
 
 
 
 
 
412
 
 
 
413
  st.markdown("---")
414
  st.header("Your Decision (Step 4)")
415
 
@@ -418,66 +504,57 @@ else:
418
  for name in echelon_order:
419
  e_curr = echelons[name] # This is END OF LAST WEEK state
420
  arrived = 0
421
- # Peek at what *will* arrive this week (Step 1) based on current queues
422
  if name == "Factory":
423
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
424
  else:
425
  if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0]
426
 
427
- # Calculate the state AFTER arrivals and incoming orders for the prompt
428
- inv_after_arrival = e_curr['inventory'] + arrived
429
-
430
- # Determine incoming order for *this* week again for prompt state
431
  inc_order_this_week = 0
432
  if name == "Retailer": inc_order_this_week = get_customer_demand(week)
433
  else:
434
  ds_name = e_curr['downstream_name']
435
  if ds_name: inc_order_this_week = state['last_week_orders'].get(ds_name, 0)
436
-
 
437
  backlog_after_new_order = e_curr['backlog'] + inc_order_this_week
438
 
439
  all_decision_point_states[name] = {
440
  'name': name, 'inventory': inv_after_arrival, 'backlog': backlog_after_new_order,
441
- 'incoming_order': inc_order_this_week, # Use the correctly calculated incoming order
442
  'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
443
  }
444
-
445
  human_echelon_state_for_prompt = all_decision_point_states[human_role]
446
 
 
447
  if state['decision_step'] == 'initial_order':
448
  with st.form(key="initial_order_form"):
449
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
450
  # =============== UI CHANGE: Removed Default Value ===============
451
- initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1) # No 'value' argument
452
  # ===============================================================
453
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
454
- # Handle case where user leaves it blank (input returns None)
455
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
456
  state['decision_step'] = 'final_order'
457
  st.rerun()
458
 
459
  elif state['decision_step'] == 'final_order':
460
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
461
-
462
- # Use the correctly timed state for the prompt
463
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
464
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
465
-
466
  with st.form(key="final_order_form"):
467
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
468
  st.markdown("Considering the AI's advice, submit your **final** order to end the week. (This order will arrive in 3 weeks).")
469
  # =============== UI CHANGE: Removed Default Value ===============
470
- st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input') # No 'value' argument
471
  # ===============================================================
472
 
473
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
474
- # Handle case where user leaves it blank
475
- final_order_value = st.session_state.get('final_order_input', 0) # Use .get with default
476
  final_order_value = int(final_order_value) if final_order_value is not None else 0
477
-
478
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
479
 
480
- # Clean up session state for the input key
481
  if 'final_order_input' in st.session_state: del st.session_state.final_order_input
482
  st.rerun()
483
 
@@ -502,6 +579,7 @@ else:
502
  f'{human_role}.arriving_next_week', f'{human_role}.weekly_cost'
503
  ]
504
  final_cols_to_display = [col for col in ordered_display_cols_keys if col in history_df.columns]
 
505
  if not final_cols_to_display:
506
  st.write("No data columns available to display.")
507
  else:
@@ -514,6 +592,7 @@ else:
514
 
515
  try: st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
516
  except FileNotFoundError: st.sidebar.warning("Image file not found.")
 
517
  st.sidebar.header("Game Info")
518
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
519
  if st.sidebar.button("🔄 Reset Game"):
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.21 - Corrected 3-Week Lead Time Logic & UI)
3
+
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
6
  # -----------------------------------------------------------------------------
 
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
61
  # -----------------------------------------------------------------------------
62
+
63
  def get_customer_demand(week: int) -> int:
64
  return 4 if week <= 4 else 8
65
 
66
+ # =============== CORRECTED Initialization (v4.17 logic) ===============
67
  def init_game_state(llm_personality: str, info_sharing: str):
68
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
69
  human_role = "Distributor" # Role is fixed
70
  participant_id = str(uuid.uuid4())[:8]
71
+
72
  st.session_state.game_state = {
73
  'game_running': True, 'participant_id': participant_id, 'week': 1,
74
  'human_role': human_role, 'llm_personality': llm_personality,
 
76
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
77
  'decision_step': 'initial_order',
78
  'human_initial_order': None,
79
+ # Initialize last week's orders to 0
80
  'last_week_orders': {name: 0 for name in roles}
81
  }
82
+
83
  for i, name in enumerate(roles):
84
  upstream = roles[i + 1] if i + 1 < len(roles) else None
85
  downstream = roles[i - 1] if i - 1 >= 0 else None
86
+
87
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
88
  elif name == "Factory": shipping_weeks = 0
89
  else: shipping_weeks = SHIPPING_DELAY
90
+
91
  st.session_state.game_state['echelons'][name] = {
92
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
93
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
 
95
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
96
  }
97
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
98
+ # ==============================================================================
99
 
100
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
101
  # This function remains correct.
 
130
  else:
131
  task_word = "order quantity"
132
  base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}"
 
133
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
134
  stable_demand = 8
135
  if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
 
146
  inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + OrderToSupplier={order_in_transit_to_supplier})"
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
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
150
  safety_stock = 4; anchor_demand = e_state['incoming_order']
151
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
 
159
  calculated_order = anchor_demand + inventory_correction - supply_line
160
  rational_local_order = max(0, int(calculated_order))
161
  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."
 
162
  elif llm_personality == 'human_like' and info_sharing == 'full':
163
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
164
  for name, other_e_state in all_echelons_state_decision_point.items():
 
173
  You are still human and might get anxious about your own stock levels.
174
  What {task_word} should you decide on this week? Respond with a single integer.
175
  """
 
176
  elif llm_personality == 'human_like' and info_sharing == 'local':
177
  return f"""
178
  **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.
 
183
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
184
  """
185
 
186
+ # =============== CORRECTED step_game FUNCTION (Fixed Lead Time Logic) ===============
187
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
188
  state = st.session_state.game_state
189
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
190
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
191
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
192
  llm_raw_responses = {}
193
 
194
+ # Store state at the very beginning of the week (End of last week)
195
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
196
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
 
197
  arrived_this_week = {name: 0 for name in echelon_order}
 
198
 
199
+ # --- Game Simulation Steps ---
200
+
201
+ # Step 1a: Factory Production completes
202
  factory_state = echelons["Factory"]
203
  produced_units = 0
204
  if state['factory_production_pipeline']:
205
+ produced_units = state['factory_production_pipeline'].popleft() # Pop completed production
206
  arrived_this_week["Factory"] = produced_units
207
+ inventory_after_arrival = {} # Store intermediate inventory state
208
  inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
209
 
210
+ # Step 1b: Shipments arrive at downstream echelons
211
  for name in ["Retailer", "Wholesaler", "Distributor"]:
212
  arrived_shipment = 0
213
  if echelons[name]['incoming_shipments']:
214
+ arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop arrived shipment
215
  arrived_this_week[name] = arrived_shipment
216
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
217
 
218
+ # Step 2: Orders Arrive from Downstream
219
+ total_backlog_before_shipping = {} # Store intermediate backlog state
220
  for name in echelon_order:
221
  incoming_order_for_this_week = 0
222
+ if name == "Retailer":
223
+ incoming_order_for_this_week = get_customer_demand(week)
224
  else:
225
  downstream_name = echelons[name]['downstream_name']
226
+ if downstream_name:
227
+ incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
228
+
229
+ echelons[name]['incoming_order'] = incoming_order_for_this_week # Store for logging/UI this week
230
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
231
 
232
+ # --- Create State Snapshot for AI/Human Decision Point ---
233
  decision_point_states = {}
234
  for name in echelon_order:
235
  decision_point_states[name] = {
236
+ 'name': name,
237
+ 'inventory': inventory_after_arrival[name], # Inventory available
238
+ 'backlog': total_backlog_before_shipping[name], # Total demand to meet
239
+ 'incoming_order': echelons[name]['incoming_order'], # Order received this week
240
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
241
  }
242
 
243
+ # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
244
+ current_week_orders = {} # Store THIS week's decisions
245
  for name in echelon_order:
246
+ e = echelons[name]
247
+ prompt_state = decision_point_states[name]
248
+
249
+ if name == human_role:
250
+ order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
251
  else:
252
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
253
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
 
254
 
255
+ llm_raw_responses[name] = raw_resp
256
+ e['order_placed'] = max(0, order_amount)
257
+ current_week_orders[name] = e['order_placed'] # Store for NEXT week's Step 2
258
 
259
+ # --- Step 3 (Logic Moved): Fulfill orders (Ship Beer) ---
260
  units_shipped = {name: 0 for name in echelon_order}
261
  for name in echelon_order:
262
+ e = echelons[name]
263
+ demand_to_meet = total_backlog_before_shipping[name]
264
+ available_inv = inventory_after_arrival[name]
265
+
266
+ e['shipment_sent'] = min(available_inv, demand_to_meet)
267
+ units_shipped[name] = e['shipment_sent'] # Store temporarily
268
+
269
+ # Update the main state dict's inventory and backlog to reflect END OF WEEK state
270
+ e['inventory'] = available_inv - e['shipment_sent']
271
+ e['backlog'] = demand_to_meet - e['shipment_sent']
272
+
273
+ # --- Step 5: Advance Pipelines (New Logic) ---
274
+ # Factory's decision ('order_placed') from this week enters the production pipeline
275
+ # This simulates the FACTORY_LEAD_TIME
276
+ state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
277
 
278
+ # Items shipped in Step 3 now enter their respective shipping pipelines
279
+ # Factory -> Distributor (uses FACTORY_SHIPPING_DELAY)
280
+ if units_shipped["Factory"] > 0:
281
+ echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
282
+ # Distributor -> Wholesaler (uses SHIPPING_DELAY)
283
+ if units_shipped['Distributor'] > 0:
284
+ echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
285
+ # Wholesaler -> Retailer (uses SHIPPING_DELAY)
286
+ if units_shipped['Wholesaler'] > 0:
287
+ echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
288
 
289
+
290
+ # --- Calculate Costs & Log (End of Week) ---
291
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
292
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
293
+
294
  for name in echelon_order:
295
+ e = echelons[name]
296
+ e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
297
+ e['total_cost'] += e['weekly_cost']
298
+
299
+ log_entry[f'{name}.inventory'] = e['inventory']; log_entry[f'{name}.backlog'] = e['backlog']
300
+ log_entry[f'{name}.incoming_order'] = e['incoming_order']; log_entry[f'{name}.order_placed'] = e['order_placed']
301
+ log_entry[f'{name}.shipment_sent'] = e['shipment_sent']; log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
302
+ log_entry[f'{name}.total_cost'] = e['total_cost']; log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
303
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
304
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
305
+
306
+ if name != 'Factory':
307
+ log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
308
+ else:
309
+ log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
310
+
311
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
312
  state['logs'].append(log_entry)
313
 
314
+ # --- Advance Week ---
315
  state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
316
  if state['week'] > WEEKS: state['game_running'] = False
317
+ # ==============================================================================
318
+
319
 
320
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
321
  # This function remains correct.
 
376
  else:
377
  # --- Game Setup & Instructions ---
378
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
379
+
380
+ # --- Introduction Section (Remains Correct) ---
381
+ st.markdown("---")
382
+ st.header("📖 Welcome to the Beer Game!")
383
+ 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.")
384
+ st.subheader("1. Your Goal: Minimize Costs")
385
+ st.success("**Your single, most important goal is to: Minimize the total cost for your position in the supply chain.**")
386
+ st.markdown("You get costs from two things every week:")
387
+ 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)")
388
+ with st.expander("Click to see a cost calculation example"):
389
+ 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.")
390
+ st.subheader("2. Your Role: The Distributor")
391
+ 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.")
392
+ try: st.image(IMAGE_PATH, caption="You are the Distributor. You get orders from the Wholesaler and place orders to the Factory.")
393
+ except FileNotFoundError: st.warning("Image file not found.")
394
+ st.subheader("3. The Core Challenge: Delays!")
395
+ 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.")
396
+ with st.expander("Click to see a detailed example of the 3-week delay"):
397
+ 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.")
398
+ st.subheader("4. Understanding Inventory & Backlog")
399
+ 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).")
400
+ st.subheader("5. The Bullwhip Effect (What to Avoid)")
401
+ 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.")
402
+ st.subheader("6. How Each Week Works & Understanding Your Dashboard")
403
+ 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).\n* `Arriving This Week`: The shipment from the Factory that **will arrive** *during* this week (Step 1).\n* `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**.\n\n**C) Your Decision (Step 4 - Two Parts):**\nNow, looking at the dashboard, you decide how much to order:\n* **(Step 4a - Initial Order):** Submit your first estimate. Input box starts blank.\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.")
404
+
405
+ # --- Game Configuration ---
406
+ st.markdown("---")
407
  st.header("⚙️ Game Configuration")
408
  c1, c2 = st.columns(2)
409
  with c1:
410
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
411
  with c2:
412
  info_sharing = st.selectbox("Information Sharing Level", ('local', 'full'), format_func=lambda x: x.title(), help="**Local:** You and the AI agents can only see your own inventory and incoming orders. **Full:** Everyone can see the entire supply chain's status and the true end-customer demand.")
413
+
414
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
415
  init_game_state(llm_personality, info_sharing)
416
  st.rerun()
 
420
  state = st.session_state.game_state
421
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
422
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
423
+
424
  st.header(f"Week {week} / {WEEKS}")
425
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
426
  st.markdown("---")
 
427
  st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
428
+
429
+ # =============== MODIFIED UI LOGIC (v4.21) ===============
430
  if info_sharing == 'full':
431
  cols = st.columns(4)
432
+ for i, name in enumerate(echelon_order):
433
  with cols[i]:
434
+ e = echelons[name]
435
  icon = "👤" if name == human_role else "🤖"
436
+
 
437
  if name == human_role:
 
438
  st.markdown(f"##### **<span style='border: 1px solid #FF4B4B; padding: 2px 5px; border-radius: 3px;'>{icon} {name} (You)</span>**", unsafe_allow_html=True)
439
  else:
440
  st.markdown(f"##### {icon} {name}")
 
441
 
 
442
  st.metric("Inventory (Opening)", e['inventory'])
443
  st.metric("Backlog (Opening)", e['backlog'])
444
+
445
+ # --- Calculate and Display This Week's Events ---
446
+ # Incoming Order (arriving in Step 2)
 
 
 
 
447
  current_incoming_order = 0
448
  if name == "Retailer":
449
  current_incoming_order = get_customer_demand(week)
 
451
  downstream_name = e['downstream_name']
452
  if downstream_name:
453
  current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
454
+ st.write(f"Incoming Order (This Week): **{current_incoming_order}**")
455
+
 
 
456
  if name == "Factory":
457
+ # Production completing THIS week (Step 1a)
458
+ arriving_this_week = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
459
+ st.write(f"Completing This Week: **{arriving_this_week}**")
460
+ # Production completing NEXT week
461
+ prod_completing_next = list(state['factory_production_pipeline'])[1] if len(state['factory_production_pipeline']) > 1 else 0
462
  st.write(f"Completing Next Week: **{prod_completing_next}**")
463
  else:
464
+ # Shipment arriving THIS week (Step 1b)
465
+ arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
466
+ st.write(f"Arriving This Week: **{arriving_this_week}**")
467
+ # Shipment arriving NEXT week
468
+ arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) > 1 else 0
469
  st.write(f"Arriving Next Week: **{arriving_next}**")
470
+
471
  else: # Local Info Mode
472
  st.info("In Local Information mode, you can only see your own status dashboard.")
473
  e = echelons[human_role]
474
+ st.markdown(f"### 👤 **<span style='color:#FF4B4B;'>{human_role} (Your Dashboard - Start of Week State)</span>**", unsafe_allow_html=True)
475
+
476
+ col1, col2 = st.columns(2)
477
+ with col1:
478
+ st.metric("Inventory (Opening)", e['inventory'])
479
+ st.metric("Backlog (Opening)", e['backlog'])
 
 
 
 
 
 
 
 
 
 
480
 
481
+ with col2:
482
+ # Calculate Incoming Order for this week
483
+ current_incoming_order = 0
484
+ downstream_name = e['downstream_name'] # Wholesaler
485
+ if downstream_name:
486
+ current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
487
+ st.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}")
488
+
489
+ # Arriving THIS week (Step 1)
490
+ arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
491
+ st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
492
+
493
+ # Arriving NEXT week
494
+ arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) > 1 else 0
495
+ st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
496
 
497
+ # =======================================================
498
+
499
  st.markdown("---")
500
  st.header("Your Decision (Step 4)")
501
 
 
504
  for name in echelon_order:
505
  e_curr = echelons[name] # This is END OF LAST WEEK state
506
  arrived = 0
 
507
  if name == "Factory":
508
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
509
  else:
510
  if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0]
511
 
 
 
 
 
512
  inc_order_this_week = 0
513
  if name == "Retailer": inc_order_this_week = get_customer_demand(week)
514
  else:
515
  ds_name = e_curr['downstream_name']
516
  if ds_name: inc_order_this_week = state['last_week_orders'].get(ds_name, 0)
517
+
518
+ inv_after_arrival = e_curr['inventory'] + arrived
519
  backlog_after_new_order = e_curr['backlog'] + inc_order_this_week
520
 
521
  all_decision_point_states[name] = {
522
  'name': name, 'inventory': inv_after_arrival, 'backlog': backlog_after_new_order,
523
+ 'incoming_order': inc_order_this_week,
524
  'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
525
  }
 
526
  human_echelon_state_for_prompt = all_decision_point_states[human_role]
527
 
528
+
529
  if state['decision_step'] == 'initial_order':
530
  with st.form(key="initial_order_form"):
531
  st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
532
  # =============== UI CHANGE: Removed Default Value ===============
533
+ initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1) # No 'value'
534
  # ===============================================================
535
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
 
536
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
537
  state['decision_step'] = 'final_order'
538
  st.rerun()
539
 
540
  elif state['decision_step'] == 'final_order':
541
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
 
 
542
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
543
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
544
+
545
  with st.form(key="final_order_form"):
546
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
547
  st.markdown("Considering the AI's advice, submit your **final** order to end the week. (This order will arrive in 3 weeks).")
548
  # =============== UI CHANGE: Removed Default Value ===============
549
+ st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input') # No 'value'
550
  # ===============================================================
551
 
552
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
553
+ final_order_value = st.session_state.get('final_order_input', 0)
 
554
  final_order_value = int(final_order_value) if final_order_value is not None else 0
555
+
556
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
557
 
 
558
  if 'final_order_input' in st.session_state: del st.session_state.final_order_input
559
  st.rerun()
560
 
 
579
  f'{human_role}.arriving_next_week', f'{human_role}.weekly_cost'
580
  ]
581
  final_cols_to_display = [col for col in ordered_display_cols_keys if col in history_df.columns]
582
+
583
  if not final_cols_to_display:
584
  st.write("No data columns available to display.")
585
  else:
 
592
 
593
  try: st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
594
  except FileNotFoundError: st.sidebar.warning("Image file not found.")
595
+
596
  st.sidebar.header("Game Info")
597
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
598
  if st.sidebar.button("🔄 Reset Game"):