better scripts

This commit is contained in:
johba 2025-08-09 19:17:46 +02:00
parent 9f0b163303
commit e021aff978
5 changed files with 290 additions and 52 deletions

View file

@ -49,6 +49,7 @@ contract FuzzingAnalysis is Test, CSVManager {
bool public trackPositions;
string public optimizerClass;
uint256 public tradesPerRun;
uint256 public seedOffset;
// Optimizers
BullMarketOptimizer bullOptimizer;
@ -80,7 +81,7 @@ contract FuzzingAnalysis is Test, CSVManager {
console.log(string.concat("=== FUZZING with ", optimizerClass, " ==="));
for (uint256 seed = 0; seed < fuzzingRuns; seed++) {
for (uint256 seed = seedOffset; seed < seedOffset + fuzzingRuns; seed++) {
if (seed % 10 == 0 && seed > 0) {
console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns)));
}
@ -107,15 +108,18 @@ contract FuzzingAnalysis is Test, CSVManager {
scenariosAnalyzed++;
// Check profitability
if (finalBalance > initialBalance) {
// Calculate profit/loss
bool isProfitable = finalBalance > initialBalance;
uint256 profitOrLoss;
uint256 profitOrLossPercentage;
if (isProfitable) {
profitOrLoss = finalBalance - initialBalance;
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
profitableScenarios++;
marketProfitable++;
uint256 profit = finalBalance - initialBalance;
uint256 profitPercentage = (profit * 100) / initialBalance;
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%"));
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%"));
// Add to CSV
profitableCSV = string.concat(
@ -124,11 +128,17 @@ contract FuzzingAnalysis is Test, CSVManager {
vm.toString(seed), ",",
vm.toString(initialBalance), ",",
vm.toString(finalBalance), ",",
vm.toString(profit), ",",
vm.toString(profitPercentage), "\n"
vm.toString(profitOrLoss), ",",
vm.toString(profitOrLossPercentage), "\n"
);
profitableCount++;
} else {
profitOrLoss = initialBalance - finalBalance;
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
}
// Always log result for cumulative tracking
console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss)));
}
console.log(string.concat("\nResults for ", optimizerClass, ":"));
@ -138,6 +148,7 @@ contract FuzzingAnalysis is Test, CSVManager {
console.log("=== ANALYSIS COMPLETE ===");
console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios)));
console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
// Write profitable scenarios CSV if any found
if (profitableCount > 0) {
@ -157,6 +168,7 @@ contract FuzzingAnalysis is Test, CSVManager {
trackPositions = vm.envOr("TRACK_POSITIONS", false);
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20));
seedOffset = vm.envOr("SEED_OFFSET", uint256(0));
}
function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) {
@ -189,14 +201,24 @@ contract FuzzingAnalysis is Test, CSVManager {
if (wethBal > 0) {
uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100%
uint256 buyAmount = (wethBal * buyPercent) / 1000;
if (buyAmount > 0) _executeBuy(buyAmount);
if (buyAmount > 0) {
_executeBuy(buyAmount);
if (trackPositions) {
_recordPositionData(string.concat("Buy_", vm.toString(i)));
}
}
}
} else if (action < 80) { // 40% chance sell
uint256 harbBal = harberg.balanceOf(account);
if (harbBal > 0) {
uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100%
uint256 sellAmount = (harbBal * sellPercent) / 1000;
if (sellAmount > 0) _executeSell(sellAmount);
if (sellAmount > 0) {
_executeSell(sellAmount);
if (trackPositions) {
_recordPositionData(string.concat("Sell_", vm.toString(i)));
}
}
}
} else if (action < 95) { // 15% chance recenter
uint256 waitTime = 1 minutes + (rand % 10 hours);
@ -218,7 +240,12 @@ contract FuzzingAnalysis is Test, CSVManager {
// Sell remaining HARB
uint256 finalHarb = harberg.balanceOf(account);
if (finalHarb > 0) _executeSell(finalHarb);
if (finalHarb > 0) {
_executeSell(finalHarb);
if (trackPositions) {
_recordPositionData("Final_Sell");
}
}
// Final recenters
for (uint256 j = 0; j < 1 + (rand % 3); j++) {
@ -229,10 +256,12 @@ contract FuzzingAnalysis is Test, CSVManager {
// Write position tracking CSV if enabled
if (trackPositions) {
_recordPositionData("Final");
string memory positionFilename = string.concat(
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
);
writeCSVToFile(positionFilename);
console.log(string.concat("Position tracking CSV written to: ", positionFilename));
}
return weth.balanceOf(account);
@ -308,21 +337,52 @@ contract FuzzingAnalysis is Test, CSVManager {
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Create position data row
// Calculate ETH and HARB amounts in each position
// For visualization purposes, we'll estimate based on liquidity distribution
uint256 totalLiq = uint256(floorLiq) + uint256(anchorLiq) + uint256(discoveryLiq);
uint256 totalEth = weth.balanceOf(address(lm));
uint256 totalHarb = harberg.balanceOf(address(lm));
uint256 floorEth = 0;
uint256 floorHarb = 0;
uint256 anchorEth = 0;
uint256 anchorHarb = 0;
uint256 discoveryEth = 0;
uint256 discoveryHarb = 0;
if (totalLiq > 0) {
// Rough approximation based on whether current tick is in range
if (currentTick >= floorLower && currentTick < floorUpper && floorLiq > 0) {
floorEth = (totalEth * uint256(floorLiq)) / totalLiq / 2;
floorHarb = (totalHarb * uint256(floorLiq)) / totalLiq / 2;
}
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiq > 0) {
anchorEth = (totalEth * uint256(anchorLiq)) / totalLiq / 2;
anchorHarb = (totalHarb * uint256(anchorLiq)) / totalLiq / 2;
}
if (currentTick >= discoveryLower && currentTick < discoveryUpper && discoveryLiq > 0) {
discoveryEth = (totalEth * uint256(discoveryLiq)) / totalLiq / 2;
discoveryHarb = (totalHarb * uint256(discoveryLiq)) / totalLiq / 2;
}
}
// Create position data row matching the expected CSV format
string memory row = string.concat(
label, ",",
vm.toString(currentTick), ",",
vm.toString(floorLiq), ",",
vm.toString(floorLower), ",",
vm.toString(floorUpper), ",",
vm.toString(anchorLiq), ",",
vm.toString(anchorLower), ",",
vm.toString(anchorUpper), ",",
vm.toString(discoveryLiq), ",",
vm.toString(discoveryLower), ",",
vm.toString(discoveryUpper), ",",
vm.toString(weth.balanceOf(address(lm))), ",",
vm.toString(harberg.balanceOf(address(lm)))
label, ", ",
vm.toString(currentTick), ", ",
vm.toString(floorLower), ", ",
vm.toString(floorUpper), ", ",
vm.toString(floorEth), ", ",
vm.toString(floorHarb), ", ",
vm.toString(anchorLower), ", ",
vm.toString(anchorUpper), ", ",
vm.toString(anchorEth), ", ",
vm.toString(anchorHarb), ", ",
vm.toString(discoveryLower), ", ",
vm.toString(discoveryUpper), ", ",
vm.toString(discoveryEth), ", ",
vm.toString(discoveryHarb), ", ",
token0isWeth ? "true" : "false"
);
appendCSVRow(row);
}

View file

@ -3,25 +3,43 @@
# Change to the analysis directory (where this script is located)
cd "$(dirname "$0")"
# Function to cleanup on exit
cleanup() {
if [ -n "$VIEWER_PID" ]; then
echo -e "\n${YELLOW}Stopping viewer...${NC}"
# Kill the entire process group
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
sleep 1
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
fi
rm -f profitable_scenario.csv 2>/dev/null || true
}
# Set trap to cleanup on script exit
trap cleanup EXIT
# Default values
OPTIMIZER_CLASS=""
TOTAL_RUNS=50
TRADES_PER_RUN=20
DEBUG_CSV=false
# Function to show usage
show_usage() {
echo "Usage: $0 <optimizer_class> [runs=N] [trades=N]"
echo "Usage: $0 <optimizer_class> [runs=N] [trades=N] [debugCSV]"
echo ""
echo "Parameters:"
echo " optimizer_class Required. The optimizer class to use"
echo " runs=N Optional. Number of fuzzing runs (default: 50)"
echo " trades=N Optional. Trades per run (default: 20, actual will be ±5)"
echo " debugCSV Optional. Enable debug mode with position tracking CSV (forces runs=1)"
echo ""
echo "Examples:"
echo " $0 BullMarketOptimizer"
echo " $0 WhaleOptimizer runs=100"
echo " $0 BearMarketOptimizer runs=10 trades=50"
echo " $0 NeutralMarketOptimizer trades=30 runs=25"
echo " $0 BullMarketOptimizer debugCSV"
echo ""
echo "Available optimizers:"
echo " - BullMarketOptimizer"
@ -60,6 +78,10 @@ for arg in "$@"; do
exit 1
fi
;;
debugCSV)
DEBUG_CSV=true
TOTAL_RUNS=1
;;
*)
echo "Error: Unknown parameter '$arg'"
show_usage
@ -81,19 +103,30 @@ echo -e "${GREEN}=== Fuzzing Campaign ===${NC}"
echo "Optimizer: $OPTIMIZER_CLASS"
echo "Total runs: $TOTAL_RUNS"
echo "Trades per run: $TRADES_PER_RUN (±5)"
if [ "$DEBUG_CSV" = true ]; then
echo -e "${YELLOW}Debug mode: ENABLED (position tracking CSV will be generated)${NC}"
fi
echo "Output directory: $OUTPUT_DIR"
echo ""
# Validate that the optimizer class exists by doing a dry run
echo "Validating optimizer class..."
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=0 forge script FuzzingAnalysis.s.sol --ffi --via-ir > /tmp/optimizer_check.log 2>&1
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=1 TRADES_PER_RUN=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > /tmp/optimizer_check.log 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}"
echo -e "${RED}Check the error:${NC}"
grep -E "(Unknown optimizer|revert)" /tmp/optimizer_check.log
echo ""
show_usage
exit 1
# Check specifically for unknown optimizer error
if grep -q "Unknown optimizer class" /tmp/optimizer_check.log; then
echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}"
echo -e "${RED}Check the error:${NC}"
grep "Unknown optimizer" /tmp/optimizer_check.log
echo ""
show_usage
exit 1
else
# Other errors are ok during validation, we just want to check the optimizer exists
echo "Optimizer validation passed (non-optimizer errors ignored)"
fi
else
echo "Optimizer validation passed"
fi
# Create output directory
@ -105,6 +138,9 @@ echo "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %" > "$MERGED_CS
# Track statistics
TOTAL_PROFITABLE=0
FAILED_RUNS=0
CUMULATIVE_PNL=0
CSV_GENERATED=false
LATEST_CSV=""
# Save configuration
CONFIG_FILE="$OUTPUT_DIR/config.txt"
@ -122,28 +158,90 @@ for i in $(seq 1 $TOTAL_RUNS); do
echo -e "${YELLOW}Running fuzzing iteration $i/$TOTAL_RUNS...${NC}"
# Run single fuzzing iteration with specified optimizer and trades
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
# Use iteration number as seed offset to ensure different scenarios
# Enable position tracking if debugCSV is set
if [ "$DEBUG_CSV" = true ]; then
TRACK_POSITIONS=true SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
else
SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
fi
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ Run $i completed${NC}"
# Check if profitable scenarios were found
if grep -q "PROFITABLE!" "$OUTPUT_DIR/run_$i.log"; then
echo -e "${GREEN} Found profitable scenario!${NC}"
((TOTAL_PROFITABLE++))
# Extract P&L from RESULT line (may have leading spaces from forge output)
RESULT_LINE=$(grep "RESULT|" "$OUTPUT_DIR/run_$i.log")
if [ -n "$RESULT_LINE" ]; then
# Parse the RESULT line to extract P&L
PNL_VALUE=$(echo "$RESULT_LINE" | awk -F'|' '{print $5}' | sed 's/PNL://' | tr -d ' ')
# Extract profit percentage
PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+")
echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}"
# Debug: show values before calculation
# echo "DEBUG: Current cumulative: $CUMULATIVE_PNL, New P&L: $PNL_VALUE"
# Extract CSV file path if generated
# Add to cumulative P&L using awk (handles large numbers better than bash arithmetic)
CUMULATIVE_PNL=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" -v pnl="$PNL_VALUE" 'BEGIN {printf "%.0f", cum + pnl}')
# Format cumulative P&L for display (convert from wei to ETH)
CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}')
# Check if profitable
if [[ "$PNL_VALUE" == +* ]]; then
echo -e "${GREEN} Found profitable scenario!${NC}"
((TOTAL_PROFITABLE++))
# Extract profit percentage
PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+")
echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}"
else
echo -e "${YELLOW} Loss scenario${NC}"
fi
# Display cumulative P&L
if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then
echo -e "${GREEN} Cumulative P&L: +${CUMULATIVE_ETH} ETH${NC}"
else
echo -e "${RED} Cumulative P&L: ${CUMULATIVE_ETH} ETH${NC}"
fi
# Extract CSV file path if generated (for profitable scenarios)
CSV_FILE=$(grep "Profitable scenarios written to:" "$OUTPUT_DIR/run_$i.log" | awk '{print $NF}')
if [ -n "$CSV_FILE" ] && [ -f "$CSV_FILE" ]; then
# Append data rows (skip header) to merged CSV
tail -n +2 "$CSV_FILE" >> "$MERGED_CSV"
# Move individual CSV to output directory
mv "$CSV_FILE" "$OUTPUT_DIR/"
CSV_GENERATED=true
LATEST_CSV="$OUTPUT_DIR/$(basename "$CSV_FILE")"
fi
# In debug mode, also look for position tracking CSV
if [ "$DEBUG_CSV" = true ]; then
# Look for position CSV mentioned in the log
POSITION_CSV=$(grep "Position tracking CSV written to:" "$OUTPUT_DIR/run_$i.log" | awk -F': ' '{print $2}')
if [ -n "$POSITION_CSV" ]; then
# The CSV is generated in the parent directory (onchain), so check there
PARENT_CSV="../$POSITION_CSV"
if [ -f "$PARENT_CSV" ]; then
echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}"
# Move to output directory with a more descriptive name
FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv"
mv "$PARENT_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME"
echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}"
CSV_GENERATED=true
LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME"
elif [ -f "$POSITION_CSV" ]; then
# Fallback if it's in the current directory
echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}"
FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv"
mv "$POSITION_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME"
echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}"
CSV_GENERATED=true
LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME"
fi
fi
fi
else
echo -e "${YELLOW} Warning: No RESULT line found in output${NC}"
fi
else
echo -e "${RED}✗ Run $i failed${NC}"
@ -168,6 +266,15 @@ echo "Trades per run: $TRADES_PER_RUN (±5)"
echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS))"
echo "Failed runs: $FAILED_RUNS"
echo "Total profitable scenarios: $TOTAL_PROFITABLE"
# Display final cumulative P&L
FINAL_CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}')
if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then
echo -e "${GREEN}Final Cumulative P&L: +${FINAL_CUMULATIVE_ETH} ETH${NC}"
else
echo -e "${RED}Final Cumulative P&L: ${FINAL_CUMULATIVE_ETH} ETH${NC}"
fi
echo ""
echo "Results saved in: $OUTPUT_DIR"
echo "Merged CSV: $MERGED_CSV"
@ -188,6 +295,11 @@ SUMMARY="$OUTPUT_DIR/summary.txt"
echo "Failed runs: $FAILED_RUNS"
echo "Total profitable scenarios: $TOTAL_PROFITABLE / $((TOTAL_RUNS - FAILED_RUNS))"
echo "Success rate: $(awk "BEGIN {if ($TOTAL_RUNS - $FAILED_RUNS > 0) printf \"%.2f\", $TOTAL_PROFITABLE/($TOTAL_RUNS-$FAILED_RUNS)*100; else print \"0.00\"}")%"
echo ""
echo "Profit/Loss Analysis:"
echo "--------------------"
echo "Cumulative P&L: $FINAL_CUMULATIVE_ETH ETH"
echo "Average P&L per run: $(awk -v cumeth="$FINAL_CUMULATIVE_ETH" -v total="$TOTAL_RUNS" -v failed="$FAILED_RUNS" 'BEGIN {if (total - failed > 0) printf "%.6f ETH", cumeth/(total-failed); else print "0.000000 ETH"}')"
} > "$SUMMARY"
echo ""
@ -198,4 +310,60 @@ if [ $TOTAL_PROFITABLE -gt 0 ]; then
echo ""
echo -e "${GREEN}Sample profitable scenarios:${NC}"
head -5 "$MERGED_CSV"
fi
# If debug mode was used, mention the position tracking CSV
if [ "$DEBUG_CSV" = true ]; then
echo ""
echo -e "${GREEN}Debug position tracking CSV generated!${NC}"
echo "View it with: ./view-scenarios.sh"
echo "Then navigate to the output directory and select the debug CSV file"
fi
# If any CSV was generated, launch the viewer
if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
echo ""
echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}"
echo "CSV file: $LATEST_CSV"
# Create a temporary symlink to the CSV for the viewer
TEMP_LINK="profitable_scenario.csv"
if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then
rm -f "$TEMP_LINK"
fi
# Use absolute path for the symlink
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
# Start the viewer in background in its own process group
setsid ./view-scenarios.sh &
VIEWER_PID=$!
# Give the server time to start and browser to open
sleep 2
# Show the URL
echo ""
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
echo "(If port 8000 was busy, check the port number mentioned above)"
echo "If browser didn't open, manually navigate to that URL"
# Wait for user input
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
# Kill the viewer process and its children
if [ -n "$VIEWER_PID" ]; then
# Kill the entire process group (includes python server)
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
# Give it a moment to clean up
sleep 1
# Force kill if still running
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
fi
# Clean up the symlink
rm -f "$TEMP_LINK"
echo -e "${GREEN}Viewer stopped.${NC}"
fi

View file

@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")"
echo -e "\n" | timeout 15 ./run-fuzzing.sh BullMarketOptimizer debugCSV trades=3

View file

@ -40,17 +40,24 @@ start_server() {
echo "Press Ctrl+C to stop the server"
echo "=================================="
# Start server in background first
$python_cmd -m http.server $port &
SERVER_PID=$!
# Give server a moment to start
sleep 1
# Try to open browser (works on most systems)
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$port/scenario-visualizer.html" &
xdg-open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null &
elif command -v open &> /dev/null; then
open "http://localhost:$port/scenario-visualizer.html" &
open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null &
else
echo "🔗 Manual: Open http://localhost:$port/scenario-visualizer.html in your browser"
fi
# Start server (this will block until Ctrl+C)
$python_cmd -m http.server $port
# Wait for the server process
wait $SERVER_PID
}
# Main execution