openenv / main.py
ishraddhapawar's picture
Update main.py
dc32404 verified
Raw History Blame Contribute Delete
14.8 kB
from fastapi import FastAPI, HTTPException # type: ignore
from fastapi.middleware.cors import CORSMiddleware # type: ignore
from fastapi.staticfiles import StaticFiles # type: ignore
from fastapi.responses import FileResponse # type: ignore
from pydantic import BaseModel # type: ignore
import random
import uuid
import uvicorn # type: ignore
import os
from typing import Any, List, Dict, Optional, cast
from engine import (
GameState, INITIAL_FUNDS, SALE_PRICE, STORAGE_COST,
CONTAINER_SIZE, SHIPPING_COST, LATE_PENALTY_RATE,
INITIAL_FACTORIES, INITIAL_PORTS, WAREHOUSES, INITIAL_DEMAND
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
state = GameState()
def generate_smart_alerts():
alerts = []
delayed_ports = [p for p in state.portDelays if p["currentDelay"] > p["baseDelay"] or p["isClosed"]]
if delayed_ports:
best_port = min((p for p in state.portDelays if not p["isClosed"]), key=lambda x: x["currentDelay"], default=None)
if best_port:
alerts.append({
"type": "warning",
"msg": f"Risk Flag: {len(delayed_ports)} ports are experiencing delays. Recommendation: Route via {best_port['name']} (Wait: {best_port['currentDelay']} mo)."
})
for region, demand in state.demands.items():
base_demand = INITIAL_DEMAND[region]
if demand < base_demand * 0.8:
alerts.append({"type": "warning", "msg": f"Anomaly: {region.upper()} demand dropped severely to {demand}. Reduce future order volume!"})
elif demand > base_demand * 1.2:
alerts.append({"type": "success", "msg": f"Opportunity: {region.upper()} demand spiked to {demand}. Increase inventory to maximize revenue."})
total_inv = sum(state.inventory.values())
if total_inv * STORAGE_COST > getattr(state, 'last_revenue', 1000) * 0.3:
alerts.append({"type": "danger", "msg": f"High storage cost warning! Holding {total_inv} units costs ${total_inv * STORAGE_COST}/mo. Avoid over-ordering."})
return alerts
# --- ROUTES ---
@app.get("/api/state")
def get_state():
state.alerts = generate_smart_alerts()
leaderboard = []
for f in state.factoryCosts:
for p in state.portDelays:
if not p["isClosed"]:
cost_score = f["currentCost"] * 100 + SHIPPING_COST
reliability = 100 - (p["currentDelay"] * 5)
if state.scenario == "crisis": reliability -= 10
leaderboard.append({
"route": f"{f['name'].split(' ')[0]} → {p['name'].replace('Port of ','')}",
"cost": cost_score,
"time": p["currentDelay"],
"reliability": reliability
})
leaderboard = sorted(leaderboard, key=lambda x: (x["cost"], x["time"]))
leaderboard = leaderboard[:5] # type: ignore
return {
"turn": state.turn,
"scenario": state.scenario,
"balance": state.balance,
"benchmark_balance": state.benchmark_balance,
"inventory": state.inventory,
"demands": state.demands,
"factoryCosts": state.factoryCosts,
"portDelays": state.portDelays,
"inTransit": state.inTransit,
"history": state.history,
"events": state.events[-50:] if len(state.events) > 50 else state.events, # type: ignore
"alerts": state.alerts,
"leaderboard": leaderboard
}
class OrderRequest(BaseModel):
factory: str
quantity: int
port: str
dest: str
@app.post("/api/order")
def place_order(order: OrderRequest):
factory = next((f for f in state.factoryCosts if f["id"] == order.factory), None)
port = next((p for p in state.portDelays if p["id"] == order.port), None)
if factory is None or port is None:
raise HTTPException(status_code=400, detail="Invalid factory or port")
if cast(Dict[str, Any], port).get("isClosed"): # type: ignore
raise HTTPException(status_code=400, detail=f"Cannot route through {port['name']}, port is closed!")
current_cost = cast(float, factory["currentCost"])
cost_of_goods = current_cost * order.quantity
containers = (order.quantity + CONTAINER_SIZE - 1) // CONTAINER_SIZE
shipping = containers * SHIPPING_COST
total_cost = cost_of_goods + shipping
if state.balance < total_cost:
raise HTTPException(status_code=400, detail=f"Insufficient funds. Need {total_cost}, have {state.balance}")
state.balance -= total_cost
state.month_shipping_costs += shipping
state.month_goods_costs += cost_of_goods
uid_str = str(uuid.uuid4())
state.inTransit.append({
"id": uid_str[:8], # type: ignore
"factory": factory["id"], # type: ignore
"port": port["id"], # type: ignore
"dest": order.dest,
"quantity": order.quantity,
"delayLeft": port["currentDelay"], # type: ignore
"totalDelay": port["currentDelay"], # type: ignore
"costBasis": total_cost
})
state.events.append({
"turn": state.turn,
"type": "info",
"msg": f"Ordered {order.quantity} units from {factory['name']} via {port['name']}. Cost: ${total_cost:,.0f}" # type: ignore
})
return {"status": "success", "balance": state.balance}
class ScenarioRequest(BaseModel):
scenario: str
@app.post("/api/scenario")
def set_scenario(req: ScenarioRequest):
state.scenario = req.scenario
state.events.append({
"turn": state.turn,
"type": "warning" if req.scenario == "crisis" else "success",
"msg": f"SCENARIO CHANGED: Rules updated to {req.scenario.upper()} mode!"
})
return get_state()
@app.post("/api/advance")
def advance_turn():
turn_revenue = 0
turn_penalty = 0
turn_events = []
# 1. Process shipments
new_in_transit = []
for shipment in state.inTransit:
if shipment["delayLeft"] <= 1:
state.inventory[shipment["dest"]] += shipment["quantity"]
warehouse = next((w for w in WAREHOUSES if w["id"] == shipment["dest"]), None)
warehouse_name = warehouse["name"] if warehouse else "Unknown"
turn_events.append({
"turn": state.turn,
"type": "success",
"msg": f"{shipment['quantity']} units arrived at {warehouse_name}"
})
else:
shipment["delayLeft"] -= 1
new_in_transit.append(shipment)
state.inTransit = new_in_transit
# 2. Fulfill demand
new_demands = dict(state.demands)
for region in state.demands:
demand = new_demands[region]
avail = state.inventory[region]
sold = min(demand, avail)
missed = demand - sold
state.inventory[region] -= sold
turn_revenue += sold * SALE_PRICE
if missed > 0:
penalty = missed * (SALE_PRICE * LATE_PENALTY_RATE)
turn_penalty += penalty # type: ignore
if missed > demand * 0.5:
turn_events.append({
"turn": state.turn,
"type": "danger",
"msg": f"{region.upper()} CRITICAL STOCKOUT! Missed {missed} units. Penalty: ${penalty:,.0f}"
})
# 3. Holding costs
holding_cost = sum(amt * STORAGE_COST for amt in state.inventory.values())
state.last_revenue = turn_revenue
# 4. Update balance
period_profit = turn_revenue - holding_cost - turn_penalty - state.month_goods_costs - state.month_shipping_costs # type: ignore
state.balance += turn_revenue - holding_cost - turn_penalty # type: ignore
# Compute Benchmark AI Balance
# Benchmark buys exactly the demand each month at lowest fixed rate.
benchmark_revenue = sum(state.demands.values()) * SALE_PRICE
benchmark_cost = sum(state.demands.values()) * (50 + 30) # 50 = china max cheap, 30 = generic average shipping
benchmark_profit = benchmark_revenue - benchmark_cost
if state.scenario == "crisis":
benchmark_profit *= 0.6 # AI suffers in crisis too
state.benchmark_balance += benchmark_profit
# 5. Shocks & Fluctuation
demand_shock_chance = 0.9 if state.scenario == "normal" else 0.8
is_competitor_shock = random.random() > demand_shock_chance
demand_multiplier = random.uniform(0.8, 1.2)
if state.scenario == "growth": demand_multiplier += 0.2
if is_competitor_shock:
demand_multiplier = 0.5
turn_events.append({"turn": state.turn + 1, "type": "warning", "msg": "Competitors undercut prices! Demand collapsed."})
for region in INITIAL_DEMAND:
state.demands[region] = int(INITIAL_DEMAND[region] * demand_multiplier)
price_spike_chance = 0.9 if state.scenario == "normal" else 0.75 # more spikes in crisis
is_price_spike = random.random() > price_spike_chance
if is_price_spike:
turn_events.append({"turn": state.turn + 1, "type": "danger", "msg": "Global component shortage! Factory costs spiked +40%."})
for f in state.factoryCosts:
base = f["baseCost"]
if is_price_spike:
f["currentCost"] = int(base * 1.4)
else:
f["currentCost"] = int(base * random.uniform(0.85, 1.15))
closure_chance = 0.95 if state.scenario == "normal" else 0.85
for p in state.portDelays:
is_closed = random.random() > closure_chance
if is_closed:
turn_events.append({"turn": state.turn + 1, "type": "danger", "msg": f"GEOPOLITICAL SHOCK: {p['name']} closed for 2 months!"})
p["isClosed"] = is_closed
p["currentDelay"] = max(2, p["baseDelay"] + 2) if is_closed else p["baseDelay"] + (1 if random.random() > 0.5 else 0)
# SET STATE
state.turn += 1
state.history.append({
"month": state.turn - 1,
"profit": period_profit,
"revenue": turn_revenue,
"storage_costs": holding_cost,
"shipping_costs": state.month_shipping_costs,
"penalties": turn_penalty,
"balance": state.balance,
"benchmark": state.benchmark_balance
})
# Reset monthly trackers
state.month_shipping_costs = 0
state.month_goods_costs = 0
state.events.extend(turn_events)
return get_state()
@app.get("/api/oracle")
def get_oracle_advice():
# Generate some strategic advice based on current state
closed_ports = [p["name"] for p in state.portDelays if p["isClosed"]]
delayed_ports = [p["name"] for p in state.portDelays if p["currentDelay"] > p["baseDelay"] and not p["isClosed"]]
advice = "Market conditions are stable. Maintain standard logistics routing."
action = "Continue optimal ordering."
if closed_ports:
advice = f"CRITICAL: {', '.join(closed_ports)} is closed! This will severely disrupt supply chains."
action = "Reroute all shipments immediately to alternative ports to avoid stockouts."
elif delayed_ports:
advice = f"WARNING: Delays detected at {', '.join(delayed_ports)}."
action = "Consider holding more inventory to buffer against transit delays."
elif state.scenario == "crisis":
advice = "Global geopolitical crisis is increasing closure probabilities."
action = "Pre-stock warehouses to maximum capacity now before ports close."
elif state.scenario == "growth":
advice = "Aggressive growth scenario detected. Demand is inflating rapidly."
action = "Scale up order volumes by 20% to meet surging demand and capture market share."
return {"advice": advice, "action": action}
@app.post("/api/autoplay")
def auto_play_turn():
# Simple AI agent: for each region, if inventory < demand, order the difference
# Find cheapest available factory and port
open_ports = [p for p in state.portDelays if not p["isClosed"]]
if not open_ports:
# Cannot order anything, just advance turn
return advance_turn()
best_port = min(open_ports, key=lambda x: x["currentDelay"])
best_factory = min(state.factoryCosts, key=lambda x: x["currentCost"])
for region, demand in state.demands.items():
if state.inventory[region] < demand:
needed = demand - state.inventory[region]
# Order 'needed' amount
cost_of_goods = best_factory["currentCost"] * needed
containers = (needed + CONTAINER_SIZE - 1) // CONTAINER_SIZE
shipping = containers * SHIPPING_COST
total_cost = cost_of_goods + shipping
if state.balance >= total_cost:
state.balance -= total_cost
state.month_shipping_costs += shipping
state.month_goods_costs += cost_of_goods
uid_str = str(uuid.uuid4())
state.inTransit.append({
"id": uid_str[:8],
"factory": best_factory["id"],
"port": best_port["id"],
"dest": region,
"quantity": needed,
"delayLeft": best_port["currentDelay"],
"totalDelay": best_port["currentDelay"],
"costBasis": total_cost
})
state.events.append({
"turn": state.turn,
"type": "info",
"msg": f"AI ORACLE ordered {needed} units from {best_factory['name']} via {best_port['name']} for {region.upper()}."
})
return advance_turn()
@app.post("/api/reset")
def reset_game():
state.reset()
return get_state()
# --- SERVE FRONTEND ---
@app.get("/")
async def serve_index():
return FileResponse("dist/index.html")
@app.get("/{full_path:path}")
async def serve_static(full_path: str):
path = os.path.join("dist", full_path)
if os.path.isfile(path):
return FileResponse(path)
return FileResponse("dist/index.html")
if os.path.isdir("dist"):
app.mount("/assets", StaticFiles(directory="dist/assets"), name="assets")
# Serve static files if 'dist' folder exists (for Docker/production)
if os.path.isdir("dist"):
app.mount("/assets", StaticFiles(directory="dist/assets"), name="assets")
@app.get("/{full_path:path}")
async def serve_frontend(full_path: str):
path = os.path.join("dist", full_path)
if os.path.isfile(path):
return FileResponse(path)
return FileResponse("dist/index.html")
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8000))
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)