Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# app.py
|
| 2 |
-
# @title Beer Game Final Version (v4.
|
| 3 |
|
| 4 |
# -----------------------------------------------------------------------------
|
| 5 |
# 1. Import Libraries
|
|
@@ -31,7 +31,7 @@ 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
|
| 37 |
HOLDING_COST = 0.5
|
|
@@ -63,6 +63,7 @@ else:
|
|
| 63 |
def get_customer_demand(week: int) -> int:
|
| 64 |
return 4 if week <= 4 else 8
|
| 65 |
|
|
|
|
| 66 |
def init_game_state(llm_personality: str, info_sharing: str):
|
| 67 |
roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 68 |
human_role = "Distributor" # Role is fixed
|
|
@@ -72,10 +73,14 @@ def init_game_state(llm_personality: str, info_sharing: str):
|
|
| 72 |
'game_running': True, 'participant_id': participant_id, 'week': 1,
|
| 73 |
'human_role': human_role, 'llm_personality': llm_personality,
|
| 74 |
'info_sharing': info_sharing, 'logs': [], 'echelons': {},
|
|
|
|
| 75 |
'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
|
|
|
|
|
|
|
|
|
|
| 76 |
'decision_step': 'initial_order',
|
| 77 |
'human_initial_order': None,
|
| 78 |
-
|
| 79 |
}
|
| 80 |
|
| 81 |
for i, name in enumerate(roles):
|
|
@@ -83,21 +88,21 @@ def init_game_state(llm_personality: str, info_sharing: str):
|
|
| 83 |
downstream = roles[i - 1] if i - 1 >= 0 else None
|
| 84 |
|
| 85 |
if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
|
| 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
|
| 93 |
-
'backlog': INITIAL_BACKLOG, # End-of-week state
|
| 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}**.")
|
|
|
|
| 101 |
|
| 102 |
def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
| 103 |
# This function remains correct.
|
|
@@ -122,20 +127,34 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
|
| 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
|
| 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 |
e_state = echelon_state_after_arrivals # Use the passed-in state for prompts
|
|
|
|
|
|
|
| 130 |
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"
|
|
|
|
| 131 |
if e_state['name'] == 'Factory':
|
| 132 |
task_word = "production quantity"
|
| 133 |
-
# Factory needs
|
| 134 |
-
base_info += f"- Production
|
| 135 |
else:
|
| 136 |
task_word = "order quantity"
|
| 137 |
-
#
|
| 138 |
-
base_info += f"- Shipments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
# --- Perfect Rational ---
|
| 141 |
if llm_personality == 'perfect_rational' and info_sharing == 'full':
|
|
@@ -146,13 +165,19 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
|
|
| 146 |
safety_stock = 4
|
| 147 |
target_inventory_level = (stable_demand * total_lead_time) + safety_stock
|
| 148 |
|
| 149 |
-
# Calculate Inventory Position based on state AFTER arrivals/orders
|
| 150 |
if e_state['name'] == 'Factory':
|
| 151 |
-
|
| 152 |
inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
|
|
|
|
| 153 |
else:
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
optimal_order = max(0, int(target_inventory_level - inventory_position))
|
| 158 |
|
|
@@ -160,26 +185,28 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
|
|
| 160 |
|
| 161 |
elif llm_personality == 'perfect_rational' and info_sharing == 'local':
|
| 162 |
safety_stock = 4; anchor_demand = e_state['incoming_order']
|
| 163 |
-
|
| 164 |
-
# Use state AFTER arrivals/orders for inventory correction calculation
|
| 165 |
inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
|
| 166 |
|
| 167 |
if e_state['name'] == 'Factory':
|
| 168 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 169 |
supply_line_desc = "In Production"
|
| 170 |
else:
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
calculated_order = anchor_demand + inventory_correction - supply_line
|
| 175 |
rational_local_order = max(0, int(calculated_order))
|
| 176 |
|
| 177 |
-
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
|
| 178 |
|
| 179 |
# --- Human-like ---
|
| 180 |
elif llm_personality == 'human_like' and info_sharing == 'full':
|
| 181 |
full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
|
| 182 |
-
# Display other players' state AFTER arrivals/orders
|
| 183 |
for name, other_e_state in all_echelons_state_after_arrivals.items():
|
| 184 |
if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
|
| 185 |
|
|
@@ -203,10 +230,9 @@ def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personalit
|
|
| 203 |
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.
|
| 204 |
**React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
|
| 205 |
"""
|
| 206 |
-
|
| 207 |
# ==============================================================================
|
| 208 |
|
| 209 |
-
# =============== CORRECTED step_game FUNCTION ===============
|
| 210 |
def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
|
| 211 |
state = st.session_state.game_state
|
| 212 |
week, echelons, human_role = state['week'], state['echelons'], state['human_role']
|
|
@@ -215,69 +241,71 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
|
|
| 215 |
llm_raw_responses = {}
|
| 216 |
|
| 217 |
# Store state at the very beginning of the week for logging opening balances
|
| 218 |
-
# These are the inventory/backlog values from the END of the previous week
|
| 219 |
opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
|
| 220 |
opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
|
| 221 |
arrived_this_week = {name: 0 for name in echelon_order} # Track arrivals for logging
|
| 222 |
|
| 223 |
# --- Game Simulation Steps ---
|
| 224 |
|
| 225 |
-
# Step 1a:
|
|
|
|
|
|
|
| 226 |
factory_state = echelons["Factory"]
|
| 227 |
produced_units = 0
|
| 228 |
if state['factory_production_pipeline']:
|
| 229 |
-
produced_units = state['factory_production_pipeline'].popleft()
|
| 230 |
-
# Temporarily store, don't update main state yet
|
| 231 |
-
inventory_after_production = factory_state['inventory'] + produced_units
|
| 232 |
arrived_this_week["Factory"] = produced_units
|
| 233 |
-
|
| 234 |
-
inventory_after_production = factory_state['inventory']
|
| 235 |
|
| 236 |
-
#
|
| 237 |
-
inventory_after_arrival = {} # Store intermediate state
|
| 238 |
for name in ["Retailer", "Wholesaler", "Distributor"]:
|
| 239 |
arrived_shipment = 0
|
| 240 |
if echelons[name]['incoming_shipments']:
|
| 241 |
-
arrived_shipment = echelons[name]['incoming_shipments'].popleft()
|
| 242 |
arrived_this_week[name] = arrived_shipment
|
| 243 |
inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
|
| 244 |
-
inventory_after_arrival["Factory"] = inventory_after_production # Add factory's state
|
| 245 |
|
| 246 |
|
| 247 |
-
# Step 2: Orders
|
| 248 |
-
|
|
|
|
| 249 |
for name in echelon_order:
|
|
|
|
| 250 |
if name == "Retailer":
|
| 251 |
-
|
| 252 |
else:
|
|
|
|
| 253 |
downstream_name = echelons[name]['downstream_name']
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
# --- Create State Snapshot for AI/Human Decision Point ---
|
| 262 |
-
# This reflects the state AFTER arrivals and new orders, BEFORE shipping
|
| 263 |
decision_point_states = {}
|
| 264 |
for name in echelon_order:
|
| 265 |
-
# Need to create a copy, including deque if needed for prompt
|
| 266 |
decision_point_states[name] = {
|
| 267 |
'name': name,
|
| 268 |
'inventory': inventory_after_arrival[name], # Inventory available
|
| 269 |
'backlog': total_backlog_before_shipping[name], # Total demand to meet
|
| 270 |
'incoming_order': echelons[name]['incoming_order'], # Order received this week
|
| 271 |
-
# Pass the current state of queues for prompt generation
|
| 272 |
'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
|
| 273 |
}
|
| 274 |
|
| 275 |
# --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
|
| 276 |
-
# Agents make decisions based on the decision_point_states
|
| 277 |
current_week_orders = {}
|
| 278 |
for name in echelon_order:
|
| 279 |
-
e = echelons[name]
|
| 280 |
-
prompt_state = decision_point_states[name]
|
| 281 |
|
| 282 |
if name == human_role:
|
| 283 |
order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
|
|
@@ -286,14 +314,24 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
|
|
| 286 |
order_amount, raw_resp = get_llm_order_decision(prompt, name)
|
| 287 |
|
| 288 |
llm_raw_responses[name] = raw_resp
|
| 289 |
-
e['order_placed'] = max(0, order_amount)
|
| 290 |
current_week_orders[name] = e['order_placed']
|
| 291 |
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
|
| 294 |
|
|
|
|
| 295 |
# --- Step 3: Fulfill orders (Ship Beer) ---
|
| 296 |
-
#
|
|
|
|
| 297 |
for name in echelon_order:
|
| 298 |
e = echelons[name]
|
| 299 |
demand_to_meet = total_backlog_before_shipping[name]
|
|
@@ -304,47 +342,47 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
|
|
| 304 |
e['inventory'] = available_inv - e['shipment_sent']
|
| 305 |
e['backlog'] = demand_to_meet - e['shipment_sent']
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
|
| 315 |
# --- Calculate Costs & Log (End of Week) ---
|
| 316 |
log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
for name in echelon_order:
|
| 321 |
e = echelons[name]
|
| 322 |
-
# Costs are based on the END OF WEEK state
|
| 323 |
e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
|
| 324 |
e['total_cost'] += e['weekly_cost']
|
| 325 |
|
| 326 |
-
|
| 327 |
-
log_entry[f'{name}.
|
| 328 |
-
log_entry[f'{name}.
|
| 329 |
-
log_entry[f'{name}.
|
| 330 |
-
log_entry[f'{name}.
|
| 331 |
-
log_entry[f'{name}.shipment_sent'] = e['shipment_sent'] # Shipped this week
|
| 332 |
-
log_entry[f'{name}.weekly_cost'] = e['weekly_cost'] # Cost for this week
|
| 333 |
-
log_entry[f'{name}.total_cost'] = e['total_cost'] # Cumulative cost
|
| 334 |
-
log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
|
| 335 |
-
|
| 336 |
-
# Log opening balances for the week
|
| 337 |
-
log_entry[f'{name}.opening_inventory'] = opening_inventories[name]
|
| 338 |
-
log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
|
| 339 |
log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
|
| 340 |
|
| 341 |
-
# Log prediction for next week's arrival/completion (based on queues AFTER this week's processing)
|
| 342 |
if name != 'Factory':
|
| 343 |
log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
|
| 344 |
else:
|
| 345 |
log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
|
| 346 |
|
| 347 |
-
# Log human-specific decisions
|
| 348 |
log_entry[f'{human_role}.initial_order'] = human_initial_order
|
| 349 |
log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
|
| 350 |
|
|
@@ -353,12 +391,13 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
|
|
| 353 |
# --- Advance Week ---
|
| 354 |
state['week'] += 1
|
| 355 |
state['decision_step'] = 'initial_order'
|
| 356 |
-
|
|
|
|
| 357 |
|
| 358 |
if state['week'] > WEEKS: state['game_running'] = False
|
| 359 |
-
|
| 360 |
# ==============================================================================
|
| 361 |
|
|
|
|
| 362 |
def plot_results(df: pd.DataFrame, title: str, human_role: str):
|
| 363 |
# This function remains correct.
|
| 364 |
fig, axes = plt.subplots(4, 1, figsize=(12, 22))
|
|
@@ -450,7 +489,7 @@ def save_logs_and_upload(state: dict):
|
|
| 450 |
st.error(f"Upload to Hugging Face failed: {e}")
|
| 451 |
|
| 452 |
# -----------------------------------------------------------------------------
|
| 453 |
-
# 4. Streamlit UI
|
| 454 |
# -----------------------------------------------------------------------------
|
| 455 |
st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
|
| 456 |
|
|
@@ -510,7 +549,7 @@ else:
|
|
| 510 |
Managing your inventory and backlog is key to minimizing costs. Here's how they work:
|
| 511 |
* **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.
|
| 512 |
* **If you DON'T have enough inventory:**
|
| 513 |
-
* You ship **all** the inventory you have.
|
| 514 |
* The remaining unfilled "Orders to Fill" becomes your **new Backlog** for next week.
|
| 515 |
* **Backlog is cumulative!** If you start Week 10 with a backlog of 5, get an order for 8 (total needed = 13), receive 10 units, and ship those 10 units, your new backlog for Week 11 is `13 - 10 = 3`.
|
| 516 |
* **If you DO have enough inventory:**
|
|
@@ -535,10 +574,10 @@ else:
|
|
| 535 |
|
| 536 |
**B) Your Dashboard (What You See for Your Turn):**
|
| 537 |
The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
|
| 538 |
-
* `Inventory (Opening)`: Your stock **at the beginning of the week**.
|
| 539 |
* `Backlog (Opening)`: Unfilled orders **carried over from the end of last week**.
|
| 540 |
-
* `Incoming Order (This Week)`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2).
|
| 541 |
-
* `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**.
|
| 542 |
* `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
|
| 543 |
* `Cost Last Week`: The specific cost incurred just **last week**.
|
| 544 |
|
|
@@ -567,8 +606,7 @@ else:
|
|
| 567 |
elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
|
| 568 |
state = st.session_state.game_state
|
| 569 |
week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
|
| 570 |
-
|
| 571 |
-
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 572 |
|
| 573 |
|
| 574 |
st.header(f"Week {week} / {WEEKS}")
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# @title Beer Game Final Version (v4.16 - Corrected 3-Week Lead Time Logic)
|
| 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 (R->W, W->D)
|
| 35 |
FACTORY_LEAD_TIME = 1
|
| 36 |
FACTORY_SHIPPING_DELAY = 1 # Specific delay from Factory to Distributor
|
| 37 |
HOLDING_COST = 0.5
|
|
|
|
| 63 |
def get_customer_demand(week: int) -> int:
|
| 64 |
return 4 if week <= 4 else 8
|
| 65 |
|
| 66 |
+
# =============== MODIFIED FUNCTION (Corrected Initialization) ===============
|
| 67 |
def init_game_state(llm_personality: str, info_sharing: str):
|
| 68 |
roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 69 |
human_role = "Distributor" # Role is fixed
|
|
|
|
| 73 |
'game_running': True, 'participant_id': participant_id, 'week': 1,
|
| 74 |
'human_role': human_role, 'llm_personality': llm_personality,
|
| 75 |
'info_sharing': info_sharing, 'logs': [], 'echelons': {},
|
| 76 |
+
# Pipeline now needs to cover ORDER_PASSING_DELAY + FACTORY_LEAD_TIME
|
| 77 |
'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
|
| 78 |
+
'distributor_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY), # For D -> F order passing
|
| 79 |
+
'wholesaler_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY), # For W -> D order passing
|
| 80 |
+
'retailer_order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY), # For R -> W order passing
|
| 81 |
'decision_step': 'initial_order',
|
| 82 |
'human_initial_order': None,
|
| 83 |
+
# No need for last_week_orders anymore
|
| 84 |
}
|
| 85 |
|
| 86 |
for i, name in enumerate(roles):
|
|
|
|
| 88 |
downstream = roles[i - 1] if i - 1 >= 0 else None
|
| 89 |
|
| 90 |
if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
|
| 91 |
+
elif name == "Factory": shipping_weeks = 0 # Factory produces, doesn't receive shipments
|
| 92 |
+
else: shipping_weeks = SHIPPING_DELAY # Retailer/Wholesaler use general delay
|
| 93 |
|
|
|
|
| 94 |
st.session_state.game_state['echelons'][name] = {
|
| 95 |
'name': name,
|
| 96 |
+
'inventory': INITIAL_INVENTORY, # End-of-week state
|
| 97 |
+
'backlog': INITIAL_BACKLOG, # End-of-week state
|
| 98 |
+
'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks), # Represents only SHIPPING delay
|
| 99 |
'incoming_order': 0, # Order received THIS week
|
| 100 |
'order_placed': 0, # Order placed THIS week
|
| 101 |
'shipment_sent': 0, # Shipment sent THIS week
|
| 102 |
'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
|
| 103 |
}
|
| 104 |
st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
|
| 105 |
+
# ==============================================================================
|
| 106 |
|
| 107 |
def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
| 108 |
# This function remains correct.
|
|
|
|
| 127 |
st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 8.")
|
| 128 |
return 8, f"API_ERROR: {e}"
|
| 129 |
|
| 130 |
+
# =============== MODIFIED FUNCTION (Prompt adjusted for new pipeline view) ===============
|
| 131 |
def get_llm_prompt(echelon_state_after_arrivals: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_after_arrivals: dict) -> str:
|
| 132 |
"""Generates the prompt for the LLM based on the game scenario.
|
| 133 |
Uses the state AFTER arrivals and new orders are processed, as this is the decision point."""
|
| 134 |
+
|
| 135 |
e_state = echelon_state_after_arrivals # Use the passed-in state for prompts
|
| 136 |
+
|
| 137 |
+
# Base Info reflects state before shipping
|
| 138 |
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"
|
| 139 |
+
|
| 140 |
if e_state['name'] == 'Factory':
|
| 141 |
task_word = "production quantity"
|
| 142 |
+
# Factory prompt needs its view of the production pipeline
|
| 143 |
+
base_info += f"- Your Production Pipeline (completing next week onwards): {list(st.session_state.game_state['factory_production_pipeline'])}"
|
| 144 |
else:
|
| 145 |
task_word = "order quantity"
|
| 146 |
+
# Others need their incoming shipments and orders placed but not yet received by supplier
|
| 147 |
+
base_info += f"- Shipments In Transit To You (arriving next week onwards): {list(e_state['incoming_shipments'])}\n"
|
| 148 |
+
# Show orders placed but not yet received by supplier (1 week delay)
|
| 149 |
+
if e_state['name'] == 'Distributor':
|
| 150 |
+
orders_in_transit = list(st.session_state.game_state['distributor_order_pipeline'])
|
| 151 |
+
elif e_state['name'] == 'Wholesaler':
|
| 152 |
+
orders_in_transit = list(st.session_state.game_state['wholesaler_order_pipeline'])
|
| 153 |
+
elif e_state['name'] == 'Retailer':
|
| 154 |
+
orders_in_transit = list(st.session_state.game_state['retailer_order_pipeline'])
|
| 155 |
+
else: orders_in_transit = []
|
| 156 |
+
base_info += f"- Orders You Placed (in transit to supplier): {orders_in_transit}"
|
| 157 |
+
|
| 158 |
|
| 159 |
# --- Perfect Rational ---
|
| 160 |
if llm_personality == 'perfect_rational' and info_sharing == 'full':
|
|
|
|
| 165 |
safety_stock = 4
|
| 166 |
target_inventory_level = (stable_demand * total_lead_time) + safety_stock
|
| 167 |
|
| 168 |
+
# Calculate Inventory Position based on state AFTER arrivals/orders AND pipelines
|
| 169 |
if e_state['name'] == 'Factory':
|
| 170 |
+
# IP = Inv - Backlog + In Production
|
| 171 |
inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(st.session_state.game_state['factory_production_pipeline']))
|
| 172 |
+
inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={sum(st.session_state.game_state['factory_production_pipeline'])})"
|
| 173 |
else:
|
| 174 |
+
# IP = Inv - Backlog + In Transit Shipments + Orders Placed but not yet received by supplier
|
| 175 |
+
if e_state['name'] == 'Distributor': orders_in_transit_sum = sum(st.session_state.game_state['distributor_order_pipeline'])
|
| 176 |
+
elif e_state['name'] == 'Wholesaler': orders_in_transit_sum = sum(st.session_state.game_state['wholesaler_order_pipeline'])
|
| 177 |
+
elif e_state['name'] == 'Retailer': orders_in_transit_sum = sum(st.session_state.game_state['retailer_order_pipeline'])
|
| 178 |
+
else: orders_in_transit_sum = 0
|
| 179 |
+
inventory_position = (e_state['inventory'] - e_state['backlog'] + sum(e_state['incoming_shipments']) + orders_in_transit_sum)
|
| 180 |
+
inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={sum(e_state['incoming_shipments'])} + InTransitOrder={orders_in_transit_sum})"
|
| 181 |
|
| 182 |
optimal_order = max(0, int(target_inventory_level - inventory_position))
|
| 183 |
|
|
|
|
| 185 |
|
| 186 |
elif llm_personality == 'perfect_rational' and info_sharing == 'local':
|
| 187 |
safety_stock = 4; anchor_demand = e_state['incoming_order']
|
|
|
|
|
|
|
| 188 |
inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
|
| 189 |
|
| 190 |
if e_state['name'] == 'Factory':
|
| 191 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 192 |
supply_line_desc = "In Production"
|
| 193 |
else:
|
| 194 |
+
# Supply line includes shipments AND orders in transit
|
| 195 |
+
if e_state['name'] == 'Distributor': orders_in_transit_sum = sum(st.session_state.game_state['distributor_order_pipeline'])
|
| 196 |
+
elif e_state['name'] == 'Wholesaler': orders_in_transit_sum = sum(st.session_state.game_state['wholesaler_order_pipeline'])
|
| 197 |
+
elif e_state['name'] == 'Retailer': orders_in_transit_sum = sum(st.session_state.game_state['retailer_order_pipeline'])
|
| 198 |
+
else: orders_in_transit_sum = 0
|
| 199 |
+
supply_line = sum(e_state['incoming_shipments']) + orders_in_transit_sum
|
| 200 |
+
supply_line_desc = "Supply Line (In Transit Shipments + Orders)"
|
| 201 |
|
| 202 |
calculated_order = anchor_demand + inventory_correction - supply_line
|
| 203 |
rational_local_order = max(0, int(calculated_order))
|
| 204 |
|
| 205 |
+
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 (either shipping or orders in transit). These should be subtracted from your new decision.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
|
| 206 |
|
| 207 |
# --- Human-like ---
|
| 208 |
elif llm_personality == 'human_like' and info_sharing == 'full':
|
| 209 |
full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
|
|
|
|
| 210 |
for name, other_e_state in all_echelons_state_after_arrivals.items():
|
| 211 |
if name != e_state['name']: full_info_str += f"- {name}: Inv={other_e_state['inventory']}, Backlog={other_e_state['backlog']}\n"
|
| 212 |
|
|
|
|
| 230 |
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.
|
| 231 |
**React emotionally.** What is your knee-jerk {task_word}? Respond with a single integer.
|
| 232 |
"""
|
|
|
|
| 233 |
# ==============================================================================
|
| 234 |
|
| 235 |
+
# =============== CORRECTED step_game FUNCTION (Handles 3-week LT) ===============
|
| 236 |
def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
|
| 237 |
state = st.session_state.game_state
|
| 238 |
week, echelons, human_role = state['week'], state['echelons'], state['human_role']
|
|
|
|
| 241 |
llm_raw_responses = {}
|
| 242 |
|
| 243 |
# Store state at the very beginning of the week for logging opening balances
|
|
|
|
| 244 |
opening_inventories = {name: e['inventory'] for name, e in echelons.items()}
|
| 245 |
opening_backlogs = {name: e['backlog'] for name, e in echelons.items()}
|
| 246 |
arrived_this_week = {name: 0 for name in echelon_order} # Track arrivals for logging
|
| 247 |
|
| 248 |
# --- Game Simulation Steps ---
|
| 249 |
|
| 250 |
+
# Step 1a & 1b: Shipments/Production Arrive & Update Temp Inventory
|
| 251 |
+
inventory_after_arrival = {} # Store intermediate inventory state
|
| 252 |
+
# Factory production completion
|
| 253 |
factory_state = echelons["Factory"]
|
| 254 |
produced_units = 0
|
| 255 |
if state['factory_production_pipeline']:
|
| 256 |
+
produced_units = state['factory_production_pipeline'].popleft() # Pop from beginning (what was scheduled LEAST recently)
|
|
|
|
|
|
|
| 257 |
arrived_this_week["Factory"] = produced_units
|
| 258 |
+
inventory_after_arrival["Factory"] = factory_state['inventory'] + produced_units
|
|
|
|
| 259 |
|
| 260 |
+
# Downstream shipment arrivals
|
|
|
|
| 261 |
for name in ["Retailer", "Wholesaler", "Distributor"]:
|
| 262 |
arrived_shipment = 0
|
| 263 |
if echelons[name]['incoming_shipments']:
|
| 264 |
+
arrived_shipment = echelons[name]['incoming_shipments'].popleft() # Pop from beginning
|
| 265 |
arrived_this_week[name] = arrived_shipment
|
| 266 |
inventory_after_arrival[name] = echelons[name]['inventory'] + arrived_shipment
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
+
# Step 2: Orders Arrive from Downstream & Update Temp Backlog
|
| 270 |
+
# Orders arrive after ORDER_PASSING_DELAY
|
| 271 |
+
total_backlog_before_shipping = {} # Store intermediate backlog state
|
| 272 |
for name in echelon_order:
|
| 273 |
+
incoming_order_for_this_week = 0
|
| 274 |
if name == "Retailer":
|
| 275 |
+
incoming_order_for_this_week = get_customer_demand(week)
|
| 276 |
else:
|
| 277 |
+
# Check the correct order pipeline based on the downstream partner
|
| 278 |
downstream_name = echelons[name]['downstream_name']
|
| 279 |
+
if downstream_name == 'Distributor':
|
| 280 |
+
if state['distributor_order_pipeline']:
|
| 281 |
+
incoming_order_for_this_week = state['distributor_order_pipeline'].popleft()
|
| 282 |
+
elif downstream_name == 'Wholesaler':
|
| 283 |
+
if state['wholesaler_order_pipeline']:
|
| 284 |
+
incoming_order_for_this_week = state['wholesaler_order_pipeline'].popleft()
|
| 285 |
+
elif downstream_name == 'Retailer':
|
| 286 |
+
if state['retailer_order_pipeline']:
|
| 287 |
+
incoming_order_for_this_week = state['retailer_order_pipeline'].popleft()
|
| 288 |
+
|
| 289 |
+
echelons[name]['incoming_order'] = incoming_order_for_this_week # Store for logging/display
|
| 290 |
+
total_backlog_before_shipping[name] = echelons[name]['backlog'] + incoming_order_for_this_week
|
| 291 |
+
|
| 292 |
|
| 293 |
# --- Create State Snapshot for AI/Human Decision Point ---
|
|
|
|
| 294 |
decision_point_states = {}
|
| 295 |
for name in echelon_order:
|
|
|
|
| 296 |
decision_point_states[name] = {
|
| 297 |
'name': name,
|
| 298 |
'inventory': inventory_after_arrival[name], # Inventory available
|
| 299 |
'backlog': total_backlog_before_shipping[name], # Total demand to meet
|
| 300 |
'incoming_order': echelons[name]['incoming_order'], # Order received this week
|
|
|
|
| 301 |
'incoming_shipments': echelons[name]['incoming_shipments'].copy() if name != "Factory" else deque(),
|
| 302 |
}
|
| 303 |
|
| 304 |
# --- Step 4: Agent Decisions (Place Orders / Schedule Production) ---
|
|
|
|
| 305 |
current_week_orders = {}
|
| 306 |
for name in echelon_order:
|
| 307 |
+
e = echelons[name]
|
| 308 |
+
prompt_state = decision_point_states[name]
|
| 309 |
|
| 310 |
if name == human_role:
|
| 311 |
order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
|
|
|
|
| 314 |
order_amount, raw_resp = get_llm_order_decision(prompt, name)
|
| 315 |
|
| 316 |
llm_raw_responses[name] = raw_resp
|
| 317 |
+
e['order_placed'] = max(0, order_amount)
|
| 318 |
current_week_orders[name] = e['order_placed']
|
| 319 |
|
| 320 |
+
# Put the order into the correct pipeline to simulate ORDER_PASSING_DELAY
|
| 321 |
+
if name == 'Distributor': state['distributor_order_pipeline'].append(e['order_placed'])
|
| 322 |
+
elif name == 'Wholesaler': state['wholesaler_order_pipeline'].append(e['order_placed'])
|
| 323 |
+
elif name == 'Retailer': state['retailer_order_pipeline'].append(e['order_placed'])
|
| 324 |
+
# Factory's 'order_placed' is its production decision
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
# Step 4b: Factory schedules production
|
| 328 |
+
# Factory's decision ('order_placed') enters the production pipeline
|
| 329 |
state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
|
| 330 |
|
| 331 |
+
|
| 332 |
# --- Step 3: Fulfill orders (Ship Beer) ---
|
| 333 |
+
# Occurs after decisions, uses inventory_after_arrival and total_backlog_before_shipping
|
| 334 |
+
units_produced_and_shipped_by_factory = 0 # Track for adding to Distributor's shipment queue
|
| 335 |
for name in echelon_order:
|
| 336 |
e = echelons[name]
|
| 337 |
demand_to_meet = total_backlog_before_shipping[name]
|
|
|
|
| 342 |
e['inventory'] = available_inv - e['shipment_sent']
|
| 343 |
e['backlog'] = demand_to_meet - e['shipment_sent']
|
| 344 |
|
| 345 |
+
# If Factory, what it 'sent' was actually produced and ready for shipping delay
|
| 346 |
+
if name == "Factory":
|
| 347 |
+
units_produced_and_shipped_by_factory = e['shipment_sent']
|
| 348 |
+
|
| 349 |
+
# Step 3b: Place items shipped by Factory/Distributor/Wholesaler into appropriate shipment queues
|
| 350 |
+
# Factory -> Distributor (uses FACTORY_SHIPPING_DELAY)
|
| 351 |
+
if units_produced_and_shipped_by_factory > 0:
|
| 352 |
+
echelons['Distributor']['incoming_shipments'].append(units_produced_and_shipped_by_factory)
|
| 353 |
+
# Distributor -> Wholesaler (uses SHIPPING_DELAY)
|
| 354 |
+
if echelons['Distributor']['shipment_sent'] > 0:
|
| 355 |
+
echelons['Wholesaler']['incoming_shipments'].append(echelons['Distributor']['shipment_sent'])
|
| 356 |
+
# Wholesaler -> Retailer (uses SHIPPING_DELAY)
|
| 357 |
+
if echelons['Wholesaler']['shipment_sent'] > 0:
|
| 358 |
+
echelons['Retailer']['incoming_shipments'].append(echelons['Wholesaler']['shipment_sent'])
|
| 359 |
|
| 360 |
|
| 361 |
# --- Calculate Costs & Log (End of Week) ---
|
| 362 |
log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
|
| 363 |
+
del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs']
|
| 364 |
+
# Delete the specific order pipelines as well
|
| 365 |
+
for key in ['distributor_order_pipeline', 'wholesaler_order_pipeline', 'retailer_order_pipeline']:
|
| 366 |
+
if key in log_entry: del log_entry[key]
|
| 367 |
+
|
| 368 |
|
| 369 |
for name in echelon_order:
|
| 370 |
e = echelons[name]
|
|
|
|
| 371 |
e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
|
| 372 |
e['total_cost'] += e['weekly_cost']
|
| 373 |
|
| 374 |
+
log_entry[f'{name}.inventory'] = e['inventory']; log_entry[f'{name}.backlog'] = e['backlog']
|
| 375 |
+
log_entry[f'{name}.incoming_order'] = e['incoming_order']; log_entry[f'{name}.order_placed'] = e['order_placed']
|
| 376 |
+
log_entry[f'{name}.shipment_sent'] = e['shipment_sent']; log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
|
| 377 |
+
log_entry[f'{name}.total_cost'] = e['total_cost']; log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
|
| 378 |
+
log_entry[f'{name}.opening_inventory'] = opening_inventories[name]; log_entry[f'{name}.opening_backlog'] = opening_backlogs[name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
log_entry[f'{name}.arrived_this_week'] = arrived_this_week[name]
|
| 380 |
|
|
|
|
| 381 |
if name != 'Factory':
|
| 382 |
log_entry[f'{name}.arriving_next_week'] = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
|
| 383 |
else:
|
| 384 |
log_entry[f'{name}.production_completing_next_week'] = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
|
| 385 |
|
|
|
|
| 386 |
log_entry[f'{human_role}.initial_order'] = human_initial_order
|
| 387 |
log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
|
| 388 |
|
|
|
|
| 391 |
# --- Advance Week ---
|
| 392 |
state['week'] += 1
|
| 393 |
state['decision_step'] = 'initial_order'
|
| 394 |
+
# 'last_week_orders' is no longer needed with pipeline approach
|
| 395 |
+
# if 'last_week_orders' in state: del state['last_week_orders']
|
| 396 |
|
| 397 |
if state['week'] > WEEKS: state['game_running'] = False
|
|
|
|
| 398 |
# ==============================================================================
|
| 399 |
|
| 400 |
+
|
| 401 |
def plot_results(df: pd.DataFrame, title: str, human_role: str):
|
| 402 |
# This function remains correct.
|
| 403 |
fig, axes = plt.subplots(4, 1, figsize=(12, 22))
|
|
|
|
| 489 |
st.error(f"Upload to Hugging Face failed: {e}")
|
| 490 |
|
| 491 |
# -----------------------------------------------------------------------------
|
| 492 |
+
# 4. Streamlit UI (Minor adjustments for clarity)
|
| 493 |
# -----------------------------------------------------------------------------
|
| 494 |
st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
|
| 495 |
|
|
|
|
| 549 |
Managing your inventory and backlog is key to minimizing costs. Here's how they work:
|
| 550 |
* **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.
|
| 551 |
* **If you DON'T have enough inventory:**
|
| 552 |
+
* You ship **all** the inventory you have (after receiving any arrivals for the week).
|
| 553 |
* The remaining unfilled "Orders to Fill" becomes your **new Backlog** for next week.
|
| 554 |
* **Backlog is cumulative!** If you start Week 10 with a backlog of 5, get an order for 8 (total needed = 13), receive 10 units, and ship those 10 units, your new backlog for Week 11 is `13 - 10 = 3`.
|
| 555 |
* **If you DO have enough inventory:**
|
|
|
|
| 574 |
|
| 575 |
**B) Your Dashboard (What You See for Your Turn):**
|
| 576 |
The dashboard shows your status **at the start of the week, BEFORE Steps 1, 2, and 3 happen**:
|
| 577 |
+
* `Inventory (Opening)`: Your stock **at the beginning of the week**.
|
| 578 |
* `Backlog (Opening)`: Unfilled orders **carried over from the end of last week**.
|
| 579 |
+
* `Incoming Order (This Week)`: The specific order quantity that **will arrive** from the Wholesaler *during* this week (Step 2).
|
| 580 |
+
* `Arriving Next Week`: The quantity scheduled to arrive at the start of the **next week**.
|
| 581 |
* `Your Total Cumulative Cost`: Sum of all weekly costs up to the **end of last week**.
|
| 582 |
* `Cost Last Week`: The specific cost incurred just **last week**.
|
| 583 |
|
|
|
|
| 606 |
elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
|
| 607 |
state = st.session_state.game_state
|
| 608 |
week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
|
| 609 |
+
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
|
|
|
|
| 610 |
|
| 611 |
|
| 612 |
st.header(f"Week {week} / {WEEKS}")
|