Lilli98 commited on
Commit
56e71a6
·
verified ·
1 Parent(s): ae1c5f7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -39
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title Beer Game Final Version (v4 - English UI & Introduction)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
@@ -56,15 +56,13 @@ else:
56
 
57
 
58
  # -----------------------------------------------------------------------------
59
- # 3. Core Game Logic Functions
60
  # -----------------------------------------------------------------------------
61
 
62
  def get_customer_demand(week: int) -> int:
63
- """Defines the end-customer demand pattern."""
64
  return 4 if week <= 4 else 8
65
 
66
  def init_game_state(llm_personality: str, info_sharing: str):
67
- """Initializes or resets the game state in st.session_state."""
68
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
69
  human_role = random.choice(roles)
70
  participant_id = str(uuid.uuid4())[:8]
@@ -74,8 +72,8 @@ def init_game_state(llm_personality: str, info_sharing: str):
74
  'human_role': human_role, 'llm_personality': llm_personality,
75
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
76
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
77
- 'decision_step': 'initial_order', # Controls the two-step decision process
78
- 'human_initial_order': None, # Stores the player's initial order
79
  }
80
 
81
  for i, name in enumerate(roles):
@@ -95,7 +93,6 @@ def init_game_state(llm_personality: str, info_sharing: str):
95
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You have been randomly assigned the role of: **{human_role}**.")
96
 
97
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
98
- """Calls the OpenAI API to get a decision and returns the integer order and raw text."""
99
  if not client: return 8, "NO_API_KEY_DEFAULT"
100
  with st.spinner(f"Getting AI decision for {echelon_name}..."):
101
  try:
@@ -117,7 +114,6 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
117
  return 8, f"API_ERROR: {e}"
118
 
119
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
120
- """Generates the prompt for the LLM based on the game scenario."""
121
  # This function's logic is complex and correct, so it remains unchanged.
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- 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'])}"
123
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
@@ -132,7 +128,7 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
132
  supply_line = sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline'])
133
  calculated_order = anchor_demand + inventory_correction - supply_line
134
  rational_local_order = max(0, int(calculated_order))
135
- 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:** You already have **{supply_line} units** in transit or being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Order = (Anchor Demand) + (Inventory Adjustment) - (Supply Line)\n* Order = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n\n**Your Task:** Confirm this locally rational quantity. Respond with a single integer."
136
  elif llm_personality == 'human_like' and info_sharing == 'full':
137
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
138
  for name, e_state in all_echelons_state.items():
@@ -142,14 +138,12 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
142
  return f"**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.\n\n**Your Mindset: **Your top priority is try to not have a backlog.\n\n{base_info}\n\n**Your Task:** You just saw your own inventory and a new order coming. Your gut instinct is to panic and order enough to ensure you are never caught with a backlog again.\n\n**React emotionally.** What is your knee-jerk order quantity? Respond with a single integer."
143
 
144
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
145
- """Processes one week of the game and records detailed logs, including two-step decision data."""
146
  state = st.session_state.game_state
147
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
148
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
149
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
150
  llm_raw_responses = {}
151
 
152
- # Core game simulation steps (unchanged)
153
  factory_state = echelons["Factory"]
154
  if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
155
  for name in ["Retailer", "Wholesaler", "Distributor"]:
@@ -167,7 +161,6 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
167
  receiver = echelons[sender]['downstream_name']
168
  if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
169
 
170
- # Agents place orders
171
  for name in echelon_order:
172
  e = echelons[name]
173
  if name == human_role:
@@ -181,9 +174,8 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
181
 
182
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
183
 
184
- # Update costs and record logs
185
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
186
- del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'] # Clean up complex objects
187
  for name in echelon_order:
188
  e = echelons[name]
189
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
@@ -191,20 +183,16 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
191
  log_entry[f'{name}.{key}'] = e[key]
192
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
193
 
194
- # Record two-step decision data
195
  log_entry[f'{human_role}.initial_order'] = human_initial_order
196
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
197
 
198
  state['logs'].append(log_entry)
199
 
200
- # Advance week and reset decision step
201
  state['week'] += 1
202
  state['decision_step'] = 'initial_order'
203
  if state['week'] > WEEKS: state['game_running'] = False
204
 
205
-
206
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
207
- """Generates and returns the end-of-game plots."""
208
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
209
  fig.suptitle(title, fontsize=16)
210
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
@@ -217,19 +205,15 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
217
  'total_cost': row[f'{e}.total_cost']})
218
  plot_df = pd.DataFrame(plot_data)
219
 
220
- # Plot 1: Inventory
221
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
222
  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)')
223
 
224
- # Plot 2: Orders (Bullwhip Effect)
225
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
226
  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)')
227
 
228
- # Plot 3: Costs
229
  total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
230
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
231
 
232
- # Plot 4: Human Decision Analysis
233
  human_df = df[['week', f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']].copy()
234
  human_df.rename(columns={
235
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
@@ -239,7 +223,6 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
239
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
240
 
241
  def save_logs_and_upload(state: dict):
242
- """Saves logs locally and uploads to Hugging Face Hub at the end of the game."""
243
  if not state.get('logs'): return
244
  participant_id = state['participant_id']
245
  df = pd.json_normalize(state['logs'])
@@ -266,21 +249,17 @@ if st.session_state.get('initialization_error'):
266
  else:
267
  # --- Game Setup & Instructions ---
268
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
269
-
270
- # --- NEW: Introduction Section ---
271
  st.markdown("---")
272
  st.header("📖 Welcome to the Beer Game!")
273
  st.markdown("""
274
  The Beer Game is a classic supply chain simulation that demonstrates a phenomenon called the **"Bullwhip Effect."** Even with stable customer demand, small variations in orders can amplify as they move up the supply chain, causing massive shortages and overstocks.
275
  """)
276
-
277
  st.subheader("Your Goal")
278
  st.markdown("""
279
  Minimize the total cost for your position in the supply chain. Costs are incurred for:
280
  - **Holding Inventory:** **$0.50 per unit per week**
281
  - **Backlog (Unfilled Orders):** **$1.00 per unit per week**
282
  """)
283
-
284
  col1, col2 = st.columns(2)
285
  with col1:
286
  st.subheader("🔗 The Supply Chain")
@@ -296,7 +275,6 @@ else:
296
  st.markdown("""
297
  The key challenge is managing delays. There is a **communication delay** for orders to reach your supplier and a **shipping delay** for goods to arrive. You must order enough to meet future demand without creating a huge pile of expensive inventory.
298
  """)
299
-
300
  st.subheader("🎮 How to Play This Version")
301
  st.markdown("""
302
  1. **Configure the Game:** Choose the AI's behavior and the level of information sharing.
@@ -307,15 +285,12 @@ else:
307
  4. **Advance:** Once you submit your final order, the week advances, and all AI agents make their moves.
308
  """)
309
  st.markdown("---")
310
-
311
- # --- Game Configuration ---
312
  st.header("⚙️ Game Configuration")
313
  c1, c2 = st.columns(2)
314
  with c1:
315
  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.")
316
  with c2:
317
  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.")
318
-
319
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
320
  init_game_state(llm_personality, info_sharing)
321
  st.rerun()
@@ -327,7 +302,6 @@ else:
327
 
328
  st.header(f"Week {week} / {WEEKS}")
329
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
330
-
331
  st.markdown("---")
332
  st.subheader("Supply Chain Status")
333
  if info_sharing == 'full':
@@ -339,7 +313,7 @@ else:
339
  st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
340
  st.write(f"Incoming Order: **{e['incoming_order']}**")
341
  st.write(f"Arriving Next Week: **{list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}**")
342
- else: # local information
343
  st.info("In Local Information mode, you can only see your own status dashboard.")
344
  e = echelons[human_role]
345
  st.markdown(f"### 👤 {human_role} (Your Dashboard)")
@@ -349,8 +323,6 @@ else:
349
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
350
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
351
  st.markdown("---")
352
-
353
- # --- Two-Step Decision UI ---
354
  st.header("Your Decision")
355
  human_echelon_state = echelons[human_role]
356
 
@@ -368,18 +340,49 @@ else:
368
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
369
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
370
 
 
 
 
 
 
 
371
  with st.form(key="final_order_form"):
372
  st.markdown(f"#### **Step 2:** The AI suggests ordering **{ai_suggestion}** units.")
373
  st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
374
- final_order = st.number_input("Your Final Order Quantity:", min_value=0, step=1, value=ai_suggestion)
 
 
 
 
 
 
 
 
 
 
 
375
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
376
- step_game(int(final_order), state['human_initial_order'], ai_suggestion)
 
 
 
 
 
 
 
 
 
 
377
  st.rerun()
378
 
379
  st.sidebar.header("Game Info")
380
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
381
  if st.sidebar.button("🔄 Reset Game"):
382
- del st.session_state.game_state; st.rerun()
 
 
 
 
383
 
384
  # --- Game Over Interface ---
385
  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:
@@ -390,4 +393,9 @@ else:
390
  st.pyplot(fig)
391
  save_logs_and_upload(state)
392
  if st.button("✨ Start a New Game"):
393
- del st.session_state.game_state; st.rerun()
 
 
 
 
 
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4.2 - Input State Bug Fixed)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. Import Libraries
 
56
 
57
 
58
  # -----------------------------------------------------------------------------
59
+ # 3. Core Game Logic Functions (No changes in this section)
60
  # -----------------------------------------------------------------------------
61
 
62
  def get_customer_demand(week: int) -> int:
 
63
  return 4 if week <= 4 else 8
64
 
65
  def init_game_state(llm_personality: str, info_sharing: str):
 
66
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
67
  human_role = random.choice(roles)
68
  participant_id = str(uuid.uuid4())[:8]
 
72
  'human_role': human_role, 'llm_personality': llm_personality,
73
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
74
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
75
+ 'decision_step': 'initial_order',
76
+ 'human_initial_order': None,
77
  }
78
 
79
  for i, name in enumerate(roles):
 
93
  st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You have been randomly assigned the role of: **{human_role}**.")
94
 
95
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
 
96
  if not client: return 8, "NO_API_KEY_DEFAULT"
97
  with st.spinner(f"Getting AI decision for {echelon_name}..."):
98
  try:
 
114
  return 8, f"API_ERROR: {e}"
115
 
116
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
 
117
  # This function's logic is complex and correct, so it remains unchanged.
118
  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- 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'])}"
119
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
 
128
  supply_line = sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline'])
129
  calculated_order = anchor_demand + inventory_correction - supply_line
130
  rational_local_order = max(0, int(calculated_order))
131
+ 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:** You already have **{supply_line} units** in transit or being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Order = (Anchor Demand) + (Inventory Adjustment) - (Supply Line)\n* Order = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational quantity. Respond with a single integer."
132
  elif llm_personality == 'human_like' and info_sharing == 'full':
133
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
134
  for name, e_state in all_echelons_state.items():
 
138
  return f"**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.\n\n**Your Mindset: **Your top priority is try to not have a backlog.\n\n{base_info}\n\n**Your Task:** You just saw your own inventory and a new order coming. Your gut instinct is to panic and order enough to ensure you are never caught with a backlog again.\n\n**React emotionally.** What is your knee-jerk order quantity? Respond with a single integer."
139
 
140
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
 
141
  state = st.session_state.game_state
142
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
143
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
144
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
145
  llm_raw_responses = {}
146
 
 
147
  factory_state = echelons["Factory"]
148
  if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
149
  for name in ["Retailer", "Wholesaler", "Distributor"]:
 
161
  receiver = echelons[sender]['downstream_name']
162
  if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
163
 
 
164
  for name in echelon_order:
165
  e = echelons[name]
166
  if name == human_role:
 
174
 
175
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
176
 
 
177
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
178
+ del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
179
  for name in echelon_order:
180
  e = echelons[name]
181
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
 
183
  log_entry[f'{name}.{key}'] = e[key]
184
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
185
 
 
186
  log_entry[f'{human_role}.initial_order'] = human_initial_order
187
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
188
 
189
  state['logs'].append(log_entry)
190
 
 
191
  state['week'] += 1
192
  state['decision_step'] = 'initial_order'
193
  if state['week'] > WEEKS: state['game_running'] = False
194
 
 
195
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
 
196
  fig, axes = plt.subplots(4, 1, figsize=(12, 22))
197
  fig.suptitle(title, fontsize=16)
198
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
 
205
  'total_cost': row[f'{e}.total_cost']})
206
  plot_df = pd.DataFrame(plot_data)
207
 
 
208
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
209
  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)')
210
 
 
211
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
212
  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)')
213
 
 
214
  total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
215
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
216
 
 
217
  human_df = df[['week', f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']].copy()
218
  human_df.rename(columns={
219
  f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
 
223
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
224
 
225
  def save_logs_and_upload(state: dict):
 
226
  if not state.get('logs'): return
227
  participant_id = state['participant_id']
228
  df = pd.json_normalize(state['logs'])
 
249
  else:
250
  # --- Game Setup & Instructions ---
251
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
 
 
252
  st.markdown("---")
253
  st.header("📖 Welcome to the Beer Game!")
254
  st.markdown("""
255
  The Beer Game is a classic supply chain simulation that demonstrates a phenomenon called the **"Bullwhip Effect."** Even with stable customer demand, small variations in orders can amplify as they move up the supply chain, causing massive shortages and overstocks.
256
  """)
 
257
  st.subheader("Your Goal")
258
  st.markdown("""
259
  Minimize the total cost for your position in the supply chain. Costs are incurred for:
260
  - **Holding Inventory:** **$0.50 per unit per week**
261
  - **Backlog (Unfilled Orders):** **$1.00 per unit per week**
262
  """)
 
263
  col1, col2 = st.columns(2)
264
  with col1:
265
  st.subheader("🔗 The Supply Chain")
 
275
  st.markdown("""
276
  The key challenge is managing delays. There is a **communication delay** for orders to reach your supplier and a **shipping delay** for goods to arrive. You must order enough to meet future demand without creating a huge pile of expensive inventory.
277
  """)
 
278
  st.subheader("🎮 How to Play This Version")
279
  st.markdown("""
280
  1. **Configure the Game:** Choose the AI's behavior and the level of information sharing.
 
285
  4. **Advance:** Once you submit your final order, the week advances, and all AI agents make their moves.
286
  """)
287
  st.markdown("---")
 
 
288
  st.header("⚙️ Game Configuration")
289
  c1, c2 = st.columns(2)
290
  with c1:
291
  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.")
292
  with c2:
293
  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.")
 
294
  if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
295
  init_game_state(llm_personality, info_sharing)
296
  st.rerun()
 
302
 
303
  st.header(f"Week {week} / {WEEKS}")
304
  st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
 
305
  st.markdown("---")
306
  st.subheader("Supply Chain Status")
307
  if info_sharing == 'full':
 
313
  st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
314
  st.write(f"Incoming Order: **{e['incoming_order']}**")
315
  st.write(f"Arriving Next Week: **{list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}**")
316
+ else:
317
  st.info("In Local Information mode, you can only see your own status dashboard.")
318
  e = echelons[human_role]
319
  st.markdown(f"### 👤 {human_role} (Your Dashboard)")
 
323
  col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
324
  col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
325
  st.markdown("---")
 
 
326
  st.header("Your Decision")
327
  human_echelon_state = echelons[human_role]
328
 
 
340
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
341
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
342
 
343
+ # =============== BUG FIX STARTS HERE ===============
344
+ # Initialize the session_state key for the input box only once per step.
345
+ if 'final_order_input' not in st.session_state:
346
+ st.session_state.final_order_input = ai_suggestion
347
+ # =============== BUG FIX ENDS HERE =================
348
+
349
  with st.form(key="final_order_form"):
350
  st.markdown(f"#### **Step 2:** The AI suggests ordering **{ai_suggestion}** units.")
351
  st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
352
+
353
+ # =============== BUG FIX STARTS HERE ===============
354
+ # Use the 'key' parameter to bind the input to session state.
355
+ # Remove the problematic 'value' parameter.
356
+ st.number_input(
357
+ "Your Final Order Quantity:",
358
+ min_value=0,
359
+ step=1,
360
+ key='final_order_input'
361
+ )
362
+ # =============== BUG FIX ENDS HERE =================
363
+
364
  if st.form_submit_button("Submit Final Order & Advance to Next Week"):
365
+ # =============== BUG FIX STARTS HERE ===============
366
+ # Read the final order value directly from session state.
367
+ final_order_value = st.session_state.final_order_input
368
+
369
+ # Call the game logic
370
+ step_game(final_order_value, state['human_initial_order'], ai_suggestion)
371
+
372
+ # Clean up the session state key to prepare for the next week.
373
+ del st.session_state.final_order_input
374
+ # =============== BUG FIX ENDS HERE =================
375
+
376
  st.rerun()
377
 
378
  st.sidebar.header("Game Info")
379
  st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
380
  if st.sidebar.button("🔄 Reset Game"):
381
+ # Clean up state if the user resets
382
+ if 'final_order_input' in st.session_state:
383
+ del st.session_state.final_order_input
384
+ del st.session_state.game_state
385
+ st.rerun()
386
 
387
  # --- Game Over Interface ---
388
  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:
 
393
  st.pyplot(fig)
394
  save_logs_and_upload(state)
395
  if st.button("✨ Start a New Game"):
396
+ del st.session_state.game_state
397
+ st.rerun()
398
+
399
+
400
+
401
+