Forwarding was a single global ForwardProb; make it a per-student composite (Config.ForwardChance): baseline propensity raised by the fake's Novelty, lowered by ambient HarmAwareness, and scaled down for an educated student by ProgramEffect (1 = today's hard block). RunCascade now takes a precomputed per-student []float64 chance and has no education special case. Defaults are behaviour-neutral, so the 82/58/6 golden tests are unchanged; the model is tuned in a later slice.
81 lines
2.9 KiB
Go
81 lines
2.9 KiB
Go
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 that student's forwarding chance. forwardChance[student] already
|
|
// encodes the education lever (an educated student's chance is scaled down,
|
|
// to zero under a full-strength program), so the cascade has no education
|
|
// special case; an educated student receives the fake like anyone else and
|
|
// simply forwards it with a lower chance.
|
|
func RunCascade(graph *Graph, origin int, forwardChance []float64, thresholds EdgeThresholds) CascadeResult {
|
|
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 {
|
|
chance := forwardChance[forwarder]
|
|
for _, receiver := range graph.Neighbors(forwarder) {
|
|
alreadyReached := reachedAtRound[receiver] != NeverReached
|
|
forwards := thresholds[[2]int{forwarder, receiver}] < chance
|
|
if !alreadyReached && forwards {
|
|
reachedAtRound[receiver] = round
|
|
numReached++
|
|
lastRound = round
|
|
nextFrontier = append(nextFrontier, receiver)
|
|
}
|
|
}
|
|
}
|
|
frontier = nextFrontier
|
|
}
|
|
|
|
return CascadeResult{
|
|
ReachedAtRound: reachedAtRound,
|
|
NumReached: numReached,
|
|
NumRounds: lastRound + 1,
|
|
}
|
|
}
|