Random Walks Dashboard

Interactive dashboard with walker paths, step distributions, and fractal visualizations
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 1800

# ============================================================================
# Package Loading + Inline Fallbacks for WebR
# ============================================================================

cat("\n=== DASHBOARD SETUP ===\n")

# Load shiny (pre-installed in Shinylive)
library(shiny)
cat("✅ shiny\n")

# Install and load ggplot2
tryCatch({
  library(ggplot2)
  cat("✅ ggplot2\n")
}, error = function(e) {
  cat("⚠️  ggplot2 not found, installing...\n")
  webr::install("munsell", repos = "https://repo.r-wasm.org")
  webr::install("ggplot2", repos = "https://repo.r-wasm.org")
  library(ggplot2)
  cat("✅ ggplot2 installed\n")
})

# Try to load randomwalk package (may fail if WebR binaries not available)
has_randomwalk <- tryCatch({
  webr::install("randomwalk", repos = c(
    "https://johngavin.github.io/randomwalk",
    "https://repo.r-wasm.org"
  ))
  library(randomwalk)
  cat("✅ randomwalk (v", as.character(packageVersion("randomwalk")), ")\n", sep = "")
  TRUE
}, error = function(e) {
  cat("⚠️  randomwalk not available — using inline simulation engine\n")
  FALSE
})

# ============================================================================
# Inline simulation functions (used when package unavailable in WebR)
# These are minimal versions of initialize_grid() and generate_walker_positions()
# ============================================================================
if (!has_randomwalk) {
  initialize_grid <- function(n, center_black = TRUE) {
    grid <- matrix(0L, nrow = n, ncol = n)
    if (center_black) { ctr <- ceiling(n / 2); grid[ctr, ctr] <- 1L }
    grid
  }
  generate_walker_positions <- function(n_walkers, grid, avoid_black = TRUE) {
    n <- nrow(grid); ctr <- ceiling(n / 2)
    lapply(seq_len(n_walkers), function(i) {
      for (attempt in 1:1000) {
        pos <- c(sample.int(n, 1L), sample.int(n, 1L))
        if (!all(pos == c(ctr, ctr)) && !(avoid_black && grid[pos[1], pos[2]] == 1L))
          return(pos)
      }
      pos
    })
  }
  # Minimal plot fallback
  plot_grid_enhanced <- function(result, quantiles = 5, color_scheme = "viridis") {
    g <- result$grid; n <- nrow(g)
    df <- expand.grid(x = 1:n, y = 1:n)
    df$black <- as.vector(g)
    p <- ggplot2::ggplot(df, ggplot2::aes(x = .data$x, y = .data$y)) +
      ggplot2::geom_tile(ggplot2::aes(fill = factor(.data$black)),
                         color = NA, linewidth = 0) +
      ggplot2::scale_fill_manual(values = c("0" = "gray60", "1" = "black"),
                                 guide = "none") +
      ggplot2::coord_fixed() +
      ggplot2::labs(title = sprintf("DLA Pattern: %d black pixels (%d%% of %dx%d)",
                                    sum(g == 1L), round(100 * sum(g == 1L) / (n*n)), n, n)) +
      ggplot2::theme_minimal() +
      ggplot2::theme(panel.background = ggplot2::element_rect(fill = "gray60", color = NA),
                     plot.background = ggplot2::element_rect(fill = "gray60", color = NA))
    attr(p, "caption") <- ""
    p
  }
  cat("✅ Inline simulation engine ready\n")
}

cat("\n⚠️  WebR Mode: Synchronous only (workers=0)\n")
cat("   Simulation is fully vectorized for speed\n")
cat("\n=== SETUP COMPLETE ===\n\n")

# ============================================================================
# Helper Functions
# ============================================================================

is_webr <- function() {
  exists(".webr_env", envir = .GlobalEnv) ||
    identical(Sys.getenv("WEBR"), "1")
}

format_duration <- function(seconds) {
  if (is.null(seconds) || is.na(seconds)) return("00:00")
  seconds <- round(seconds)
  if (seconds < 60) {
    sprintf("00:%02d", seconds)
  } else if (seconds < 3600) {
    sprintf("%02d:%02d", floor(seconds / 60), seconds %% 60)
  } else {
    sprintf("%02d:%02d:%02d", floor(seconds / 3600),
            floor((seconds %% 3600) / 60), seconds %% 60)
  }
}

# ============================================================================
# Shiny UI
# ============================================================================

ui <- fluidPage(

  # CSS for light/dark themes + collapsible panels + progress
  tags$head(
    tags$style(HTML("
      /* ── Dark mode ────────────────────────────────────── */
      body.dark-mode, body.dark-mode * {
        color: #e0e0e0;
      }
      body.dark-mode {
        background-color: #1e1e1e !important;
      }
      body.dark-mode .well {
        background-color: #2b2b2b;
        border-color: #444;
        color: #e0e0e0;
      }
      body.dark-mode .btn { color: #fff; }
      body.dark-mode .btn-primary { background-color: #2a6cb6; border-color: #2a6cb6; }
      body.dark-mode .btn-success { background-color: #1a7a3a; border-color: #1a7a3a; }
      body.dark-mode .btn-warning { background-color: #8a6d2b; border-color: #8a6d2b; color: #fff; }
      body.dark-mode label, body.dark-mode .control-label {
        color: #e0e0e0 !important;
      }
      body.dark-mode .irs--shiny .irs-single,
      body.dark-mode .irs--shiny .irs-min,
      body.dark-mode .irs--shiny .irs-max {
        color: #e0e0e0;
      }
      body.dark-mode .form-control,
      body.dark-mode .selectize-input,
      body.dark-mode .selectize-dropdown {
        background-color: #333;
        color: #e0e0e0;
        border-color: #555;
      }
      body.dark-mode details {
        background: #2b2b2b;
        border-color: #444;
      }
      body.dark-mode details[open] {
        background: #333;
      }
      body.dark-mode summary {
        color: #ccc;
      }
      body.dark-mode summary:hover {
        color: #6cb4ee;
      }
      body.dark-mode .nav-tabs > li > a {
        color: #aaa;
      }
      body.dark-mode .nav-tabs > li.active > a,
      body.dark-mode .nav-tabs > li > a:hover {
        color: #fff;
        background-color: #333;
        border-color: #555;
      }
      body.dark-mode pre,
      body.dark-mode .shiny-text-output {
        background-color: #2b2b2b;
        color: #e0e0e0;
        border-color: #444;
      }
      body.dark-mode h4, body.dark-mode h5 {
        color: #e0e0e0;
      }
      body.dark-mode .help-block {
        color: #999;
      }
      body.dark-mode table {
        color: #e0e0e0;
      }
      body.dark-mode table th {
        background-color: #333 !important;
      }
      body.dark-mode table td {
        border-color: #444 !important;
      }
      body.dark-mode hr {
        border-color: #444;
      }
      /* Dark mode toggle button */
      #dark_mode_btn {
        position: fixed;
        top: 8px;
        right: 12px;
        z-index: 10000;
        font-size: 20px;
        background: none;
        border: 1px solid #888;
        border-radius: 50%;
        width: 36px;
        height: 36px;
        cursor: pointer;
        padding: 0;
        line-height: 34px;
        text-align: center;
      }
      body:not(.dark-mode) #dark_mode_btn { color: #333; }
      body.dark-mode #dark_mode_btn { color: #eee; border-color: #666; }

      /* ── Light mode defaults ──────────────────────────── */
      .timer-display {
        font-size: 24px;
        font-weight: bold;
        color: #007bff;
        text-align: center;
        padding: 10px;
        background: #f0f8ff;
        border-radius: 5px;
        margin: 10px 0;
      }

      #run_sim:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }

      details {
        margin-bottom: 15px;
        border: 1px solid #ddd;
        border-radius: 4px;
        padding: 10px;
        background: #f8f9fa;
      }

      details[open] {
        background: white;
      }

      summary {
        font-weight: bold;
        cursor: pointer;
        padding: 5px;
        color: #333;
        user-select: none;
      }

      summary:hover {
        color: #007bff;
      }

      details > *:not(summary) {
        margin-top: 10px;
      }

      .progress-popup {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0, 0, 0, 0.8);
        color: white;
        padding: 20px;
        border-radius: 10px;
        z-index: 9999;
        font-size: 18px;
        min-width: 300px;
        text-align: center;
      }
    ")),
    # Dark mode toggle script
    tags$script(HTML("
      document.addEventListener('DOMContentLoaded', function() {
        // Respect OS preference on first load
        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
          document.body.classList.add('dark-mode');
        }
        // Restore saved preference
        if (localStorage.getItem('rw-dark-mode') === 'true') {
          document.body.classList.add('dark-mode');
        } else if (localStorage.getItem('rw-dark-mode') === 'false') {
          document.body.classList.remove('dark-mode');
        }
      });
      function toggleDarkMode() {
        document.body.classList.toggle('dark-mode');
        var isDark = document.body.classList.contains('dark-mode');
        localStorage.setItem('rw-dark-mode', isDark);
        var btn = document.getElementById('dark_mode_btn');
        if (btn) btn.textContent = isDark ? '\\u2600' : '\\u263E';
      }
      // JS-driven batch trigger: R sends 'batch_done' after each batch,
      // JS waits 50ms (browser renders, handles clicks), then triggers
      // next batch in R. This guarantees a true yield to the browser
      // event loop between batches — invalidateLater() alone doesn't
      // yield the WebR Worker thread.
      $(document).on('shiny:connected', function() {
        Shiny.addCustomMessageHandler('batch_done', function(msg) {
          setTimeout(function() {
            Shiny.setInputValue('next_batch', Math.random());
          }, 50);
        });
      });
    "))
  ),

  # TOP SECTION: Run Button (always visible)
  fluidRow(
    column(12,
      wellPanel(
        style = "background-color: #f0f8ff;",
        fluidRow(
          column(6,
            h5("Estimated Runtime:"),
            uiOutput("runtime_estimate")
          ),
          column(6,
            uiOutput("run_button_ui")
          )
        )
      )
    )
  ),

  hr(),

  # MAIN SECTION: FULL-WIDTH TABSET
  fluidRow(
    column(12,
      tabsetPanel(
        id = "tabs",

        # PAGE 1: Fractal Graph (Black Pixels)
        tabPanel("Fractal Graph",
                 h4("Grid Visualization - Black Pixels"),
                 plotOutput("fractal_plot", height = "600px"),
                 tags$p(style = "margin-top: 10px; margin-bottom: 10px; color: #666; font-style: italic; text-align: center;",
                        textOutput("fractal_caption", inline = TRUE)),
                 tags$p(style = "margin-top: 10px; font-size: 12px; color: #666;",
                        "Right-click the plot image above to save it."),
                 hr(),
                 verbatimTextOutput("fractal_stats"),
                 hr(),
                 h5("Simulation Status:"),
                 verbatimTextOutput("status", placeholder = TRUE),
                 tags$div(
                   style = "background: #f0f0f0; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 14px; border: 1px solid #ccc;",
                   h5("Live Progress:"),
                   verbatimTextOutput("progress_timer")
                 )),

        # PAGE 2: Distributions (Path Lengths by Termination Reason)
        tabPanel("Distributions",
                 h4("Path Length Distributions"),
                 plotOutput("dist_overall", height = "300px"),
                 h5("Overall Distribution"),
                 hr(),
                 plotOutput("dist_by_reason", height = "300px"),
                 h5("By Termination Reason")),

        # PAGE 4: Statistics - Quintile Analysis
        tabPanel("Statistics",
                 h4("Quintile-Based Statistics"),
                 radioButtons("stats_group_by", "Group walkers by:",
                              choices = c("Termination order" = "termination",
                                          "Launch order" = "launch"),
                              selected = "termination", inline = TRUE),
                 helpText(textOutput("stats_group_desc", inline = TRUE)),
                 hr(),
                 h5("Path Length by Termination Quintile"),
                 plotOutput("stats_quintile_boxplot", height = "350px"),
                 hr(),
                 h5("Histogram Panels by Quintile"),
                 plotOutput("stats_quintile_histograms", height = "400px"),
                 hr(),
                 h5("Distribution Shape Comparison"),
                 fluidRow(
                   column(6, plotOutput("stats_violin", height = "300px")),
                   column(6, plotOutput("stats_trend", height = "300px"))
                 ),
                 hr(),
                 h5("Summary Statistics by Quintile"),
                 verbatimTextOutput("stats_quintile_summary")),

        # PAGE 5: Survival Curve
        tabPanel("Survival Curve",
                 h4("Walker Survival Analysis"),

                 # Interactive slider for step threshold
                 sliderInput("survival_step", "Show survival at step:",
                             min = 0, max = 1000, value = 500, step = 50),
                 helpText("Drag to see what proportion of walkers survived to this step"),

                 # Main survival curve plot
                 plotOutput("survival_curve", height = "400px"),

                 hr(),

                 # Competing risks breakdown
                 h5("Termination by Reason"),
                 plotOutput("termination_breakdown", height = "250px"),

                 hr(),

                 # Summary statistics
                 verbatimTextOutput("survival_stats"),

                 hr(),

                 # Hypothesis Testing Section
                 h4("Hypothesis Test: Survival Decay"),
                 tags$p("Tests whether later walkers terminate faster as black pixels accumulate."),

                 # Hypothesis plot
                 plotOutput("hypothesis_plot", height = "350px"),

                 # Hypothesis statistics
                 verbatimTextOutput("hypothesis_stats")),

        # PAGE 6: Debug Info (renumbered from PAGE 5)
        tabPanel("Debug",
                 h4("Debug Information"),
                 h5("Package Versions"),
                 verbatimTextOutput("debug_versions"),
                 hr(),
                 h5("Current Inputs"),
                 verbatimTextOutput("debug_inputs"),
                 hr(),
                 h5("Backend Information"),
                 verbatimTextOutput("debug_backend"),
                 hr(),
                 h5("Walker Paths QA"),
                 verbatimTextOutput("debug_walker_qa"),
                 hr(),
                 h5("Periodic Updates"),
                 verbatimTextOutput("debug_periodic"),
                 hr(),
                 h5("Simulation Status:"),
                 verbatimTextOutput("status_debug", placeholder = TRUE),
                 tags$div(
                   style = "background: #f0f0f0; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 14px; border: 1px solid #ccc;",
                   h5("Live Progress:"),
                   verbatimTextOutput("progress_timer_debug")
                 )),

        # PAGE 7: Notes
        tabPanel("Notes",
                 h4("About This Dashboard"),
                 tags$p("Interactive random walk fractal simulation running entirely in your browser via WebR/Shinylive. ",
                        "No server required — all computation happens in WebAssembly."),
                 hr(),

                 h4("Tips"),
                 tags$ul(
                   tags$li(tags$strong("Larger grid"), " (200-400): More exploration space, slower"),
                   tags$li(tags$strong("Fewer walkers"), " (100-500): Faster simulation"),
                   tags$li(tags$strong("8-hood"), ": Diagonal movement creates different fractal patterns"),
                   tags$li(tags$strong("Wrap boundary"), ": Toroidal grid (no edge termination)")
                 ),
                 hr(),

                 h4("Browser Limitations"),
                 tags$ul(
                   tags$li("Single-threaded WebAssembly execution"),
                   tags$li("First load: 15-30s (WebR + package downloads)"),
                   tags$li("Default params (100×100, 200 walkers): ~30s"),
                   tags$li("Large sims (300×300, 10k walkers): several minutes"),
                   tags$li("For very large simulations, use native R with crew workers")
                 ),
                 hr(),

                 h5("Vectorized Engine"),
                 tags$p("The simulation moves ALL active walkers simultaneously using matrix operations ",
                        "(10-50x faster than per-walker loops). Walker paths are not stored in this mode. ",
                        "Use native R for path visualization."),
                 hr(),

                 tags$a(href="https://johngavin.github.io/randomwalk/", "Documentation"),
                 tags$span(" | "),
                 tags$a(href="https://github.com/JohnGavin/randomwalk", "GitHub"))
      ) # End of tabs
    ) # End of column
  ), # End of fluidRow

  hr(),

  # BOTTOM SECTION: COLLAPSIBLE PARAMETER GROUPS
  fluidRow(
    column(12,
      # Use Shiny's wellPanel with conditional rendering for collapsible sections
      wellPanel(
        actionButton("toggle_sim_params", "▶ Simulation Parameters",
                    style = "background: none; border: none; font-weight: bold; font-size: 16px; padding: 0; margin-bottom: 10px;"),
        conditionalPanel(
          condition = "output.show_sim_params",
          fluidRow(
            column(4,
              h5("Grid Settings"),
              sliderInput("grid_size", "Grid Size:",
                          min = 20, max = 400, value = 100, step = 20),
              helpText("Size of the square grid (pixels per side)")
            ),
            column(8,
              h5("Walker Settings"),
              sliderInput("n_walkers", "Number of Walkers:",
                          min = 1, max = 1000, value = 200, step = 50),
              helpText(HTML("Number of walkers to simulate<br><em>Max: 70% of grid pixels (updates automatically)</em>")),

              sliderInput("max_steps", "Max Steps per Walker:",
                          min = 100, max = 10000, value = 5000, step = 100),
              helpText("Maximum steps before walker terminates")
            )
          )
        )
      ),

      wellPanel(
        actionButton("toggle_move_params", "▶ Movement Settings",
                    style = "background: none; border: none; font-weight: bold; font-size: 16px; padding: 0; margin-bottom: 10px;"),
        conditionalPanel(
          condition = "output.show_move_params",
          fluidRow(
            column(6,
              selectInput("neighborhood", "Neighborhood:",
                          choices = c("4-hood", "8-hood"),
                          selected = "4-hood"),
              helpText("4-hood: up/down/left/right, 8-hood: includes diagonals")
            ),
            column(6,
              selectInput("boundary", "Boundary Behavior:",
                          choices = c("terminate", "wrap"),
                          selected = "terminate"),
              helpText("terminate: stop at edges, wrap: toroidal topology")
            )
          )
        )
      ),

      wellPanel(
        actionButton("toggle_validation_params", "▶ Advanced Settings",
                    style = "background: none; border: none; font-weight: bold; font-size: 16px; padding: 0; margin-bottom: 10px;"),
        conditionalPanel(
          condition = "output.show_validation_params",
          fluidRow(
            column(4,
              selectInput("sync_mode", "Sync Mode:",
                         choices = c("static", "chunked"),
                         selected = "static"),
              helpText("static: Frozen snapshots (~5% collision). chunked: Batched updates (~15% collision)")
            ),
            column(4,
              checkboxInput("validate_strict", "Strict Validation",
                           value = FALSE),
              helpText("ON: Stop on isolated pixel. OFF: Warn and continue")
            ),
            column(4,
              sliderInput("validate_percent", "Validation %:",
                         min = 0, max = 20, value = 5, step = 1),
              helpText("Check every N% of walkers (0 = off)")
            )
          )
        )
      )
    )
  ) # End of parameter fluidRow
)

# ============================================================================
# Shiny Server
# ============================================================================

server <- function(input, output, session) {

  # Dynamic walker constraint: Limit to 70% of grid pixels
  observe({
    max_walkers <- floor(0.7 * input$grid_size^2)
    updateSliderInput(
      session,
      "n_walkers",
      max = max_walkers,
      value = min(input$n_walkers, max_walkers)
    )
  })

  # Reactive values for simulation state and history
  sim_result <- reactiveVal(NULL)
  sim_count <- reactiveVal(0)  # Count total simulations run
  sim_state <- reactiveVal("idle")  # States: idle, running, complete, error
  sim_error_msg <- reactiveVal("")   # Store actual error message for debugging
  sim_start_time <- reactiveVal(NULL)
  sim_end_time <- reactiveVal(NULL)
  sim_current_step <- reactiveVal(0)  # Track current simulation step
  sim_completed_walkers <- reactiveVal(0)  # Track completed walkers
  sim_black_pixels <- reactiveVal(0)  # Track black pixels

  # Reactive values for BATCHED simulation state (non-blocking execution)
  batch_grid <- reactiveVal(NULL)
  batch_walkers <- reactiveVal(NULL)
  batch_active <- reactiveVal(NULL)
  batch_step_count <- reactiveVal(0L)
  batch_term_counter <- reactiveVal(0L)

  # Reactive values for collapsible panels
  show_sim_params <- reactiveVal(FALSE)
  show_move_params <- reactiveVal(FALSE)
  show_validation_params <- reactiveVal(FALSE)

  # Toggle simulation parameters panel
  observeEvent(input$toggle_sim_params, {
    current <- show_sim_params()
    show_sim_params(!current)
    # Update button text
    updateActionButton(session, "toggle_sim_params",
                      label = ifelse(!current, "▼ Simulation Parameters", "▶ Simulation Parameters"))
  })

  # Toggle movement settings panel
  observeEvent(input$toggle_move_params, {
    current <- show_move_params()
    show_move_params(!current)
    # Update button text
    updateActionButton(session, "toggle_move_params",
                      label = ifelse(!current, "▼ Movement Settings", "▶ Movement Settings"))
  })

  # Toggle validation settings panel
  observeEvent(input$toggle_validation_params, {
    current <- show_validation_params()
    show_validation_params(!current)
    # Update button text
    updateActionButton(session, "toggle_validation_params",
                      label = ifelse(!current, "▼ Validation Settings", "▶ Validation Settings"))
  })

  # Output for conditional panels
  output$show_sim_params <- reactive({ show_sim_params() })
  output$show_move_params <- reactive({ show_move_params() })
  output$show_validation_params <- reactive({ show_validation_params() })
  outputOptions(output, "show_sim_params", suspendWhenHidden = FALSE)
  outputOptions(output, "show_move_params", suspendWhenHidden = FALSE)
  outputOptions(output, "show_validation_params", suspendWhenHidden = FALSE)

  # Runtime estimate (WebR performance varies: ~50-500 steps/sec depending on grid size & walker count)
  output$runtime_estimate <- renderUI({
    # Runtime calculation considers BOTH factors:
    # 1. Simulation steps = max_steps (all walkers move simultaneously each step)
    # 2. Per-step cost increases with more walkers due to collision checks & neighbor lookups
    #
    # Total cost = max_steps × (base_per_step_cost + walker_scaling × n_walkers)
    # Rearranged: effective_steps_per_sec = base_steps_per_sec / (1 + walker_scaling × n_walkers)

    grid_area <- input$grid_size^2

    # Empirical base performance model (1 walker) based on grid size:
    # Small grids (≤100x100): ~300 steps/sec
    # Medium grids (100-200): ~150 steps/sec
    # Large grids (>200): ~75 steps/sec
    if (grid_area <= 10000) {
      base_steps_per_sec <- 300
    } else if (grid_area <= 40000) {
      base_steps_per_sec <- 150
    } else {
      base_steps_per_sec <- 75
    }

    # Walker scaling factor: each walker adds ~0.2% overhead per step
    # (collision checks, neighbor lookups, state updates scale with active walkers)
    walker_scaling <- 0.002

    # Effective throughput accounting for both factors
    effective_steps_per_sec <- base_steps_per_sec / (1 + walker_scaling * input$n_walkers)

    # Total simulation time
    total_steps <- input$max_steps
    est_seconds <- total_steps / effective_steps_per_sec

    if (est_seconds < 60) {
      est_text <- paste0("~", format_duration(est_seconds))
      color <- "green"
    } else if (est_seconds < 300) {
      est_text <- paste0("~", format_duration(est_seconds))
      color <- "orange"
    } else {
      est_text <- paste0("~", format_duration(est_seconds), " (consider smaller params)")
      color <- "red"
    }

    # Calculate total walker steps for reference
    total_walker_steps <- input$n_walkers * input$max_steps

    tags$div(
      style = paste0("color: ", color, "; font-weight: bold;"),
      est_text,
      tags$br(),
      tags$small(style = "color: gray;",
                 sprintf("Max steps: %s | Walkers: %s\n(%sK total walker-steps, ~%.0f steps/sec effective)",
                         format(input$max_steps, big.mark=","),
                         format(input$n_walkers, big.mark=","),
                         format(round(total_walker_steps/1000), big.mark=","),
                         effective_steps_per_sec))
    )
  })

  # Reactive timer for periodic UI refresh (progress display, debug tab)
  autoInvalidate <- reactiveTimer(500)  # Update displays every 500ms

  # Status output — updates reactively as batch_* values change
  output$status <- renderText({
    if (sim_state() == "running" && !is.null(sim_start_time())) {
      elapsed <- as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs"))
      n_active <- length(batch_active())
      progress <- if (input$n_walkers > 0) {
        round(100 * sim_completed_walkers() / input$n_walkers)
      } else 0
      paste0(
        "SIMULATION RUNNING...\n",
        "Run #", sim_count(), "\n",
        "━━━━━━━━━━━━━━━━━━━━━━\n",
        "Started:  ", format(sim_start_time(), "%H:%M:%S"), "\n",
        "Elapsed:  ", format_duration(elapsed), "\n",
        "Progress: ", progress, "%\n",
        "━━━━━━━━━━━━━━━━━━━━━━\n",
        "Grid: ", input$grid_size, "×", input$grid_size, "\n",
        "Walkers: ", sim_completed_walkers(), "/", input$n_walkers,
        " (", n_active, " active)\n",
        "Black Pixels: ", sim_black_pixels(), "\n",
        "Step: ", sim_current_step(), "\n",
        "Workers: 0 (batched sequential)"
      )
    } else if (sim_state() == "complete" && !is.null(sim_result())) {
      result <- sim_result()
      elapsed <- as.numeric(difftime(sim_end_time(), sim_start_time(), units = "secs"))
      paste0(
        "SIMULATION COMPLETE\n",
        "Run #", sim_count(), "\n",
        "━━━━━━━━━━━━━━━━━━━━━━\n",
        "Started:  ", format(sim_start_time(), "%H:%M:%S"), "\n",
        "Finished: ", format(sim_end_time(), "%H:%M:%S"), "\n",
        "Elapsed:  ", format_duration(elapsed), "\n",
        "━━━━━━━━━━━━━━━━━━━━━━\n",
        "Backend: batched sequential (WebR browser)\n",
        "Black Pixels: ", result$statistics$black_pixels, "\n",
        "Walkers: ", result$statistics$completed_walkers, " completed"
      )
    } else if (sim_state() == "error") {
      paste0("SIMULATION ERROR\n━━━━━━━━━━━━━━━━━━━━━━\n", sim_error_msg(),
             "\n━━━━━━━━━━━━━━━━━━━━━━\nPlease try again with different parameters.")
    } else {
      "Ready to run simulation..."
    }
  })

  # Real-time progress timer — now updates accurately because simulation
  # yields to the event loop between batches (non-blocking execution)
  output$progress_timer <- renderText({
    autoInvalidate()

    if (sim_state() == "running") {
      elapsed <- if (!is.null(sim_start_time())) {
        round(as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs")), 1)
      } else { 0 }

      sprintf("⏱ Time: %s | Walkers: %d/%d | Black: %d | Step: %d",
              format_duration(elapsed),
              sim_completed_walkers(), input$n_walkers,
              sim_black_pixels(), sim_current_step())
    } else if (sim_state() == "complete" && !is.null(sim_result())) {
      result <- sim_result()
      sprintf("Done | Black: %d | Grid: %d%%",
              result$statistics$black_pixels,
              round(result$statistics$black_percentage))
    } else {
      sprintf("Ready | %s", format(Sys.time(), "%H:%M:%S"))
    }
  })

  # Duplicate outputs for Debug tab (Shiny requires unique output IDs)
  output$status_debug <- renderText({
    autoInvalidate()  # Refresh periodically for elapsed time
    if (sim_state() == "running") {
      elapsed <- if (!is.null(sim_start_time())) {
        as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs"))
      } else { 0 }
      paste0(
        "SIMULATION RUNNING...\n",
        "Run #", sim_count(), "\n",
        "Started: ", format(sim_start_time(), "%H:%M:%S"), "\n",
        "Elapsed: ", format_duration(elapsed), "\n",
        "Walkers: ", sim_completed_walkers(), "/", input$n_walkers, "\n",
        "Black: ", sim_black_pixels(), " | Step: ", sim_current_step()
      )
    } else if (sim_state() == "complete" && !is.null(sim_result())) {
      result <- sim_result()
      paste0(
        "SIMULATION COMPLETE\n",
        "Run #", sim_count(), "\n",
        "Black pixels: ", result$statistics$black_pixels, "\n",
        "Elapsed: ", format_duration(result$statistics$elapsed_time_secs)
      )
    } else if (sim_state() == "error") {
      "SIMULATION ERROR"
    } else {
      "Ready to run simulation..."
    }
  })

  output$progress_timer_debug <- renderText({
    autoInvalidate()  # Use the reactive timer
    if (sim_state() == "running") {
      elapsed <- if (!is.null(sim_start_time())) {
        round(as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs")), 1)
      } else { 0 }
      sprintf("⏱️ Time: %s | Walkers: %d/%d | Black: %d",
              format_duration(elapsed),
              sim_completed_walkers(), input$n_walkers,
              sim_black_pixels())
    } else if (sim_state() == "complete" && !is.null(sim_result())) {
      result <- sim_result()
      sprintf("✓ Complete | Black: %d | Grid: %d%%",
              result$statistics$black_pixels,
              round(result$statistics$black_percentage))
    } else {
      sprintf("Ready | %s", format(Sys.time(), "%H:%M:%S"))
    }
  })

  # Render button with walker/pixel counts
  output$run_button_ui <- renderUI({
    if (sim_state() == "running") {
      # Reactive values update automatically between batches — no polling needed
      actionButton("run_sim_disabled",
                   sprintf("Running... W: %d/%d | B: %d",
                          sim_completed_walkers(), input$n_walkers,
                          sim_black_pixels()),
                   class = "btn-warning btn-lg",
                   style = "width: 100%; font-size: 14px;",
                   disabled = TRUE)
    } else if (sim_state() == "complete" && !is.null(sim_result())) {
      # Show walker count and black pixel count when complete
      result <- sim_result()
      total_secs <- as.numeric(difftime(sim_end_time(), sim_start_time(), units = "secs"))
      mins <- floor(total_secs / 60)
      secs <- floor(total_secs %% 60)

      # Get walker and black pixel counts
      walker_count <- result$statistics$completed_walkers
      black_count <- result$statistics$black_pixels

      actionButton("run_sim", sprintf("Run Simulation (Walkers: %d. Black: %d. Time: %02d:%02d)",
                                     walker_count, black_count, mins, secs),
                   class = "btn-success btn-lg",
                   style = "width: 100%;")
    } else if (sim_state() == "error") {
      actionButton("run_sim", "Run Simulation (Error - Try Again)",
                   class = "btn-danger btn-lg",
                   style = "width: 100%;")
    } else {
      actionButton("run_sim", "Run Simulation",
                   class = "btn-primary btn-lg",
                   style = "width: 100%;")
    }
  })


  # Run simulation when button clicked — INITIALIZE only (non-blocking)
  # Uses VECTORIZED state: matrices instead of walker lists for 10-50x speed.
  observeEvent(input$run_sim, {
    max_allowed <- floor(0.7 * input$grid_size^2)
    if (input$n_walkers > max_allowed) { sim_state("error"); return() }

    sim_state("running")
    sim_start_time(Sys.time())
    sim_count(sim_count() + 1)
    sim_current_step(0); sim_completed_walkers(0); sim_black_pixels(0)

    tryCatch({
      gs <- input$grid_size
      nw <- input$n_walkers
      grid <- initialize_grid(gs)

      # Vectorized state: N×2 position matrix + vectors
      positions <- generate_walker_positions(nw, grid)
      pos_mat <- do.call(rbind, positions)  # N×2 integer matrix
      steps_vec <- integer(nw)
      active_vec <- rep(TRUE, nw)
      reason_vec <- character(nw)
      order_vec <- integer(nw)

      # Padded grid for boundary-free neighbor lookups
      padded <- matrix(0L, nrow = gs + 2L, ncol = gs + 2L)
      padded[2:(gs+1), 2:(gs+1)] <- grid

      batch_grid(grid)
      batch_walkers(list(  # store as vectorized structure
        pos_mat = pos_mat, steps_vec = steps_vec,
        active_vec = active_vec, reason_vec = reason_vec,
        order_vec = order_vec, padded = padded
      ))
      batch_active(which(active_vec))
      batch_step_count(0L)
      batch_term_counter(0L)
    }, error = function(e) {
      sim_error_msg(paste("Init error:", conditionMessage(e)))
      message(paste("INIT ERROR:", conditionMessage(e)))
      sim_state("error")
    })
  })

  # === VECTORIZED BATCHED SIMULATION (non-blocking via JS round-trip) ===
  # Moves ALL active walkers simultaneously using matrix ops.
  # ~10-50x faster than per-walker R loops.
  observe({
    req(sim_state() == "running")
    req(!is.null(batch_grid()))
    invalidateLater(200)
    isolate({
      if (batch_step_count() == 0L)
        session$sendCustomMessage("batch_done", list(kick = TRUE))
    })
  })

  observeEvent(input$next_batch, {
    req(sim_state() == "running")
    req(!is.null(batch_grid()))

    grid <- batch_grid()
    st <- batch_walkers()  # vectorized state
    pos_mat <- st$pos_mat; steps_vec <- st$steps_vec
    active_vec <- st$active_vec; reason_vec <- st$reason_vec
    order_vec <- st$order_vec; padded <- st$padded
    step_count <- batch_step_count()
    term_counter <- batch_term_counter()
    gs <- input$grid_size
    max_steps <- input$max_steps
    is_4hood <- (input$neighborhood == "4-hood")
    is_wrap <- (input$boundary == "wrap")

    # Direction deltas (pre-computed)
    if (is_4hood) {
      DR <- c(-1L, 1L, 0L, 0L); DC <- c(0L, 0L, 1L, -1L); nd <- 4L
    } else {
      DR <- c(-1L,1L,0L,0L,-1L,-1L,1L,1L); DC <- c(0L,0L,1L,-1L,-1L,1L,-1L,1L); nd <- 8L
    }

    # Adaptive batch: ~200k walker-steps per batch for large sims
    n_active <- sum(active_vec)
    STEPS <- max(10L, as.integer(200000 / max(1L, n_active)))

    tryCatch({
      for (b in seq_len(STEPS)) {
        idx <- which(active_vec)
        na <- length(idx)
        if (na == 0L) break
        step_count <- step_count + 1L

        # 1. Random directions for ALL active walkers (one C call)
        dirs <- sample.int(nd, na, replace = TRUE)
        new_r <- pos_mat[idx, 1] + DR[dirs]
        new_c <- pos_mat[idx, 2] + DC[dirs]

        # 2. Boundary handling (vectorized)
        if (is_wrap) {
          new_r <- ((new_r - 1L) %% gs) + 1L
          new_c <- ((new_c - 1L) %% gs) + 1L
        } else {
          oob <- new_r < 1L | new_r > gs | new_c < 1L | new_c > gs
          if (any(oob)) {
            oob_idx <- idx[oob]
            active_vec[oob_idx] <- FALSE
            reason_vec[oob_idx] <- "hit_boundary"
            steps_vec[oob_idx] <- steps_vec[oob_idx] + 1L
          }
          # Continue with in-bounds walkers
          inb <- !oob
          idx <- idx[inb]; new_r <- new_r[inb]; new_c <- new_c[inb]
        }

        if (length(idx) == 0L) next

        # 3. Update positions + step counts
        pos_mat[idx, 1] <- new_r
        pos_mat[idx, 2] <- new_c
        steps_vec[idx] <- steps_vec[idx] + 1L

        # 4. Check ON black pixel (vectorized grid lookup)
        on_black <- grid[cbind(new_r, new_c)] == 1L
        if (any(on_black)) {
          tb_idx <- idx[on_black]
          active_vec[tb_idx] <- FALSE
          reason_vec[tb_idx] <- "touched_black"
        }

        # 5. Check black NEIGHBOR using padded grid (no bounds checks)
        still <- idx[!on_black & active_vec[idx]]
        if (length(still) > 0L) {
          pr <- pos_mat[still, 1] + 1L  # offset for padding
          pc <- pos_mat[still, 2] + 1L
          if (is_4hood) {
            has_bn <- padded[cbind(pr-1L,pc)] == 1L | padded[cbind(pr+1L,pc)] == 1L |
                      padded[cbind(pr,pc-1L)] == 1L | padded[cbind(pr,pc+1L)] == 1L
          } else {
            has_bn <- padded[cbind(pr-1L,pc)] == 1L | padded[cbind(pr+1L,pc)] == 1L |
                      padded[cbind(pr,pc-1L)] == 1L | padded[cbind(pr,pc+1L)] == 1L |
                      padded[cbind(pr-1L,pc-1L)] == 1L | padded[cbind(pr-1L,pc+1L)] == 1L |
                      padded[cbind(pr+1L,pc-1L)] == 1L | padded[cbind(pr+1L,pc+1L)] == 1L
          }
          if (any(has_bn)) {
            bn_idx <- still[has_bn]
            active_vec[bn_idx] <- FALSE
            reason_vec[bn_idx] <- "black_neighbor"
            # Paint pixels + update padded grid
            br <- pos_mat[bn_idx, 1]; bc <- pos_mat[bn_idx, 2]
            grid[cbind(br, bc)] <- 1L
            padded[cbind(br + 1L, bc + 1L)] <- 1L  # offset for padding
          }
        }

        # 6. Max steps check
        ms_hit <- active_vec[idx] & steps_vec[idx] >= max_steps
        if (any(ms_hit)) {
          ms_idx <- idx[ms_hit]
          active_vec[ms_idx] <- FALSE
          reason_vec[ms_idx] <- "max_steps"
        }

        # 7. Assign termination order to newly terminated walkers
        newly_dead <- idx[!active_vec[idx] & order_vec[idx] == 0L]
        if (length(newly_dead) > 0L) {
          new_orders <- seq(term_counter + 1L, term_counter + length(newly_dead))
          order_vec[newly_dead] <- new_orders
          term_counter <- term_counter + length(newly_dead)
        }
      }

      # Update batch state
      batch_grid(grid)
      batch_walkers(list(
        pos_mat = pos_mat, steps_vec = steps_vec,
        active_vec = active_vec, reason_vec = reason_vec,
        order_vec = order_vec, padded = padded
      ))
      batch_active(which(active_vec))
      batch_step_count(step_count)
      batch_term_counter(term_counter)

      n_done <- sum(!active_vec)
      sim_completed_walkers(n_done)
      sim_black_pixels(sum(grid == 1L))
      sim_current_step(step_count)

      # Complete or request next batch via JS round-trip
      if (all(!active_vec)) {
        sim_end_time(Sys.time())
        elapsed <- as.numeric(difftime(sim_end_time(), sim_start_time(), units = "secs"))
        nw <- input$n_walkers

        # Build walker list from vectorized state (for downstream plots)
        walkers <- lapply(seq_len(nw), function(i) {
          list(id = i, pos = pos_mat[i, ], steps = steps_vec[i],
               active = FALSE, termination_reason = reason_vec[i],
               termination_order = order_vec[i],
               path = NULL, store_path = FALSE)
        })

        result <- list(
          grid = grid,
          walkers = walkers,
          statistics = list(
            black_pixels = sum(grid == 1L),
            black_percentage = round(100 * sum(grid == 1L) / (gs^2), 2),
            grid_size = gs,
            total_walkers = nw,
            completed_walkers = nw,
            total_steps = sum(steps_vec),
            min_steps = min(steps_vec),
            max_steps = max(steps_vec),
            mean_steps = mean(steps_vec),
            median_steps = median(steps_vec),
            percentile_25 = quantile(steps_vec, 0.25),
            percentile_75 = quantile(steps_vec, 0.75),
            elapsed_time_secs = elapsed,
            termination_reasons = table(reason_vec)
          ),
          parameters = list(
            grid_size = gs, n_walkers = nw,
            neighborhood = input$neighborhood,
            boundary = input$boundary,
            workers = 0L, max_steps = max_steps
          )
        )
        sim_result(result)
        sim_state("complete")
        message(sprintf("=== SIMULATION COMPLETE === Black: %d, Steps: %d, Time: %s",
                        sum(grid == 1L), step_count, format_duration(elapsed)))
      } else {
        # Tell JS we're done with this batch — JS will setTimeout(50)
        # then send next_batch back, guaranteeing a browser yield.
        session$sendCustomMessage("batch_done", list(step = step_count))
      }
    }, error = function(e) {
      sim_error_msg(paste("Batch error:", conditionMessage(e)))
      message(sprintf("Simulation error: %s", conditionMessage(e)))
      sim_state("error")
      sim_end_time(Sys.time())
    })
  }, ignoreInit = TRUE)


  # PAGE 1: Fractal Graph with arrival time coloring
  output$fractal_plot <- renderPlot({
    # If simulation is running, show live progress in plot area
    if (sim_state() == "running") {
      autoInvalidate()  # Refresh with timer

      par(bg = "gray70", mar = c(2, 2, 3, 2))
      plot(1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
           xlab = "", ylab = "", axes = FALSE,
           main = "Simulation Running...")

      progress_pct <- if (input$n_walkers > 0) {
        round(100 * sim_completed_walkers() / input$n_walkers)
      } else 0

      text(0.5, 0.7, sprintf("Walkers: %d / %d (%d%%)",
                            sim_completed_walkers(), input$n_walkers, progress_pct),
           cex = 2, font = 2)
      text(0.5, 0.5, sprintf("Black pixels: %d", sim_black_pixels()),
           cex = 2, font = 2)

      elapsed <- if (!is.null(sim_start_time())) {
        as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs"))
      } else { 0 }
      text(0.5, 0.3, sprintf("Time: %s | Step: %d",
                            format_duration(elapsed), sim_current_step()),
           cex = 1.5)
      return()
    }

    req(sim_result())
    result <- sim_result()

    # Defensive checks for WebR environment (Issue #157)
    req(!is.null(result$grid))
    req(nrow(result$grid) > 0)
    req(sum(result$grid == 1) > 0)  # Ensure has black pixels

    # Use enhanced plot with color-coded arrival times (with device error handling)
    tryCatch({
      p <- plot_grid_enhanced(result,
                             quantiles = 5,
                             color_scheme = "viridis")
      p
    }, error = function(e) {
      # Fallback for WebR device issues
      par(bg = "gray70", mar = c(2, 2, 3, 2))
      image(result$grid, col = c("gray60", "black"),
            main = sprintf("DLA Fractal: %d black pixels", sum(result$grid == 1)),
            axes = FALSE)
    })
  })

  # Display the caption separately
  output$fractal_caption <- renderText({
    req(sim_result())
    result <- sim_result()

    # Generate the plot to get the caption
    p <- plot_grid_enhanced(result,
                           quantiles = 5,
                           color_scheme = "viridis")

    # Extract and return the caption
    caption <- attr(p, "caption")
    if (!is.null(caption) && caption != "") {
      caption
    } else {
      ""
    }
  })

  output$fractal_stats <- renderText({
    req(sim_result())
    stats <- sim_result()$statistics
    paste0(
      "Black Pixels: ", stats$black_pixels,
      " (", sprintf("%d%%", round(stats$black_percentage)), " of grid)\n",
      "Grid Size: ", stats$grid_size, "×", stats$grid_size,
      " (", stats$grid_size^2, " total cells)"
    )
  })

  # Note: downloadHandler doesn't work in Shinylive/WebR (no filesystem).
  # Users can right-click the rendered plot to save it.

  # PAGE 2: Walker Paths
  output$paths_plot_first25 <- renderPlot({
    req(sim_result())
    result <- sim_result()
    n_walkers <- length(result$walkers)

    if (n_walkers >= 1) {
      first_n <- min(input$first_n_paths, n_walkers)
      last_n <- min(input$last_n_paths, n_walkers)

      # CRITICAL FIX: Only select from walkers that HAVE paths stored
      # Paths are only stored for BLACK_NEIGHBOR terminators (walkers that found the fractal)
      # Boundary walkers terminate quickly but don't have paths - skip them

      # Filter to walkers with paths first
      walkers_with_paths <- Filter(function(w) {
        !is.null(w$path) && length(w$path) > 0
      }, result$walkers)

      n_with_paths <- length(walkers_with_paths)

      if (n_with_paths == 0) {
        plot.new()
        text(0.5, 0.5, paste0(
          "Walker paths not available in vectorized mode.\n",
          "The simulation uses matrix operations for speed\n",
          "(10-50x faster) which don't store individual paths.\n\n",
          "Walkers: ", n_walkers, " | With paths: 0\n",
          "Use native R (not WebR) for path visualization."
        ), cex = 1.0, col = "gray30")
        return()
      }

      # Sort by termination order (among walkers with paths)
      walkers_ordered <- walkers_with_paths[order(sapply(walkers_with_paths, function(w) {
        if (!is.null(w$termination_order)) w$termination_order else w$id
      }))]

      # Select first N and last N from walkers WITH PATHS
      first_n <- min(first_n, n_with_paths)
      last_n <- min(last_n, n_with_paths)

      walker_indices <- unique(c(
        if(first_n > 0) 1:first_n else NULL,
        if(last_n > 0) (n_with_paths - last_n + 1):n_with_paths else NULL
      ))

      if (length(walker_indices) > 0) {
        # Plot only selected walkers' paths
        filtered_walkers <- walkers_ordered[walker_indices]

        # Create custom plot with better visibility
        grid_size <- result$parameters$grid_size

        # Create plot with gray background for contrast
        par(bg = "gray70")
        plot(1, type = "n", xlim = c(1, grid_size), ylim = c(1, grid_size),
             xlab = "", ylab = "",
             main = paste(length(walker_indices), "of", n_with_paths, "Fractal-Finding Walkers (by termination order)"),
             asp = 1, col.main = "black", col.lab = "black", col.axis = "black")

        # Add subtle grid for visibility
        grid(nx = grid_size, ny = grid_size, col = "gray70", lty = 1, lwd = 0.3)

        # Create STABLE color mapping based on termination order (not position)
        distinct_colors <- c("red", "blue", "darkgreen", "purple", "orange",
                           "brown", "deeppink", "darkcyan", "darkmagenta", "darkblue",
                           "darkred", "forestgreen", "black", "goldenrod", "darkviolet")

        # Map each walker to a consistent color based on their termination order
        # This ensures walker #20 always gets the same color regardless of selection
        color_map <- list()
        for (i in seq_along(walkers_ordered)) {
          term_order <- walkers_ordered[[i]]$termination_order
          # Use modulo to cycle through colors if more walkers than colors
          color_index <- ((term_order - 1) %% length(distinct_colors)) + 1
          color_map[[as.character(term_order)]] <- distinct_colors[color_index]
        }

        # Collect all paths with STABLE colors
        all_paths <- list()
        all_starts <- list()
        all_ends <- list()

        for (i in seq_along(filtered_walkers)) {
          walker <- filtered_walkers[[i]]
          # Get stable color for this walker based on termination order
          walker_color <- color_map[[as.character(walker$termination_order)]]

          # Check if path exists and is not empty
          if (!is.null(walker$path) && length(walker$path) > 0) {
            path_matrix <- do.call(rbind, walker$path)
            # Use walker termination order as key to ensure uniqueness
            key <- paste0("walker_", walker$termination_order)

            all_paths[[key]] <- list(
              x = path_matrix[, 2],
              y = path_matrix[, 1],
              col = walker_color
            )
            all_starts[[key]] <- list(
              x = path_matrix[1, 2],
              y = path_matrix[1, 1],
              col = walker_color
            )
          }

          # Always mark end position
          if (!is.null(walker$pos)) {
            key <- paste0("walker_", walker$termination_order)
            all_ends[[key]] <- list(
              x = walker$pos[2],
              y = walker$pos[1],
              col = walker_color
            )
          }
        }

        # Draw all paths
        for (path_data in all_paths) {
          if (!is.null(path_data)) {
            points(path_data$x, path_data$y,
                   pch = 16, col = path_data$col, cex = 0.8)
          }
        }

        # Draw all start positions
        for (start_data in all_starts) {
          if (!is.null(start_data)) {
            points(start_data$x, start_data$y,
                   pch = 21, bg = start_data$col, col = "black", cex = 2, lwd = 2)
          }
        }

        # Draw all end positions
        for (end_data in all_ends) {
          if (!is.null(end_data)) {
            points(end_data$x, end_data$y,
                   pch = 22, bg = end_data$col, col = "black", cex = 2.5, lwd = 2)
          }
        }

        # Caption moved to separate textOutput below plot
      } else {
        plot.new()
        text(0.5, 0.5, "No walkers selected")
      }
    } else {
      plot.new()
      text(0.5, 0.5, "No walkers available")
    }
  })

  # Caption for Walker Paths plot
  output$paths_caption <- renderText({
    "Symbols: Circle = Start | Square = End | Dots = Path. Colors by termination order."
  })

  # PAGE 3: Distributions
  output$dist_overall <- renderPlot({
    req(sim_result())
    result <- sim_result()

    # Set gray background
    par(bg = "gray70")

    # Defensive check for walkers
    if (is.null(result$walkers) || length(result$walkers) == 0) {
      plot.new()
      text(0.5, 0.5, "Run a simulation first", cex = 1.5, col = "gray")
      return()
    }

    # Extract path lengths safely
    # NOTE: walker$path is a LIST of positions, not a matrix
    # Use walker$steps for accurate step count
    path_lengths <- tryCatch({
      sapply(result$walkers, function(w) {
        if (!is.null(w$steps)) {
          w$steps
        } else {
          NA
        }
      })
    }, error = function(e) {
      numeric(0)
    })

    # Remove NAs and ensure numeric type
    path_lengths <- as.numeric(path_lengths[!is.na(path_lengths)])

    if (length(path_lengths) == 0 || !is.numeric(path_lengths)) {
      plot.new()
      text(0.5, 0.5, "No valid path data available", cex = 1.5, col = "gray")
      return()
    }

    # Need at least 2 values for meaningful distribution
    if (length(path_lengths) < 2) {
      plot.new()
      text(0.5, 0.5, paste0("Only ", length(path_lengths), " path - need at least 2"),
           cex = 1.2, col = "gray")
      return()
    }

    # Plot histogram with error handling
    tryCatch({
      hist(path_lengths,
           breaks = min(30, max(5, length(unique(path_lengths)))),
           col = "steelblue",
           border = "white",
           main = "Overall Path Length Distribution",
           xlab = "Path Length (steps)",
           ylab = "Frequency")
      abline(v = median(path_lengths), col = "red", lwd = 2, lty = 2)
      legend("topright",
             legend = c(paste("Median:", median(path_lengths)),
                       paste("Mean:", sprintf("%.1f", mean(path_lengths)))),
             col = c("red", "black"),
             lty = c(2, 1),
             lwd = 2)
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error creating plot:\n", e$message), cex = 1, col = "red")
    })
  })

  output$dist_by_reason <- renderPlot({
    req(sim_result())
    result <- sim_result()

    # Defensive check for walkers
    if (is.null(result$walkers) || length(result$walkers) == 0) {
      plot.new()
      text(0.5, 0.5, "Run a simulation first", cex = 1.5, col = "gray")
      return()
    }

    # Extract termination reasons and path lengths safely
    # NOTE: walker$path is a LIST, not a matrix - use walker$steps
    path_lengths <- tryCatch({
      sapply(result$walkers, function(w) {
        if (!is.null(w$steps)) w$steps else NA
      })
    }, error = function(e) numeric(0))

    reasons <- tryCatch({
      sapply(result$walkers, function(w) {
        if (!is.null(w$termination_reason)) w$termination_reason else "unknown"
      })
    }, error = function(e) character(0))

    # Remove NAs and ensure same length
    valid_idx <- !is.na(path_lengths)
    path_lengths <- as.numeric(path_lengths[valid_idx])
    reasons <- as.character(reasons[valid_idx])

    # Defensive check: ensure vectors are same length
    min_len <- min(length(path_lengths), length(reasons))
    if (min_len < length(path_lengths) || min_len < length(reasons)) {
      path_lengths <- path_lengths[1:min_len]
      reasons <- reasons[1:min_len]
    }

    # Check if we have valid data
    if (length(path_lengths) == 0 || length(reasons) == 0 ||
        !is.numeric(path_lengths) || !is.character(reasons)) {
      plot.new()
      text(0.5, 0.5, "No valid termination data available", cex = 1.5, col = "gray")
      return()
    }

    # Need at least 2 data points
    if (length(path_lengths) < 2) {
      plot.new()
      text(0.5, 0.5, "Need at least 2 walkers for distribution", cex = 1.2, col = "gray")
      return()
    }

    # Create data frame with error handling
    walkers_data <- tryCatch({
      data.frame(
        path_length = path_lengths,
        reason = reasons,
        stringsAsFactors = FALSE
      )
    }, error = function(e) {
      NULL
    })

    if (is.null(walkers_data)) {
      plot.new()
      text(0.5, 0.5, "Error creating data frame", cex = 1.2, col = "red")
      return()
    }

    # Plot boxplot by termination reason with error handling
    tryCatch({
      print(
        ggplot(walkers_data, aes(x = reason, y = path_length, fill = reason)) +
          geom_boxplot(alpha = 0.7) +
          geom_jitter(width = 0.2, alpha = 0.3) +
          labs(title = "Path Lengths by Termination Reason",
               x = "Termination Reason",
               y = "Path Length (steps)") +
          theme_minimal() +
          theme(legend.position = "none",
                panel.background = element_rect(fill = "gray70"),
                plot.background = element_rect(fill = "gray70"))
      )
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error creating plot:\n", e$message), cex = 1, col = "red")
    })
  })

  # PAGE 4: Statistics - Quintile-based Analysis
  # Returns data with both termination and launch quintiles
  get_quintile_data <- reactive({
    req(sim_result())
    result <- sim_result()

    steps <- sapply(result$walkers, function(w) w$steps)
    term_order <- sapply(result$walkers, function(w) w$termination_order)
    reasons <- sapply(result$walkers, function(w) w$termination_reason)
    launch_order <- sapply(result$walkers, function(w) w$id)

    df <- data.frame(steps = steps, termination_order = term_order,
                     launch_order = launch_order,
                     reason = reasons, stringsAsFactors = FALSE)
    n <- nrow(df)

    # Termination quintiles
    df <- df[order(df$termination_order), ]
    df$term_quintile <- factor(paste0("Q", ceiling(seq_len(n) / n * 5)),
                               levels = paste0("Q", 1:5))

    # Launch quintiles (walker index IS launch order)
    df <- df[order(df$launch_order), ]
    df$launch_quintile <- factor(paste0("Q", ceiling(seq_len(n) / n * 5)),
                                 levels = paste0("Q", 1:5))

    # Active column based on toggle
    grp <- input$stats_group_by
    df$quintile <- if (identical(grp, "launch")) df$launch_quintile else df$term_quintile
    df
  })

  # Dynamic description based on toggle
  output$stats_group_desc <- renderText({
    if (identical(input$stats_group_by, "launch")) {
      "Q1 = first 20% launched (fewer black pixels on grid), Q5 = last 20% launched (more black pixels)."
    } else {
      "Q1 = first 20% to terminate (found black quickly), Q5 = last 20% to terminate (wandered longest)."
    }
  })

  # Boxplot by quintile
  output$stats_quintile_boxplot <- renderPlot({
    req(sim_result())
    df <- get_quintile_data()

    if (is.null(df) || nrow(df) < 4) {
      plot.new()
      text(0.5, 0.5, "Need at least 4 walkers for quintile analysis", cex = 1.2, col = "gray")
      return()
    }

    tryCatch({
      print(
        ggplot(df, aes(x = quintile, y = steps)) +
          geom_boxplot(fill = "steelblue", alpha = 0.7, outlier.shape = 21) +
          geom_jitter(width = 0.15, alpha = 0.3, size = 1) +
          labs(
            title = "Path Length Distribution by Termination Quintile",
            subtitle = "Q1 = earliest terminators, Q4 = latest terminators",
            x = "Termination Quintile",
            y = "Path Length (steps)"
          ) +
          theme_minimal(base_size = 12) +
          theme(legend.position = "none",
                axis.text.x = element_text(angle = 15, hjust = 1),
                panel.background = element_rect(fill = "gray70"),
                plot.background = element_rect(fill = "gray70"))
      )
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error: ", e$message), cex = 1, col = "red")
    })
  })

  # Histogram panels by quintile
  output$stats_quintile_histograms <- renderPlot({
    req(sim_result())
    df <- get_quintile_data()

    if (is.null(df) || nrow(df) < 4) {
      plot.new()
      text(0.5, 0.5, "Need at least 4 walkers for quintile analysis", cex = 1.2, col = "gray")
      return()
    }

    tryCatch({
      print(
        ggplot(df, aes(x = steps)) +
          geom_histogram(bins = 20, fill = "steelblue", alpha = 0.8, color = "white") +
          facet_wrap(~ quintile, scales = "free_y", ncol = 2) +
          labs(
            title = "Path Length Histograms by Termination Quintile",
            x = "Path Length (steps)",
            y = "Count"
          ) +
          theme_minimal(base_size = 11) +
          theme(legend.position = "none",
                strip.text = element_text(face = "bold"),
                panel.background = element_rect(fill = "gray70"),
                plot.background = element_rect(fill = "gray70"))
      )
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error: ", e$message), cex = 1, col = "red")
    })
  })

  # Violin plot for distribution shape comparison
  output$stats_violin <- renderPlot({
    req(sim_result())
    df <- get_quintile_data()

    if (is.null(df) || nrow(df) < 4) {
      plot.new()
      text(0.5, 0.5, "Need at least 4 walkers", cex = 1.2, col = "gray")
      return()
    }

    tryCatch({
      print(
        ggplot(df, aes(x = quintile, y = steps)) +
          geom_violin(fill = "steelblue", alpha = 0.6, trim = FALSE) +
          geom_boxplot(width = 0.15, fill = "white", alpha = 0.8) +
          labs(
            title = "Distribution Shape by Quintile",
            x = "Termination Quintile",
            y = "Path Length (steps)"
          ) +
          theme_minimal(base_size = 11) +
          theme(axis.text.x = element_text(angle = 15, hjust = 1),
                panel.background = element_rect(fill = "gray70"),
                plot.background = element_rect(fill = "gray70"))
      )
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error: ", e$message), cex = 1, col = "red")
    })
  })

  # Trend plot showing mean/median across quintiles
  output$stats_trend <- renderPlot({
    req(sim_result())
    df <- get_quintile_data()

    if (is.null(df) || nrow(df) < 4) {
      plot.new()
      text(0.5, 0.5, "Need at least 4 walkers", cex = 1.2, col = "gray")
      return()
    }

    tryCatch({
      # Calculate summary statistics by quintile
      summary_df <- do.call(rbind, lapply(levels(df$quintile), function(q) {
        q_data <- df[df$quintile == q, ]
        data.frame(
          quintile = q,
          mean_steps = mean(q_data$steps),
          median_steps = median(q_data$steps),
          q_num = match(q, levels(df$quintile))
        )
      }))

      print(
        ggplot(summary_df, aes(x = q_num)) +
          geom_line(aes(y = mean_steps, color = "Mean"), linewidth = 1.2) +
          geom_point(aes(y = mean_steps, color = "Mean"), size = 3) +
          geom_line(aes(y = median_steps, color = "Median"), linewidth = 1.2, linetype = "dashed") +
          geom_point(aes(y = median_steps, color = "Median"), size = 3) +
          scale_x_continuous(breaks = 1:5, labels = paste0("Q", 1:5)) +
          scale_color_manual(values = c("Mean" = "steelblue", "Median" = "darkred")) +
          labs(
            title = "Path Length Trend Across Quintiles",
            x = "Termination Quintile",
            y = "Path Length (steps)",
            color = "Statistic"
          ) +
          theme_minimal(base_size = 11) +
          theme(legend.position = "bottom",
                panel.background = element_rect(fill = "gray70"),
                plot.background = element_rect(fill = "gray70"))
      )
    }, error = function(e) {
      plot.new()
      text(0.5, 0.5, paste0("Error: ", e$message), cex = 1, col = "red")
    })
  })

  # Summary statistics by quintile
  output$stats_quintile_summary <- renderPrint({
    req(sim_result())
    df <- get_quintile_data()

    if (is.null(df) || nrow(df) < 4) {
      cat("Need at least 4 walkers for quintile analysis\n")
      return()
    }

    cat("=== QUARTILE SUMMARY STATISTICS ===\n\n")

    for (q in levels(df$quintile)) {
      q_data <- df[df$quintile == q, ]
      cat(sprintf("%s (n=%d):\n", q, nrow(q_data)))
      cat(sprintf("  Min:    %d steps\n", min(q_data$steps)))
      cat(sprintf("  Q1:     %d steps\n", as.integer(quantile(q_data$steps, 0.25))))
      cat(sprintf("  Median: %d steps\n", as.integer(median(q_data$steps))))
      cat(sprintf("  Q3:     %d steps\n", as.integer(quantile(q_data$steps, 0.75))))
      cat(sprintf("  Max:    %d steps\n", max(q_data$steps)))
      cat(sprintf("  Mean:   %.1f steps\n", mean(q_data$steps)))
      cat(sprintf("  SD:     %.1f steps\n", sd(q_data$steps)))
      cat("\n")
    }

    # Overall comparison
    cat("=== QUARTILE COMPARISON ===\n\n")
    means <- tapply(df$steps, df$quintile, mean)
    cat("Mean steps by quintile:\n")
    for (i in seq_along(means)) {
      cat(sprintf("  %s: %.1f\n", names(means)[i], means[i]))
    }

    # Trend indicator
    # Q1 = earliest terminators (found black quickly), Q5 = latest (wandered longest)
    # Expected: Q5 mean > Q1 mean (later terminators took more steps by definition)
    cat("\n=== TREND ===\n")
    cat(sprintf("Q1 (earliest 20%%): mean %.0f steps\n", means[1]))
    cat(sprintf("Q5 (latest 20%%):   mean %.0f steps\n", means[5]))
    cat(sprintf("Ratio Q5/Q1: %.1fx\n", means[5] / means[1]))
  })

  # ============================================================================
  # PAGE 5: Survival Curve - Server Logic
  # ============================================================================

  # Dynamic slider: max = max steps, default = median (50% survival)
  observe({
    req(sim_result())
    result <- sim_result()
    steps <- sapply(result$walkers, function(w) w$steps)
    max_step <- max(steps)
    median_step <- as.integer(median(steps))
    updateSliderInput(session, "survival_step",
                      max = max_step,
                      value = median_step)
  })

  # Survival curve plot (Kaplan-Meier style)
  output$survival_curve <- renderPlot({
    req(sim_result())
    result <- sim_result()

    # Build survival data
    steps <- sapply(result$walkers, function(w) w$steps)
    n_total <- length(steps)

    # Calculate survival proportion at each unique step value
    unique_steps <- sort(unique(steps))
    survival_at_step <- sapply(unique_steps, function(s) {
      sum(steps > s) / n_total
    })

    # Current slider position
    current_step <- input$survival_step
    current_survival <- sum(steps > current_step) / n_total

    # Plot using ggplot2
    df <- data.frame(step = unique_steps, survival = survival_at_step)

    ggplot(df, aes(x = step, y = survival)) +
      geom_step(color = "steelblue", linewidth = 1.2) +
      geom_vline(xintercept = current_step, linetype = "dashed", color = "red") +
      geom_point(aes(x = current_step, y = current_survival),
                 color = "red", size = 3) +
      annotate("text", x = current_step, y = min(current_survival + 0.08, 1),
               label = sprintf("%.1f%% alive", current_survival * 100),
               color = "red", hjust = 0, size = 4) +
      labs(
        title = "Walker Survival Curve",
        subtitle = sprintf("Red line: step %d (%.1f%% still walking)",
                           current_step, current_survival * 100),
        x = "Steps",
        y = "Proportion Surviving"
      ) +
      scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1)) +
      theme_minimal(base_size = 14) +
      theme(
        plot.title = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle = element_text(hjust = 0.5, color = "gray40"),
        panel.background = element_rect(fill = "gray70"),
        plot.background = element_rect(fill = "gray70")
      )
  })

  # Termination breakdown by reason
  output$termination_breakdown <- renderPlot({
    req(sim_result())
    result <- sim_result()

    reasons <- sapply(result$walkers, function(w) w$termination_reason)
    reason_counts <- table(reasons)

    df <- data.frame(
      reason = names(reason_counts),
      count = as.integer(reason_counts)
    )
    df$pct <- df$count / sum(df$count) * 100

    # Color mapping for different termination reasons
    color_map <- c(
      "black_neighbor" = "steelblue",
      "hit_boundary" = "orange",
      "max_steps" = "gray50",
      "touched_black" = "darkblue"
    )

    # Create fill colors vector matching data
    df$fill_color <- ifelse(df$reason %in% names(color_map),
                            color_map[df$reason],
                            "gray70")

    ggplot(df, aes(x = reorder(reason, -count), y = count, fill = reason)) +
      geom_col() +
      geom_text(aes(label = sprintf("%.1f%%", pct)), vjust = -0.5, size = 3.5) +
      scale_fill_manual(values = color_map, na.value = "gray70") +
      labs(
        title = "How Walkers Terminated",
        x = "Termination Reason",
        y = "Count"
      ) +
      theme_minimal(base_size = 12) +
      theme(
        legend.position = "none",
        axis.text.x = element_text(angle = 30, hjust = 1),
        plot.title = element_text(hjust = 0.5, face = "bold"),
        panel.background = element_rect(fill = "gray70"),
        plot.background = element_rect(fill = "gray70")
      )
  })

  # Survival statistics text
  output$survival_stats <- renderPrint({
    req(sim_result())
    result <- sim_result()

    steps <- sapply(result$walkers, function(w) w$steps)

    cat("=== SURVIVAL STATISTICS ===\n\n")
    cat(sprintf("Total walkers: %d\n", length(steps)))
    cat(sprintf("Median survival: %d steps\n", median(steps)))
    cat(sprintf("Mean survival: %.1f steps\n", mean(steps)))
    cat(sprintf("25th percentile: %d steps\n", as.integer(quantile(steps, 0.25))))
    cat(sprintf("75th percentile: %d steps\n", as.integer(quantile(steps, 0.75))))
    cat("\n")
    cat("At current slider position:\n")
    current_survival <- sum(steps > input$survival_step)
    cat(sprintf("  %d walkers (%.1f%%) survived past step %d\n",
                current_survival,
                current_survival / length(steps) * 100,
                input$survival_step))
  })

  # Hypothesis test: Geometric vs Exponential decay models
  output$hypothesis_plot <- renderPlot({
    req(sim_result())
    result <- sim_result()

    # Build survival data with termination order
    survdat <- data.frame(
      order = sapply(result$walkers, function(w) {
        if (!is.null(w$termination_order)) w$termination_order else NA
      }),
      steps = sapply(result$walkers, function(w) w$steps)
    )

    # Remove NA orders (if termination_order not available)
    survdat <- survdat[!is.na(survdat$order), ]

    if (nrow(survdat) < 10) {
      # Not enough data for hypothesis testing
      plot.new()
      text(0.5, 0.5, "Need at least 10 walkers with termination order for hypothesis test",
           cex = 1.2, col = "gray")
      return()
    }

    survdat <- survdat[order(survdat$order), ]

    # H0: Geometric decay model
    # Geometric: P(survive n steps) = (1-p)^n, E[steps] = (1-p)/p
    # Fit p via method of moments: p_hat = 1 / (1 + mean(steps))
    geom_p <- 1 / (1 + mean(survdat$steps))
    # Predicted mean at each order position (linear decay in expected value)
    survdat$geom_pred <- (1 / geom_p) * (1 - survdat$order / max(survdat$order) * 0.5)

    # H1: Exponential decay model
    # E[steps | order] = a * exp(-lambda * order)
    survdat$log_steps <- log(survdat$steps + 1)
    exp_model <- lm(log_steps ~ order, data = survdat)
    survdat$exp_pred <- exp(fitted(exp_model))

    # Non-parametric: Kolmogorov-Smirnov tests
    geom_ks <- tryCatch({
      ks.test(survdat$steps, function(x) pgeom(x, prob = geom_p))
    }, error = function(e) list(p.value = 0, statistic = 1))

    exp_rate <- 1 / mean(survdat$steps)
    exp_ks <- tryCatch({
      ks.test(survdat$steps, function(x) pexp(x, rate = exp_rate))
    }, error = function(e) list(p.value = 0, statistic = 1))

    # Determine which model fits better (higher p-value = better fit)
    geom_better <- geom_ks$p.value > exp_ks$p.value

    # Plot
    ggplot(survdat, aes(x = order, y = steps)) +
      geom_point(alpha = 0.4, size = 1.5, color = "gray40") +
      geom_line(aes(y = geom_pred), color = "blue", linewidth = 1.2, linetype = "dashed") +
      geom_line(aes(y = exp_pred), color = "red", linewidth = 1.2) +
      annotate("text", x = max(survdat$order) * 0.6, y = max(survdat$steps) * 0.9,
               label = sprintf("H0 Geometric: KS p=%.3f\nH1 Exponential: KS p=%.3f\nBetter fit: %s",
                               geom_ks$p.value, exp_ks$p.value,
                               ifelse(geom_better, "Geometric", "Exponential")),
               hjust = 0, size = 3.5,
               color = ifelse(geom_better, "blue", "red")) +
      labs(
        title = "Survival Decay: Geometric vs Exponential",
        subtitle = sprintf("Best fit: %s model (higher KS p-value = better fit)",
                           ifelse(geom_better, "Geometric (H0)", "Exponential (H1)")),
        x = "Walker Termination Order",
        y = "Steps Survived",
        caption = "Blue dashed: Geometric (H0) | Red solid: Exponential (H1)"
      ) +
      theme_minimal(base_size = 12) +
      theme(
        panel.background = element_rect(fill = "gray70"),
        plot.background = element_rect(fill = "gray70"),
        plot.title = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle = element_text(hjust = 0.5,
                                     color = ifelse(geom_better, "blue", "red"))
      )
  })

  # Hypothesis test summary text
  output$hypothesis_stats <- renderPrint({
    req(sim_result())
    result <- sim_result()

    # Build survival data with termination order
    survdat <- data.frame(
      order = sapply(result$walkers, function(w) {
        if (!is.null(w$termination_order)) w$termination_order else NA
      }),
      steps = sapply(result$walkers, function(w) w$steps)
    )

    survdat <- survdat[!is.na(survdat$order), ]

    if (nrow(survdat) < 10) {
      cat("Insufficient data for hypothesis testing.\n")
      cat("Need at least 10 walkers with termination_order field.\n")
      return()
    }

    cat("=== HYPOTHESIS TEST: GEOMETRIC vs EXPONENTIAL DECAY ===\n\n")
    cat("H0 (Geometric): Step counts follow a geometric distribution\n")
    cat("H1 (Exponential): Step counts follow an exponential distribution\n\n")

    # Geometric model
    geom_p <- 1 / (1 + mean(survdat$steps))
    cat("--- Geometric Model (H0) ---\n")
    cat(sprintf("Estimated p: %.4f\n", geom_p))
    cat(sprintf("Expected mean: %.1f steps\n", (1 - geom_p) / geom_p))

    # Exponential model
    exp_rate <- 1 / mean(survdat$steps)
    survdat$log_steps <- log(survdat$steps + 1)
    exp_model <- lm(log_steps ~ order, data = survdat)

    cat("\n--- Exponential Model (H1) ---\n")
    cat(sprintf("Rate parameter (lambda): %.6f\n", exp_rate))
    cat(sprintf("Expected mean: %.1f steps\n", 1 / exp_rate))
    cat(sprintf("Decay coefficient: %.6f\n", coef(exp_model)[2]))

    # KS tests
    geom_ks <- tryCatch({
      ks.test(survdat$steps, function(x) pgeom(x, prob = geom_p))
    }, error = function(e) list(p.value = 0, statistic = 1))

    exp_ks <- tryCatch({
      ks.test(survdat$steps, function(x) pexp(x, rate = exp_rate))
    }, error = function(e) list(p.value = 0, statistic = 1))

    cat("\n--- Kolmogorov-Smirnov Tests ---\n")
    cat(sprintf("Geometric KS statistic: %.4f, p-value: %.4f\n",
                geom_ks$statistic, geom_ks$p.value))
    cat(sprintf("Exponential KS statistic: %.4f, p-value: %.4f\n",
                exp_ks$statistic, exp_ks$p.value))

    # Significance threshold
    alpha <- 0.05

    cat("\n=== CONCLUSION ===\n")

    # Check if BOTH models are rejected (p < alpha)
    geom_rejected <- geom_ks$p.value < alpha
    exp_rejected <- exp_ks$p.value < alpha

    if (geom_rejected && exp_rejected) {
      cat("NEITHER model fits well (both p-values < 0.05)\n")
      cat("The step distribution may be more complex than geometric or exponential.\n")
      cat("Consider: mixture models, heavy-tailed distributions, or spatial effects.\n")
    } else if (!geom_rejected && !exp_rejected) {
      # Both acceptable - compare which is better
      if (geom_ks$p.value > exp_ks$p.value) {
        cat("GEOMETRIC model fits better (p=", round(geom_ks$p.value, 4), ")\n")
        cat("Step counts suggest discrete process with constant termination probability.\n")
      } else {
        cat("EXPONENTIAL model fits better (p=", round(exp_ks$p.value, 4), ")\n")
        cat("Step counts suggest continuous decay process.\n")
      }
    } else if (!geom_rejected) {
      cat("GEOMETRIC model fits (p=", round(geom_ks$p.value, 4), ")\n")
      cat("Exponential model rejected (p=", round(exp_ks$p.value, 4), ")\n")
    } else {
      cat("EXPONENTIAL model fits (p=", round(exp_ks$p.value, 4), ")\n")
      cat("Geometric model rejected (p=", round(geom_ks$p.value, 4), ")\n")
    }
  })

  # PAGE 6: Debug - Versions
  output$debug_versions <- renderText({
    paste0(
      "=== PACKAGE VERSIONS ===\n",
      "randomwalk: ", tryCatch(as.character(packageVersion("randomwalk")), error = function(e) "inline engine"), "\n",
      "shiny: ", packageVersion("shiny"), "\n",
      "ggplot2: ", packageVersion("ggplot2"), "\n",
      if (requireNamespace("mirai", quietly = TRUE)) {
        paste0("mirai: ", packageVersion("mirai"), "\n")
      } else "",
      if (requireNamespace("nanonext", quietly = TRUE)) {
        paste0("nanonext: ", packageVersion("nanonext"), "\n")
      } else "",
      if (requireNamespace("crew", quietly = TRUE)) {
        paste0("crew: ", packageVersion("crew"), "\n")
      } else ""
    )
  })

  # PAGE 5: Debug - Inputs
  output$debug_inputs <- renderText({
    paste0(
      "=== CURRENT INPUTS ===\n",
      "Grid Size: ", input$grid_size, "\n",
      "Number of Walkers: ", input$n_walkers, "\n",
      "Mode: sync sequential (workers=0)\n",
      "Neighborhood: ", input$neighborhood, "\n",
      "Boundary: ", input$boundary, "\n",
      "Max Steps: ", input$max_steps
    )
  })

  # PAGE 5: Debug - Backend
  output$debug_backend <- renderText({
    env_is_webr <- is_webr()

    paste0(
      "=== BACKEND INFORMATION ===\n",
      "Environment: WebR/Browser\n",
      "R Version: ", R.version.string, "\n",
      "Backend: synchronous (no parallelization)\n",
      "Sync Mode: static (grid snapshots)\n",
      "\n=== ENVIRONMENT DETECTION ===\n",
      ".webr_env exists: ", exists(".webr_env", envir = .GlobalEnv), "\n",
      "WEBR env var: ", Sys.getenv("WEBR"), "\n",
      "is_webr() result: ", env_is_webr
    )
  })

  # Walker Paths QA
  output$debug_walker_qa <- renderText({
    if (is.null(sim_result())) return("No simulation run yet.")
    result <- sim_result()
    walkers <- result$walkers
    n <- length(walkers)
    has_path <- sum(sapply(walkers, function(w) !is.null(w$path) && length(w$path) > 0))
    reasons <- table(sapply(walkers, function(w) w$termination_reason))
    steps <- sapply(walkers, function(w) w$steps)
    orders <- sapply(walkers, function(w) w$termination_order)

    paste0(
      "=== WALKER PATHS QA ===\n",
      "Total walkers: ", n, "\n",
      "With stored paths: ", has_path, " (vectorized mode stores none)\n",
      "Termination orders assigned: ", sum(orders > 0), "/", n, "\n",
      "Steps range: ", min(steps), " - ", max(steps), "\n",
      "Median steps: ", median(steps), "\n",
      "\n=== TERMINATION REASONS ===\n",
      paste(sprintf("  %s: %d (%.1f%%)", names(reasons), as.integer(reasons),
                    100 * as.integer(reasons) / n), collapse = "\n"),
      "\n\n=== SIMULATION MODE ===\n",
      "Engine: ", if (has_path > 0) "per-walker (with paths)" else "vectorized (no paths, 10-50x faster)", "\n",
      "Batch target: 200k walker-ops/batch\n",
      "JS round-trip delay: 50ms"
    )
  })

  # PAGE 5: Debug - Periodic Updates
  output$debug_periodic <- renderText({
    # Depend on timer to update every 100ms
    autoInvalidate()

    # Enhanced debug info during execution
    state_info <- switch(sim_state(),
      "idle" = "💤 Idle - waiting for user to click 'Run Simulation'",
      "running" = {
        elapsed <- if (!is.null(sim_start_time())) {
          as.numeric(difftime(Sys.time(), sim_start_time(), units = "secs"))
        } else {
          0
        }
        paste0("Running - ", format_duration(elapsed), " elapsed")
      },
      "complete" = {
        elapsed <- if (!is.null(sim_end_time()) && !is.null(sim_start_time())) {
          as.numeric(difftime(sim_end_time(), sim_start_time(), units = "secs"))
        } else {
          0
        }
        paste0("Complete - took ", format_duration(elapsed))
      },
      "error" = "❌ Error - see status tab for details",
      "Unknown state"
    )

    paste0(
      "=== PERIODIC UPDATES ===\n",
      "Current Time: ", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "\n",
      "Simulation State: ", state_info, "\n",
      "Simulation Count: ", sim_count(), "\n",
      "Result Available: ", if (is.null(sim_result())) "No" else "Yes", "\n",
      "Active Tab: ", input$tabs, "\n",
      if (sim_state() == "running" && !is.null(sim_start_time())) {
        n_active <- length(batch_active())
        paste0(
          "\n=== EXECUTION INFO ===\n",
          "Grid Size: ", input$grid_size, "×", input$grid_size, "\n",
          "Walkers: ", sim_completed_walkers(), "/", input$n_walkers,
          " (", n_active, " active)\n",
          "Black Pixels: ", sim_black_pixels(), "\n",
          "Step: ", sim_current_step(), "\n",
          "Backend: batched sequential (non-blocking)"
        )
      } else {
        ""
      }
    )
  })
}

# ============================================================================
# Run Shiny App
# ============================================================================

shinyApp(ui = ui, server = server)

Build Info

randomwalk 2.1.1 | Git 507cc3f | R 4.5.2 | Built 2026-05-06 13:34:18