Skip to content

Commit 2095ea7

Browse files
authored
Penismessungen mit cryptographisch sicherem Zufall und realistischer Normalverteilung (#503)
* Add some random API * Add SafeRandomSource * Add exports * Add clamp * Use normal distribution based on real data for penis size * Safe -> Secure * Add #getOrCreateMeasurement * Fix RNG * Align * Fix radius * Better display
1 parent 7f607cf commit 2095ea7

File tree

3 files changed

+159
-35
lines changed

3 files changed

+159
-35
lines changed

src/commands/penis.ts

+61-35
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { time, TimestampStyles, type User } from "discord.js";
33
import type { BotContext } from "@/context.js";
44
import type { MessageCommand } from "@/commands/command.js";
55
import type { ProcessableMessage } from "@/service/command.js";
6+
import type { Penis } from "@/storage/db/model.js";
67

78
import * as penis from "@/storage/penis.js";
89
import log from "@log";
9-
import { randomValue } from "@/service/random.js";
10+
import { NormalDistribution, RandomNumberGenerator, SecureRandomSource } from "@/service/random.js";
11+
import { clamp } from "@/utils/math.js";
1012

1113
export type Radius = 0 | 1 | 2 | 3;
1214

@@ -17,8 +19,8 @@ const RADIUS_CHARS: Record<Radius, string> = {
1719
3: "≡",
1820
};
1921

20-
const PENIS_MAX = 30;
21-
const RADIUS_MAX = 3;
22+
const PENIS_LENGTH_MAX = 30;
23+
const PENIS_RADIUS_MAX = 3;
2224

2325
const sendPenis = async (
2426
user: User,
@@ -28,11 +30,11 @@ const sendPenis = async (
2830
measurement: Date = new Date(),
2931
): Promise<void> => {
3032
const radiusChar = RADIUS_CHARS[radius];
31-
const penis = `8${radiusChar.repeat(size)}D`;
33+
const penis = `8${radiusChar.repeat(size | 0)}D`;
3234
const circumference = (Math.PI * radius * 2).toFixed(2);
3335

3436
await message.reply(
35-
`Pimmel von <@${user.id}>:\n${penis}\n(Länge: ${size} cm, Umfang: ${circumference} cm, Gemessen um ${time(measurement, TimestampStyles.LongDateTime)})`,
37+
`Pimmel von ${user}:\n${penis}\n(Länge: ${size.toFixed(2)} cm, Umfang: ${circumference} cm, Gemessen um ${time(measurement, TimestampStyles.LongDateTime)})`,
3638
);
3739
};
3840

@@ -41,6 +43,27 @@ const isNewLongestDick = async (size: number): Promise<boolean> => {
4143
return oldLongest < size;
4244
};
4345

46+
const randomSource = new SecureRandomSource();
47+
48+
const lengthDistribution = new NormalDistribution(
49+
14.65, // chatgpt: μ ≈ 14.5 to 14.8 cm
50+
1.85, // chatgpt: σ ≈ 1.7 to 2.0 cm
51+
);
52+
53+
/**
54+
* ChatGPT emits these values for circumference:
55+
* - μ ≈ 11.7 to 12.0 cm
56+
* - σ ≈ 1.0 cm (estimated via studies like Veale et al.)
57+
*
58+
* -> we use (11.7 cm + 12.0 cm)/2 = 11.85 cm as circumference
59+
* -> radius = circumference / (2*pi)
60+
*
61+
* We do the same for σ.
62+
*/
63+
const radiusDistribution = new NormalDistribution(11.85 / (Math.PI * 2), 1 / (Math.PI * 2));
64+
65+
const sizeGenerator = new RandomNumberGenerator(lengthDistribution, randomSource);
66+
const radiusGenerator = new RandomNumberGenerator(radiusDistribution, randomSource);
4467
/**
4568
* Penis command. Displays the users penis length
4669
*/
@@ -104,40 +127,43 @@ export default class PenisCommand implements MessageCommand {
104127
const userToMeasure = mention !== undefined ? mention : author;
105128

106129
log.debug(`${author.id} wants to measure penis of user ${userToMeasure.id}`);
107-
108-
const recentMeasurement = await penis.fetchRecentMeasurement(userToMeasure);
109-
110-
if (recentMeasurement === undefined) {
111-
log.debug(`No recent measuring of ${userToMeasure.id} found. Creating Measurement`);
112-
113-
const size =
114-
userToMeasure.id === context.client.user.id
115-
? PENIS_MAX
116-
: randomValue({ min: 1, maxInclusive: PENIS_MAX });
117-
const radius: Radius =
118-
userToMeasure.id === context.client.user.id
119-
? RADIUS_MAX
120-
: size === 0
121-
? (0 as Radius)
122-
: (randomValue({ min: 1, maxInclusive: RADIUS_MAX }) as Radius);
123-
124-
if (await isNewLongestDick(size)) {
125-
log.debug(`${userToMeasure} has the new longest dick with size ${size}`);
126-
}
127-
128-
await Promise.all([
129-
penis.insertMeasurement(userToMeasure, size, radius),
130-
sendPenis(userToMeasure, message, size, radius),
131-
]);
132-
return;
133-
}
130+
const measurement = await this.#getOrCreateMeasurement(
131+
userToMeasure,
132+
userToMeasure.id === context.client.user.id,
133+
);
134134

135135
await sendPenis(
136136
userToMeasure,
137137
message,
138-
recentMeasurement.size,
139-
recentMeasurement.radius,
140-
new Date(recentMeasurement.measuredAt),
138+
measurement.size,
139+
measurement.radius,
140+
new Date(measurement.measuredAt),
141141
);
142142
}
143+
144+
async #getOrCreateMeasurement(userToMeasure: User, hasLongest: boolean): Promise<Penis> {
145+
const recentMeasurement = await penis.fetchRecentMeasurement(userToMeasure);
146+
if (recentMeasurement !== undefined) {
147+
return recentMeasurement;
148+
}
149+
150+
log.debug(`No recent measuring of ${userToMeasure.id} found. Creating Measurement`);
151+
152+
const size = hasLongest
153+
? PENIS_LENGTH_MAX
154+
: clamp(sizeGenerator.get(), 1, PENIS_LENGTH_MAX); // TODO: Do we really want to clamp here? Maybe just clamp(v, 1, Infinity)?
155+
156+
const radiusRaw = hasLongest
157+
? PENIS_RADIUS_MAX
158+
: clamp(radiusGenerator.get(), 1, PENIS_RADIUS_MAX); // TODO: Do we really want to clamp here? Maybe just clamp(v, 1, Infinity)?
159+
160+
// TODO: Maye we want the radius to be integer only for display (and keep the float internally)
161+
const radius = clamp(Math.round(radiusRaw), 1, PENIS_RADIUS_MAX) as Radius;
162+
163+
if (await isNewLongestDick(size)) {
164+
log.debug(`${userToMeasure} has the new longest dick with size ${size}`);
165+
}
166+
167+
return await penis.insertMeasurement(userToMeasure, size, radius);
168+
}
143169
}

src/service/random.ts

+85
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,88 @@ export function randomEntryWeighted<T>(
6060

6161
throw new Error("No valid entry found");
6262
}
63+
64+
export interface RandomSource {
65+
getFloat(): number;
66+
}
67+
68+
export interface RandomDistribution {
69+
get(source: RandomSource): number;
70+
}
71+
72+
export class RandomNumberGenerator {
73+
#distribution: RandomDistribution;
74+
#source: RandomSource;
75+
76+
constructor(distribution: RandomDistribution, source: RandomSource) {
77+
this.#distribution = distribution;
78+
this.#source = source;
79+
}
80+
81+
get() {
82+
return this.#distribution.get(this.#source);
83+
}
84+
}
85+
86+
export class UnsafePseudoRandomSource implements RandomSource {
87+
getFloat(): number {
88+
return Math.random();
89+
}
90+
}
91+
export class SecureRandomSource implements RandomSource {
92+
readonly #shift = 2 ** -52;
93+
94+
getFloat(): number {
95+
// https://stackoverflow.com/a/13694869
96+
// The solutions from the other comments don't produce numbers in [0, 1)
97+
98+
const ints = new Uint32Array(2);
99+
crypto.getRandomValues(ints);
100+
101+
// keep all 32 bits of the the first, top 20 of the second for 52 random bits
102+
const mantissa = ints[0] * 0x100000 + (ints[1] >>> 12);
103+
104+
// shift all 52 bits to the right of the decimal point
105+
return mantissa * this.#shift;
106+
}
107+
}
108+
109+
export class UniformDistribution implements RandomDistribution {
110+
readonly min: number;
111+
readonly maxExclusive: number;
112+
113+
constructor(min: number, maxExclusive: number) {
114+
if (min > maxExclusive) {
115+
throw new Error("Invalid boundaries.");
116+
}
117+
118+
this.min = min;
119+
this.maxExclusive = maxExclusive;
120+
}
121+
122+
get(source: RandomSource): number {
123+
return this.min + (this.maxExclusive - this.min) * source.getFloat();
124+
}
125+
}
126+
127+
/** A.k.a gaussian distribution */
128+
export class NormalDistribution implements RandomDistribution {
129+
readonly mean: number;
130+
readonly standardDeviation: number;
131+
132+
/**
133+
* @param mean mean
134+
* @param standardDeviation standard deviation
135+
*/
136+
constructor(mean: number, standardDeviation: number) {
137+
this.mean = mean;
138+
this.standardDeviation = standardDeviation;
139+
}
140+
141+
get(source: RandomSource) {
142+
const u = 1 - source.getFloat(); // Converting [0,1) to (0,1]
143+
const v = source.getFloat();
144+
const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
145+
return z * this.standardDeviation + this.mean;
146+
}
147+
}

src/utils/math.ts

+13
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,16 @@ export class Vec4 {
144144
return this.divide(this.length());
145145
}
146146
}
147+
148+
/**
149+
* Once https://github.com/tc39/proposal-math-clamp is a thing, we can use that instead.
150+
*/
151+
export function clamp(number: number, minimum: number, maximum: number) {
152+
if (number < minimum) {
153+
return minimum;
154+
}
155+
if (number > maximum) {
156+
return maximum;
157+
}
158+
return number;
159+
}

0 commit comments

Comments
 (0)