Lilli98 commited on
Commit
aed25f2
·
verified ·
1 Parent(s): 9911559

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +272 -121
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.11 - Corrected Order Passing Logic)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -30,7 +30,7 @@ st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide"
30
  WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
33
- ORDER_PASSING_DELAY = 1 # *** CRITICAL: This is now handled directly in step_game ***
34
  SHIPPING_DELAY = 2 # General shipping delay
35
  FACTORY_LEAD_TIME = 1
36
  FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
@@ -75,7 +75,7 @@ def init_game_state(llm_personality: str, info_sharing: str):
75
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
76
  'decision_step': 'initial_order',
77
  'human_initial_order': None,
78
- 'last_week_orders': {name: 0 for name in roles} # Store previous week's final orders
79
  }
80
 
81
  for i, name in enumerate(roles):
@@ -86,11 +86,15 @@ def init_game_state(llm_personality: str, info_sharing: str):
86
  elif name == "Factory": shipping_weeks = 0
87
  else: shipping_weeks = SHIPPING_DELAY
88
 
 
89
  st.session_state.game_state['echelons'][name] = {
90
- 'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
91
- # 'order_pipeline' is no longer needed for passing delay = 1
 
92
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
93
- 'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
 
 
94
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
95
  }
96
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
@@ -118,73 +122,84 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
118
  st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 8.")
119
  return 8, f"API_ERROR: {e}"
120
 
121
- def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
122
- # This function's logic remains correct.
123
- base_info = f"Your Current Status at the **{echelon_state['name']}** for **Week {week}**:\n- On-hand inventory: {echelon_state['inventory']} units.\n- Backlog (unfilled orders): {echelon_state['backlog']} units.\n- Incoming order this week (from your customer): {echelon_state['incoming_order']} units.\n"
124
- if echelon_state['name'] == 'Factory':
 
 
 
 
 
 
125
  task_word = "production quantity"
 
126
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
127
  else:
128
  task_word = "order quantity"
129
- # Base info now correctly reflects only incoming shipments for non-factory roles
130
- base_info += f"- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}"
131
- # We don't need 'Orders in pipeline' in the prompt anymore as delay=1 is handled directly
132
 
 
133
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
134
  stable_demand = 8
135
- if echelon_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
136
- elif echelon_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
137
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
138
  safety_stock = 4
139
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
140
- if echelon_state['name'] == 'Factory':
141
- inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
142
- inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
 
143
  else:
144
- # Simplified inventory position for delay=1
145
- inv_pos_components = f"(Inv: {echelon_state['inventory']} - Backlog: {echelon_state['backlog']} + In_Transit: {sum(echelon_state['incoming_shipments'])})"
146
- inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']))
147
  optimal_order = max(0, int(target_inventory_level - inventory_position))
148
  return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
149
 
150
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
151
- safety_stock = 4; anchor_demand = echelon_state['incoming_order']
152
- inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
153
- if echelon_state['name'] == 'Factory':
 
154
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
155
  supply_line_desc = "In Production"
156
  else:
157
- # Simplified supply line for delay=1
158
- supply_line = sum(echelon_state['incoming_shipments'])
159
  supply_line_desc = "In Transit Shipments"
160
  calculated_order = anchor_demand + inventory_correction - supply_line
161
  rational_local_order = max(0, int(calculated_order))
162
- return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock is {echelon_state['inventory'] - echelon_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
163
 
 
164
  elif llm_personality == 'human_like' and info_sharing == 'full':
165
- full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
166
- for name, e_state in all_echelons_state.items():
167
- if name != echelon_state['name']: full_info_str += f"- {name}: Inventory={e_state['inventory']}, Backlog={e_state['backlog']}\n"
 
168
  return f"""
169
- **You are a supply chain manager ({echelon_state['name']}) with full system visibility.**
170
- You can see everyone's inventory and the real customer demand.
171
  {base_info}
172
  {full_info_str}
173
- **Your Task:** Your primary responsibility is to meet the demand from your direct customer (your `Incoming order this week`: **{echelon_state['incoming_order']}** units).
174
- While you can see the stable end-customer demand ({get_customer_demand(week)} units), your priority is to fulfill the order you just received to avoid a backlog.
175
  You are still human and might get anxious about your own stock levels.
176
  What {task_word} should you decide on this week? Respond with a single integer.
177
  """
178
 
179
  elif llm_personality == 'human_like' and info_sharing == 'local':
180
  return f"""
181
- **You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.
182
  Your top priority is to NOT have a backlog.
183
  {base_info}
184
- **Your Task:** You just received an incoming order for **{echelon_state['incoming_order']}** units.
185
- Your gut instinct is to panic and {task_word.split(' ')[0]} enough to ensure you are never caught with a backlog again.
186
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
187
  """
 
 
188
 
189
  # =============== CORRECTED step_game FUNCTION ===============
190
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
@@ -193,92 +208,138 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
193
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
194
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
195
  llm_raw_responses = {}
196
-
197
- # Store pre-step state for logging
198
- pre_step_inventory = echelons[human_role]['inventory']
199
- pre_step_backlog = echelons[human_role]['backlog']
200
- arriving_shipment_this_week = list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0
201
 
202
- # --- Game Simulation Steps (Corrected Logic for Order Passing Delay = 1) ---
 
 
 
 
 
 
203
 
204
- # Step 1a: Factory Production completes and adds to Factory inventory
205
  factory_state = echelons["Factory"]
206
  produced_units = 0
207
  if state['factory_production_pipeline']:
208
  produced_units = state['factory_production_pipeline'].popleft()
209
- factory_state['inventory'] += produced_units
 
 
 
 
 
210
 
211
  # Step 1b: Shipments arrive at downstream echelons
 
212
  for name in ["Retailer", "Wholesaler", "Distributor"]:
213
  arrived_shipment = 0
214
  if echelons[name]['incoming_shipments']:
215
- arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop shipment for current week
216
- echelons[name]['inventory'] += arrived_shipment
 
 
217
 
218
  # Step 2: Orders arrive from downstream partners (using LAST week's placed order)
 
219
  for name in echelon_order:
220
  if name == "Retailer":
221
  echelons[name]['incoming_order'] = get_customer_demand(week)
222
  else:
223
- # Get the downstream partner's name
224
  downstream_name = echelons[name]['downstream_name']
 
225
  if downstream_name:
226
- # Retrieve the order placed by the downstream partner LAST week
227
  order_from_downstream = state['last_week_orders'].get(downstream_name, 0)
228
- echelons[name]['incoming_order'] = order_from_downstream
229
- else: # Should not happen except maybe week 1 if not handled
230
- echelons[name]['incoming_order'] = 0
231
 
232
- # Step 3: Fulfill orders (Ship Beer)
 
 
233
  for name in echelon_order:
234
- e = echelons[name]
235
- demand_to_meet = e['incoming_order'] + e['backlog']
236
- e['shipment_sent'] = min(e['inventory'], demand_to_meet)
237
- e['inventory'] -= e['shipment_sent']
238
- e['backlog'] = demand_to_meet - e['shipment_sent']
 
 
 
 
239
 
240
- # Step 3b: Place shipped items into the *end* of the downstream partner's incoming shipment queue
241
- for sender_name in ["Factory", "Distributor", "Wholesaler"]:
242
- sender = echelons[sender_name]
243
- receiver_name = sender['downstream_name']
244
- if receiver_name:
245
- # Append the shipment SENT this week to the receiver's incoming queue for the FUTURE
246
- echelons[receiver_name]['incoming_shipments'].append(sender['shipment_sent'])
247
 
248
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
249
- current_week_orders = {} # Store this week's decisions
 
250
  for name in echelon_order:
251
- e = echelons[name]
 
 
252
  if name == human_role:
253
  order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
254
  else:
255
- prompt = get_llm_prompt(e, week, llm_personality, info_sharing, echelons)
256
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
257
  llm_raw_responses[name] = raw_resp
258
- e['order_placed'] = max(0, order_amount) # This is the order/production decision for the week
259
- current_week_orders[name] = e['order_placed'] # Store decision for next week's Step 2
260
 
261
  # Factory schedules production based on its 'order_placed' decision
262
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
263
 
264
- # --- Logging (End of Week) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
266
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
267
 
268
  for name in echelon_order:
269
  e = echelons[name]
270
- e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
271
- for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
272
- log_entry[f'{name}.{key}'] = e[key]
 
 
 
 
 
 
 
 
 
273
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
 
 
274
  if name != 'Factory':
275
  log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
276
  else:
277
  log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
278
 
279
- log_entry[f'{human_role}.opening_inventory'] = pre_step_inventory
280
- log_entry[f'{human_role}.opening_backlog'] = pre_step_backlog
281
- log_entry[f'{human_role}.arrived_this_week'] = arriving_shipment_this_week
282
  log_entry[f'{human_role}.initial_order'] = human_initial_order
283
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
284
 
@@ -287,10 +348,11 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
287
  # --- Advance Week ---
288
  state['week'] += 1
289
  state['decision_step'] = 'initial_order'
290
- state['last_week_orders'] = current_week_orders # Store current decisions for next week
291
  if state['week'] > WEEKS: state['game_running'] = False
292
  # ==============================================================================
293
 
 
294
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
295
  # This function remains correct.
296
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
@@ -300,14 +362,14 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
300
  for _, row in df.iterrows():
301
  for e in echelons:
302
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
303
- 'inventory': row.get(f'{e}.inventory', 0),
304
  'order_placed': row.get(f'{e}.order_placed', 0),
305
  'total_cost': row.get(f'{e}.total_cost', 0)})
306
  plot_df = pd.DataFrame(plot_data)
307
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
308
- inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
309
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
310
- order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities (The Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered (Units)')
311
  total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
312
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
313
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
@@ -329,6 +391,8 @@ def save_logs_and_upload(state: dict):
329
  participant_id = state['participant_id']
330
  df = pd.json_normalize(state['logs'])
331
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
 
 
332
  df.to_csv(fname, index=False)
333
  st.success(f"Log successfully saved locally: `{fname}`")
334
  with open(fname, "rb") as f:
@@ -342,7 +406,7 @@ def save_logs_and_upload(state: dict):
342
  st.error(f"Upload to Hugging Face failed: {e}")
343
 
344
  # -----------------------------------------------------------------------------
345
- # 4. Streamlit UI (No changes needed in this section)
346
  # -----------------------------------------------------------------------------
347
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
348
 
@@ -357,13 +421,24 @@ else:
357
  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.")
358
 
359
  st.subheader("1. Your Goal: Minimize Costs")
360
- st.success("**You Need to: Fulfill the order and Minimize the total cost for your position in the supply chain.**")
361
  st.markdown("You get costs from two things every week:")
362
  st.markdown(f"""
363
- - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.
364
- - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.
365
  """)
366
-
 
 
 
 
 
 
 
 
 
 
 
367
  st.subheader("2. Your Role: The Distributor")
368
  st.markdown("""
369
  You will always play as the **Distributor**. The other 3 roles are played by AI.
@@ -376,28 +451,65 @@ else:
376
  except FileNotFoundError: st.warning("Image file not found.")
377
 
378
  st.subheader("3. The Core Challenge: Delays!")
379
- st.warning(f"This is the most important rule: **It takes {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks for an order you place to actually arrive in your inventory.**")
380
  with st.expander("Click to see a detailed example of the 3-week delay"):
381
  st.markdown(f"""
382
- Let's follow a single order you place:
383
- * **Week 10 (You):** You decide you need 50 units. You place an order for **50**.
384
- * **Week 11 (System):** Your order of 50 *arrives* at the Factory. (This is the **{ORDER_PASSING_DELAY} week Order Delay**). The Factory AI sees your order and decides to produce 50.
385
- * **Week 12 (System):** The Factory *finishes* producing the 50 units. (This is the **{FACTORY_LEAD_TIME} week Production Delay**). The Factory ships the 50 units to you.
386
- * **Week 13 (System):** The 50 units *arrive* at your warehouse. (This is the **{FACTORY_SHIPPING_DELAY} week Shipping Delay**). You can now use this inventory.
387
- **Conclusion:** You must always think 3 weeks ahead. The order you place in Week 10 will not help you until Week 13.
388
  """)
389
-
390
- st.subheader("4. How Each Week Works (Your Task)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  st.markdown(f"""
392
- Your main job is simple: place one order each week.
393
- **A) At the start of every week, the system automatically does 3 things:**
394
- * **(Step 1) Your Shipments Arrive:** The beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives and is added to your `Current Inventory`.
395
- * **(Step 2) New Orders Arrive:** You receive a new `Incoming Order` from the Wholesaler (This was their order from *last* week).
396
- * **(Step 3) You Ship Beer:** The system automatically ships as much beer as possible from your inventory to fulfill the Wholesaler's order (plus any old `Backlog`).
397
- **B) After this, you will see your new dashboard and must make your 2-part decision:**
398
- * **Step 4a (Initial Order):** Based on your new status, submit your **initial order** to the Factory.
399
- * **Step 4b (Final Order):** You will then see an **AI suggestion**. Review it, then submit your **final order** to end the week.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  """)
 
 
401
 
402
  st.markdown("---")
403
  st.header("⚙️ Game Configuration")
@@ -419,42 +531,63 @@ else:
419
  st.header(f"Week {week} / {WEEKS}")
420
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
421
  st.markdown("---")
422
- st.subheader("Supply Chain Status")
423
  if info_sharing == 'full':
424
  cols = st.columns(4)
425
  for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
426
  with cols[i]:
427
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
428
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
429
- st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
430
- if name == human_role: st.metric("Your Total Cost", f"${e['total_cost']:,.2f}")
431
- st.write(f"Incoming Order: **{e['incoming_order']}**")
 
 
 
 
 
 
 
432
  if name == "Factory":
433
- prod_completing = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
434
- st.write(f"Production Completing: **{prod_completing}**")
 
435
  else:
436
- arriving = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
437
- st.write(f"Arriving Next Week: **{arriving}**")
 
438
  else:
439
  st.info("In Local Information mode, you can only see your own status dashboard.")
440
  e = echelons[human_role]
441
- st.markdown(f"### 👤 {human_role} (Your Dashboard)")
442
  col1, col2, col3, col4 = st.columns(4)
443
- col1.metric("Current Inventory", e['inventory'])
444
- col2.metric("Current Backlog", e['backlog'])
 
 
445
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
446
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
 
447
  st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
 
 
 
448
 
449
  st.markdown("---")
450
  st.header("Your Decision (Step 4)")
451
- human_echelon_state = echelons[human_role]
 
 
 
 
 
 
 
452
 
453
  if state['decision_step'] == 'initial_order':
454
  with st.form(key="initial_order_form"):
455
- st.markdown("#### **Step 4a:** Based on the information available, submit your **initial** order.")
456
- # Default initial order to incoming order, or 4 if incoming is 0 initially
457
- default_initial = human_echelon_state['incoming_order'] if human_echelon_state['incoming_order'] > 0 else 4
458
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=default_initial)
459
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
460
  state['human_initial_order'] = int(initial_order)
@@ -463,7 +596,25 @@ else:
463
 
464
  elif state['decision_step'] == 'final_order':
465
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
466
- prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
468
 
469
  if 'final_order_input' not in st.session_state:
@@ -471,12 +622,12 @@ else:
471
 
472
  with st.form(key="final_order_form"):
473
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
474
- st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
475
  st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input')
476
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
477
  final_order_value = st.session_state.final_order_input
478
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
479
- del st.session_state.final_order_input
480
  st.rerun()
481
 
482
  st.markdown("---")
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.13 - Classic UI Timing & Backlog Explanation)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
30
  WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
33
+ ORDER_PASSING_DELAY = 1
34
  SHIPPING_DELAY = 2 # General shipping delay
35
  FACTORY_LEAD_TIME = 1
36
  FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
 
75
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
76
  'decision_step': 'initial_order',
77
  'human_initial_order': None,
78
+ 'last_week_orders': {name: 4 for name in roles} # Seed initial orders/production for week 1
79
  }
80
 
81
  for i, name in enumerate(roles):
 
86
  elif name == "Factory": shipping_weeks = 0
87
  else: shipping_weeks = SHIPPING_DELAY
88
 
89
+ # 'inventory' and 'backlog' now consistently represent END-OF-WEEK state
90
  st.session_state.game_state['echelons'][name] = {
91
+ 'name': name,
92
+ 'inventory': INITIAL_INVENTORY, # End-of-week state (used as opening state for next week)
93
+ 'backlog': INITIAL_BACKLOG, # End-of-week state (used as opening state for next week)
94
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
95
+ 'incoming_order': 0, # Order received THIS week
96
+ 'order_placed': 0, # Order placed THIS week
97
+ 'shipment_sent': 0, # Shipment sent THIS week
98
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
99
  }
100
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
 
122
  st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 8.")
123
  return 8, f"API_ERROR: {e}"
124
 
125
+ # =============== MODIFIED FUNCTION (Prompt uses state AFTER arrivals/orders) ===============
126
+ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_after_arrivals: dict) -> str:
127
+ """Generates the prompt for the LLM based on the game scenario.
128
+ Uses the state AFTER arrivals and new orders are processed, as this is the decision point."""
129
+
130
+ e_state = echelon_state_after_arrivals # Use the passed-in state for prompts
131
+
132
+ base_info = f"Your Current Status at the **{e_state['name']}** for **Week {week}** (Before Shipping):\n- On-hand inventory: {e_state['inventory']} units.\n- Backlog (total unfilled orders): {e_state['backlog']} units.\n- Incoming order this week (just received): {e_state['incoming_order']} units.\n"
133
+
134
+ if e_state['name'] == 'Factory':
135
  task_word = "production quantity"
136
+ # Factory needs access to the global pipeline state
137
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
138
  else:
139
  task_word = "order quantity"
140
+ # Non-factory prompt needs its incoming shipments queue
141
+ base_info += f"- Shipments on the way to you (arriving next week and later): {list(e_state['incoming_shipments'])}"
 
142
 
143
+ # --- Perfect Rational ---
144
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
145
  stable_demand = 8
146
+ if e_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
147
+ elif e_state['name'] == 'Distributor': total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY
148
  else: total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY
149
  safety_stock = 4
150
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
151
+ # Calculate Inventory Position based on state AFTER arrivals/orders
152
+ if e_state['name'] == 'Factory':
153
+ inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Production: {sum(st.session_state.game_state['factory_production_pipeline'])})"
154
+ inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
155
  else:
156
+ inv_pos_components = f"(Current Inv: {e_state['inventory']} - Current Backlog: {e_state['backlog']} + In_Transit: {sum(e_state['incoming_shipments'])})"
157
+ inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']))
 
158
  optimal_order = max(0, int(target_inventory_level - inventory_position))
159
  return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
160
 
161
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
162
+ safety_stock = 4; anchor_demand = e_state['incoming_order']
163
+ # Use state AFTER arrivals/orders for inventory correction calculation
164
+ inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
165
+ if e_state['name'] == 'Factory':
166
  supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
167
  supply_line_desc = "In Production"
168
  else:
169
+ supply_line = sum(e_state['incoming_shipments'])
 
170
  supply_line_desc = "In Transit Shipments"
171
  calculated_order = anchor_demand + inventory_correction - supply_line
172
  rational_local_order = max(0, int(calculated_order))
173
+ return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock (before shipping) is {e_state['inventory'] - e_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
174
 
175
+ # --- Human-like ---
176
  elif llm_personality == 'human_like' and info_sharing == 'full':
177
+ full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
178
+ # Display other players' state AFTER arrivals/orders
179
+ for name, other_e_state in all_echelons_state_after_arrivals.items():
180
+ if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
181
  return f"""
182
+ **You are a supply chain manager ({e_state['name']}) with full system visibility.**
183
+ You can see everyone's current inventory and backlog before shipping, and the real customer demand.
184
  {base_info}
185
  {full_info_str}
186
+ **Your Task:** Your primary responsibility is to meet the demand from your direct customer (your `Incoming order this week`: **{e_state['incoming_order']}** units), which contributes to your total current backlog of {e_state['backlog']}.
187
+ While you can see the stable end-customer demand ({get_customer_demand(week)} units), your priority is to fulfill the order you just received and manage your inventory/backlog.
188
  You are still human and might get anxious about your own stock levels.
189
  What {task_word} should you decide on this week? Respond with a single integer.
190
  """
191
 
192
  elif llm_personality == 'human_like' and info_sharing == 'local':
193
  return f"""
194
+ **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.
195
  Your top priority is to NOT have a backlog.
196
  {base_info}
197
+ **Your Task:** You just received an incoming order for **{e_state['incoming_order']}** units, adding to your total backlog.
198
+ Your gut instinct is to panic and {task_word.split(' ')[0]} enough to ensure you are never caught with a backlog again, considering your current inventory.
199
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
200
  """
201
+ # ==============================================================================
202
+
203
 
204
  # =============== CORRECTED step_game FUNCTION ===============
205
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
208
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
209
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
210
  llm_raw_responses = {}
 
 
 
 
 
211
 
212
+ # Store state at the very beginning of the week for logging opening balances
213
+ # These are the inventory/backlog values from the END of the previous week
214
+ opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
215
+ opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
216
+ arrived_this_week = {name: 0 for name in roles} # Track arrivals for logging
217
+
218
+ # --- Game Simulation Steps ---
219
 
220
+ # Step 1a: Factory Production completes
221
  factory_state = echelons["Factory"]
222
  produced_units = 0
223
  if state['factory_production_pipeline']:
224
  produced_units = state['factory_production_pipeline'].popleft()
225
+ # Temporarily store, don't update main state yet
226
+ inventory_after_production = factory_state['inventory'] + produced_units
227
+ arrived_this_week["Factory"] = produced_units
228
+ else:
229
+ inventory_after_production = factory_state['inventory']
230
+
231
 
232
  # Step 1b: Shipments arrive at downstream echelons
233
+ inventory_after_arrival = {} # Store intermediate state
234
  for name in ["Retailer", "Wholesaler", "Distributor"]:
235
  arrived_shipment = 0
236
  if echelons[name]['incoming_shipments']:
237
+ arrived_shipment = echelons[name]['incoming_shipments'].popleft()
238
+ arrived_this_week[name] = arrived_shipment
239
+ inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
240
+ inventory_after_arrival["Factory"] = inventory_after_production # Add factory's state
241
 
242
  # Step 2: Orders arrive from downstream partners (using LAST week's placed order)
243
+ total_backlog_before_shipping = {} # Store intermediate state
244
  for name in echelon_order:
245
  if name == "Retailer":
246
  echelons[name]['incoming_order'] = get_customer_demand(week)
247
  else:
 
248
  downstream_name = echelons[name]['downstream_name']
249
+ order_from_downstream = 0
250
  if downstream_name:
 
251
  order_from_downstream = state['last_week_orders'].get(downstream_name, 0)
252
+ echelons[name]['incoming_order'] = order_from_downstream
253
+ # Calculate the total backlog BEFORE shipping
254
+ total_backlog_before_shipping[name] = echelons[name]['backlog'] + echelons[name]['incoming_order']
255
 
256
+ # --- Create State Snapshot for AI/Human Decision Point ---
257
+ # This reflects the state AFTER arrivals and new orders, BEFORE shipping
258
+ decision_point_states = {}
259
  for name in echelon_order:
260
+ # Need to create a copy, including deque if needed for prompt
261
+ decision_point_states[name] = {
262
+ 'name': name,
263
+ 'inventory': inventory_after_arrival[name], # Inventory available
264
+ 'backlog': total_backlog_before_shipping[name], # Total demand to meet
265
+ 'incoming_order': echelons[name]['incoming_order'], # Order received this week
266
+ # Pass the current state of queues for prompt generation
267
+ 'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
268
+ }
269
 
 
 
 
 
 
 
 
270
 
271
  # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
272
+ # Agents make decisions based on the decision_point_states
273
+ current_week_orders = {}
274
  for name in echelon_order:
275
+ e = echelons[name] # Get the main state dict to store results
276
+ prompt_state = decision_point_states[name] # Use the snapshot for the prompt
277
+
278
  if name == human_role:
279
  order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
280
  else:
281
+ prompt = get_llm_prompt(prompt_state, week, llm_personality, info_sharing, decision_point_states)
282
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
283
  llm_raw_responses[name] = raw_resp
284
+ e['order_placed'] = max(0, order_amount) # Store the decision in the main state dict
285
+ current_week_orders[name] = e['order_placed']
286
 
287
  # Factory schedules production based on its 'order_placed' decision
288
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
289
 
290
+
291
+ # --- Step 3: Fulfill orders (Ship Beer) ---
292
+ # Now perform the shipping based on the inventory_after_arrival and total_backlog_before_shipping
293
+ for name in echelon_order:
294
+ e = echelons[name]
295
+ demand_to_meet = total_backlog_before_shipping[name]
296
+ available_inv = inventory_after_arrival[name]
297
+
298
+ e['shipment_sent'] = min(available_inv, demand_to_meet)
299
+ # Update the main state dict's inventory and backlog to reflect END OF WEEK state
300
+ e['inventory'] = available_inv - e['shipment_sent']
301
+ e['backlog'] = demand_to_meet - e['shipment_sent']
302
+
303
+ # Step 3b: Place shipped items into the *end* of the downstream partner's incoming shipment queue
304
+ for sender_name in ["Factory", "Distributor", "Wholesaler"]:
305
+ sender = echelons[sender_name]
306
+ receiver_name = sender['downstream_name']
307
+ if receiver_name:
308
+ echelons[receiver_name]['incoming_shipments'].append(sender['shipment_sent'])
309
+
310
+
311
+ # --- Calculate Costs & Log (End of Week) ---
312
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
313
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'], log_entry['last_week_orders']
314
 
315
  for name in echelon_order:
316
  e = echelons[name]
317
+ # Costs are based on the END OF WEEK state
318
+ e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
319
+ e['total_cost'] += e['weekly_cost']
320
+
321
+ # Log end-of-week internal state and decisions/events of the week
322
+ log_entry[f'{name}.inventory'] = e['inventory'] # End of week inventory
323
+ log_entry[f'{name}.backlog'] = e['backlog'] # End of week backlog
324
+ log_entry[f'{name}.incoming_order'] = e['incoming_order'] # Order received this week
325
+ log_entry[f'{name}.order_placed'] = e['order_placed'] # Decision made this week
326
+ log_entry[f'{name}.shipment_sent'] = e['shipment_sent'] # Shipped this week
327
+ log_entry[f'{name}.weekly_cost'] = e['weekly_cost'] # Cost for this week
328
+ log_entry[f'{name}.total_cost'] = e['total_cost'] # Cumulative cost
329
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
330
+
331
+ # Log opening balances for the week
332
+ log_entry[f'{name}.opening_inventory'] = opening_inventories[name]
333
+ log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
334
+ log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
335
+
336
+ # Log prediction for next week's arrival/completion (based on queues AFTER this week's processing)
337
  if name != 'Factory':
338
  log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
339
  else:
340
  log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
341
 
342
+ # Log human-specific decisions
 
 
343
  log_entry[f'{human_role}.initial_order'] = human_initial_order
344
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
345
 
 
348
  # --- Advance Week ---
349
  state['week'] += 1
350
  state['decision_step'] = 'initial_order'
351
+ state['last_week_orders'] = current_week_orders # Store current decisions for next week's Step 2
352
  if state['week'] > WEEKS: state['game_running'] = False
353
  # ==============================================================================
354
 
355
+
356
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
357
  # This function remains correct.
358
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
 
362
  for _, row in df.iterrows():
363
  for e in echelons:
364
  plot_data.append({'week': row.get('week', 0), 'echelon': e,
365
+ 'inventory': row.get(f'{e}.inventory', 0), # Use end-of-week inventory for plots
366
  'order_placed': row.get(f'{e}.order_placed', 0),
367
  'total_cost': row.get(f'{e}.total_cost', 0)})
368
  plot_df = pd.DataFrame(plot_data)
369
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
370
+ inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels (End of Week)'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
371
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
372
+ order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities / Production Decisions'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered/Produced (Units)')
373
  total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()]
374
  total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
375
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
 
391
  participant_id = state['participant_id']
392
  df = pd.json_normalize(state['logs'])
393
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
394
+ for col in df.columns:
395
+ if df[col].dtype == 'object': df[col] = df[col].astype(str)
396
  df.to_csv(fname, index=False)
397
  st.success(f"Log successfully saved locally: `{fname}`")
398
  with open(fname, "rb") as f:
 
406
  st.error(f"Upload to Hugging Face failed: {e}")
407
 
408
  # -----------------------------------------------------------------------------
409
+ # 4. Streamlit UI
410
  # -----------------------------------------------------------------------------
411
  st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
412
 
 
421
  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.")
422
 
423
  st.subheader("1. Your Goal: Minimize Costs")
424
+ st.success("**Your single, most important goal is to: Minimize the total cost for your position in the supply chain.**")
425
  st.markdown("You get costs from two things every week:")
426
  st.markdown(f"""
427
+ - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.** (Cost applies to inventory left *after* shipping)
428
+ - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.** (Cost applies to orders you couldn't fill *after* shipping)
429
  """)
430
+ with st.expander("Click to see a cost calculation example"):
431
+ st.markdown(f"""
432
+ Imagine at the **end** of Week 5, *after* you shipped beer to the Wholesaler, your final state is:
433
+ - Inventory: 10 units
434
+ - Backlog: 0 units
435
+ Your cost for Week 5 would be calculated *at this point*:
436
+ - `(10 units of Inventory * ${HOLDING_COST:,.2f})` = $5.00
437
+ - `(0 units of Backlog * ${BACKLOG_COST:,.2f})` = $0.00
438
+ - **Total Weekly Cost:** = **$5.00**
439
+ This cost is added to your cumulative total.
440
+ """)
441
+
442
  st.subheader("2. Your Role: The Distributor")
443
  st.markdown("""
444
  You will always play as the **Distributor**. The other 3 roles are played by AI.
 
451
  except FileNotFoundError: st.warning("Image file not found.")
452
 
453
  st.subheader("3. The Core Challenge: Delays!")
454
+ 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.")
455
  with st.expander("Click to see a detailed example of the 3-week delay"):
456
  st.markdown(f"""
457
+ * **Week 10 (You):** You place an order for **50**.
458
+ * **Week 11 (System):** Your order arrives at the Factory (**{ORDER_PASSING_DELAY}w Order Delay**). Factory AI decides to produce 50.
459
+ * **Week 12 (System):** Factory finishes producing 50 (**{FACTORY_LEAD_TIME}w Production Delay**) & ships it.
460
+ * **Week 13 (System):** The 50 units arrive at your warehouse (**{FACTORY_SHIPPING_DELAY}w Shipping Delay**).
461
+ **Conclusion:** Think 3 weeks ahead! Your order in Week 10 arrives at the start of Week 13.
 
462
  """)
463
+
464
+ # =============== NEW: Understanding Inventory & Backlog ===============
465
+ st.subheader("4. Understanding Inventory & Backlog")
466
+ st.markdown("""
467
+ Managing your inventory and backlog is key to minimizing costs. Here's how they work:
468
+ * **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.
469
+ * **If you DON'T have enough inventory:**
470
+ * You ship **all** the inventory you have.
471
+ * The remaining unfilled "Orders to Fill" becomes your **new Backlog** for next week.
472
+ * **Backlog is cumulative!** If you have a backlog of 5 and get a new order for 8 but can only ship 10, your new backlog is `(5 + 8) - 10 = 3`.
473
+ * **If you DO have enough inventory:**
474
+ * You ship all the "Orders to Fill".
475
+ * Your Backlog becomes 0.
476
+ * The remaining inventory is carried over to next week (and incurs holding costs).
477
+ """)
478
+ # ====================================================================
479
+
480
+ #st.subheader("5. The Bullwhip Effect (What to Avoid)")
481
+ #st.markdown("""
482
+ #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.
483
+ #""")
484
+
485
+ # =============== UPDATED: How Each Week Works & Dashboard Explanation ===============
486
+ st.subheader("5. How Each Week Works & Understanding Your Dashboard")
487
  st.markdown(f"""
488
+ Your main job is simple: place one order each week based on the dashboard presented to you.
489
+
490
+ **A) At the start of every week, BEFORE your turn:**
491
+ * **(Step 1) Shipments Arrive:** Beer you ordered {ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY} weeks ago arrives.
492
+ * **(Step 2) New Orders Arrive:** You receive a new order from the Wholesaler (their order from *last* week).
493
+ * **(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.
494
+
495
+ **B) Your Dashboard (What You See for Your Turn):**
496
+ The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
497
+ * `Inventory`: Your stock **at the beginning of the week**. This is the inventory carried over from the end of last week.
498
+ * `Backlog`: Unfilled orders **carried over from the end of last week**.
499
+ * `Incoming Order`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2).
500
+ * `Shipment Arriving (Next Week)`: The quantity scheduled to arrive at the start of the *next* week (Week {week+1}).
501
+ * `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
502
+ * `Cost Last Week`: The specific cost incurred just **last week**.
503
+
504
+ **C) Your Decision (Step 4 - Two Parts):**
505
+ Now, looking at the dashboard (showing the start-of-week state) and considering the incoming order and future arrivals, you decide how much to order:
506
+ * **(Step 4a - Initial Order):** Submit your first estimate.
507
+ * **(Step 4b - Final Order):** See the AI's suggestion, then submit your final decision. This order will arrive in 3 weeks.
508
+
509
+ Submitting 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.
510
  """)
511
+ # ==============================================================================
512
+
513
 
514
  st.markdown("---")
515
  st.header("⚙️ Game Configuration")
 
531
  st.header(f"Week {week} / {WEEKS}")
532
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
533
  st.markdown("---")
534
+ st.subheader("Supply Chain Status (Start of Week State)") # Clarified Timing
535
  if info_sharing == 'full':
536
  cols = st.columns(4)
537
  for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
538
  with cols[i]:
539
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
540
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
541
+ # Display the END OF LAST WEEK state (which is OPENING state for this week)
542
+ st.metric("Inventory (Opening)", e['inventory']); st.metric("Backlog (Opening)", e['backlog'])
543
+
544
+ if name == human_role:
545
+ st.metric("Total Cost (Cumulative)", f"${e['total_cost']:,.2f}")
546
+ last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
547
+ st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
548
+
549
+ # Display info about THIS week's events
550
+ st.write(f"Incoming Order (This Week): **{e['incoming_order']}**") # Order arriving in Step 2
551
  if name == "Factory":
552
+ # Production completing NEXT week
553
+ prod_completing_next = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
554
+ st.write(f"Completing Next Week: **{prod_completing_next}**")
555
  else:
556
+ # Shipment arriving NEXT week
557
+ arriving_next = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
558
+ st.write(f"Arriving Next Week: **{arriving_next}**")
559
  else:
560
  st.info("In Local Information mode, you can only see your own status dashboard.")
561
  e = echelons[human_role]
562
+ st.markdown(f"### 👤 {human_role} (Your Dashboard - Start of Week State)")
563
  col1, col2, col3, col4 = st.columns(4)
564
+ # Display OPENING state
565
+ col1.metric("Inventory (Opening)", e['inventory'])
566
+ col2.metric("Backlog (Opening)", e['backlog'])
567
+ # Display info about THIS week's events / NEXT week's arrivals
568
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
569
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
570
+
571
  st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
572
+ last_week_cost = state['logs'][-1][f"{human_role}.weekly_cost"] if week > 1 and state['logs'] else 0
573
+ st.metric("Cost Last Week", f"${last_week_cost:,.2f}")
574
+
575
 
576
  st.markdown("---")
577
  st.header("Your Decision (Step 4)")
578
+ human_echelon_state_for_prompt = { # Construct the state needed for the prompt (AFTER arrivals/orders)
579
+ 'name': human_role,
580
+ 'inventory': echelons[human_role]['inventory'] + (list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0), # Opening + Arriving This Week (approx)
581
+ 'backlog': echelons[human_role]['backlog'] + echelons[human_role]['incoming_order'], # Opening + Incoming Order This Week
582
+ 'incoming_order': echelons[human_role]['incoming_order'],
583
+ 'incoming_shipments': echelons[human_role]['incoming_shipments'] # Pass the queue
584
+ }
585
+
586
 
587
  if state['decision_step'] == 'initial_order':
588
  with st.form(key="initial_order_form"):
589
+ st.markdown("#### **Step 4a:** Based on the dashboard, submit your **initial** order to the Factory.")
590
+ default_initial = echelons[human_role]['incoming_order'] if echelons[human_role]['incoming_order'] > 0 else 4
 
591
  initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=default_initial)
592
  if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
593
  state['human_initial_order'] = int(initial_order)
 
596
 
597
  elif state['decision_step'] == 'final_order':
598
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
599
+ # Prepare all states AFTER arrivals/orders for the full info prompt context
600
+ all_decision_point_states = {}
601
+ for name in echelon_order:
602
+ e_curr = echelons[name]
603
+ arrived = 0
604
+ if name == "Factory":
605
+ if state['factory_production_pipeline']: arrived = list(state['factory_production_pipeline'])[0] # Peek, don't pop here
606
+ else:
607
+ if e_curr['incoming_shipments']: arrived = list(e_curr['incoming_shipments'])[0] # Peek
608
+
609
+ all_decision_point_states[name] = {
610
+ 'name': name,
611
+ 'inventory': e_curr['inventory'] + arrived,
612
+ 'backlog': e_curr['backlog'] + e_curr['incoming_order'],
613
+ 'incoming_order': e_curr['incoming_order'],
614
+ 'incoming_shipments': e_curr['incoming_shipments'].copy() if name != "Factory" else deque()
615
+ }
616
+
617
+ prompt_sugg = get_llm_prompt(all_decision_point_states[human_role], week, state['llm_personality'], state['info_sharing'], all_decision_point_states)
618
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
619
 
620
  if 'final_order_input' not in st.session_state:
 
622
 
623
  with st.form(key="final_order_form"):
624
  st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
625
+ st.markdown("Considering the AI's advice, submit your **final** order to end the week. (This order will arrive in 3 weeks).")
626
  st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input')
627
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
628
  final_order_value = st.session_state.final_order_input
629
  step_game(final_order_value, state['human_initial_order'], ai_suggestion)
630
+ if 'final_order_input' in st.session_state: del st.session_state.final_order_input
631
  st.rerun()
632
 
633
  st.markdown("---")