A virtual currency betting bot for Twitch chat.
https://ddark.net/better
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
7.3 KiB
221 lines
7.3 KiB
#pragma warning (disable: 4996) // This function or variable may be unsafe (strcpy, sprintf, ...)
|
|
#pragma warning (disable: 4200) // nonstandard extension used: zero-sized array in struct/union
|
|
|
|
#include <cstdio>
|
|
#include <stb_ds.h>
|
|
|
|
#include "better_alloc.h"
|
|
#include "better_App.h"
|
|
#include "better_bets.h"
|
|
#include "better_func.h"
|
|
#include "better_irc.h"
|
|
|
|
u8 bets_status(App* app)
|
|
{
|
|
if (app->timer_left > 0.0f)
|
|
return BETS_STATUS_OPEN;
|
|
if (app->timer_left > - (f32)app->settings.coyote_time)
|
|
return BETS_STATUS_COYOTE;
|
|
return BETS_STATUS_CLOSED;
|
|
}
|
|
|
|
// betting_on_option is 0-indexed
|
|
u64 available_points(App* app, const char* name, i32 betting_on_option)
|
|
{
|
|
assert(betting_on_option >= 0 && betting_on_option < arrlen(app->bet_options));
|
|
|
|
UserInfo* user = shget(app->users, name);
|
|
if (!user) return 0;
|
|
u64 res = user->points;
|
|
|
|
if (app->settings.add_mode)
|
|
{
|
|
// Subtract wager on this option.
|
|
auto bet_map = app->bet_options[betting_on_option].bets;
|
|
auto bet = shgeti(bet_map, name);
|
|
if (bet != -1)
|
|
{
|
|
if (bet_map[bet].value > res)
|
|
res = 0;
|
|
else
|
|
res -= bet_map[bet].value;
|
|
}
|
|
}
|
|
|
|
if (app->settings.allow_multibets)
|
|
{
|
|
// Subtract wagers on other options.
|
|
for (int i = 0; i < arrlen(app->bet_options); ++i)
|
|
{
|
|
if (i == betting_on_option)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto bet_map = app->bet_options[i].bets;
|
|
auto bet = shgeti(bet_map, name);
|
|
if (bet != -1)
|
|
{
|
|
if (bet_map[bet].value > res)
|
|
res = 0;
|
|
else
|
|
res -= bet_map[bet].value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void register_max_bet(App* app, const char* user, i32 option)
|
|
{
|
|
if (option < 0 || option >= arrlen(app->bet_options)) return;
|
|
|
|
u64 amount = available_points(app, user, option);
|
|
if (amount > 0) register_bet(app, user, amount, option);
|
|
}
|
|
|
|
void register_bet(App* app, const char* user, i64 amount, i32 option)
|
|
{
|
|
if (option < 0 || option >= arrlen(app->bet_options)) return;
|
|
|
|
u64 avail = available_points(app, user, option);
|
|
if (amount > 0 && avail < (u64)amount)
|
|
{
|
|
amount = avail;
|
|
}
|
|
|
|
// If multibets are not allowed, remove wagers on other options
|
|
if (!app->settings.allow_multibets)
|
|
{
|
|
for (int i = 0; i < arrlen(app->bet_options); ++i)
|
|
if (i != option) shdel(app->bet_options[i].bets, user);
|
|
}
|
|
|
|
if (app->settings.add_mode)
|
|
{
|
|
u64 current_amount = shget(app->bet_options[option].bets, user);
|
|
i64 new_amount = current_amount + amount;
|
|
if (new_amount <= 0)
|
|
shdel(app->bet_options[option].bets, user);
|
|
else
|
|
shput(app->bet_options[option].bets, user, (u64)new_amount);
|
|
}
|
|
else
|
|
{
|
|
if (amount <= 0)
|
|
shdel(app->bet_options[option].bets, user);
|
|
else
|
|
shput(app->bet_options[option].bets, user, (u64)amount);
|
|
}
|
|
}
|
|
|
|
void open_bets(App* app)
|
|
{
|
|
if (arrlen(app->bet_options) == 0)
|
|
{
|
|
add_log(app, LOGLEVEL_USERERROR, "Cannot open bets with no options.");
|
|
return;
|
|
}
|
|
|
|
add_log(app, LOGLEVEL_INFO, "Opening bets for the next %i+%u seconds.", app->settings.timer_setting, app->settings.coyote_time);
|
|
|
|
app->timer_left = (f32) app->settings.timer_setting;
|
|
|
|
app->fish = app->shark = NULL;
|
|
app->fish_loss = app->shark_win = 0;
|
|
|
|
if (app->settings.announce_bets_open)
|
|
{
|
|
#define MSG_LEN (200 + CHANNEL_NAME_MAX + 2*POINTS_NAME_MAX + OPTION_NAME_MAX)
|
|
static_assert(MSG_LEN < 512);
|
|
char* buf = (char*) BETTER_ALLOC(MSG_LEN, "writebuf_announcement_open_bets");
|
|
sprintf(buf, "PRIVMSG #%s :Bets are now OPEN! Type %s%s to see how many %s you have, and %sbet (amount) (option) to place a bet. E.g. \"%sbet 100 %s\" or \"%sbet all %llu\"\r\n", app->settings.channel, app->settings.command_prefix, app->settings.points_name, app->settings.points_name, app->settings.command_prefix, app->settings.command_prefix, app->bet_options[0].option_name, app->settings.command_prefix, arrlen(app->bet_options));
|
|
irc_queue_write(app, buf, true);
|
|
}
|
|
}
|
|
|
|
void close_bets(App* app)
|
|
{
|
|
add_log(app, LOGLEVEL_INFO, "Closing bets.");
|
|
|
|
app->timer_left = -INFINITY;
|
|
|
|
if (app->settings.announce_bets_close)
|
|
{
|
|
char* buf = (char*) BETTER_ALLOC(CHANNEL_NAME_MAX+50, "writebuf_announcement_close_bets");
|
|
sprintf(buf, "PRIVMSG #%s :Bets are now CLOSED!\r\n", app->settings.channel);
|
|
irc_queue_write(app, buf, true);
|
|
}
|
|
}
|
|
|
|
void reset_bets(App* app)
|
|
{
|
|
add_log(app, LOGLEVEL_INFO, "Resetting bets.");
|
|
for (int i = 0; i < arrlen(app->bet_options); ++i)
|
|
BetTable_reset_bets(&app->bet_options[i]);
|
|
}
|
|
|
|
// NOTE: winning_option is 0-indexed
|
|
void do_payout(App* app, i32 winning_option, f64 table_total, f64 grand_total)
|
|
{
|
|
add_log(app, LOGLEVEL_INFO, "Collecting bets. Payout for %i betters on option %i (%s).", shlen(app->bet_options[winning_option].bets), winning_option+1, app->bet_options[winning_option].option_name);
|
|
|
|
i32 better_count = 0;
|
|
BETTER_ASSERT(winning_option >= 0 && winning_option < arrlen(app->bet_options) && "winning option is outside the range of existing options");
|
|
for (int option_idx = 0; option_idx < arrlen(app->bet_options); ++option_idx)
|
|
{
|
|
auto bet_map = app->bet_options[option_idx].bets;
|
|
for (int bet_idx = 0; bet_idx < shlen(bet_map); ++bet_idx)
|
|
{
|
|
++better_count;
|
|
|
|
UserInfo* user = shget(app->users, bet_map[bet_idx].key);
|
|
if (!user)
|
|
{
|
|
add_log(app, LOGLEVEL_DEVERROR, "User from bet doesn't exist in table: %s", bet_map[bet_idx].key);
|
|
continue;
|
|
}
|
|
|
|
if (bet_map[bet_idx].value > user->points)
|
|
{
|
|
add_log(app, LOGLEVEL_DEVERROR, "User's (%s) bet (%llu) was higher than their points (%llu)", bet_map[bet_idx].key, bet_map[bet_idx].value, user->points);
|
|
user->points = 0;
|
|
}
|
|
else
|
|
{
|
|
user->points -= bet_map[bet_idx].value;
|
|
}
|
|
|
|
if (option_idx == winning_option)
|
|
{
|
|
u64 win = BETTER_CLAMP((u64)((grand_total*bet_map[bet_idx].value)/table_total), 0, POINTS_MAX);
|
|
user->points = BETTER_CLAMP(user->points + win, 0, POINTS_MAX);
|
|
|
|
if (!app->shark || win - bet_map[bet_idx].value > app->shark_win)
|
|
{
|
|
app->shark = user;
|
|
app->shark_win = win - bet_map[bet_idx].value;
|
|
}
|
|
}
|
|
else if (!app->fish || bet_map[bet_idx].value > app->fish_loss)
|
|
{
|
|
app->fish = user;
|
|
app->fish_loss = bet_map[bet_idx].value;
|
|
}
|
|
}
|
|
}
|
|
|
|
rebuild_leaderboard(app);
|
|
|
|
if (app->settings.announce_payout && better_count > 0)
|
|
{
|
|
char* buf = (char*) BETTER_ALLOC(100 + CHANNEL_NAME_MAX + OPTION_NAME_MAX, "writebuf_announcement_payout");
|
|
sprintf(buf, "PRIVMSG #%s :Option %i (%s) wins! Bets have been collected, and rewards paid out.\r\n", app->settings.channel, winning_option+1, app->bet_options[winning_option].option_name);
|
|
irc_queue_write(app, buf, true);
|
|
}
|
|
|
|
save_leaderboard_to_disk(app);
|
|
|
|
reset_bets(app);
|
|
}
|
|
|