Zum Inhalt

apple/PhysarumApp/Sources/Model/RandomStrategy.swift

import Foundation

/// How Randomize samples a single scalar param. Bands come from the observed
/// value ranges of the 25 built-in presets, kept inside the "alive" region.
enum RandomStrategy {
    case uniform(ClosedRange<Double>)
    /// Emit 0 with probability `zeroProb`, else uniform in `band` (off-by-default accents).
    case mostlyZero(zeroProb: Double, ClosedRange<Double>)
    /// Pick band A with probability `probA`, else band B, then uniform inside it.
    case bimodal(probA: Double, ClosedRange<Double>, ClosedRange<Double>)

    func sample<G: RandomNumberGenerator>(using g: inout G) -> Double {
        switch self {
        case let .uniform(r):
            return Double.random(in: r, using: &g)
        case let .mostlyZero(p, r):
            return Double.random(in: 0...1, using: &g) < p ? 0 : Double.random(in: r, using: &g)
        case let .bimodal(p, a, b):
            let r = Double.random(in: 0...1, using: &g) < p ? a : b
            return Double.random(in: r, using: &g)
        }
    }

    /// Representative band used for Mutate magnitude + clamping (keeps mutate inside the lively zone).
    var span: ClosedRange<Double> {
        switch self {
        case let .uniform(r): return r
        case let .mostlyZero(_, r): return 0...r.upperBound
        case let .bimodal(_, a, b): return min(a.lowerBound, b.lowerBound)...max(a.upperBound, b.upperBound)
        }
    }
}

enum RandomizeProfiles {
    /// Safe sampling strategy for every scalar ParamID. Species index and matrix
    /// position are collapsed (all species share a profile; matrix diagonal vs
    /// off-diagonal differ).
    static func strategy(for id: ParamID) -> RandomStrategy {
        switch id {
        case .agentCount:        return .bimodal(probA: 0.8, 6_000...14_000, 100_000...180_000)
        case .exposure:          return .uniform(1.5...3.6)
        case .simSpeed:          return .uniform(1...1)            // time knob, not a look knob → keep at 1
        case .decayRate:         return .uniform(0.94...0.99)
        case .diffusionFreq:     return .uniform(1...4)
        case .fadeStrength:      return .uniform(0.03...0.12)
        case .trailSaturation:   return .uniform(110...255)
        case .chaosInterval:     return .mostlyZero(zeroProb: 0.7, 20...120)
        case .chaosStrength:     return .mostlyZero(zeroProb: 0.7, 0.1...0.6)
        case .rhythmAmplitude:   return .uniform(0.05...0.2)
        case .headIntensity:     return .mostlyZero(zeroProb: 0.6, 0.3...1.2)
        case .kaleidoscope:      return .mostlyZero(zeroProb: 0.8, 4...6)
        case .feedbackAmount:    return .uniform(0...0.6)
        case .bloomThreshold:    return .uniform(0.15...1.3)
        case .bloomIntensity:    return .uniform(0.3...2.4)
        case .vignette:          return .uniform(0...0.6)
        case .saturation:        return .uniform(0.6...1.5)
        case .contrast:          return .uniform(0.85...1.4)
        case .populationShare(_):   return .uniform(0.1...1)
        case .sensorAngle(_):       return .uniform(0.3...0.95)
        case .sensorDistance(_):    return .uniform(12...40)
        case .turnSpeed(_):         return .uniform(0.2...0.55)
        case .moveSpeed(_):         return .uniform(0.5...1.9)
        case .depositAmount(_):     return .uniform(4...13)
        case let .matrix(r, c):  return .uniform(r == c ? 0.5...1.5 : -0.6...0.6)
        // non-scalars are not sampled here (handled explicitly in randomize())
        case .spawnPattern, .blendMode, .backgroundColor, .speciesColor:
            return .uniform(0...0)
        }
    }
}