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
|
|
@@ -57,15 +57,21 @@ else:
|
|
| 57 |
|
| 58 |
|
| 59 |
# -----------------------------------------------------------------------------
|
| 60 |
-
# 3. Core Game Logic Functions
|
| 61 |
# -----------------------------------------------------------------------------
|
| 62 |
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
participant_id = str(uuid.uuid4())[:8]
|
| 70 |
|
| 71 |
st.session_state.game_state = {
|
|
@@ -80,6 +86,8 @@ 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 |
if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
|
| 84 |
elif name == "Factory": shipping_weeks = 0
|
| 85 |
else: shipping_weeks = SHIPPING_DELAY
|
|
@@ -91,7 +99,7 @@ def init_game_state(llm_personality: str, info_sharing: str):
|
|
| 91 |
'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
|
| 92 |
'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
|
| 93 |
}
|
| 94 |
-
st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You
|
| 95 |
|
| 96 |
def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
| 97 |
if not client: return 8, "NO_API_KEY_DEFAULT"
|
|
@@ -139,6 +147,11 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
|
|
| 139 |
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."
|
| 140 |
|
| 141 |
def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
state = st.session_state.game_state
|
| 143 |
week, echelons, human_role = state['week'], state['echelons'], state['human_role']
|
| 144 |
llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
|
|
@@ -215,7 +228,7 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
|
|
| 215 |
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)')
|
| 216 |
|
| 217 |
order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
|
| 218 |
-
order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1,
|
| 219 |
|
| 220 |
total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
|
| 221 |
total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
|
|
@@ -271,42 +284,49 @@ else:
|
|
| 271 |
col1, col2 = st.columns(2)
|
| 272 |
with col1:
|
| 273 |
st.subheader("🔗 The Supply Chain")
|
| 274 |
-
|
| 275 |
-
# =============== IMAGE ADDED HERE ===============
|
| 276 |
try:
|
| 277 |
st.image(IMAGE_PATH, caption="Orders flow upstream (right), beer flows downstream (left).")
|
| 278 |
except FileNotFoundError:
|
| 279 |
st.warning("Image file not found. Please ensure 'beer_game_diagram.png' is uploaded to the repository.")
|
| 280 |
-
# ================================================
|
| 281 |
|
| 282 |
st.markdown("""
|
| 283 |
-
You will
|
| 284 |
- **Retailer:** Fulfills end-customer demand. Orders from the Wholesaler.
|
| 285 |
- **Wholesaler:** Fulfills Retailer orders. Orders from the Distributor.
|
| 286 |
-
- **Distributor:** Fulfills Wholesaler orders. Orders from the Factory.
|
| 287 |
-
- **Factory:** Fulfills
|
| 288 |
""")
|
| 289 |
with col2:
|
| 290 |
st.subheader("⏳ The Challenge: Delays!")
|
| 291 |
st.markdown("""
|
| 292 |
-
The key challenge is managing
|
| 293 |
|
| 294 |
-
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
|
| 297 |
""")
|
| 298 |
|
| 299 |
-
st.subheader("🎮 How
|
| 300 |
st.markdown("""
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
""")
|
|
|
|
| 308 |
st.markdown("---")
|
| 309 |
-
|
| 310 |
st.header("⚙️ Game Configuration")
|
| 311 |
c1, c2 = st.columns(2)
|
| 312 |
with c1:
|
|
@@ -335,23 +355,33 @@ else:
|
|
| 335 |
st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
|
| 336 |
st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
|
| 337 |
st.write(f"Incoming Order: **{e['incoming_order']}**")
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
else:
|
| 340 |
st.info("In Local Information mode, you can only see your own status dashboard.")
|
| 341 |
-
e = echelons[human_role]
|
| 342 |
st.markdown(f"### 👤 {human_role} (Your Dashboard)")
|
| 343 |
col1, col2, col3, col4 = st.columns(4)
|
| 344 |
col1.metric("Current Inventory", e['inventory'])
|
| 345 |
col2.metric("Current Backlog", e['backlog'])
|
| 346 |
col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
|
|
|
|
| 347 |
col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
|
|
|
|
| 348 |
st.markdown("---")
|
| 349 |
-
st.header("Your Decision")
|
| 350 |
human_echelon_state = echelons[human_role]
|
| 351 |
|
| 352 |
if state['decision_step'] == 'initial_order':
|
| 353 |
with st.form(key="initial_order_form"):
|
| 354 |
-
st.markdown("#### **Step
|
| 355 |
initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=human_echelon_state['incoming_order'])
|
| 356 |
if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
|
| 357 |
state['human_initial_order'] = int(initial_order)
|
|
@@ -367,7 +397,7 @@ else:
|
|
| 367 |
st.session_state.final_order_input = ai_suggestion
|
| 368 |
|
| 369 |
with st.form(key="final_order_form"):
|
| 370 |
-
st.markdown(f"#### **Step
|
| 371 |
st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
|
| 372 |
st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input')
|
| 373 |
if st.form_submit_button("Submit Final Order & Advance to Next Week"):
|
|
@@ -394,12 +424,10 @@ else:
|
|
| 394 |
display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}")
|
| 395 |
st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
|
| 396 |
|
| 397 |
-
# =============== IMAGE ADDED TO SIDEBAR ===============
|
| 398 |
try:
|
| 399 |
st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
|
| 400 |
except FileNotFoundError:
|
| 401 |
st.sidebar.warning("Image file not found.")
|
| 402 |
-
# ======================================================
|
| 403 |
|
| 404 |
st.sidebar.header("Game Info")
|
| 405 |
st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
|
|
@@ -414,7 +442,15 @@ else:
|
|
| 414 |
st.header("🎉 Game Over!")
|
| 415 |
state = st.session_state.game_state
|
| 416 |
logs_df = pd.json_normalize(state['logs'])
|
| 417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
st.pyplot(fig)
|
| 419 |
save_logs_and_upload(state)
|
| 420 |
if st.button("✨ Start a New Game"):
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# @title Beer Game Final Version (v4.6 - Fixed Role, UI Logic, and Upload Bug)
|
| 3 |
|
| 4 |
# -----------------------------------------------------------------------------
|
| 5 |
# 1. Import Libraries
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
# -----------------------------------------------------------------------------
|
| 60 |
+
# 3. Core Game Logic Functions
|
| 61 |
# -----------------------------------------------------------------------------
|
| 62 |
|
| 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 |
+
"""Initializes or resets the game state in st.session_state."""
|
| 68 |
roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 69 |
+
|
| 70 |
+
# =============== CHANGE 1: Fixed Role ===============
|
| 71 |
+
# Human role is no longer random. Everyone is Distributor.
|
| 72 |
+
human_role = "Distributor"
|
| 73 |
+
# ======================================================
|
| 74 |
+
|
| 75 |
participant_id = str(uuid.uuid4())[:8]
|
| 76 |
|
| 77 |
st.session_state.game_state = {
|
|
|
|
| 86 |
for i, name in enumerate(roles):
|
| 87 |
upstream = roles[i + 1] if i + 1 < len(roles) else None
|
| 88 |
downstream = roles[i - 1] if i - 1 >= 0 else None
|
| 89 |
+
|
| 90 |
+
# This logic remains correct: Factory has 0 shipping weeks for incoming.
|
| 91 |
if name == "Distributor": shipping_weeks = FACTORY_SHIPPING_DELAY
|
| 92 |
elif name == "Factory": shipping_weeks = 0
|
| 93 |
else: shipping_weeks = SHIPPING_DELAY
|
|
|
|
| 99 |
'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
|
| 100 |
'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
|
| 101 |
}
|
| 102 |
+
st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You are playing as the: **{human_role}**.")
|
| 103 |
|
| 104 |
def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
| 105 |
if not client: return 8, "NO_API_KEY_DEFAULT"
|
|
|
|
| 147 |
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."
|
| 148 |
|
| 149 |
def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
|
| 150 |
+
# This function's core logic is sound and correctly models the delays.
|
| 151 |
+
# Factory inventory is correctly handled by 'factory_production_pipeline'.
|
| 152 |
+
# Other echelons are correctly handled by 'incoming_shipments'.
|
| 153 |
+
# No changes needed here.
|
| 154 |
+
|
| 155 |
state = st.session_state.game_state
|
| 156 |
week, echelons, human_role = state['week'], state['echelons'], state['human_role']
|
| 157 |
llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
|
|
|
|
| 228 |
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)')
|
| 229 |
|
| 230 |
order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
|
| 231 |
+
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)')
|
| 232 |
|
| 233 |
total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
|
| 234 |
total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
|
|
|
|
| 284 |
col1, col2 = st.columns(2)
|
| 285 |
with col1:
|
| 286 |
st.subheader("🔗 The Supply Chain")
|
|
|
|
|
|
|
| 287 |
try:
|
| 288 |
st.image(IMAGE_PATH, caption="Orders flow upstream (right), beer flows downstream (left).")
|
| 289 |
except FileNotFoundError:
|
| 290 |
st.warning("Image file not found. Please ensure 'beer_game_diagram.png' is uploaded to the repository.")
|
|
|
|
| 291 |
|
| 292 |
st.markdown("""
|
| 293 |
+
You will play the role of the **Distributor**. The other three roles (Retailer, Wholesaler, Factory) will be controlled by AI agents.
|
| 294 |
- **Retailer:** Fulfills end-customer demand. Orders from the Wholesaler.
|
| 295 |
- **Wholesaler:** Fulfills Retailer orders. Orders from the Distributor.
|
| 296 |
+
- **Distributor (You):** Fulfills Wholesaler orders. Orders from the Factory.
|
| 297 |
+
- **Factory:** Fulfills your orders. Produces new beer.
|
| 298 |
""")
|
| 299 |
with col2:
|
| 300 |
st.subheader("⏳ The Challenge: Delays!")
|
| 301 |
st.markdown("""
|
| 302 |
+
The key challenge is managing the **delays** shown in the diagram.
|
| 303 |
|
| 304 |
+
* **Order Delays:** It takes **1 week** for your order to reach your supplier (the Factory).
|
| 305 |
+
* **Shipping Delays:** It takes **1 week** for beer to be shipped from the Factory to you. (It takes 2 weeks for all other shipping links).
|
| 306 |
+
* **Production Delays:** The Factory needs **1 week** to produce new beer.
|
| 307 |
|
| 308 |
+
This means there is a **2-week** total lead time from when you order to when you receive beer (1 week order delay + 1 week production delay, or 1 week shipping delay). *Note: The diagram shows generic delays; our game uses the specific delays listed here.*
|
| 309 |
""")
|
| 310 |
|
| 311 |
+
st.subheader("🎮 How Each Week Works")
|
| 312 |
st.markdown("""
|
| 313 |
+
Your main job is to place an order each week. The system handles the rest.
|
| 314 |
+
|
| 315 |
+
**1. Automated Steps (Start of Week)**
|
| 316 |
+
When a new week begins, the system first automates three steps based on past decisions:
|
| 317 |
+
* **(Step 1: Shipments Arrive):** Shipments from the Factory (which you ordered weeks ago) arrive and are added to your inventory.
|
| 318 |
+
* **(Step 2: New Orders Arrive):** You receive a new incoming order from the Wholesaler.
|
| 319 |
+
* **(Step 3: Ship Beer):** The system automatically ships as much beer as you have in stock to fulfill the Wholesaler's order, plus any leftover backlog. Any unfulfilled amount becomes your new backlog for next week.
|
| 320 |
+
|
| 321 |
+
**2. Your Decision (Two-Step Process)**
|
| 322 |
+
After the automated steps, you will see your new inventory and backlog. Now, it's your turn to perform **Step 4**:
|
| 323 |
+
* **(Step 4a):** Based on your new status, submit your **initial order** to the Factory.
|
| 324 |
+
* **(Step 4b):** After submitting, you will see an **AI suggestion**. Review it, then submit your **final order**.
|
| 325 |
+
|
| 326 |
+
Submitting your final order advances the game to the next week, and the cycle repeats.
|
| 327 |
""")
|
| 328 |
+
|
| 329 |
st.markdown("---")
|
|
|
|
| 330 |
st.header("⚙️ Game Configuration")
|
| 331 |
c1, c2 = st.columns(2)
|
| 332 |
with c1:
|
|
|
|
| 355 |
st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
|
| 356 |
st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
|
| 357 |
st.write(f"Incoming Order: **{e['incoming_order']}**")
|
| 358 |
+
|
| 359 |
+
# =============== CHANGE 2: Fixed Factory UI Logic ===============
|
| 360 |
+
if name == "Factory":
|
| 361 |
+
prod_completing = list(state['factory_production_pipeline'])[0] if state['factory_production_pipeline'] else 0
|
| 362 |
+
st.write(f"Production Completing: **{prod_completing}**")
|
| 363 |
+
else:
|
| 364 |
+
arriving = list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0
|
| 365 |
+
st.write(f"Arriving Next Week: **{arriving}**")
|
| 366 |
+
# ==============================================================
|
| 367 |
else:
|
| 368 |
st.info("In Local Information mode, you can only see your own status dashboard.")
|
| 369 |
+
e = echelons[human_role] # This is always the Distributor
|
| 370 |
st.markdown(f"### 👤 {human_role} (Your Dashboard)")
|
| 371 |
col1, col2, col3, col4 = st.columns(4)
|
| 372 |
col1.metric("Current Inventory", e['inventory'])
|
| 373 |
col2.metric("Current Backlog", e['backlog'])
|
| 374 |
col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
|
| 375 |
+
# This is correct for the Distributor
|
| 376 |
col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
|
| 377 |
+
|
| 378 |
st.markdown("---")
|
| 379 |
+
st.header("Your Decision (Step 4)")
|
| 380 |
human_echelon_state = echelons[human_role]
|
| 381 |
|
| 382 |
if state['decision_step'] == 'initial_order':
|
| 383 |
with st.form(key="initial_order_form"):
|
| 384 |
+
st.markdown("#### **Step 4a:** Based on the information available, submit your **initial** order.")
|
| 385 |
initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=human_echelon_state['incoming_order'])
|
| 386 |
if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
|
| 387 |
state['human_initial_order'] = int(initial_order)
|
|
|
|
| 397 |
st.session_state.final_order_input = ai_suggestion
|
| 398 |
|
| 399 |
with st.form(key="final_order_form"):
|
| 400 |
+
st.markdown(f"#### **Step 4b:** The AI suggests ordering **{ai_suggestion}** units.")
|
| 401 |
st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
|
| 402 |
st.number_input("Your Final Order Quantity:", min_value=0, step=1, key='final_order_input')
|
| 403 |
if st.form_submit_button("Submit Final Order & Advance to Next Week"):
|
|
|
|
| 424 |
display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}")
|
| 425 |
st.dataframe(display_df.sort_values(by="Week", ascending=False), hide_index=True, use_container_width=True)
|
| 426 |
|
|
|
|
| 427 |
try:
|
| 428 |
st.sidebar.image(IMAGE_PATH, caption="Supply Chain Reference")
|
| 429 |
except FileNotFoundError:
|
| 430 |
st.sidebar.warning("Image file not found.")
|
|
|
|
| 431 |
|
| 432 |
st.sidebar.header("Game Info")
|
| 433 |
st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
|
|
|
|
| 442 |
st.header("🎉 Game Over!")
|
| 443 |
state = st.session_state.game_state
|
| 444 |
logs_df = pd.json_normalize(state['logs'])
|
| 445 |
+
|
| 446 |
+
# =============== CHANGE 3: Fixed Typo Bug ===============
|
| 447 |
+
fig = plot_results(
|
| 448 |
+
logs_df,
|
| 449 |
+
f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality']} | Info: {state['info_sharing']})",
|
| 450 |
+
state['human_role']
|
| 451 |
+
)
|
| 452 |
+
# ========================================================
|
| 453 |
+
|
| 454 |
st.pyplot(fig)
|
| 455 |
save_logs_and_upload(state)
|
| 456 |
if st.button("✨ Start a New Game"):
|