Lilli98 commited on
Commit
874b8e0
·
verified ·
1 Parent(s): 82b5b25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -223
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.24 - Corrected 3-Week Lead Time Logic & UI)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -32,10 +32,10 @@ st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide"
32
  WEEKS = 24
33
  INITIAL_INVENTORY = 12
34
  INITIAL_BACKLOG = 0
35
- ORDER_PASSING_DELAY = 1 # 订单传递延迟
36
- SHIPPING_DELAY = 2 # 通用运输延迟 (R->W, W->D)
37
- FACTORY_LEAD_TIME = 1 # 工厂生产延迟
38
- FACTORY_SHIPPING_DELAY = 1 # 工厂到分销商的运输延迟
39
  HOLDING_COST = 0.5
40
  BACKLOG_COST = 1.0
41
 
@@ -65,42 +65,30 @@ else:
65
  def get_customer_demand(week: int) -> int:
66
  return 4 if week <= 4 else 8
67
 
68
- # =============== MODIFIED Initialization (Corrected Queues) ===============
69
  def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
70
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
71
- human_role = "Distributor"
72
 
73
  st.session_state.game_state = {
74
- 'game_running': True, 'participant_id': participant_id, 'week': 1,
 
 
75
  'human_role': human_role, 'llm_personality': llm_personality,
76
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
77
-
78
- # 管道现在必须模拟总延迟
79
- # Factory: 1 (订单) + 1 (生产) = 2 周延迟
80
- # 我们需要一个队列来处理订单 (1周),一个队列处理生产 (1周)
81
- 'factory_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
82
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
83
-
84
- # Distributor -> Factory: 1(订单) + 1(生产) + 1(运输) = 3 周
85
- # Wholesaler -> Distributor: 1(订单) + 2(运输) = 3 周
86
- # Retailer -> Wholesaler: 1(订单) + 2(运输) = 3 周
87
- 'distributor_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
88
- 'wholesaler_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
89
- 'retailer_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
90
-
91
  'decision_step': 'initial_order',
92
  'human_initial_order': None,
93
- 'current_ai_suggestion': None,
 
94
  }
95
 
96
  for i, name in enumerate(roles):
97
  upstream = roles[i + 1] if i + 1 < len(roles) else None
98
  downstream = roles[i - 1] if i - 1 >= 0 else None
99
-
100
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
101
- elif name == "Factory": shipping_weeks = 0 # 工厂不收货
102
- else: shipping_weeks = SHIPPING_DELAY # R和W是2周
103
-
104
  st.session_state.game_state['echelons'][name] = {
105
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
106
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
@@ -133,70 +121,45 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
133
  st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 4.")
134
  return 4, f"API_ERROR: {e}"
135
 
136
- # =============== MODIFIED FUNCTION (Prompt uses new pipeline view) ===============
137
  def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
 
138
  e_state = echelon_state_decision_point
139
  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"
140
-
141
- # 查找正确的订单队列
142
- order_pipeline_to_show = deque()
143
- if e_state['name'] == 'Distributor':
144
- order_pipeline_to_show = st.session_state.game_state['distributor_order_pipeline']
145
- elif e_state['name'] == 'Wholesaler':
146
- order_pipeline_to_show = st.session_state.game_state['wholesaler_order_pipeline']
147
- elif e_state['name'] == 'Retailer':
148
- order_pipeline_to_show = st.session_state.game_state['retailer_order_pipeline']
149
-
150
  if e_state['name'] == 'Factory':
151
  task_word = "production quantity"
152
- base_info += f"- Your Production Pipeline (In Production): {list(st.session_state.game_state['factory_production_pipeline'])}\n"
153
- base_info += f"- Orders waiting for production (Just Arrived): {list(st.session_state.game_state['factory_order_pipeline'])}"
154
  else:
155
  task_word = "order quantity"
156
- base_info += f"- Shipments In Transit To You (On the way): {list(e_state['incoming_shipments'])}\n"
157
- base_info += f"- Orders You Placed (In transit to supplier): {list(order_pipeline_to_show)}"
158
-
159
- # --- Perfect Rational ---
160
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
161
  stable_demand = 8
162
- if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME + ORDER_PASSING_DELAY
163
  elif e_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
164
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
165
  safety_stock = 4
166
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
167
-
168
  if e_state['name'] == 'Factory':
169
- # IP = Inv - Backlog + In Production + Orders Waiting
170
- inventory_position = (e_state['inventory'] - e_state['backlog']
171
- + sum(st.session_state.game_state['factory_production_pipeline'])
172
- + sum(st.session_state.game_state['factory_order_pipeline']))
173
- inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={sum(st.session_state.game_state['factory_production_pipeline'])} + Waiting={sum(st.session_state.game_state['factory_order_pipeline'])})"
174
  else:
175
- # IP = Inv - Backlog + In Transit Shipments + Orders In Transit to Supplier
176
- inventory_position = (e_state['inventory'] - e_state['backlog']
177
- + sum(e_state['incoming_shipments'])
178
- + sum(order_pipeline_to_show))
179
- inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + InTransitOrder={sum(order_pipeline_to_show)})"
180
-
181
  optimal_order = max(0, int(target_inventory_level - inventory_position))
182
  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."
183
-
184
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
185
  safety_stock = 4; anchor_demand = e_state['incoming_order']
186
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
187
-
188
  if e_state['name'] == 'Factory':
189
- supply_line = sum(st.session_state.game_state['factory_production_pipeline']) + sum(st.session_state.game_state['factory_order_pipeline'])
190
- supply_line_desc = "In Production / Waiting"
191
  else:
192
- supply_line = sum(e_state['incoming_shipments']) + sum(order_pipeline_to_show)
193
- supply_line_desc = "Supply Line (In Transit Shipments + Orders)"
194
-
195
  calculated_order = anchor_demand + inventory_correction - supply_line
196
  rational_local_order = max(0, int(calculated_order))
197
  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."
198
-
199
- # --- Human-like ---
200
  elif llm_personality == 'human_like' and info_sharing == 'full':
201
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
202
  for name, other_e_state in all_echelons_state_decision_point.items():
@@ -220,167 +183,79 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
220
  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.
221
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
222
  """
223
- # ==============================================================================
224
 
225
- # =============== CORRECTED step_game FUNCTION (Fixed Lead Time Logic v4.21) ===============
226
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
227
  state = st.session_state.game_state
228
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
229
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
230
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
231
  llm_raw_responses = {}
232
-
233
- # Store state at the very beginning of the week (End of last week)
234
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
235
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
236
  arrived_this_week = {name: 0 for name in echelon_order}
237
-
238
- # --- Game Simulation Steps ---
239
-
240
- # Step 1: Shipments Arrive (from incoming_shipments queue)
241
  inventory_after_arrival = {}
 
 
 
 
 
 
242
  for name in ["Retailer", "Wholesaler", "Distributor"]:
243
  arrived_shipment = 0
244
  if echelons[name]['incoming_shipments']:
245
  arrived_shipment = echelons[name]['incoming_shipments'].popleft()
246
  arrived_this_week[name] = arrived_shipment
247
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
248
-
249
- # Step 2: Orders Arrive (from order_pipeline queue)
250
  total_backlog_before_shipping = {}
251
  for name in echelon_order:
252
  incoming_order_for_this_week = 0
253
- if name == "Retailer":
254
- incoming_order_for_this_week = get_customer_demand(week)
255
  else:
256
- # Check the correct order pipeline based on the downstream partner
257
  downstream_name = echelons[name]['downstream_name']
258
- if downstream_name == 'Distributor':
259
- if state['distributor_order_pipeline']: incoming_order_for_this_week = state['distributor_order_pipeline'].popleft()
260
- elif downstream_name == 'Wholesaler':
261
- if state['wholesaler_order_pipeline']: incoming_order_for_this_week = state['wholesaler_order_pipeline'].popleft()
262
- elif downstream_name == 'Retailer':
263
- if state['retailer_order_pipeline']: incoming_order_for_this_week = state['retailer_order_pipeline'].popleft()
264
-
265
  echelons[name]['incoming_order'] = incoming_order_for_this_week
266
- # Factory's 'incoming_order' is now set (from distributor_order_pipeline)
267
-
268
- # Calculate intermediate state for Factory (production completion)
269
- if name == "Factory":
270
- produced_units = 0
271
- if state['factory_production_pipeline']:
272
- produced_units = state['factory_production_pipeline'].popleft()
273
- arrived_this_week["Factory"] = produced_units
274
- inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
275
-
276
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
277
-
278
- # --- Create State Snapshot for AI/Human Decision Point ---
279
  decision_point_states = {}
280
  for name in echelon_order:
281
  decision_point_states[name] = {
282
- 'name': name,
283
- 'inventory': inventory_after_arrival[name],
284
- 'backlog': total_backlog_before_shipping[name],
285
- 'incoming_order': echelons[name]['incoming_order'],
286
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
287
  }
288
-
289
- # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
290
  for name in echelon_order:
291
- e = echelons[name]
292
- prompt_state = decision_point_states[name]
293
-
294
- if name == human_role:
295
- order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
296
  else:
297
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
298
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
299
-
300
- llm_raw_responses[name] = raw_resp
301
- e['order_placed'] = max(0, order_amount)
302
-
303
- # Put the order into the correct pipeline to simulate ORDER_PASSING_DELAY
304
- if name == 'Distributor': state['distributor_order_pipeline'].append(e['order_placed'])
305
- elif name == 'Wholesaler': state['wholesaler_order_pipeline'].append(e['order_placed'])
306
- elif name == 'Retailer': state['retailer_order_pipeline'].append(e['order_placed'])
307
- # Factory's 'order_placed' is its production decision
308
-
309
-
310
- # --- Step 3 (Logic): Fulfill orders (Ship Beer) ---
311
  units_shipped = {name: 0 for name in echelon_order}
312
  for name in echelon_order:
313
- e = echelons[name]
314
- demand_to_meet = total_backlog_before_shipping[name]
315
- available_inv = inventory_after_arrival[name]
316
-
317
- e['shipment_sent'] = min(available_inv, demand_to_meet)
318
- units_shipped[name] = e['shipment_sent']
319
-
320
- # Update the main state dict's inventory and backlog to reflect END OF WEEK state
321
- e['inventory'] = available_inv - e['shipment_sent']
322
- e['backlog'] = demand_to_meet - e['shipment_sent']
323
-
324
- # --- Step 5: Advance Pipelines (New Logic) ---
325
-
326
- # Factory: takes its incoming order (from distributor_order_pipeline)
327
- # and schedules it for production (adds to factory_production_pipeline)
328
- # This simulates FACTORY_LEAD_TIME
329
- # *** BUG FIX: Factory's order_placed IS its production decision ***
330
- # *** Factory's incoming_order is what drives its decision ***
331
-
332
- # Factory's decision ('order_placed') from Step 4 enters the production pipeline
333
- state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
334
-
335
- # What the Factory *shipped* in Step 3 (units_shipped["Factory"])
336
- # now enters the Distributor's shipping queue
337
- if units_shipped["Factory"] > 0:
338
- echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
339
-
340
- # What the Distributor *shipped* in Step 3...
341
- if units_shipped['Distributor'] > 0:
342
- echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
343
-
344
- # What the Wholesaler *shipped* in Step 3...
345
- if units_shipped['Wholesaler'] > 0:
346
- echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
347
-
348
-
349
- # --- Calculate Costs & Log (End of Week) ---
350
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
351
- # Clean up fields
352
- del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
353
- for key in ['distributor_order_pipeline', 'wholesaler_order_pipeline', 'retailer_order_pipeline', 'factory_order_pipeline']:
354
- if key in log_entry: del log_entry[key]
355
-
356
-
357
  for name in echelon_order:
358
- e = echelons[name]
359
- e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
360
- e['total_cost'] += e['weekly_cost']
361
-
362
- log_entry[f'{name}.inventory'] = e['inventory']; log_entry[f'{name}.backlog'] = e['backlog']
363
- log_entry[f'{name}.incoming_order'] = e['incoming_order']; log_entry[f'{name}.order_placed'] = e['order_placed']
364
- log_entry[f'{name}.shipment_sent'] = e['shipment_sent']; log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
365
- log_entry[f'{name}.total_cost'] = e['total_cost']; log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
366
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
367
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
368
-
369
- if name != 'Factory':
370
- log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
371
- else:
372
- log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
373
-
374
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
375
  state['logs'].append(log_entry)
376
-
377
- # --- Advance Week ---
378
- state['week'] += 1; state['decision_step'] = 'initial_order'
379
- # 'last_week_orders' is no longer needed with this pipeline logic
380
- # We rely on the order pipelines
381
  if state['week'] > WEEKS: state['game_running'] = False
382
- # ==============================================================================
383
-
384
 
385
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
386
  # This function remains correct.
@@ -412,7 +287,8 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
412
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - Error Plotting Data'); axes[3].text(0.5, 0.5, f"Error: {plot_err}", ha='center', va='center'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
413
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
414
 
415
- # =============== Leaderboard Functions (Unchanged) ===============
 
416
  @st.cache_data(ttl=60)
417
  def load_leaderboard_data():
418
  if not hf_api or not HF_REPO_ID: return {}
@@ -538,7 +414,7 @@ def save_logs_and_upload(state: dict):
538
  # ==============================================================================
539
 
540
  # -----------------------------------------------------------------------------
541
- # 4. Streamlit UI (Adjusted for Custom ID, Leaderboard, and UI Fixes)
542
  # -----------------------------------------------------------------------------
543
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
544
 
@@ -548,12 +424,12 @@ else:
548
  # --- Game Setup & Instructions ---
549
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
550
 
551
- # Introduction is removed as requested
552
-
553
  st.markdown("---")
554
  st.header("⚙️ Game Configuration")
555
 
 
556
  participant_id = st.text_input("Enter Your Name or Team ID:", key="participant_id_input", placeholder="e.g., Team A")
 
557
 
558
  c1, c2 = st.columns(2)
559
  with c1:
@@ -561,14 +437,16 @@ else:
561
  with c2:
562
  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.")
563
 
 
564
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
565
  if not participant_id:
566
  st.error("Please enter a Name or Team ID to start!")
567
  else:
568
  existing_data = load_leaderboard_data()
569
  if participant_id in existing_data:
570
- # Check for a re-click confirmation
571
  if st.session_state.get('last_id_warning') == participant_id:
 
572
  st.session_state.pop('last_id_warning', None)
573
  init_game_state(llm_personality, info_sharing, participant_id)
574
  st.rerun()
@@ -576,18 +454,22 @@ else:
576
  st.session_state['last_id_warning'] = participant_id
577
  st.warning(f"ID '{participant_id}' already exists! Your score will be overwritten. Click 'Start Game' again to confirm.")
578
  else:
 
579
  if 'last_id_warning' in st.session_state:
580
  del st.session_state['last_id_warning']
581
  init_game_state(llm_personality, info_sharing, participant_id)
582
  st.rerun()
 
583
 
 
584
  show_leaderboard_ui()
 
585
 
586
  # --- Main Game Interface ---
587
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
588
  state = st.session_state.game_state
589
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
590
- echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
591
 
592
 
593
  st.header(f"Week {week} / {WEEKS}")
@@ -595,7 +477,7 @@ else:
595
  st.markdown("---")
596
  st.subheader("Supply Chain Status (Start of Week State)")
597
 
598
- # =============== MODIFIED UI LOGIC (v4.21) ===============
599
  if info_sharing == 'full':
600
  cols = st.columns(4)
601
  for i, name in enumerate(echelon_order):
@@ -610,21 +492,17 @@ else:
610
 
611
  st.metric("Inventory (Opening)", e['inventory'])
612
  st.metric("Backlog (Opening)", e['backlog'])
 
 
613
 
614
- # --- Calculate and Display This Week's Events ---
615
- # Incoming Order (arriving in Step 2)
616
  current_incoming_order = 0
617
  if name == "Retailer":
618
  current_incoming_order = get_customer_demand(week)
619
  else:
620
- # PEEK at the order pipeline
621
  downstream_name = e['downstream_name']
622
- if downstream_name == 'Distributor': pipeline_peek = state['distributor_order_pipeline']
623
- elif downstream_name == 'Wholesaler': pipeline_peek = state['wholesaler_order_pipeline']
624
- elif downstream_name == 'Retailer': pipeline_peek = state['retailer_order_pipeline']
625
- else: pipeline_peek = deque()
626
- current_incoming_order = list(pipeline_peek)[0] if pipeline_peek else 0
627
-
628
  st.write(f"Incoming Order (This Week): **{current_incoming_order}**")
629
 
630
  if name == "Factory":
@@ -637,23 +515,23 @@ else:
637
  st.write(f"Arriving This Week: **{arriving_this_week}**")
638
 
639
  arriving_next = 0
 
640
  if len(e['incoming_shipments']) > 1:
641
  arriving_next = list(e['incoming_shipments'])[1]
642
- # Handle 2-week delay display for R/W
643
  elif name in ('Wholesaler', 'Retailer') and e['incoming_shipments'].maxlen == 2:
644
- if len(e['incoming_shipments']) == 1: # Only one item in queue
645
- arriving_next = list(e['incoming_shipments'])[0] # This must be the item for next week
646
- arriving_this_week = 0 # This week's arrival must have been 0
647
- # Re-calculate arriving_this_week for R/W based on maxlen
648
- arriving_this_week = list(e['incoming_shipments'])[0] if len(e['incoming_shipments']) == e['incoming_shipments'].maxlen else 0
649
- arriving_next = list(e['incoming_shipments'])[1] if len(e['incoming_shipments']) == e['incoming_shipments'].maxlen else (list(e['incoming_shipments'])[0] if len(e['incoming_shipments']) == 1 else 0)
 
650
 
651
- # Overwrite for clarity
652
  st.write(f"Arriving This Week: **{arriving_this_week}**")
653
- st.write(f"Arriving Next Week: **{arriving_next}**")
654
- else: # Distributor case (maxlen 1)
655
- arriving_next = 0 # Peek at index 1 is correct, it doesn't exist for maxlen=1
656
- st.write(f"Arriving Next Week: **{arriving_next}**")
657
 
658
  else: # Local Info Mode
659
  st.info("In Local Information mode, you can only see your own status dashboard.")
@@ -666,10 +544,10 @@ else:
666
  st.metric("Backlog (Opening)", e['backlog'])
667
 
668
  with col2:
669
- # Calculate Incoming Order for this week
670
  current_incoming_order = 0
671
- pipeline_peek = state['wholesaler_order_pipeline']
672
- current_incoming_order = list(pipeline_peek)[0] if pipeline_peek else 0
 
673
  st.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}")
674
 
675
  with col3:
@@ -677,8 +555,8 @@ else:
677
  arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
678
  st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
679
 
680
- # Arriving NEXT week (Peek at the next item in the 1-week delay queue)
681
- arriving_next = 0 # maxlen is 1, so index [1] doesn't exist
682
  st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
683
 
684
  # =======================================================
@@ -689,7 +567,7 @@ else:
689
  # Prepare the state snapshot for the AI prompt (State AFTER arrivals/orders, BEFORE shipping)
690
  all_decision_point_states = {}
691
  for name in echelon_order:
692
- e_curr = echelons[name]
693
  arrived = 0
694
  if name == "Factory":
695
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
@@ -700,11 +578,7 @@ else:
700
  if name == "Retailer": inc_order_this_week = get_customer_demand(week)
701
  else:
702
  ds_name = e_curr['downstream_name']
703
- pipeline_peek = deque()
704
- if ds_name == 'Distributor': pipeline_peek = state['distributor_order_pipeline']
705
- elif ds_name == 'Wholesaler': pipeline_peek = state['wholesaler_order_pipeline']
706
- elif ds_name == 'Retailer': pipeline_peek = state['retailer_order_pipeline']
707
- inc_order_this_week = list(pipeline_peek)[0] if pipeline_peek else 0
708
 
709
  inv_after_arrival = e_curr['inventory'] + arrived
710
  backlog_after_new_order = e_curr['backlog'] + inc_order_this_week
@@ -725,16 +599,20 @@ else:
725
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
726
  state['decision_step'] = 'final_order'
727
 
 
728
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
729
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
730
  state['current_ai_suggestion'] = ai_suggestion # Store it
 
731
 
732
  st.rerun()
733
 
734
  elif state['decision_step'] == 'final_order':
735
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
736
 
 
737
  ai_suggestion = state.get('current_ai_suggestion', 4) # Read stored value
 
738
 
739
  with st.form(key="final_order_form"):
740
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.25 - Based on v4.21 Logic + UI Fixes)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
32
  WEEKS = 24
33
  INITIAL_INVENTORY = 12
34
  INITIAL_BACKLOG = 0
35
+ ORDER_PASSING_DELAY = 1 # Handled by last_week_orders
36
+ SHIPPING_DELAY = 2 # General shipping delay (R->W, W->D)
37
+ FACTORY_LEAD_TIME = 1
38
+ FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
39
  HOLDING_COST = 0.5
40
  BACKLOG_COST = 1.0
41
 
 
65
  def get_customer_demand(week: int) -> int:
66
  return 4 if week <= 4 else 8
67
 
68
+ # =============== MODIFIED Initialization (v4.21 logic + v4.23 bugfix) ===============
69
  def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
70
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
71
+ human_role = "Distributor" # Role is fixed
72
 
73
  st.session_state.game_state = {
74
+ 'game_running': True,
75
+ 'participant_id': participant_id,
76
+ 'week': 1,
77
  'human_role': human_role, 'llm_personality': llm_personality,
78
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
 
 
 
 
 
79
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
 
 
 
 
 
 
 
 
80
  'decision_step': 'initial_order',
81
  'human_initial_order': None,
82
+ 'current_ai_suggestion': None, # v4.23 Bugfix: 用于存储AI建议
83
+ 'last_week_orders': {name: 0 for name in roles} # v4.21 Logic: 初始化为0
84
  }
85
 
86
  for i, name in enumerate(roles):
87
  upstream = roles[i + 1] if i + 1 < len(roles) else None
88
  downstream = roles[i - 1] if i - 1 >= 0 else None
 
89
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
90
+ elif name == "Factory": shipping_weeks = 0
91
+ else: shipping_weeks = SHIPPING_DELAY
 
92
  st.session_state.game_state['echelons'][name] = {
93
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
94
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
 
121
  st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 4.")
122
  return 4, f"API_ERROR: {e}"
123
 
 
124
  def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
125
+ # This function's logic remains correct (from v4.21).
126
  e_state = echelon_state_decision_point
127
  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"
 
 
 
 
 
 
 
 
 
 
128
  if e_state['name'] == 'Factory':
129
  task_word = "production quantity"
130
+ base_info += f"- Your Production Pipeline (completing next week onwards): {list(st.session_state.game_state['factory_production_pipeline'])}"
 
131
  else:
132
  task_word = "order quantity"
133
+ base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}"
 
 
 
134
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
135
  stable_demand = 8
136
+ if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
137
  elif e_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
138
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
139
  safety_stock = 4
140
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
 
141
  if e_state['name'] == 'Factory':
142
+ inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
143
+ inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={sum(st.session_state.game_state['factory_production_pipeline'])})"
 
 
 
144
  else:
145
+ order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
146
+ inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']) + order_in_transit_to_supplier)
147
+ inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + OrderToSupplier={order_in_transit_to_supplier})"
 
 
 
148
  optimal_order = max(0, int(target_inventory_level - inventory_position))
149
  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."
 
150
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
151
  safety_stock = 4; anchor_demand = e_state['incoming_order']
152
  inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
 
153
  if e_state['name'] == 'Factory':
154
+ supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
155
+ supply_line_desc = "In Production"
156
  else:
157
+ order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
158
+ supply_line = sum(e_state['incoming_shipments']) + order_in_transit_to_supplier
159
+ supply_line_desc = "Supply Line (In Transit Shipments + Order To Supplier)"
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 (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."
 
 
163
  elif llm_personality == 'human_like' and info_sharing == 'full':
164
  full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
165
  for name, other_e_state in all_echelons_state_decision_point.items():
 
183
  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.
184
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
185
  """
 
186
 
 
187
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
188
+ # This is the correct logic from v4.17
189
  state = st.session_state.game_state
190
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
191
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
192
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
193
  llm_raw_responses = {}
 
 
194
  opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
195
  opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
196
  arrived_this_week = {name: 0 for name in echelon_order}
 
 
 
 
197
  inventory_after_arrival = {}
198
+ factory_state = echelons["Factory"]
199
+ produced_units = 0
200
+ if state['factory_production_pipeline']:
201
+ produced_units = state['factory_production_pipeline'].popleft()
202
+ arrived_this_week["Factory"] = produced_units
203
+ inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
204
  for name in ["Retailer", "Wholesaler", "Distributor"]:
205
  arrived_shipment = 0
206
  if echelons[name]['incoming_shipments']:
207
  arrived_shipment = echelons[name]['incoming_shipments'].popleft()
208
  arrived_this_week[name] = arrived_shipment
209
  inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
 
 
210
  total_backlog_before_shipping = {}
211
  for name in echelon_order:
212
  incoming_order_for_this_week = 0
213
+ if name == "Retailer": incoming_order_for_this_week = get_customer_demand(week)
 
214
  else:
 
215
  downstream_name = echelons[name]['downstream_name']
216
+ if downstream_name: incoming_order_for_this_week = state['last_week_orders'].get(downstream_name, 0)
 
 
 
 
 
 
217
  echelons[name]['incoming_order'] = incoming_order_for_this_week
 
 
 
 
 
 
 
 
 
 
218
  total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
 
 
219
  decision_point_states = {}
220
  for name in echelon_order:
221
  decision_point_states[name] = {
222
+ 'name': name, 'inventory': inventory_after_arrival[name],
223
+ 'backlog': total_backlog_before_shipping[name], 'incoming_order': echelons[name]['incoming_order'],
 
 
224
  'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
225
  }
226
+ current_week_orders = {}
 
227
  for name in echelon_order:
228
+ e = echelons[name]; prompt_state = decision_point_states[name]
229
+ if name == human_role: order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
 
 
 
230
  else:
231
  prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
232
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
233
+ llm_raw_responses[name] = raw_resp; e['order_placed'] = max(0, order_amount); current_week_orders[name] = e['order_placed']
234
+ state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
 
 
 
 
 
 
 
 
 
 
235
  units_shipped = {name: 0 for name in echelon_order}
236
  for name in echelon_order:
237
+ e = echelons[name]; demand_to_meet = total_backlog_before_shipping[name]; available_inv = inventory_after_arrival[name]
238
+ e['shipment_sent'] = min(available_inv, demand_to_meet); units_shipped[name] = e['shipment_sent']
239
+ e['inventory'] = available_inv - e['shipment_sent']; e['backlog'] = demand_to_meet - e['shipment_sent']
240
+ if units_shipped["Factory"] > 0: echelons['Distributor']['incoming_shipments'].append(units_shipped["Factory"])
241
+ if units_shipped['Distributor'] > 0: echelons['Wholesaler']['incoming_shipments'].append(units_shipped['Distributor'])
242
+ if units_shipped['Wholesaler'] > 0: echelons['Retailer']['incoming_shipments'].append(units_shipped['Wholesaler'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
244
+ del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
245
+ if 'current_ai_suggestion' in log_entry: del log_entry['current_ai_suggestion']
 
 
 
 
246
  for name in echelon_order:
247
+ e = echelons[name]; e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
248
+ for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']: log_entry[f'{name}.{key}'] = e[key]
249
+ log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
250
  log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
251
  log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
252
+ if name != 'Factory': log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
253
+ else: log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
 
 
 
254
  log_entry[f'{human_role}.initial_order'] = human_initial_order; log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
255
  state['logs'].append(log_entry)
256
+ state['week'] += 1; state['decision_step'] = 'initial_order'; state['last_week_orders'] = current_week_orders
257
+ state['current_ai_suggestion'] = None # Clean up
 
 
 
258
  if state['week'] > WEEKS: state['game_running'] = False
 
 
259
 
260
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
261
  # This function remains correct.
 
287
  axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - Error Plotting Data'); axes[3].text(0.5, 0.5, f"Error: {plot_err}", ha='center', va='center'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
288
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
289
 
290
+
291
+ # =============== NEW: Leaderboard Functions ===============
292
  @st.cache_data(ttl=60)
293
  def load_leaderboard_data():
294
  if not hf_api or not HF_REPO_ID: return {}
 
414
  # ==============================================================================
415
 
416
  # -----------------------------------------------------------------------------
417
+ # 4. Streamlit UI (Applying v4.22 + v4.23 fixes)
418
  # -----------------------------------------------------------------------------
419
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
420
 
 
424
  # --- Game Setup & Instructions ---
425
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
426
 
 
 
427
  st.markdown("---")
428
  st.header("⚙️ Game Configuration")
429
 
430
+ # =============== NEW: Participant ID Input ===============
431
  participant_id = st.text_input("Enter Your Name or Team ID:", key="participant_id_input", placeholder="e.g., Team A")
432
+ # =======================================================
433
 
434
  c1, c2 = st.columns(2)
435
  with c1:
 
437
  with c2:
438
  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.")
439
 
440
+ # =============== MODIFIED: Start Game Button ===============
441
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
442
  if not participant_id:
443
  st.error("Please enter a Name or Team ID to start!")
444
  else:
445
  existing_data = load_leaderboard_data()
446
  if participant_id in existing_data:
447
+ # 如果ID已存在,添加一个session_state标志,要求再次点击
448
  if st.session_state.get('last_id_warning') == participant_id:
449
+ # 这是第二次点击,确认覆盖
450
  st.session_state.pop('last_id_warning', None)
451
  init_game_state(llm_personality, info_sharing, participant_id)
452
  st.rerun()
 
454
  st.session_state['last_id_warning'] = participant_id
455
  st.warning(f"ID '{participant_id}' already exists! Your score will be overwritten. Click 'Start Game' again to confirm.")
456
  else:
457
+ # 新ID,直接开始
458
  if 'last_id_warning' in st.session_state:
459
  del st.session_state['last_id_warning']
460
  init_game_state(llm_personality, info_sharing, participant_id)
461
  st.rerun()
462
+ # ===========================================================
463
 
464
+ # =============== NEW: Show Leaderboard on Start Page ===============
465
  show_leaderboard_ui()
466
+ # =================================================================
467
 
468
  # --- Main Game Interface ---
469
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
470
  state = st.session_state.game_state
471
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
472
+ echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
473
 
474
 
475
  st.header(f"Week {week} / {WEEKS}")
 
477
  st.markdown("---")
478
  st.subheader("Supply Chain Status (Start of Week State)")
479
 
480
+ # =============== MODIFIED UI LOGIC (v4.22) ===============
481
  if info_sharing == 'full':
482
  cols = st.columns(4)
483
  for i, name in enumerate(echelon_order):
 
492
 
493
  st.metric("Inventory (Opening)", e['inventory'])
494
  st.metric("Backlog (Opening)", e['backlog'])
495
+
496
+ # 移除成本显示
497
 
498
+ # --- NEW: Added Arriving This Week ---
 
499
  current_incoming_order = 0
500
  if name == "Retailer":
501
  current_incoming_order = get_customer_demand(week)
502
  else:
 
503
  downstream_name = e['downstream_name']
504
+ if downstream_name:
505
+ current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
 
 
 
 
506
  st.write(f"Incoming Order (This Week): **{current_incoming_order}**")
507
 
508
  if name == "Factory":
 
515
  st.write(f"Arriving This Week: **{arriving_this_week}**")
516
 
517
  arriving_next = 0
518
+ # 检查队列长度是否大于1
519
  if len(e['incoming_shipments']) > 1:
520
  arriving_next = list(e['incoming_shipments'])[1]
521
+ # 专门处理RW的2周延迟
522
  elif name in ('Wholesaler', 'Retailer') and e['incoming_shipments'].maxlen == 2:
523
+ # 如果队列长度为1,说明下周的货在[0],这周的货是0
524
+ if len(e['incoming_shipments']) == 1:
525
+ arriving_next = list(e['incoming_shipments'])[0]
526
+ arriving_this_week = 0 # 覆盖
527
+ else: # 队列为0
528
+ arriving_next = 0
529
+ arriving_this_week = 0 # 覆盖
530
 
531
+ # 重新显示修正后的"Arriving This Week"
532
  st.write(f"Arriving This Week: **{arriving_this_week}**")
533
+
534
+ st.write(f"Arriving Next Week: **{arriving_next}**")
 
 
535
 
536
  else: # Local Info Mode
537
  st.info("In Local Information mode, you can only see your own status dashboard.")
 
544
  st.metric("Backlog (Opening)", e['backlog'])
545
 
546
  with col2:
 
547
  current_incoming_order = 0
548
+ downstream_name = e['downstream_name'] # Wholesaler
549
+ if downstream_name:
550
+ current_incoming_order = state['last_week_orders'].get(downstream_name, 0)
551
  st.write(f"**Incoming Order (This Week):**\n# {current_incoming_order}")
552
 
553
  with col3:
 
555
  arriving_this_week = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
556
  st.write(f"**Shipment Arriving (This Week):**\n# {arriving_this_week}")
557
 
558
+ # Arriving NEXT week (Distributor的队列长度为1,所以[1]永远是0)
559
+ arriving_next = 0
560
  st.write(f"**Shipment Arriving (Next Week):**\n# {arriving_next}")
561
 
562
  # =======================================================
 
567
  # Prepare the state snapshot for the AI prompt (State AFTER arrivals/orders, BEFORE shipping)
568
  all_decision_point_states = {}
569
  for name in echelon_order:
570
+ e_curr = echelons[name] # This is END OF LAST WEEK state
571
  arrived = 0
572
  if name == "Factory":
573
  if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0]
 
578
  if name == "Retailer": inc_order_this_week = get_customer_demand(week)
579
  else:
580
  ds_name = e_curr['downstream_name']
581
+ if ds_name: inc_order_this_week = state['last_week_orders'].get(ds_name, 0)
 
 
 
 
582
 
583
  inv_after_arrival = e_curr['inventory'] + arrived
584
  backlog_after_new_order = e_curr['backlog'] + inc_order_this_week
 
599
  state['human_initial_order'] = int(initial_order) if initial_order is not None else 0
600
  state['decision_step'] = 'final_order'
601
 
602
+ # --- NEW: Calculate and store suggestion ONCE ---
603
  prompt_sugg = get_llm_prompt(human_echelon_state_for_prompt, week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
604
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
605
  state['current_ai_suggestion'] = ai_suggestion # Store it
606
+ # ------------------------------------------------
607
 
608
  st.rerun()
609
 
610
  elif state['decision_step'] == 'final_order':
611
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
612
 
613
+ # --- NEW: Read stored suggestion ---
614
  ai_suggestion = state.get('current_ai_suggestion', 4) # Read stored value
615
+ # -----------------------------------
616
 
617
  with st.form(key="final_order_form"):
618
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")