Adds a sports betting/polling app (#90421)

## About The Pull Request

Adds a new PDA app that allows you to create polls that people can bet
on with credits, the owner can then lock the bets, decide what the
answer is (up to the player for whatever poll they made), and send it
off, giving the money the losers bet into the accounts of the winners.

It's a small PDA app that currently doesn't make any announcements or
come preinstalled in anything, but that's subject to change. PDA screen
sprite is codersprited.

Video demonstration


https://github.com/user-attachments/assets/449e1f0b-7fd3-4948-bff8-2793af831360

Not shown (as I added later), it now uses newscasters as well.


![image](https://github.com/user-attachments/assets/d3defa60-03cc-4557-98b7-b4088b158b3d)

###### Code bounty of
https://forums.tgstation13.org/viewtopic.php?t=38278

## Why It's Good For The Game

Allows people to host polls for station events, allowing for the SS13
version of sports betting.

## Changelog
🆑
add: Adds a new sports betting app on your PDA, you can now host and
vote on polls using in-game credits.
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
John Willard
2025-04-10 00:27:28 -04:00
committed by GitHub
parent eb42c34497
commit b25d7cb2cb
10 changed files with 573 additions and 14 deletions

View File

@@ -5,3 +5,7 @@
#define ANNOUNCEMENT_TYPE_CAPTAIN "Captain"
/// Make it sound like it's coming from the Syndicate
#define ANNOUNCEMENT_TYPE_SYNDICATE "Syndicate"
//Defines for newscaster news stations, the defined thing is what it'll be called in the Newscaster.
#define NEWSCASTER_STATION_ANNOUNCEMENTS "Station Announcements"
#define NEWSCASTER_SPACE_BETTING "SpaceBet"

View File

@@ -63,7 +63,7 @@
header += SUBHEADER_ANNOUNCEMENT_TITLE(title)
if(ANNOUNCEMENT_TYPE_CAPTAIN)
header = MAJOR_ANNOUNCEMENT_TITLE("Captain's Announcement")
GLOB.news_network.submit_article(text, "Captain's Announcement", "Station Announcements", null)
GLOB.news_network.submit_article(text, "Captain's Announcement", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
if(ANNOUNCEMENT_TYPE_SYNDICATE)
header = MAJOR_ANNOUNCEMENT_TITLE("Syndicate Captain's Announcement")
else
@@ -87,9 +87,9 @@
if(isnull(sender_override) && players == GLOB.player_list)
if(length(title) > 0)
GLOB.news_network.submit_article(title + "<br><br>" + text, "[command_name()]", "Station Announcements", null)
GLOB.news_network.submit_article(title + "<br><br>" + text, "[command_name()]", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
else
GLOB.news_network.submit_article(text, "[command_name()] Update", "Station Announcements", null)
GLOB.news_network.submit_article(text, "[command_name()] Update", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
/proc/print_command_report(text = "", title = null, announce=TRUE)
if(!title)

View File

@@ -187,7 +187,7 @@ SUBSYSTEM_DEF(economy)
update_alerts = TRUE
inflict_moneybags(moneybags)
earning_report += "That's all from the <i>Nanotrasen Economist Division</i>."
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", "Station Announcements", null, update_alert = update_alerts)
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", NEWSCASTER_STATION_ANNOUNCEMENTS, null, update_alert = update_alerts)
return TRUE
/**

View File

@@ -185,7 +185,8 @@ GLOBAL_LIST_EMPTY(request_list)
var/message_count = 0
/datum/feed_network/New()
create_feed_channel("Station Announcements", "SS13", "Company news, staff announcements, and all the latest information. Have a secure shift!", locked = TRUE, hardset_channel = 1000)
create_feed_channel(NEWSCASTER_STATION_ANNOUNCEMENTS, "SS13", "Company news, staff announcements, and all the latest information. Have a secure shift!", locked = TRUE, hardset_channel = 1000)
create_feed_channel(NEWSCASTER_SPACE_BETTING, "NtOS", "News from the SpaceBet PDA App! Download now and make your own bets!", locked = TRUE, hardset_channel = 1001)
wanted_issue = new /datum/wanted_message
/datum/feed_network/proc/create_feed_channel(channel_name, author, desc, locked, adminChannel = FALSE, hardset_channel = null, author_ckey = null, cross_sector = FALSE, cross_sector_delay = null, receiving_cross_sector = FALSE)
@@ -236,7 +237,6 @@ GLOBAL_LIST_EMPTY(request_list)
newMsg.parent_ID = channel.channel_ID
if (!channel.cross_sector)
break
// Newscaster articles could be huge, and usefulness of first 50 symbols is dubious
message_admins(span_adminnotice("Outgoing cross-sector newscaster article by [key_name(author_mob) || author] in channel [channel_name]."))
var/list/payload = list(
@@ -249,6 +249,18 @@ GLOBAL_LIST_EMPTY(request_list)
for(var/obj/machinery/newscaster/caster in GLOB.allCasters)
caster.news_alert(channel_name, update_alert)
return newMsg
///Submits a comment on the news network
/datum/feed_network/proc/submit_comment(mob/user, comment_text, newscaster_username, datum/feed_message/current_message)
var/datum/feed_comment/new_feed_comment = new/datum/feed_comment
new_feed_comment.author = newscaster_username
new_feed_comment.body = comment_text
new_feed_comment.time_stamp = station_time_timestamp()
GLOB.news_network.last_action ++
current_message.comments += new_feed_comment
if(user)
user.log_message("(as [newscaster_username]) commented on message [current_message.return_body(-1)] -- [current_message.body]", LOG_COMMENT)
/datum/feed_network/proc/submit_wanted(criminal, body, scanned_user, datum/picture/picture, adminMsg = FALSE, newMessage = FALSE)
wanted_issue.active = TRUE

View File

@@ -741,13 +741,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster, 30)
if(!newscaster_username)
creating_comment = FALSE
return TRUE
var/datum/feed_comment/new_feed_comment = new/datum/feed_comment
new_feed_comment.author = newscaster_username
new_feed_comment.body = comment_text
new_feed_comment.time_stamp = station_time_timestamp()
GLOB.news_network.last_action ++
current_message.comments += new_feed_comment
user.log_message("(as [newscaster_username]) commented on message [current_message.return_body(-1)] -- [current_message.body]", LOG_COMMENT)
GLOB.news_network.submit_comment(user, comment_text, newscaster_username, current_message)
creating_comment = FALSE
/**

View File

@@ -191,7 +191,7 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments)
message = L.treat_message(message)["message"]
minor_announce(message, "[department] Announcement:", html_encode = FALSE, sound_override = 'sound/announcer/announcement/announce_dig.ogg')
GLOB.news_network.submit_article(message, department, "Station Announcements", null)
GLOB.news_network.submit_article(message, department, NEWSCASTER_STATION_ANNOUNCEMENTS, null)
usr.log_talk(message, LOG_SAY, tag="station announcement from [src]")
message_admins("[ADMIN_LOOKUPFLW(usr)] has made a station announcement from [src] at [AREACOORD(usr)].")
deadchat_broadcast(" made a station announcement from [span_name("[get_area_name(usr, TRUE)]")].", span_name("[usr.real_name]"), usr, message_type=DEADCHAT_ANNOUNCEMENT)

View File

@@ -0,0 +1,285 @@
GLOBAL_LIST_EMPTY_TYPED(active_bets, /datum/active_bet)
///Max amount of characters you can have in an active bet's title
#define MAX_LENGTH_TITLE 64
///Max amount of characters you can have in an active bet's description
#define MAX_LENGTH_DESCRIPTION 200
/datum/computer_file/program/betting
filename = "betting"
filedesc = "SpaceBet"
downloader_category = PROGRAM_CATEGORY_GAMES
program_open_overlay = "gambling"
extended_desc = "A multi-platform network for placing requests across the station, with payment across the network being possible."
program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
can_run_on_flags = PROGRAM_PDA
size = 4
tgui_id = "NtosSpaceBetting"
program_icon = "dice"
///The active bet this program made, as we can only have 1 going at a time to prevent flooding/spam.
var/datum/active_bet/created_bet
/datum/computer_file/program/betting/New()
. = ..()
RegisterSignal(src, COMSIG_COMPUTER_FILE_DELETE, PROC_REF(on_delete))
///Called when we're deleted, we'll be taking the bet with us.
/datum/computer_file/program/betting/proc/on_delete(datum/source, obj/item/modular_computer/computer_uninstalling)
SIGNAL_HANDLER
created_bet.payout()
QDEL_NULL(created_bet)
/datum/computer_file/program/betting/ui_data(mob/user)
var/list/data = list()
data["active_bets"] = list()
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
data["active_bets"] += list(list(
"name" = bets.name,
"description" = bets.description,
"owner" = bets == created_bet,
"creator" = bets.bet_owner,
"current_bets" = bets.get_bets(computer.computer_id_slot?.registered_account),
"locked" = bets.locked,
))
data["can_create_bet"] = !!isnull(created_bet)
if(isnull(computer.computer_id_slot))
data["bank_name"] = null
data["bank_money"] = null
else
data["bank_name"] = computer.computer_id_slot.registered_account.account_holder
data["bank_money"] = computer.computer_id_slot.registered_account.account_balance
return data
/datum/computer_file/program/betting/ui_static_data(mob/user)
var/list/data = list()
data["max_title_length"] = MAX_LENGTH_TITLE
data["max_description_length"] = MAX_LENGTH_DESCRIPTION
return data
/datum/computer_file/program/betting/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = ui.user
if(isnull(computer.computer_id_slot))
to_chat(user, span_danger("\The [computer] flashes an \"RFID Error - Unable to scan ID\" warning."))
return
switch(action)
if("create_bet")
var/title = reject_bad_name(params["title"], allow_numbers = TRUE, max_length = MAX_LENGTH_TITLE, cap_after_symbols = FALSE)
var/description = reject_bad_name(params["description"], allow_numbers = TRUE, max_length = MAX_LENGTH_DESCRIPTION, cap_after_symbols = FALSE)
if(isnull(title) || isnull(description))
return
var/list/options = list(params["option1"], params["option2"], params["option3"], params["option4"])
for(var/option in options)
options -= option
//remove nulls, empty, and duplicates.
if(isnull(option) || option == "" || options.Find(option))
continue
options += option
option = reject_bad_name(option, allow_numbers = TRUE, max_length = MAX_LENGTH_TITLE, cap_after_symbols = FALSE)
if(length(options) < 2)
to_chat(user, span_danger("2 options minimum required to start a bet."))
return
created_bet = new(user, title, description, options)
return TRUE
if("place_bet")
var/datum/active_bet/bet_placed_on
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
if(bets.name == params["bet_selected"])
bet_placed_on = bets
//can't bet on your own bet
if(isnull(bet_placed_on))
return
if(bet_placed_on == created_bet)
to_chat(user, span_danger("You can't bet on your own poll!"))
return
var/money_betting = params["money_betting"]
if(!isnum(money_betting))
return
var/option = params["option_selected"]
if(isnull(bet_placed_on))
return
bet_placed_on.bet_money(computer.computer_id_slot.registered_account, money_betting, option)
return TRUE
if("cancel_bet")
var/datum/active_bet/bet_cancelling
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
if(bets.name == params["bet_selected"])
bet_cancelling = bets
bet_cancelling.cancel_bet(computer.computer_id_slot.registered_account)
return TRUE
if("select_winner")
var/datum/active_bet/bets_ending
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
if(bets.name == params["bet_selected"])
bets_ending = bets
if(isnull(bets_ending) || bets_ending != created_bet)
return
created_bet.payout(params["winning_answer"])
QDEL_NULL(created_bet)
return TRUE
if("lock_betting")
var/datum/active_bet/bet_locking
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
if(bets.name == params["bet_selected"])
bet_locking = bets
if(bet_locking != created_bet)
return
bet_locking.locked = TRUE
/**
* The active bet that our app will create & use, handles following who owns the bet,
* who is betting, and also takes care of paying out at the end.
*/
/datum/active_bet
///The person owning the bet, who will choose which option has won.
var/bet_owner
///The name of the bet
var/name
///The description of the bet
var/description
///Boolean on whether the bet is locked from getting new betters, or current ones from taking their money out.
var/locked
///Total amount of money that has been bet.
var/total_amount_bet
/** Assoc list of options, with each option having a list of people betting and the amount they've bet.
options = list(
OPTION_A = list(
PERSON_1_ACCOUNT = bet_amount,
PERSON_2_ACCOUNT = bet_amount,
),
OPTION_B = list(
PERSON_3_ACCOUNT = bet_amount,
),
)
*/
var/list/options
///The message we sent to the newscaster, which we'll then reply to once the betting is over.
var/datum/feed_message/newscaster_message
/datum/active_bet/New(creator, name, description, options)
src.bet_owner = creator
src.name = name
src.description = description
src.options = options
GLOB.active_bets += src
for(var/option in options)
if(!length(options[option]))
options[option] = list()
//we'll only advertise it on the first bet of the round, as to not make this overly annoying.
var/should_alert = FALSE
for(var/datum/feed_channel/FC in GLOB.news_network.network_channels)
if(FC.channel_name == NEWSCASTER_SPACE_BETTING)
if(!length(FC.messages))
should_alert = TRUE
newscaster_message = GLOB.news_network.submit_article("The bet [name] has started, place your bets now!", "NtOS Space Betting App", NEWSCASTER_SPACE_BETTING, null, update_alert = should_alert)
/datum/active_bet/Destroy(force)
GLOB.active_bets -= src
newscaster_message = null
return ..()
///Returns how many bets there is per option
/datum/active_bet/proc/get_bets(datum/bank_account/user_account)
var/list/bets_per_option = list()
for(var/option in options)
var/amount_personally_invested = 0
var/total_amount = 0
for(var/list/existing_bets in options[option])
var/existing_bet_amount = text2num(existing_bets[2])
if(user_account && (existing_bets[1] == user_account))
amount_personally_invested = existing_bet_amount
total_amount += existing_bet_amount
bets_per_option += list(list("option_name" = option, "amount" = total_amount, "personally_invested" = amount_personally_invested))
return bets_per_option
///Pays out the loser's money equally to all the winners, or refunds it all if no winning option was given.
/datum/active_bet/proc/payout(winning_option)
if(isnull(winning_option) || !(winning_option in options))
//no winner was selected (likely the host's PDA was destroyed or attempted href exploit), so let's refund everyone.
for(var/list/option in options)
for(var/list/existing_bets in options[option])
var/datum/bank_account/refunded_account = existing_bets[1]
refunded_account.adjust_money(text2num(existing_bets[2]), "Refund: [name] gamble cancelled.")
return
GLOB.news_network.submit_comment(
comment_text = "The bet [name] has ended, the winner was [winning_option]!",
newscaster_username = "NtOS Betting Results",
current_message = newscaster_message,
)
var/list/winners = options[winning_option]
if(!length(winners))
return
for(var/list/winner in winners)
//they aren't winning their own money, so people betting a ton of money won't lose their money to those who bet few.
total_amount_bet -= text2num(winner[2])
for(var/list/winner in winners)
var/datum/bank_account/winner_account = winner[1]
var/money_won = text2num(winner[2]) + total_amount_bet / length(winners)
winner_account.adjust_money(money_won, "Won gamble: [name]") //give them their money back & whatever they won.
//they only made their money back, don't tell them they won anything.
if((money_won - text2num(winner[2])) == 0)
continue
winner_account.bank_card_talk("You won [money_won]cr from having a correct guess on [name]!")
///Puts a bank account's money bet on a given option.
/datum/active_bet/proc/bet_money(datum/bank_account/better, money_betting, option_betting)
if(locked)
return
for(var/option in options)
for(var/list/existing_bets in options[option])
if(existing_bets[1] == better)
//We're already betting, but now we're betting on another one, clear our previous and we'll bet on the new.
if(option != option_betting)
better.adjust_money(text2num(existing_bets[2]), "Refunded: changed bet for [name].")
options[option] -= list(existing_bets)
//We're already betting on the same one, we'll add it together instead of making it a separate bet, or the user is taking money out.
else
//putting more money in
if(text2num(existing_bets[2]) < money_betting)
if(better.account_balance < money_betting)
return
var/money_adding_in = money_betting - text2num(existing_bets[2])
total_amount_bet += money_adding_in
better.bank_card_talk("Additional [money_adding_in]cr deducted for your bet on [name].")
better.adjust_money(-money_adding_in, "Gambling on [name].")
existing_bets[2] = "[money_betting]"
return
//taking it all out, we remove them from the list so they aren't a winner with bets of 0.
if(money_betting == 0)
var/money_taking_out = text2num(existing_bets[2])
total_amount_bet -= money_taking_out
better.adjust_money(money_taking_out, "Refunded: changed bet for [name].")
options[option] -= list(existing_bets)
return
//taking money out
if(text2num(existing_bets[2]) > money_betting)
var/money_taking_out = text2num(existing_bets[2]) - money_betting
total_amount_bet -= money_taking_out
better.bank_card_talk("Refunded [money_taking_out]cr for taking money out of your bet on [name].")
better.adjust_money(money_taking_out, "Refund from gambling on [name].")
existing_bets[2] = "[money_betting]"
return
total_amount_bet += money_betting
options[option_betting] += list(list(better, "[money_betting]"))
better.adjust_money(-money_betting, "Gambling on [name]")
better.bank_card_talk("Deducted [money_betting]cr for your bet on [name].")
///Cancels your bet, removing your bet and refunding your money.
/datum/active_bet/proc/cancel_bet(datum/bank_account/better)
for(var/option in options)
for(var/list/existing_bets in options[option])
if(existing_bets[1] == better)
var/money_refunding = text2num(existing_bets[2])
total_amount_bet -= money_refunding
better.bank_card_talk("Refunded [money_refunding]cr for cancelling your bet on [name].")
better.adjust_money(money_refunding, "Refunded: changed bet for [name].")
options[option] -= list(existing_bets)
#undef MAX_LENGTH_TITLE
#undef MAX_LENGTH_DESCRIPTION

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -5505,6 +5505,7 @@
#include "code\modules\modular_computers\file_system\programs\alarm.dm"
#include "code\modules\modular_computers\file_system\programs\arcade.dm"
#include "code\modules\modular_computers\file_system\programs\atmosscan.dm"
#include "code\modules\modular_computers\file_system\programs\betting.dm"
#include "code\modules\modular_computers\file_system\programs\borg_monitor.dm"
#include "code\modules\modular_computers\file_system\programs\bounty_board.dm"
#include "code\modules\modular_computers\file_system\programs\budgetordering.dm"

View File

@@ -0,0 +1,263 @@
import { useState } from 'react';
import {
Box,
Button,
Collapsible,
Divider,
Icon,
Input,
NumberInput,
Section,
Stack,
TextArea,
} from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
import { NtosWindow } from '../layouts';
type Data = {
active_bets: ActiveBets[];
bank_name: string;
bank_money: number;
can_create_bet: BooleanLike;
max_title_length: number;
max_description_length: number;
};
type ActiveBets = {
name: string;
description: string;
owner: BooleanLike;
creator: string;
current_bets: CurrentBets[];
locked: BooleanLike;
};
type CurrentBets = {
option_name: string;
total_amount: number;
personally_invested: number;
};
export const NtosSpaceBetting = () => {
const { act, data } = useBackend<Data>();
const { bank_name, bank_money, can_create_bet } = data;
return (
<NtosWindow width={500} height={620}>
<NtosWindow.Content scrollable>
<Section title="User Information">
<Stack>
<Stack.Item mr={1.5}>
<Icon
name="id-card"
size={3}
mr={1}
color={bank_name ? 'green' : 'red'}
/>
</Stack.Item>
<Stack fill vertical>
<Stack.Item>Username: {bank_name}</Stack.Item>
<Stack.Item>Money Available: {bank_money}cr</Stack.Item>
</Stack>
</Stack>
</Section>
<PollsSection />
{!!can_create_bet && <BettingCreation />}
</NtosWindow.Content>
</NtosWindow>
);
};
export const PollsSection = () => {
const { act, data } = useBackend<Data>();
const { active_bets = [] } = data;
const [Winner, set_winner] = useState('');
return (
<Section>
{!active_bets.length ? (
<Box>
There&apos;s currently no active polls to bet on, create one below!
</Box>
) : (
active_bets.map(
(
{ name, description, owner, creator, current_bets = [], locked },
index,
) => (
<Section title={name + ' - Created by ' + creator} key={name}>
<Stack>
<Stack.Item grow>
<Stack.Item grow>{description}</Stack.Item>
<Divider />
{current_bets.map(
(
{ option_name, total_amount, personally_invested },
index,
) => (
<Stack.Item
grow
key={option_name}
className="candystripe"
my={1.5}
>
<Stack.Item>
<Stack.Item my={1}>
{option_name} (Has {total_amount || 0}cr bet on it)
{!owner ? (
<NumberInput
value={personally_invested}
unit="cr"
width="15px"
disabled={locked}
minValue={0}
maxValue={10000}
step={1}
onChange={(value) =>
act('place_bet', {
bet_selected: name,
option_selected: option_name,
money_betting: value,
})
}
/>
) : (
<Button.Checkbox
tooltip="Whether this answer won."
checked={Winner === option_name}
key={option_name}
onClick={() => set_winner(option_name)}
/>
)}
</Stack.Item>
</Stack.Item>
</Stack.Item>
),
)}
{!!owner &&
(!locked ? (
<Stack.Item>
<Button.Confirm
fluid
icon="minus"
tooltip="Lock the ability to place/retract bets. This is irreversible!"
onClick={() =>
act('lock_betting', { bet_selected: name })
}
>
Lock Betting
</Button.Confirm>
</Stack.Item>
) : (
<Button.Confirm
fluid
icon="plus"
tooltip="Finalize results as the checked answer being the winner."
onClick={() =>
act('select_winner', {
bet_selected: name,
winning_answer: Winner,
})
}
>
Finalize Results
</Button.Confirm>
))}
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="minus"
disabled={locked}
tooltip="If you have any bets, this will remove them and refund the money."
onClick={() => act('cancel_bet', { bet_selected: name })}
>
Cancel Bet
</Button>
</Stack.Item>
</Stack>
</Section>
),
)
)}
</Section>
);
};
export const BettingCreation = () => {
const { act, data } = useBackend<Data>();
const { max_title_length, max_description_length } = data;
const [Title, setTitle] = useState('');
const [Desc, setDesc] = useState('');
const [Option1, setOption1] = useState('');
const [Option2, setOption2] = useState('');
const [Option3, setOption3] = useState('');
const [Option4, setOption4] = useState('');
return (
<Collapsible title="Bet Creation">
<Stack fill vertical>
<Stack.Item grow>
<Input
fluid
placeholder="Title"
maxLength={max_title_length}
onInput={(event, value) => setTitle(value)}
/>
</Stack.Item>
<Stack.Item grow>
<TextArea
fluid
placeholder="Description"
height="100px"
width="100%"
maxLength={max_description_length}
backgroundColor="black"
textColor="white"
onChange={(event, value) => setDesc(value)}
/>
</Stack.Item>
<Input
fluid
placeholder="Option 1"
maxLength={max_title_length}
onInput={(event, value) => setOption1(value)}
/>
<Input
fluid
placeholder="Option 2"
maxLength={max_title_length}
onInput={(event, value) => setOption2(value)}
/>
<Input
fluid
placeholder="Option 3 (Optional)"
maxLength={max_title_length}
onInput={(event, value) => setOption3(value)}
/>
<Input
fluid
placeholder="Option 4 (Optional)"
maxLength={max_title_length}
onInput={(event, value) => setOption4(value)}
/>
<Stack.Item grow>
<Button
fluid
onClick={() =>
act('create_bet', {
title: Title,
description: Desc,
option1: Option1,
option2: Option2,
option3: Option3,
option4: Option4,
})
}
>
Create Bet!
</Button>
</Stack.Item>
</Stack>
</Collapsible>
);
};