Lilli98 commited on
Commit
fcf7c15
·
verified ·
1 Parent(s): 7143909

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -121
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4.9 - Detailed Intro & Real-time Cost)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -31,9 +31,9 @@ WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
33
  ORDER_PASSING_DELAY = 1
34
- SHIPPING_DELAY = 2
35
  FACTORY_LEAD_TIME = 1
36
- FACTORY_SHIPPING_DELAY = 1
37
  HOLDING_COST = 0.5
38
  BACKLOG_COST = 1.0
39
 
@@ -57,7 +57,7 @@ else:
57
 
58
 
59
  # -----------------------------------------------------------------------------
60
- # 3. Core Game Logic Functions (No changes in this section)
61
  # -----------------------------------------------------------------------------
62
 
63
  def get_customer_demand(week: int) -> int:
@@ -80,10 +80,11 @@ def init_game_state(llm_personality: str, info_sharing: str):
80
  for i, name in enumerate(roles):
81
  upstream = roles[i + 1] if i + 1 < len(roles) else None
82
  downstream = roles[i - 1] if i - 1 >= 0 else None
83
-
 
84
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
85
- elif name == "Factory": shipping_weeks = 0
86
- else: shipping_weeks = SHIPPING_DELAY
87
 
88
  st.session_state.game_state['echelons'][name] = {
89
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
@@ -110,23 +111,21 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
110
  raw_text = response.choices[0].message.content.strip()
111
  match = re.search(r'\d+', raw_text)
112
  if match: return int(match.group(0)), raw_text
113
- return 8, raw_text
 
114
  except Exception as e:
115
- st.error(f"API call failed for {echelon_name}: {e}")
116
  return 8, f"API_ERROR: {e}"
117
 
118
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
119
- """Generates the prompt for the LLM based on the game scenario."""
120
-
121
  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"
122
-
123
  if echelon_state['name'] == 'Factory':
124
  task_word = "production quantity"
125
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
126
  else:
127
  task_word = "order quantity"
128
  base_info += f"- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}\n- Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}"
129
-
130
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
131
  stable_demand = 8
132
  if echelon_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
@@ -142,7 +141,6 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
142
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline']))
143
  optimal_order = max(0, int(target_inventory_level - inventory_position))
144
  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."
145
-
146
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
147
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
148
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
@@ -155,7 +153,6 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
155
  calculated_order = anchor_demand + inventory_correction - supply_line
156
  rational_local_order = max(0, int(calculated_order))
157
  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."
158
-
159
  elif llm_personality == 'human_like' and info_sharing == 'full':
160
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
161
  for name, e_state in all_echelons_state.items():
@@ -170,7 +167,6 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
170
  You are still human and might get anxious about your own stock levels.
171
  What {task_word} should you decide on this week? Respond with a single integer.
172
  """
173
-
174
  elif llm_personality == 'human_like' and info_sharing == 'local':
175
  return f"""
176
  **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.
@@ -181,34 +177,62 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
181
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
182
  """
183
 
 
184
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
185
- # This core logic function remains correct and unchanged.
186
  state = st.session_state.game_state
187
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
188
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
189
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
190
  llm_raw_responses = {}
191
-
 
192
  pre_step_inventory = echelons[human_role]['inventory']
193
  pre_step_backlog = echelons[human_role]['backlog']
 
 
194
 
 
 
195
  factory_state = echelons["Factory"]
196
- if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
 
 
 
 
 
197
  for name in ["Retailer", "Wholesaler", "Distributor"]:
198
- if echelons[name]['incoming_shipments']: echelons[name]['inventory'] += echelons[name]['incoming_shipments'].popleft()
 
 
 
 
 
199
  for name in echelon_order:
200
- if name == "Retailer": echelons[name]['incoming_order'] = get_customer_demand(week)
 
201
  else:
202
  downstream = echelons[name]['downstream_name']
 
203
  if downstream and echelons[downstream]['order_pipeline']:
204
- echelons[name]['incoming_order'] = echelons[downstream]['order_pipeline'].popleft()
205
- for name in echelon_order:
206
- e = echelons[name]; demand = e['incoming_order'] + e['backlog']
207
- e['shipment_sent'] = min(e['inventory'], demand); e['inventory'] -= e['shipment_sent']; e['backlog'] = demand - e['shipment_sent']
208
- for sender in ["Factory", "Distributor", "Wholesaler"]:
209
- receiver = echelons[sender]['downstream_name']
210
- if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  for name in echelon_order:
213
  e = echelons[name]
214
  if name == human_role:
@@ -217,42 +241,67 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
217
  prompt = get_llm_prompt(e, week, llm_personality, info_sharing, echelons)
218
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
219
  llm_raw_responses[name] = raw_resp
220
- e['order_placed'] = max(0, order_amount)
221
- if name != "Factory": e['order_pipeline'].append(e['order_placed'])
 
 
 
222
 
 
223
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
224
 
 
225
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
 
226
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
 
227
  for name in echelon_order:
228
  e = echelons[name]
 
229
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
 
 
230
  for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
231
  log_entry[f'{name}.{key}'] = e[key]
232
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
233
-
 
 
 
 
 
 
 
 
 
 
234
  log_entry[f'{human_role}.opening_inventory'] = pre_step_inventory
235
  log_entry[f'{human_role}.opening_backlog'] = pre_step_backlog
236
- log_entry[f'{human_role}.initial_order'] = human_initial_order
237
- log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
238
-
 
239
  state['logs'].append(log_entry)
240
 
 
241
  state['week'] += 1
242
  state['decision_step'] = 'initial_order'
243
  if state['week'] > WEEKS: state['game_running'] = False
 
244
 
245
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
246
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
247
  fig.suptitle(title, fontsize=16)
248
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
249
-
250
  plot_data = []
251
  for _, row in df.iterrows():
252
  for e in echelons:
253
- plot_data.append({'week': row['week'], 'echelon': e,
254
- 'inventory': row[f'{e}.inventory'], 'order_placed': row[f'{e}.order_placed'],
255
- 'total_cost': row[f'{e}.total_cost']})
 
 
256
  plot_df = pd.DataFrame(plot_data)
257
 
258
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
@@ -261,15 +310,24 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
261
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
262
  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)')
263
 
264
- total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
 
 
265
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
266
 
267
- human_df = df[['week', f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']].copy()
 
 
 
268
  human_df.rename(columns={
269
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
270
  }, inplace=True)
271
- human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-'); axes[3].set_title(f'Analysis of Your ({human_role}) Decisions'); axes[3].set_ylabel('Order Quantity'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
272
-
 
 
 
 
273
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
274
 
275
  def save_logs_and_upload(state: dict):
@@ -299,37 +357,43 @@ if st.session_state.get('initialization_error'):
299
  else:
300
  # --- Game Setup & Instructions ---
301
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
302
-
303
- # =============== NEW DETAILED INTRODUCTION ===============
304
  st.markdown("---")
305
  st.header("📖 Welcome to the Beer Game!")
306
- st.markdown("This is a simulation of a supply chain. You will play against 3 AI agents. ** Please read these instructions carefully.")
307
 
308
  st.subheader("1. Your Goal: Minimize Costs")
309
- st.success("**Your goal is to: Minimize the total cost for your position in the supply chain.**")
310
  st.markdown("You get costs from two things every week:")
311
  st.markdown(f"""
312
- - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.
313
- - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.
314
  """)
315
-
 
 
 
 
 
 
 
 
 
 
 
316
  st.subheader("2. Your Role: The Distributor")
317
  st.markdown("""
318
  You will always play as the **Distributor**. The other 3 roles are played by AI.
319
-
320
- - **Retailer (AI):** Fulfills end-customer demand. Orders from the Wholesaler.
321
- - **Wholesaler (AI):** Fulfills Retailer orders. Orders from the Distributor.
322
- - **Distributor (You):** Fulfills Wholesaler orders. Orders from the Factory.
323
- - **Factory (AI):** Fulfills your orders. Produces new beer.
324
  """)
325
- try:
326
- st.image(IMAGE_PATH, caption="You are the Distributor. You get orders from the Wholesaler and place orders to the Factory.")
327
- except FileNotFoundError:
328
- st.warning("Image file not found. Please ensure 'beer_game_diagram.png' is uploaded to the repository.")
329
-
330
  st.subheader("3. The Core Challenge: Delays!")
331
  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.**")
332
-
333
  with st.expander("Click to see a detailed example of the 3-week delay"):
334
  st.markdown(f"""
335
  Let's follow a single order you place:
@@ -337,32 +401,27 @@ else:
337
  * **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.
338
  * **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.
339
  * **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.
340
-
341
  **Conclusion:** You must always think 3 weeks ahead. The order you place in Week 10 will not help you until Week 13.
342
  """)
343
-
344
- #st.subheader("4. The Bullwhip Effect (What to Avoid)")
345
- #st.markdown("""
346
- #The "Bullwhip Effect" is the main challenge of this game. It describes how small, normal changes in customer demand (at the Retailer) get **amplified** into huge, chaotic swings in orders as they move up the supply chain.
347
-
348
- #This often leads to a cycle of **panic ordering** (ordering way too much because you are out of stock) followed by a **massive pile-up of inventory** (when all your late orders finally arrive). This cycle is extremely expensive. Your goal is to avoid it by ordering smoothly.
349
- #""")
350
-
351
- st.subheader("4. How Each Week Works (Your Task)")
352
  st.markdown(f"""
353
  Your main job is simple: place one order each week.
354
-
355
  **A) At the start of every week, the system automatically does 3 things:**
356
- * **(Step 1) Your Shipments Arrive:** The beer you ordered 3 weeks ago arrives and is added to your `Current Inventory`.
357
  * **(Step 2) New Orders Arrive:** You receive a new `Incoming Order` from the Wholesaler.
358
  * **(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`).
359
-
360
  **B) After this, you will see your new dashboard and must make your 2-part decision:**
361
  * **Step 4a (Initial Order):** Based on your new status, submit your **initial order** to the Factory.
362
  * **Step 4b (Final Order):** You will then see an **AI suggestion**. Review it, then submit your **final order** to end the week.
363
  """)
364
- # =======================================================
365
-
366
  st.markdown("---")
367
  st.header("⚙️ Game Configuration")
368
  c1, c2 = st.columns(2)
@@ -370,7 +429,7 @@ else:
370
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
371
  with c2:
372
  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.")
373
-
374
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
375
  init_game_state(llm_personality, info_sharing)
376
  st.rerun()
@@ -379,7 +438,7 @@ else:
379
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
380
  state = st.session_state.game_state
381
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
382
-
383
  st.header(f"Week {week} / {WEEKS}")
384
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
385
  st.markdown("---")
@@ -391,12 +450,7 @@ else:
391
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
392
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
393
  st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
394
-
395
- # =============== NEW: REAL-TIME COST METRIC ===============
396
- if name == human_role:
397
- st.metric("Your Total Cost", f"${e['total_cost']:,.2f}")
398
- # =========================================================
399
-
400
  st.write(f"Incoming Order: **{e['incoming_order']}**")
401
  if name == "Factory":
402
  prod_completing = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
@@ -406,18 +460,15 @@ else:
406
  st.write(f"Arriving Next Week: **{arriving}**")
407
  else:
408
  st.info("In Local Information mode, you can only see your own status dashboard.")
409
- e = echelons[human_role] # This is always the Distributor
410
  st.markdown(f"### 👤 {human_role} (Your Dashboard)")
411
  col1, col2, col3, col4 = st.columns(4)
412
  col1.metric("Current Inventory", e['inventory'])
413
  col2.metric("Current Backlog", e['backlog'])
414
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
415
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
416
-
417
- # =============== NEW: REAL-TIME COST METRIC ===============
418
  st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
419
- # =========================================================
420
-
421
  st.markdown("---")
422
  st.header("Your Decision (Step 4)")
423
  human_echelon_state = echelons[human_role]
@@ -430,12 +481,12 @@ else:
430
  state['human_initial_order'] = int(initial_order)
431
  state['decision_step'] = 'final_order'
432
  st.rerun()
433
-
434
  elif state['decision_step'] == 'final_order':
435
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
436
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
437
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
438
-
439
  if 'final_order_input' not in st.session_state:
440
  st.session_state.final_order_input = ai_suggestion
441
 
@@ -450,34 +501,65 @@ else:
450
  st.rerun()
451
 
452
  st.markdown("---")
 
453
  with st.expander("📖 Your Weekly Decision Log", expanded=False):
454
- if not state['logs']:
455
  st.write("Your weekly history will be displayed here after you complete the first week.")
456
  else:
457
- history_df = pd.json_normalize(state['logs'])
458
- human_cols = {
459
- 'week': 'Week', f'{human_role}.opening_inventory': 'Opening Inv.',
460
- f'{human_role}.opening_backlog': 'Opening Backlog', f'{human_role}.incoming_order': 'Incoming Order',
461
- f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion',
462
- f'{human_role}.order_placed': 'Your Final Order', f'{human_role}.arriving_next_week': 'Arriving Next Week',
463
- f'{human_role}.weekly_cost': 'Weekly Cost',
464
- }
465
- display_cols = [col for col in human_cols.keys() if col in history_df.columns]
466
- display_df = history_df[display_cols].rename(columns=human_cols)
467
- if 'Weekly Cost' in display_df:
468
- display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}")
469
- st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
 
471
- try:
472
- st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
473
- except FileNotFoundError:
474
- st.sidebar.warning("Image file not found.")
475
-
476
  st.sidebar.header("Game Info")
477
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
478
  if st.sidebar.button("🔄 Reset Game"):
479
- if 'final_order_input' in st.session_state:
480
- del st.session_state.final_order_input
481
  del st.session_state.game_state
482
  st.rerun()
483
 
@@ -485,16 +567,19 @@ else:
485
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
486
  st.header("🎉 Game Over!")
487
  state = st.session_state.game_state
488
- logs_df = pd.json_normalize(state['logs'])
489
-
490
- fig = plot_results(
491
- logs_df,
492
- f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality']} | Info: {state['info_sharing']})",
493
- state['human_role']
494
- )
495
-
496
- st.pyplot(fig)
497
- save_logs_and_upload(state)
 
 
 
498
  if st.button("✨ Start a New Game"):
499
  del st.session_state.game_state
500
  st.rerun()
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.10 - Added Arrival Log Column - Complete Code)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
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
37
  HOLDING_COST = 0.5
38
  BACKLOG_COST = 1.0
39
 
 
57
 
58
 
59
  # -----------------------------------------------------------------------------
60
+ # 3. Core Game Logic Functions
61
  # -----------------------------------------------------------------------------
62
 
63
  def get_customer_demand(week: int) -> int:
 
80
  for i, name in enumerate(roles):
81
  upstream = roles[i + 1] if i + 1 < len(roles) else None
82
  downstream = roles[i - 1] if i - 1 >= 0 else None
83
+
84
+ # Determine shipping delay for incoming goods for this role
85
  if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
86
+ elif name == "Factory": shipping_weeks = 0 # Factory produces, doesn't receive shipments
87
+ else: shipping_weeks = SHIPPING_DELAY # Retailer/Wholesaler use general delay
88
 
89
  st.session_state.game_state['echelons'][name] = {
90
  'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
 
111
  raw_text = response.choices[0].message.content.strip()
112
  match = re.search(r'\d+', raw_text)
113
  if match: return int(match.group(0)), raw_text
114
+ st.warning(f"LLM for {echelon_name} did not return a valid number. Defaulting to 8. Raw Response: '{raw_text}'")
115
+ return 8, raw_text # Default if no number found
116
  except Exception as e:
117
+ st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 8.")
118
  return 8, f"API_ERROR: {e}"
119
 
120
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
121
+ # This function's logic remains correct.
 
122
  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"
 
123
  if echelon_state['name'] == 'Factory':
124
  task_word = "production quantity"
125
  base_info += f"- Production pipeline (completing in future weeks): {list(st.session_state.game_state['factory_production_pipeline'])}"
126
  else:
127
  task_word = "order quantity"
128
  base_info += f"- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}\n- Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}"
 
129
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
130
  stable_demand = 8
131
  if echelon_state['name'] == 'Factory': total_lead_time = FACTORY_LEAD_TIME
 
141
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline']))
142
  optimal_order = max(0, int(target_inventory_level - inventory_position))
143
  return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
 
144
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
145
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
146
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
 
153
  calculated_order = anchor_demand + inventory_correction - supply_line
154
  rational_local_order = max(0, int(calculated_order))
155
  return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock is {echelon_state['inventory'] - echelon_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
 
156
  elif llm_personality == 'human_like' and info_sharing == 'full':
157
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
158
  for name, e_state in all_echelons_state.items():
 
167
  You are still human and might get anxious about your own stock levels.
168
  What {task_word} should you decide on this week? Respond with a single integer.
169
  """
 
170
  elif llm_personality == 'human_like' and info_sharing == 'local':
171
  return f"""
172
  **You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.
 
177
  **React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
178
  """
179
 
180
+ # =============== CORRECTED step_game FUNCTION ===============
181
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
182
  state = st.session_state.game_state
183
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
184
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
185
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
186
  llm_raw_responses = {}
187
+
188
+ # Store pre-step state for logging
189
  pre_step_inventory = echelons[human_role]['inventory']
190
  pre_step_backlog = echelons[human_role]['backlog']
191
+ # Store arriving shipment amount *before* it's added to inventory
192
+ arriving_shipment_this_week = list(echelons[human_role]['incoming_shipments'])[0] if echelons[human_role]['incoming_shipments'] else 0
193
 
194
+ # --- Game Simulation Steps ---
195
+ # Step 1a: Factory Production completes and adds to Factory inventory
196
  factory_state = echelons["Factory"]
197
+ produced_units = 0
198
+ if state['factory_production_pipeline']:
199
+ produced_units = state['factory_production_pipeline'].popleft()
200
+ factory_state['inventory'] += produced_units
201
+
202
+ # Step 1b: Shipments arrive at downstream echelons
203
  for name in ["Retailer", "Wholesaler", "Distributor"]:
204
+ arrived_shipment = 0
205
+ if echelons[name]['incoming_shipments']:
206
+ arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop shipment for current week
207
+ echelons[name]['inventory'] += arrived_shipment
208
+
209
+ # Step 2: Orders arrive from downstream partners
210
  for name in echelon_order:
211
+ if name == "Retailer":
212
+ echelons[name]['incoming_order'] = get_customer_demand(week)
213
  else:
214
  downstream = echelons[name]['downstream_name']
215
+ order_from_downstream = 0
216
  if downstream and echelons[downstream]['order_pipeline']:
217
+ order_from_downstream = echelons[downstream]['order_pipeline'].popleft()
218
+ echelons[name]['incoming_order'] = order_from_downstream
 
 
 
 
 
219
 
220
+ # Step 3: Fulfill orders (Ship Beer)
221
+ for name in echelon_order:
222
+ e = echelons[name]
223
+ demand_to_meet = e['incoming_order'] + e['backlog']
224
+ e['shipment_sent'] = min(e['inventory'], demand_to_meet)
225
+ e['inventory'] -= e['shipment_sent']
226
+ e['backlog'] = demand_to_meet - e['shipment_sent']
227
+
228
+ # Step 3b: Place shipped items into the *end* of the downstream partner's incoming shipment queue
229
+ for sender_name in ["Factory", "Distributor", "Wholesaler"]:
230
+ sender = echelons[sender_name]
231
+ receiver_name = sender['downstream_name']
232
+ if receiver_name:
233
+ echelons[receiver_name]['incoming_shipments'].append(sender['shipment_sent'])
234
+
235
+ # --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
236
  for name in echelon_order:
237
  e = echelons[name]
238
  if name == human_role:
 
241
  prompt = get_llm_prompt(e, week, llm_personality, info_sharing, echelons)
242
  order_amount, raw_resp = get_llm_order_decision(prompt, name)
243
  llm_raw_responses[name] = raw_resp
244
+ e['order_placed'] = max(0, order_amount) # This is the order/production decision for the week
245
+
246
+ # Place the order into the *end* of the current player's own order pipeline (for upstream player to receive later)
247
+ if name != "Factory":
248
+ e['order_pipeline'].append(e['order_placed'])
249
 
250
+ # Factory schedules production based on its 'order_placed' decision
251
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
252
 
253
+ # --- Logging (End of Week) ---
254
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
255
+ # Remove complex objects before logging
256
  del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
257
+
258
  for name in echelon_order:
259
  e = echelons[name]
260
+ # Calculate costs based on inventory/backlog AFTER shipping step
261
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
262
+
263
+ # Log core metrics (state at the END of the week)
264
  for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
265
  log_entry[f'{name}.{key}'] = e[key]
266
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
267
+
268
+ # *** Explicitly log the value for 'Arriving Next Week' ***
269
+ # This reads the state of the queues *after* all steps for the week are done.
270
+ if name != 'Factory':
271
+ # The next item in incoming_shipments is what arrives at the start of next week
272
+ log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
273
+ else:
274
+ # For factory, log what completes production next week
275
+ log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
276
+
277
+ # Log human-specific metrics recorded DURING the week
278
  log_entry[f'{human_role}.opening_inventory'] = pre_step_inventory
279
  log_entry[f'{human_role}.opening_backlog'] = pre_step_backlog
280
+ log_entry[f'{human_role}.arrived_this_week'] = arriving_shipment_this_week # Log the shipment that arrived in Step 1
281
+ log_entry[f'{human_role}.initial_order'] = human_initial_order # Log Step 4a decision
282
+ log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion # Log Step 4b suggestion
283
+
284
  state['logs'].append(log_entry)
285
 
286
+ # --- Advance Week ---
287
  state['week'] += 1
288
  state['decision_step'] = 'initial_order'
289
  if state['week'] > WEEKS: state['game_running'] = False
290
+ # ==============================================================================
291
 
292
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
293
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
294
  fig.suptitle(title, fontsize=16)
295
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
296
+
297
  plot_data = []
298
  for _, row in df.iterrows():
299
  for e in echelons:
300
+ # Safely access keys, provide default if missing (e.g., first few weeks)
301
+ plot_data.append({'week': row.get('week', 0), 'echelon': e,
302
+ 'inventory': row.get(f'{e}.inventory', 0),
303
+ 'order_placed': row.get(f'{e}.order_placed', 0),
304
+ 'total_cost': row.get(f'{e}.total_cost', 0)})
305
  plot_df = pd.DataFrame(plot_data)
306
 
307
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
 
310
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
311
  order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities (The Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered (Units)')
312
 
313
+ # Ensure total_cost calculation handles potential missing data gracefully
314
+ total_costs = plot_df.loc[plot_df.groupby('echelon')['week'].idxmax()] # Get row with max week for each echelon
315
+ total_costs = total_costs.set_index('echelon')['total_cost'].reindex(echelons, fill_value=0)
316
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
317
 
318
+ # Safely access human decision columns
319
+ human_cols = [f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']
320
+ human_df_cols = ['week'] + [col for col in human_cols if col in df.columns]
321
+ human_df = df[human_df_cols].copy()
322
  human_df.rename(columns={
323
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
324
  }, inplace=True)
325
+ if len(human_df.columns) > 1: # Check if there's data to plot
326
+ human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-'); axes[3].set_title(f'Analysis of Your ({human_role}) Decisions'); axes[3].set_ylabel('Order Quantity'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
327
+ else:
328
+ axes[3].set_title(f'Analysis of Your ({human_role}) Decisions - No Data'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
329
+
330
+
331
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
332
 
333
  def save_logs_and_upload(state: dict):
 
357
  else:
358
  # --- Game Setup & Instructions ---
359
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
360
+
 
361
  st.markdown("---")
362
  st.header("📖 Welcome to the Beer Game!")
363
+ 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.")
364
 
365
  st.subheader("1. Your Goal: Minimize Costs")
366
+ st.success("**Your single, most important goal is to: Minimize the total cost for your position in the supply chain.**")
367
  st.markdown("You get costs from two things every week:")
368
  st.markdown(f"""
369
+ - **Holding Inventory:** **${HOLDING_COST:,.2f} per unit per week.** (Holding 10 units costs $5.00)
370
+ - **Backlog (Unfilled Orders):** **${BACKLOG_COST:,.2f} per unit per week.** (Having 5 unfilled orders costs $5.00)
371
  """)
372
+ with st.expander("Click to see a cost calculation example"):
373
+ st.markdown(f"""
374
+ Imagine at the end of Week 5, your dashboard shows:
375
+ - `Current Inventory: 20`
376
+ - `Current Backlog: 3`
377
+ Your cost for Week 5 would be:
378
+ - `(20 units of Inventory * ${HOLDING_COST:,.2f})` = $10.00
379
+ - `(3 units of Backlog * ${BACKLOG_COST:,.2f})` = $3.00
380
+ - **Total Weekly Cost:** = **$13.00**
381
+ Your goal is to keep this number as low as possible, every week.
382
+ """)
383
+
384
  st.subheader("2. Your Role: The Distributor")
385
  st.markdown("""
386
  You will always play as the **Distributor**. The other 3 roles are played by AI.
387
+ - **Retailer (AI):** Sells to the final customer.
388
+ - **Wholesaler (AI):** Sells to the Retailer.
389
+ - **Distributor (You):** You sell to the Wholesaler.
390
+ - **Factory (AI):** You order from the Factory.
 
391
  """)
392
+ try: st.image(IMAGE_PATH, caption="You are the Distributor. You get orders from the Wholesaler and place orders to the Factory.")
393
+ except FileNotFoundError: st.warning("Image file not found.")
394
+
 
 
395
  st.subheader("3. The Core Challenge: Delays!")
396
  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.**")
 
397
  with st.expander("Click to see a detailed example of the 3-week delay"):
398
  st.markdown(f"""
399
  Let's follow a single order you place:
 
401
  * **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.
402
  * **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.
403
  * **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.
 
404
  **Conclusion:** You must always think 3 weeks ahead. The order you place in Week 10 will not help you until Week 13.
405
  """)
406
+
407
+ st.subheader("4. The Bullwhip Effect (What to Avoid)")
408
+ st.markdown("""
409
+ The "Bullwhip Effect" is the main challenge of this game. It describes how small, normal changes in customer demand (at the Retailer) get **amplified** into huge, chaotic swings in orders as they move up the supply chain.
410
+ This often leads to a cycle of **panic ordering** (ordering way too much because you are out of stock) followed by a **massive pile-up of inventory** (when all your late orders finally arrive). This cycle is extremely expensive. Your goal is to avoid it by ordering smoothly.
411
+ """)
412
+
413
+ st.subheader("5. How Each Week Works (Your Task)")
 
414
  st.markdown(f"""
415
  Your main job is simple: place one order each week.
 
416
  **A) At the start of every week, the system automatically does 3 things:**
417
+ * **(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`.
418
  * **(Step 2) New Orders Arrive:** You receive a new `Incoming Order` from the Wholesaler.
419
  * **(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`).
 
420
  **B) After this, you will see your new dashboard and must make your 2-part decision:**
421
  * **Step 4a (Initial Order):** Based on your new status, submit your **initial order** to the Factory.
422
  * **Step 4b (Final Order):** You will then see an **AI suggestion**. Review it, then submit your **final order** to end the week.
423
  """)
424
+
 
425
  st.markdown("---")
426
  st.header("⚙️ Game Configuration")
427
  c1, c2 = st.columns(2)
 
429
  llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
430
  with c2:
431
  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.")
432
+
433
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
434
  init_game_state(llm_personality, info_sharing)
435
  st.rerun()
 
438
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
439
  state = st.session_state.game_state
440
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
441
+
442
  st.header(f"Week {week} / {WEEKS}")
443
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
444
  st.markdown("---")
 
450
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
451
  st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
452
  st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
453
+ if name == human_role: st.metric("Your Total Cost", f"${e['total_cost']:,.2f}")
 
 
 
 
 
454
  st.write(f"Incoming Order: **{e['incoming_order']}**")
455
  if name == "Factory":
456
  prod_completing = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
 
460
  st.write(f"Arriving Next Week: **{arriving}**")
461
  else:
462
  st.info("In Local Information mode, you can only see your own status dashboard.")
463
+ e = echelons[human_role]
464
  st.markdown(f"### 👤 {human_role} (Your Dashboard)")
465
  col1, col2, col3, col4 = st.columns(4)
466
  col1.metric("Current Inventory", e['inventory'])
467
  col2.metric("Current Backlog", e['backlog'])
468
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
469
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
 
 
470
  st.metric("Your Total Cumulative Cost", f"${e['total_cost']:,.2f}")
471
+
 
472
  st.markdown("---")
473
  st.header("Your Decision (Step 4)")
474
  human_echelon_state = echelons[human_role]
 
481
  state['human_initial_order'] = int(initial_order)
482
  state['decision_step'] = 'final_order'
483
  st.rerun()
484
+
485
  elif state['decision_step'] == 'final_order':
486
  st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
487
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
488
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
489
+
490
  if 'final_order_input' not in st.session_state:
491
  st.session_state.final_order_input = ai_suggestion
492
 
 
501
  st.rerun()
502
 
503
  st.markdown("---")
504
+ # =============== CORRECTED LOG DISPLAY BLOCK ===============
505
  with st.expander("📖 Your Weekly Decision Log", expanded=False):
506
+ if not state.get('logs'): # Use .get() for safety
507
  st.write("Your weekly history will be displayed here after you complete the first week.")
508
  else:
509
+ try: # Add error handling for data processing
510
+ history_df = pd.json_normalize(state['logs'])
511
+
512
+ # Define all desired columns and their display names
513
+ human_cols = {
514
+ 'week': 'Week',
515
+ f'{human_role}.opening_inventory': 'Opening Inv.',
516
+ f'{human_role}.opening_backlog': 'Opening Backlog',
517
+ f'{human_role}.arrived_this_week': 'Arrived This Week', # Shipment that arrived at Step 1
518
+ f'{human_role}.incoming_order': 'Incoming Order', # Order received at Step 2
519
+ f'{human_role}.initial_order': 'Your Initial Order', # Step 4a
520
+ f'{human_role}.ai_suggestion': 'AI Suggestion', # Step 4b
521
+ f'{human_role}.order_placed': 'Your Final Order', # Step 4b (Order for Week+3)
522
+ f'{human_role}.arriving_next_week': 'Arriving Next Week', # What will arrive in Step 1 of NEXT week
523
+ f'{human_role}.weekly_cost': 'Weekly Cost', # Calculated at end of week
524
+ }
525
+
526
+ # Define the desired order of columns
527
+ ordered_display_cols_keys = [
528
+ 'week', f'{human_role}.opening_inventory', f'{human_role}.opening_backlog',
529
+ f'{human_role}.arrived_this_week', f'{human_role}.incoming_order',
530
+ f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed',
531
+ f'{human_role}.arriving_next_week', f'{human_role}.weekly_cost'
532
+ ]
533
+
534
+ # Filter the desired columns based on what actually exists in the log data
535
+ final_cols_to_display = [col for col in ordered_display_cols_keys if col in history_df.columns]
536
+
537
+ if not final_cols_to_display:
538
+ st.write("No data columns available to display.")
539
+ else:
540
+ # Select and rename the columns that exist
541
+ display_df = history_df[final_cols_to_display].rename(columns=human_cols)
542
+
543
+ # Format the cost column
544
+ if 'Weekly Cost' in display_df.columns:
545
+ # Apply formatting safely, handling potential non-numeric data
546
+ display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}" if isinstance(x, (int, float)) else "")
547
+
548
+ # Display the dataframe
549
+ st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
550
+
551
+ except Exception as e:
552
+ st.error(f"Error displaying weekly log: {e}")
553
+ st.write("Log data structure might be inconsistent.")
554
+ # =======================================================
555
+
556
+ try: st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
557
+ except FileNotFoundError: st.sidebar.warning("Image file not found.")
558
 
 
 
 
 
 
559
  st.sidebar.header("Game Info")
560
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
561
  if st.sidebar.button("🔄 Reset Game"):
562
+ if 'final_order_input' in st.session_state: del st.session_state.final_order_input
 
563
  del st.session_state.game_state
564
  st.rerun()
565
 
 
567
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
568
  st.header("🎉 Game Over!")
569
  state = st.session_state.game_state
570
+ try: # Add error handling for final plot/save
571
+ logs_df = pd.json_normalize(state['logs'])
572
+ fig = plot_results(
573
+ logs_df,
574
+ f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality']} | Info: {state['info_sharing']})",
575
+ state['human_role']
576
+ )
577
+ st.pyplot(fig)
578
+ save_logs_and_upload(state)
579
+ except Exception as e:
580
+ st.error(f"Error generating final report: {e}")
581
+ st.write("Log data might be corrupted or incomplete.")
582
+
583
  if st.button("✨ Start a New Game"):
584
  del st.session_state.game_state
585
  st.rerun()