package engine import "math/rand/v2" // EdgeThresholds holds one random draw in [0, 1) per directed edge. The // cascade forwards across u -> v when the draw is below the forwarding // probability. Drawing all thresholds once and reusing them recreates the // prototype's "same world, different lever" comparison: scenarios that // share thresholds differ only in who is educated. type EdgeThresholds map[[2]int]float64 // NewEdgeThresholds draws a threshold for every directed edge. Both // directions get independent draws (u forwarding to v is a different event // than v forwarding to u). Node order makes the draws deterministic. func NewEdgeThresholds(graph *Graph, rng *rand.Rand) EdgeThresholds { thresholds := make(EdgeThresholds, 2*graph.NumEdges()) for node := range graph.NumNodes() { for _, neighbor := range graph.Neighbors(node) { if node < neighbor { // visit each undirected edge exactly once thresholds[[2]int{node, neighbor}] = rng.Float64() thresholds[[2]int{neighbor, node}] = rng.Float64() } } } return thresholds } // NeverReached marks a node the fake never got to. const NeverReached = -1 // CascadeResult describes one finished spread. type CascadeResult struct { // ReachedAtRound[node] is the round in which node first received the // fake (round 0 is the origin posting it), or NeverReached. This is // exactly what an animation needs: the activation time per node. ReachedAtRound []int NumReached int NumRounds int } // RunCascade spreads the fake from origin: in every round, each newly // reached student forwards to each neighbour whose edge threshold falls // below forwardProb. Educated students receive the fake but never forward // it; that is the entire effect of the education lever. func RunCascade(graph *Graph, origin int, forwardProb float64, educated []int, thresholds EdgeThresholds) CascadeResult { isEducated := make([]bool, graph.NumNodes()) for _, student := range educated { isEducated[student] = true } reachedAtRound := make([]int, graph.NumNodes()) for node := range reachedAtRound { reachedAtRound[node] = NeverReached } reachedAtRound[origin] = 0 numReached := 1 lastRound := 0 frontier := []int{origin} for round := 1; len(frontier) > 0; round++ { var nextFrontier []int for _, forwarder := range frontier { if isEducated[forwarder] { continue // received the fake, refuses to pass it on } for _, receiver := range graph.Neighbors(forwarder) { alreadyReached := reachedAtRound[receiver] != NeverReached forwards := thresholds[[2]int{forwarder, receiver}] < forwardProb if !alreadyReached && forwards { reachedAtRound[receiver] = round numReached++ lastRound = round nextFrontier = append(nextFrontier, receiver) } } } frontier = nextFrontier } return CascadeResult{ ReachedAtRound: reachedAtRound, NumReached: numReached, NumRounds: lastRound + 1, } }