forked from manifoldmarkets/manifold
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathloot-box.ts
132 lines (110 loc) · 3.26 KB
/
loot-box.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { clamp, shuffle } from 'lodash'
import { BinaryContract } from './contract'
import { User } from './user'
import { Bet } from './bet'
import { noFees } from './fees'
import { getProbability } from './calculate'
export const LOOTBOX_COST = 100
export const LOOTBOX_MAX = 1000
const LOOTBOX_MIN = 50
export interface LootBoxItem {
contract: BinaryContract
outcome: 'YES' | 'NO'
amount: number
shares: number
}
export type LootBox = LootBoxItem[]
export const createLootBox = (contracts: BinaryContract[]): LootBox => {
const boxValue = getBoxValue()
const n = Math.ceil(Math.random() * 4)
const selectedContracts = shuffle(contracts).slice(0, n)
const weights = generateWeights(selectedContracts.length)
const box = selectedContracts.map((contract, i) => {
const outcome: 'YES' | 'NO' = Math.random() > 0.5 ? 'YES' : 'NO'
const amount = Math.round(weights[i] * boxValue)
const prob = getProbability(contract)
const shares = outcome === 'YES' ? amount / prob : amount / (1 - prob)
return { contract, outcome, amount, shares }
})
return box
}
const getBoxValue = () => {
return Math.random() > 0.5 ? winDistribution() : loseDistribution()
}
const winDistribution = () =>
clamp(
Math.round(
LOOTBOX_COST + customLogNormalSample(20, LOOTBOX_MAX - LOOTBOX_COST)
),
LOOTBOX_COST,
LOOTBOX_MAX
)
const loseDistribution = () =>
clamp(
Math.round(normalSample(LOOTBOX_MIN + 5, 10)),
LOOTBOX_MIN,
0.7 * LOOTBOX_COST
)
export const lootBoxExpectation = () => {
let e = 0
for (let i = 0; i < 1e6; i++) e += getBoxValue()
return e / 1e6
}
function normalSample(mean = 0, stdev = 1) {
const u = 1 - Math.random() // Converting [0,1) to (0,1]
const v = Math.random()
const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v)
return z * stdev + mean
}
function logNormalSample(mu: number, sigma: number) {
const u1 = Math.random()
const u2 = Math.random()
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2)
return Math.exp(mu + sigma * z0)
}
function customLogNormalSample(mean: number, targetMax: number) {
const mu = Math.log(mean) - 0.5 * Math.log(1 + (targetMax - mean) / mean)
const sigma = Math.sqrt(Math.log(1 + (targetMax - mean) / mean))
return logNormalSample(mu, sigma)
}
function generateWeights(n: number) {
const randomProbabilities = new Array(n)
let remainingProb = 1
for (let i = 0; i < n - 1; i++) {
randomProbabilities[i] =
remainingProb * clamp(Math.random(), 1 / (2 * n), 2 / n)
remainingProb -= randomProbabilities[i]
}
randomProbabilities[n - 1] = remainingProb
return shuffle(randomProbabilities)
}
export const createLootBet = (
user: User,
contract: BinaryContract,
outcome: 'YES' | 'NO',
prob: number,
amount: number,
shares: number
): Omit<Bet, 'id'> => {
return {
createdTime: Date.now(),
userId: user.id,
userAvatarUrl: user.avatarUrl,
userUsername: user.username,
userName: user.name,
amount: amount,
shares,
isFilled: true,
isCancelled: false,
contractId: contract.id,
outcome,
probBefore: prob,
probAfter: prob,
loanAmount: 0,
fees: noFees,
isAnte: false,
isRedemption: false,
isChallenge: true,
visibility: contract.visibility,
}
}