@@ -12,6 +12,23 @@ const DEVICE_META = {
1212
1313const MAX_CUSTOM_OUTCOMES = 50 ;
1414
15+ /**
16+ * Sanitizes and normalizes probability arrays.
17+ * Ensures all values are non-negative and sum to 1.0.
18+ *
19+ * @param {Array<number> } probs - Array of probabilities to sanitize
20+ * @param {Array<number> } fallback - Fallback array if probs is invalid
21+ * @returns {Array<number> } Normalized probabilities summing to 1.0
22+ */
23+ function sanitizeProbabilities ( probs , fallback ) {
24+ const clamped = probs . map ( p => Math . max ( 0 , p ) ) ;
25+ const sum = clamped . reduce ( ( a , p ) => a + p , 0 ) ;
26+ if ( sum > 0 ) {
27+ return clamped . map ( p => p / sum ) ;
28+ }
29+ return fallback ;
30+ }
31+
1532function baseDefinition ( kind , extra = { } ) {
1633 const meta = DEVICE_META [ kind ] ?? { } ;
1734 return {
@@ -80,118 +97,14 @@ export function buildDeviceDefinition(config) {
8097 if ( config . device === 'coin' ) {
8198 const labels = [ 'Heads' , 'Tails' ] ;
8299 let probabilities = config . coinProbabilities ?? [ 0.5 , 0.5 ] ;
83-
84- // Ensure all probabilities are clamped and normalized
85- // First, normalize the input probabilities
86- let sum = probabilities . reduce ( ( acc , p ) => acc + p , 0 ) ;
87- if ( sum <= 0 ) {
88- probabilities = [ 0.5 , 0.5 ] ;
89- return baseDefinition ( 'coin' , { labels, probabilities, cdf : buildCdf ( probabilities ) } ) ;
90- }
91-
92- let normalized = probabilities . map ( ( p ) => p / sum ) ;
93-
94- // Clamp values that are out of bounds and redistribute excess
95- let clamped = normalized . map ( ( p ) => clamp ( p , 0.01 , 0.99 ) ) ;
96- let clampedSum = clamped . reduce ( ( acc , p ) => acc + p , 0 ) ;
97-
98- // If clamping reduced the sum, redistribute the difference proportionally
99- if ( clampedSum < 1.0 ) {
100- const excess = 1.0 - clampedSum ;
101- // Find values that can accept more probability (those below 0.99)
102- const canAcceptMore = clamped . map ( ( p , i ) => p < 0.99 ? i : - 1 ) . filter ( i => i >= 0 ) ;
103- if ( canAcceptMore . length > 0 ) {
104- // Distribute excess proportionally among values that can accept more
105- const weights = canAcceptMore . map ( i => clamped [ i ] ) ;
106- const weightSum = weights . reduce ( ( acc , w ) => acc + w , 0 ) ;
107- if ( weightSum > 0 ) {
108- canAcceptMore . forEach ( ( idx , i ) => {
109- clamped [ idx ] = Math . min ( 0.99 , clamped [ idx ] + ( weights [ i ] / weightSum ) * excess ) ;
110- } ) ;
111- } else {
112- // Equal distribution if weights are zero
113- const perValue = excess / canAcceptMore . length ;
114- canAcceptMore . forEach ( idx => {
115- clamped [ idx ] = Math . min ( 0.99 , clamped [ idx ] + perValue ) ;
116- } ) ;
117- }
118- }
119- }
120-
121- // Final normalization to ensure sum is exactly 1.0
122- clampedSum = clamped . reduce ( ( acc , p ) => acc + p , 0 ) ;
123- if ( clampedSum > 0 ) {
124- probabilities = clamped . map ( ( p ) => p / clampedSum ) ;
125- // Final clamp check (shouldn't be needed, but safety check)
126- probabilities = probabilities . map ( ( p ) => clamp ( p , 0.01 , 0.99 ) ) ;
127- const finalSum = probabilities . reduce ( ( acc , p ) => acc + p , 0 ) ;
128- if ( finalSum > 0 ) {
129- probabilities = probabilities . map ( ( p ) => p / finalSum ) ;
130- }
131- } else {
132- probabilities = [ 0.5 , 0.5 ] ;
133- }
134-
100+ probabilities = sanitizeProbabilities ( probabilities , [ 0.5 , 0.5 ] ) ;
135101 return baseDefinition ( 'coin' , { labels, probabilities, cdf : buildCdf ( probabilities ) } ) ;
136102 }
137103
138104 if ( config . device === 'die' ) {
139105 const labels = [ '1' , '2' , '3' , '4' , '5' , '6' ] ;
140- let probabilities ;
141-
142- probabilities = config . dieProbabilities ?? [ 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 ] ;
143-
144- // Ensure all probabilities are clamped and normalized
145- // First, normalize the input probabilities
146- let sum = probabilities . reduce ( ( acc , p ) => acc + p , 0 ) ;
147- if ( sum <= 0 ) {
148- probabilities = [ 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 ] ;
149- return baseDefinition ( 'die' , { labels, probabilities, cdf : buildCdf ( probabilities ) } ) ;
150- }
151-
152- let normalized = probabilities . map ( ( p ) => p / sum ) ;
153-
154- // Clamp values that are out of bounds and redistribute excess
155- let clamped = normalized . map ( ( p ) => clamp ( p , 0.01 , 0.8 ) ) ;
156- let clampedSum = clamped . reduce ( ( acc , p ) => acc + p , 0 ) ;
157-
158- // If clamping reduced the sum, redistribute the difference proportionally
159- if ( clampedSum < 1.0 ) {
160- const excess = 1.0 - clampedSum ;
161- // Find values that can accept more probability (those below 0.8)
162- const canAcceptMore = clamped . map ( ( p , i ) => p < 0.8 ? i : - 1 ) . filter ( i => i >= 0 ) ;
163- if ( canAcceptMore . length > 0 ) {
164- // Distribute excess proportionally among values that can accept more
165- const weights = canAcceptMore . map ( i => clamped [ i ] ) ;
166- const weightSum = weights . reduce ( ( acc , w ) => acc + w , 0 ) ;
167- if ( weightSum > 0 ) {
168- canAcceptMore . forEach ( ( idx , i ) => {
169- clamped [ idx ] = Math . min ( 0.8 , clamped [ idx ] + ( weights [ i ] / weightSum ) * excess ) ;
170- } ) ;
171- } else {
172- // Equal distribution if weights are zero
173- const perValue = excess / canAcceptMore . length ;
174- canAcceptMore . forEach ( idx => {
175- clamped [ idx ] = Math . min ( 0.8 , clamped [ idx ] + perValue ) ;
176- } ) ;
177- }
178- }
179- }
180-
181- // Final normalization to ensure sum is exactly 1.0
182- clampedSum = clamped . reduce ( ( acc , p ) => acc + p , 0 ) ;
183- if ( clampedSum > 0 ) {
184- probabilities = clamped . map ( ( p ) => p / clampedSum ) ;
185- // Final clamp check (shouldn't be needed, but safety check)
186- probabilities = probabilities . map ( ( p ) => clamp ( p , 0.01 , 0.8 ) ) ;
187- const finalSum = probabilities . reduce ( ( acc , p ) => acc + p , 0 ) ;
188- if ( finalSum > 0 ) {
189- probabilities = probabilities . map ( ( p ) => p / finalSum ) ;
190- }
191- } else {
192- probabilities = [ 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 ] ;
193- }
194-
106+ let probabilities = config . dieProbabilities ?? [ 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 ] ;
107+ probabilities = sanitizeProbabilities ( probabilities , [ 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 , 1 / 6 ] ) ;
195108 return baseDefinition ( 'die' , { labels, probabilities, cdf : buildCdf ( probabilities ) } ) ;
196109 }
197110
0 commit comments