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
|
|
@@ -41,6 +41,7 @@ BACKLOG_COST = 1.0
|
|
| 41 |
OPENAI_MODEL = "gpt-4o-mini"
|
| 42 |
LOCAL_LOG_DIR = Path("logs")
|
| 43 |
LOCAL_LOG_DIR.mkdir(exist_ok=True)
|
|
|
|
| 44 |
|
| 45 |
# --- API & Secrets Configuration ---
|
| 46 |
try:
|
|
@@ -144,7 +145,6 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
|
|
| 144 |
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 145 |
llm_raw_responses = {}
|
| 146 |
|
| 147 |
-
# Store pre-step state for logging
|
| 148 |
pre_step_inventory = echelons[human_role]['inventory']
|
| 149 |
pre_step_backlog = echelons[human_role]['backlog']
|
| 150 |
|
|
@@ -215,7 +215,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 ($)')
|
|
@@ -255,6 +255,7 @@ if st.session_state.get('initialization_error'):
|
|
| 255 |
else:
|
| 256 |
# --- Game Setup & Instructions ---
|
| 257 |
if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
|
|
|
|
| 258 |
st.markdown("---")
|
| 259 |
st.header("๐ Welcome to the Beer Game!")
|
| 260 |
st.markdown("""
|
|
@@ -266,9 +267,18 @@ else:
|
|
| 266 |
- **Holding Inventory:** **$0.50 per unit per week**
|
| 267 |
- **Backlog (Unfilled Orders):** **$1.00 per unit per week**
|
| 268 |
""")
|
|
|
|
| 269 |
col1, col2 = st.columns(2)
|
| 270 |
with col1:
|
| 271 |
st.subheader("๐ The Supply Chain")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
st.markdown("""
|
| 273 |
You will be randomly assigned one of four roles. The other three will be controlled by AI agents.
|
| 274 |
- **Retailer:** Fulfills end-customer demand. Orders from the Wholesaler.
|
|
@@ -279,8 +289,13 @@ else:
|
|
| 279 |
with col2:
|
| 280 |
st.subheader("โณ The Challenge: Delays!")
|
| 281 |
st.markdown("""
|
| 282 |
-
The key challenge is managing delays.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
""")
|
|
|
|
| 284 |
st.subheader("๐ฎ How to Play This Version")
|
| 285 |
st.markdown("""
|
| 286 |
1. **Configure the Game:** Choose the AI's behavior and the level of information sharing.
|
|
@@ -291,12 +306,14 @@ else:
|
|
| 291 |
4. **Advance:** Once you submit your final order, the week advances, and all AI agents make their moves.
|
| 292 |
""")
|
| 293 |
st.markdown("---")
|
|
|
|
| 294 |
st.header("โ๏ธ Game Configuration")
|
| 295 |
c1, c2 = st.columns(2)
|
| 296 |
with c1:
|
| 297 |
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.")
|
| 298 |
with c2:
|
| 299 |
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.")
|
|
|
|
| 300 |
if st.button("๐ Start Game", type="primary", disabled=(client is None)):
|
| 301 |
init_game_state(llm_personality, info_sharing)
|
| 302 |
st.rerun()
|
|
@@ -359,40 +376,31 @@ else:
|
|
| 359 |
del st.session_state.final_order_input
|
| 360 |
st.rerun()
|
| 361 |
|
| 362 |
-
# =============== NEW SECTION STARTS HERE ===============
|
| 363 |
st.markdown("---")
|
| 364 |
with st.expander("๐ Your Weekly Decision Log", expanded=False):
|
| 365 |
if not state['logs']:
|
| 366 |
st.write("Your weekly history will be displayed here after you complete the first week.")
|
| 367 |
else:
|
| 368 |
history_df = pd.json_normalize(state['logs'])
|
| 369 |
-
|
| 370 |
-
# Define columns to display for the human player
|
| 371 |
human_cols = {
|
| 372 |
-
'week': 'Week',
|
| 373 |
-
f'{human_role}.
|
| 374 |
-
f'{human_role}.
|
| 375 |
-
f'{human_role}.
|
| 376 |
-
f'{human_role}.initial_order': 'Your Initial Order',
|
| 377 |
-
f'{human_role}.ai_suggestion': 'AI Suggestion',
|
| 378 |
-
f'{human_role}.order_placed': 'Your Final Order',
|
| 379 |
-
f'{human_role}.weekly_cost': 'Weekly Cost',
|
| 380 |
}
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
display_df['Weekly Cost'] = display_df['Weekly Cost'].apply(lambda x: f"${x:,.2f}")
|
| 387 |
-
|
| 388 |
-
# Display dataframe, sorted with the latest week on top
|
| 389 |
-
st.dataframe(
|
| 390 |
-
display_df.sort_values(by="Week", ascending=False),
|
| 391 |
-
hide_index=True,
|
| 392 |
-
use_container_width=True
|
| 393 |
-
)
|
| 394 |
-
# =============== NEW SECTION ENDS HERE =================
|
| 395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
st.sidebar.header("Game Info")
|
| 397 |
st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
|
| 398 |
if st.sidebar.button("๐ Reset Game"):
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# @title Beer Game Final Version (v4.4 - Added Diagram Image)
|
| 3 |
|
| 4 |
# -----------------------------------------------------------------------------
|
| 5 |
# 1. Import Libraries
|
|
|
|
| 41 |
OPENAI_MODEL = "gpt-4o-mini"
|
| 42 |
LOCAL_LOG_DIR = Path("logs")
|
| 43 |
LOCAL_LOG_DIR.mkdir(exist_ok=True)
|
| 44 |
+
IMAGE_PATH = "beer_game_diagram.png" # Path to your uploaded image
|
| 45 |
|
| 46 |
# --- API & Secrets Configuration ---
|
| 47 |
try:
|
|
|
|
| 145 |
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 146 |
llm_raw_responses = {}
|
| 147 |
|
|
|
|
| 148 |
pre_step_inventory = echelons[human_role]['inventory']
|
| 149 |
pre_step_backlog = echelons[human_role]['backlog']
|
| 150 |
|
|
|
|
| 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, WS + 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)')
|
| 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 ($)')
|
|
|
|
| 255 |
else:
|
| 256 |
# --- Game Setup & Instructions ---
|
| 257 |
if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
|
| 258 |
+
|
| 259 |
st.markdown("---")
|
| 260 |
st.header("๐ Welcome to the Beer Game!")
|
| 261 |
st.markdown("""
|
|
|
|
| 267 |
- **Holding Inventory:** **$0.50 per unit per week**
|
| 268 |
- **Backlog (Unfilled Orders):** **$1.00 per unit per week**
|
| 269 |
""")
|
| 270 |
+
|
| 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 be randomly assigned one of four roles. The other three will be controlled by AI agents.
|
| 284 |
- **Retailer:** Fulfills end-customer demand. Orders from the Wholesaler.
|
|
|
|
| 289 |
with col2:
|
| 290 |
st.subheader("โณ The Challenge: Delays!")
|
| 291 |
st.markdown("""
|
| 292 |
+
The key challenge is managing delays. As shown in the diagram, there are **shipping delays** for beer to arrive and **production delays** at the factory.
|
| 293 |
+
|
| 294 |
+
There is also a one-week **order-passing delay** (the small boxes) for your order to reach your supplier.
|
| 295 |
+
|
| 296 |
+
You must order enough to meet future demand without creating a huge pile of expensive inventory.
|
| 297 |
""")
|
| 298 |
+
|
| 299 |
st.subheader("๐ฎ How to Play This Version")
|
| 300 |
st.markdown("""
|
| 301 |
1. **Configure the Game:** Choose the AI's behavior and the level of information sharing.
|
|
|
|
| 306 |
4. **Advance:** Once you submit your final order, the week advances, and all AI agents make their moves.
|
| 307 |
""")
|
| 308 |
st.markdown("---")
|
| 309 |
+
|
| 310 |
st.header("โ๏ธ Game Configuration")
|
| 311 |
c1, c2 = st.columns(2)
|
| 312 |
with c1:
|
| 313 |
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.")
|
| 314 |
with c2:
|
| 315 |
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.")
|
| 316 |
+
|
| 317 |
if st.button("๐ Start Game", type="primary", disabled=(client is None)):
|
| 318 |
init_game_state(llm_personality, info_sharing)
|
| 319 |
st.rerun()
|
|
|
|
| 376 |
del st.session_state.final_order_input
|
| 377 |
st.rerun()
|
| 378 |
|
|
|
|
| 379 |
st.markdown("---")
|
| 380 |
with st.expander("๐ Your Weekly Decision Log", expanded=False):
|
| 381 |
if not state['logs']:
|
| 382 |
st.write("Your weekly history will be displayed here after you complete the first week.")
|
| 383 |
else:
|
| 384 |
history_df = pd.json_normalize(state['logs'])
|
|
|
|
|
|
|
| 385 |
human_cols = {
|
| 386 |
+
'week': 'Week', f'{human_role}.opening_inventory': 'Opening Inv.',
|
| 387 |
+
f'{human_role}.opening_backlog': 'Opening Backlog', f'{human_role}.incoming_order': 'Incoming Order',
|
| 388 |
+
f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion',
|
| 389 |
+
f'{human_role}.order_placed': 'Your Final Order', f'{human_role}.weekly_cost': 'Weekly Cost',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
+
display_cols = [col for col in human_cols.keys() if col in history_df.columns]
|
| 392 |
+
display_df = history_df[display_cols].rename(columns=human_cols)
|
| 393 |
+
if 'Weekly Cost' in display_df:
|
| 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}")
|
| 406 |
if st.sidebar.button("๐ Reset Game"):
|