@moneypot/hub 1.2.7 → 1.3.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/src/db/index.d.ts +2 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/pg-versions/005-outcome-bet.sql +56 -0
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +57 -0
- package/dist/src/plugins/hub-make-outcome-bet.js +325 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ServerOptions,
|
|
29
29
|
defaultPlugins,
|
|
30
30
|
startAndListen,
|
|
31
|
+
MakeOutcomeBetPlugin,
|
|
31
32
|
} from "@moneypot/hub";
|
|
32
33
|
import path from "path";
|
|
33
34
|
|
|
@@ -40,6 +41,9 @@ const options: ServerOptions = {
|
|
|
40
41
|
// These are required for the hub server to function
|
|
41
42
|
...defaultPlugins,
|
|
42
43
|
// Add your custom plugins here to extend server functionality
|
|
44
|
+
MakeOutcomeBetPlugin({
|
|
45
|
+
houseEdge: 0.01,
|
|
46
|
+
}),
|
|
43
47
|
],
|
|
44
48
|
|
|
45
49
|
// File path where the generated GraphQL schema definition will be saved
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./public.js";
|
|
7
7
|
export * from "./util.js";
|
|
8
8
|
export declare function getPgClient(connectionString: string): InstanceType<typeof pg.Client>;
|
|
9
|
-
export declare const postgraphilePool:
|
|
10
|
-
export declare const superuserPool:
|
|
9
|
+
export declare const postgraphilePool: pg.Pool;
|
|
10
|
+
export declare const superuserPool: pg.Pool;
|
|
11
11
|
export interface QueryExecutor {
|
|
12
12
|
query<T extends pg.QueryResultRow = any>(queryText: string, values?: any[]): Promise<pg.QueryResult<T>>;
|
|
13
13
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
2
|
import { Logger } from "./logger.js";
|
|
3
|
+
export { MakeOutcomeBetPlugin } from "./plugins/hub-make-outcome-bet.js";
|
|
3
4
|
export { defaultPlugins, type PluginContext, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
|
|
4
5
|
export type ServerOptions = {
|
|
5
6
|
configureApp?: (app: Express) => void;
|
package/dist/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHubServer } from "./server/index.js";
|
|
|
5
5
|
import { initializeTransferProcessors } from "./process-transfers.js";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { logger, setLogger } from "./logger.js";
|
|
8
|
+
export { MakeOutcomeBetPlugin } from "./plugins/hub-make-outcome-bet.js";
|
|
8
9
|
export { defaultPlugins, } from "./server/graphile.config.js";
|
|
9
10
|
async function initialize(options) {
|
|
10
11
|
if (options.logger) {
|
|
@@ -54,6 +55,9 @@ async function initialize(options) {
|
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
export async function startAndListen(options) {
|
|
58
|
+
if (options.plugins && options.plugins.some((p) => typeof p === "function")) {
|
|
59
|
+
throw new Error("`plugins` should be an array of GraphileConfig.Plugin but one of the items is a function. Did you forget to call it?");
|
|
60
|
+
}
|
|
57
61
|
if (options.userDatabaseMigrationsPath &&
|
|
58
62
|
!options.userDatabaseMigrationsPath.startsWith("/")) {
|
|
59
63
|
throw new Error(`userDatabaseMigrationsPath must be an absolute path, got ${options.userDatabaseMigrationsPath}`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
create type hub.outcome as (
|
|
2
|
+
weight float,
|
|
3
|
+
profit float
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
create table hub.outcome_bet (
|
|
7
|
+
id uuid primary key default hub_hidden.uuid_generate_v7(),
|
|
8
|
+
|
|
9
|
+
-- Operator-given kind like "WHEEL", "PLINKO",
|
|
10
|
+
-- Probably not worth using a postgres enum here since they are not flexible.
|
|
11
|
+
kind text not null,
|
|
12
|
+
|
|
13
|
+
user_id uuid not null references hub.user(id),
|
|
14
|
+
experience_id uuid not null references hub.experience(id),
|
|
15
|
+
casino_id uuid not null references hub.casino(id),
|
|
16
|
+
|
|
17
|
+
currency_key text not null,
|
|
18
|
+
wager float not null,
|
|
19
|
+
|
|
20
|
+
-- -1 = lose wager, -2 = lose 2x wager
|
|
21
|
+
-- 0.5 = 1.5x wager, 1 = 2x wager
|
|
22
|
+
-- In other words, a multiplier in a game might say "2.5x",
|
|
23
|
+
-- but the profit is multiplier-1 = 1.5 profit
|
|
24
|
+
profit float not null,
|
|
25
|
+
|
|
26
|
+
-- null when no outcomes saved
|
|
27
|
+
outcome_idx smallint null,
|
|
28
|
+
outcomes hub.outcome[] not null default '{}',
|
|
29
|
+
|
|
30
|
+
-- Operator-provided data per bet
|
|
31
|
+
metadata jsonb not null default '{}',
|
|
32
|
+
|
|
33
|
+
foreign key (currency_key, casino_id) references hub.currency(key, casino_id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
create index outcome_bet_user_id_idx on hub.outcome_bet(user_id);
|
|
37
|
+
create index outcome_bet_experience_id_idx on hub.outcome_bet(experience_id);
|
|
38
|
+
create index outcome_bet_casino_id_idx on hub.outcome_bet(casino_id);
|
|
39
|
+
create index outcome_bet_kind_idx on hub.outcome_bet(kind);
|
|
40
|
+
|
|
41
|
+
-- GRANT
|
|
42
|
+
|
|
43
|
+
grant select on hub.outcome_bet to app_postgraphile;
|
|
44
|
+
|
|
45
|
+
-- RLS
|
|
46
|
+
|
|
47
|
+
alter table hub.outcome_bet enable row level security;
|
|
48
|
+
|
|
49
|
+
create policy select_outcome_bet on hub.outcome_bet for select using (
|
|
50
|
+
hub_hidden.is_operator() or
|
|
51
|
+
(
|
|
52
|
+
user_id = hub_hidden.current_user_id() and
|
|
53
|
+
experience_id = hub_hidden.current_experience_id() and
|
|
54
|
+
casino_id = hub_hidden.current_casino_id()
|
|
55
|
+
)
|
|
56
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const InputSchema: z.ZodObject<{
|
|
3
|
+
kind: z.ZodString;
|
|
4
|
+
wager: z.ZodNumber;
|
|
5
|
+
currency: z.ZodString;
|
|
6
|
+
outcomes: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodObject<{
|
|
7
|
+
weight: z.ZodNumber;
|
|
8
|
+
profit: z.ZodNumber;
|
|
9
|
+
}, "strict", z.ZodTypeAny, {
|
|
10
|
+
weight: number;
|
|
11
|
+
profit: number;
|
|
12
|
+
}, {
|
|
13
|
+
weight: number;
|
|
14
|
+
profit: number;
|
|
15
|
+
}>, "many">, {
|
|
16
|
+
weight: number;
|
|
17
|
+
profit: number;
|
|
18
|
+
}[], {
|
|
19
|
+
weight: number;
|
|
20
|
+
profit: number;
|
|
21
|
+
}[]>, {
|
|
22
|
+
weight: number;
|
|
23
|
+
profit: number;
|
|
24
|
+
}[], {
|
|
25
|
+
weight: number;
|
|
26
|
+
profit: number;
|
|
27
|
+
}[]>;
|
|
28
|
+
}, "strict", z.ZodTypeAny, {
|
|
29
|
+
currency: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
wager: number;
|
|
32
|
+
outcomes: {
|
|
33
|
+
weight: number;
|
|
34
|
+
profit: number;
|
|
35
|
+
}[];
|
|
36
|
+
}, {
|
|
37
|
+
currency: string;
|
|
38
|
+
kind: string;
|
|
39
|
+
wager: number;
|
|
40
|
+
outcomes: {
|
|
41
|
+
weight: number;
|
|
42
|
+
profit: number;
|
|
43
|
+
}[];
|
|
44
|
+
}>;
|
|
45
|
+
type Input = z.infer<typeof InputSchema>;
|
|
46
|
+
type BetConfig = {
|
|
47
|
+
houseEdge: number;
|
|
48
|
+
saveOutcomes: boolean;
|
|
49
|
+
getMetadata?: (input: Input) => Promise<Record<string, any>>;
|
|
50
|
+
};
|
|
51
|
+
export type BetConfigMap<BetKind extends string> = {
|
|
52
|
+
[betKind in BetKind]: BetConfig;
|
|
53
|
+
};
|
|
54
|
+
export declare function MakeOutcomeBetPlugin<BetKind extends string>({ betConfigs }: {
|
|
55
|
+
betConfigs: BetConfigMap<BetKind>;
|
|
56
|
+
}): GraphileConfig.Plugin;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { context, object, sideEffect } from "postgraphile/grafast";
|
|
2
|
+
import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GraphQLError } from "graphql";
|
|
5
|
+
import { exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
|
|
6
|
+
import { assert } from "tsafe";
|
|
7
|
+
const FLOAT_EPSILON = 1e-10;
|
|
8
|
+
function sum(ns) {
|
|
9
|
+
return ns.reduce((a, b) => a + b, 0);
|
|
10
|
+
}
|
|
11
|
+
function calculatePlayerEV(outcomes) {
|
|
12
|
+
const totalWeight = sum(outcomes.map((o) => o.weight));
|
|
13
|
+
return sum(outcomes.map((o) => (o.weight / totalWeight) * o.profit));
|
|
14
|
+
}
|
|
15
|
+
function calculateHouseEV(outcomes) {
|
|
16
|
+
return -calculatePlayerEV(outcomes);
|
|
17
|
+
}
|
|
18
|
+
const OutcomeSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
weight: z.number().gt(0, `Outcome weight must be > 0`),
|
|
21
|
+
profit: z.number(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
const InputSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
kind: z.string(),
|
|
27
|
+
wager: z
|
|
28
|
+
.number()
|
|
29
|
+
.int("Wager must be an integer")
|
|
30
|
+
.gte(1, "Wager must be >= 1"),
|
|
31
|
+
currency: z.string(),
|
|
32
|
+
outcomes: z
|
|
33
|
+
.array(OutcomeSchema)
|
|
34
|
+
.min(2, "Outcome count must be >= 2")
|
|
35
|
+
.max(50, "Outcome count must be <= 50")
|
|
36
|
+
.refine((data) => data.some((o) => o.profit < 0), "At least one outcome should have profit < 0")
|
|
37
|
+
.refine((data) => data.some((o) => o.profit > 0), "At least one outcome should have profit > 0"),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const BetKindSchema = z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1, "Bet kind must be at least 1 character")
|
|
43
|
+
.regex(/^[A-Z_]+$/, "Bet kind must only contain A-Z and underscores")
|
|
44
|
+
.regex(/^[A-Z]/, "Bet kind must start with a letter A-Z");
|
|
45
|
+
const BetConfigsSchema = z.record(BetKindSchema, z.object({
|
|
46
|
+
houseEdge: z
|
|
47
|
+
.number()
|
|
48
|
+
.gte(0, "House edge must be >= 0")
|
|
49
|
+
.lte(1, "House edge must be <= 1"),
|
|
50
|
+
saveOutcomes: z.boolean(),
|
|
51
|
+
getMetadata: z
|
|
52
|
+
.function()
|
|
53
|
+
.args(InputSchema)
|
|
54
|
+
.returns(z.record(z.string(), z.any()))
|
|
55
|
+
.optional(),
|
|
56
|
+
}));
|
|
57
|
+
export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
58
|
+
BetConfigsSchema.parse(betConfigs);
|
|
59
|
+
const betKinds = Object.keys(betConfigs);
|
|
60
|
+
return makeExtendSchemaPlugin((build) => {
|
|
61
|
+
const outcomeBetTable = build.input.pgRegistry.pgResources.hub_outcome_bet;
|
|
62
|
+
const typeDefs = gql `
|
|
63
|
+
enum BetKind {
|
|
64
|
+
${betKinds.join("\n")}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
input HubMakeOutcomeBetInput {
|
|
68
|
+
kind: BetKind!
|
|
69
|
+
wager: Int!
|
|
70
|
+
currency: String!
|
|
71
|
+
outcomes: [HubOutcomeInput!]!
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type HubMakeOutcomeBetPayload {
|
|
75
|
+
bet: HubOutcomeBet!
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
extend type Mutation {
|
|
79
|
+
hubMakeOutcomeBet(input: HubMakeOutcomeBetInput!): HubMakeOutcomeBetPayload
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
return {
|
|
83
|
+
typeDefs,
|
|
84
|
+
plans: {
|
|
85
|
+
Mutation: {
|
|
86
|
+
hubMakeOutcomeBet: (_, { $input }) => {
|
|
87
|
+
const $identity = context().get("identity");
|
|
88
|
+
const $betId = sideEffect([$identity, $input], async ([identity, rawInput]) => {
|
|
89
|
+
if (identity?.kind !== "user") {
|
|
90
|
+
throw new GraphQLError("Unauthorized");
|
|
91
|
+
}
|
|
92
|
+
let input;
|
|
93
|
+
let betKind;
|
|
94
|
+
try {
|
|
95
|
+
input = InputSchema.parse(rawInput);
|
|
96
|
+
betKind = BetKindSchema.parse(rawInput.kind);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (e instanceof z.ZodError) {
|
|
100
|
+
throw new GraphQLError(e.errors[0].message);
|
|
101
|
+
}
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
const betConfig = betConfigs[betKind];
|
|
105
|
+
if (!betConfig) {
|
|
106
|
+
throw new GraphQLError(`Invalid bet kind`);
|
|
107
|
+
}
|
|
108
|
+
if (!betKinds.includes(rawInput.kind)) {
|
|
109
|
+
throw new GraphQLError(`Invalid bet kind`);
|
|
110
|
+
}
|
|
111
|
+
const houseEV = calculateHouseEV(input.outcomes);
|
|
112
|
+
const minHouseEV = Math.max(0, betConfig.houseEdge - FLOAT_EPSILON);
|
|
113
|
+
if (houseEV < minHouseEV) {
|
|
114
|
+
throw new GraphQLError(`No deal. House EV too low: ${houseEV.toFixed(4)}`);
|
|
115
|
+
}
|
|
116
|
+
const { session } = identity;
|
|
117
|
+
const dbCurrency = await superuserPool
|
|
118
|
+
.query({
|
|
119
|
+
text: `
|
|
120
|
+
SELECT key
|
|
121
|
+
FROM hub.currency
|
|
122
|
+
WHERE key = $1
|
|
123
|
+
AND casino_id = $2
|
|
124
|
+
`,
|
|
125
|
+
values: [input.currency, session.casino_id],
|
|
126
|
+
})
|
|
127
|
+
.then(maybeOneRow);
|
|
128
|
+
if (!dbCurrency) {
|
|
129
|
+
throw new GraphQLError("Currency not found");
|
|
130
|
+
}
|
|
131
|
+
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
132
|
+
const { dbPlayerBalance, dbHouseBankroll, found } = await dbLockBalanceAndBankroll(pgClient, {
|
|
133
|
+
userId: session.user_id,
|
|
134
|
+
casinoId: session.casino_id,
|
|
135
|
+
experienceId: session.experience_id,
|
|
136
|
+
currencyKey: dbCurrency.key,
|
|
137
|
+
});
|
|
138
|
+
if (!found) {
|
|
139
|
+
throw new GraphQLError("No balance entry found for player or house");
|
|
140
|
+
}
|
|
141
|
+
const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
|
|
142
|
+
const maxPlayerLoss = Math.abs(input.wager * minProfit);
|
|
143
|
+
console.log("Determining if player can afford bet", {
|
|
144
|
+
wager: input.wager,
|
|
145
|
+
minProfit,
|
|
146
|
+
maxPlayerLoss,
|
|
147
|
+
dbPlayerBalance,
|
|
148
|
+
});
|
|
149
|
+
if (dbPlayerBalance < maxPlayerLoss) {
|
|
150
|
+
if (minProfit === -1) {
|
|
151
|
+
throw new GraphQLError(`You cannot afford wager`);
|
|
152
|
+
}
|
|
153
|
+
throw new GraphQLError("You cannot afford the worst outcome");
|
|
154
|
+
}
|
|
155
|
+
const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
|
|
156
|
+
const maxPotentialPayout = input.wager * maxProfitMultiplier;
|
|
157
|
+
const maxAllowablePayout = dbHouseBankroll * 0.01;
|
|
158
|
+
if (maxPotentialPayout > maxAllowablePayout) {
|
|
159
|
+
throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
|
|
160
|
+
}
|
|
161
|
+
const { outcome, outcomeIdx } = pickRandomOutcome(input.outcomes);
|
|
162
|
+
const netPlayerAmount = input.wager * outcome.profit;
|
|
163
|
+
await pgClient.query({
|
|
164
|
+
text: `
|
|
165
|
+
UPDATE hub.balance
|
|
166
|
+
SET amount = amount + $1
|
|
167
|
+
WHERE user_id = $2
|
|
168
|
+
AND casino_id = $3
|
|
169
|
+
AND experience_id = $4
|
|
170
|
+
AND currency_key = $5
|
|
171
|
+
`,
|
|
172
|
+
values: [
|
|
173
|
+
netPlayerAmount,
|
|
174
|
+
session.user_id,
|
|
175
|
+
session.casino_id,
|
|
176
|
+
session.experience_id,
|
|
177
|
+
dbCurrency.key,
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
await pgClient.query({
|
|
181
|
+
text: `
|
|
182
|
+
UPDATE hub.bankroll
|
|
183
|
+
SET amount = amount - $1,
|
|
184
|
+
bets = bets + 1,
|
|
185
|
+
wagered = wagered + $4
|
|
186
|
+
WHERE currency_key = $2
|
|
187
|
+
AND casino_id = $3
|
|
188
|
+
`,
|
|
189
|
+
values: [
|
|
190
|
+
netPlayerAmount,
|
|
191
|
+
dbCurrency.key,
|
|
192
|
+
session.casino_id,
|
|
193
|
+
input.wager,
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
const newBet = {
|
|
197
|
+
kind: rawInput.kind,
|
|
198
|
+
wager: input.wager,
|
|
199
|
+
profit: outcome.profit,
|
|
200
|
+
currency_key: dbCurrency.key,
|
|
201
|
+
user_id: session.user_id,
|
|
202
|
+
casino_id: session.casino_id,
|
|
203
|
+
experience_id: session.experience_id,
|
|
204
|
+
metadata: betConfig.getMetadata
|
|
205
|
+
? await betConfig.getMetadata(input)
|
|
206
|
+
: {},
|
|
207
|
+
...(betConfig.saveOutcomes
|
|
208
|
+
? {
|
|
209
|
+
outcomes: input.outcomes,
|
|
210
|
+
outcome_idx: outcomeIdx,
|
|
211
|
+
}
|
|
212
|
+
: {
|
|
213
|
+
outcomes: [],
|
|
214
|
+
outcome_idx: null,
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
const bet = await pgClient
|
|
218
|
+
.query({
|
|
219
|
+
text: `
|
|
220
|
+
INSERT INTO hub.outcome_bet (
|
|
221
|
+
user_id,
|
|
222
|
+
casino_id,
|
|
223
|
+
experience_id,
|
|
224
|
+
kind,
|
|
225
|
+
currency_key,
|
|
226
|
+
wager,
|
|
227
|
+
profit,
|
|
228
|
+
outcomes,
|
|
229
|
+
outcome_idx,
|
|
230
|
+
metadata
|
|
231
|
+
)
|
|
232
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
233
|
+
RETURNING id
|
|
234
|
+
`,
|
|
235
|
+
values: [
|
|
236
|
+
newBet.user_id,
|
|
237
|
+
newBet.casino_id,
|
|
238
|
+
newBet.experience_id,
|
|
239
|
+
newBet.kind,
|
|
240
|
+
newBet.currency_key,
|
|
241
|
+
newBet.wager,
|
|
242
|
+
newBet.profit,
|
|
243
|
+
newBet.outcomes.map((o) => `(${o.weight},${o.profit})`),
|
|
244
|
+
newBet.outcome_idx,
|
|
245
|
+
newBet.metadata,
|
|
246
|
+
],
|
|
247
|
+
})
|
|
248
|
+
.then(exactlyOneRow);
|
|
249
|
+
return bet.id;
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
return object({
|
|
253
|
+
bet: outcomeBetTable.get({ id: $betId }),
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}, "HubMakeOutcomeBetPlugin");
|
|
260
|
+
}
|
|
261
|
+
function generateRandomNumber() {
|
|
262
|
+
const array = new Uint32Array(1);
|
|
263
|
+
crypto.getRandomValues(array);
|
|
264
|
+
return array[0] / 2 ** 32;
|
|
265
|
+
}
|
|
266
|
+
function pickRandomOutcome(outcomes) {
|
|
267
|
+
assert(outcomes.length >= 2, "Outcome count must be >= 2");
|
|
268
|
+
const totalWeight = sum(outcomes.map((o) => o.weight));
|
|
269
|
+
const outcomesWithProbability = outcomes.map((o) => ({
|
|
270
|
+
...o,
|
|
271
|
+
probability: o.weight / totalWeight,
|
|
272
|
+
}));
|
|
273
|
+
const totalProb = outcomesWithProbability.reduce((sum, outcome) => sum + outcome.probability, 0);
|
|
274
|
+
assert(Math.abs(totalProb - 1.0) < FLOAT_EPSILON, "Probabilities must sum to ~1");
|
|
275
|
+
const randomValue = generateRandomNumber();
|
|
276
|
+
let cumProb = 0;
|
|
277
|
+
for (let i = 0; i < outcomesWithProbability.length; i++) {
|
|
278
|
+
const outcome = outcomesWithProbability[i];
|
|
279
|
+
cumProb += outcome.probability;
|
|
280
|
+
if (randomValue <= cumProb) {
|
|
281
|
+
return { outcome, outcomeIdx: i };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
outcome: outcomes[outcomes.length - 1],
|
|
286
|
+
outcomeIdx: outcomes.length - 1,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function dbLockBalanceAndBankroll(pgClient, { userId, casinoId, experienceId, currencyKey, }) {
|
|
290
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
291
|
+
const row = await pgClient
|
|
292
|
+
.query(`
|
|
293
|
+
WITH locked_balance AS (
|
|
294
|
+
SELECT amount
|
|
295
|
+
FROM hub.balance
|
|
296
|
+
WHERE user_id = $1
|
|
297
|
+
AND casino_id = $2
|
|
298
|
+
AND experience_id = $3
|
|
299
|
+
AND currency_key = $4
|
|
300
|
+
|
|
301
|
+
FOR UPDATE
|
|
302
|
+
)
|
|
303
|
+
SELECT
|
|
304
|
+
pb.amount as player_balance,
|
|
305
|
+
br.amount as house_bankroll
|
|
306
|
+
FROM locked_balance pb
|
|
307
|
+
JOIN hub.bankroll br
|
|
308
|
+
ON br.casino_id = $2
|
|
309
|
+
AND br.currency_key = $4
|
|
310
|
+
|
|
311
|
+
FOR UPDATE OF br
|
|
312
|
+
`, [userId, casinoId, experienceId, currencyKey])
|
|
313
|
+
.then(maybeOneRow);
|
|
314
|
+
return row
|
|
315
|
+
? {
|
|
316
|
+
found: true,
|
|
317
|
+
dbPlayerBalance: row.player_balance,
|
|
318
|
+
dbHouseBankroll: row.house_bankroll,
|
|
319
|
+
}
|
|
320
|
+
: {
|
|
321
|
+
found: false,
|
|
322
|
+
dbPlayerBalance: null,
|
|
323
|
+
dbHouseBankroll: null,
|
|
324
|
+
};
|
|
325
|
+
}
|