diff --git a/files/assets/js/casino/blackjack_screen.js b/files/assets/js/casino/blackjack_screen.js index 762b37d3f..97fd8fe24 100644 --- a/files/assets/js/casino/blackjack_screen.js +++ b/files/assets/js/casino/blackjack_screen.js @@ -1,6 +1,6 @@ -function makeBlackjackRequest(action) { +function makeBlackjackRequest(action, split = false) { const xhr = new XMLHttpRequest(); - xhr.open("post", `/casino/twentyone/${action}`); + xhr.open("post", `/casino/twentyone/${action}?hand=${split ? 'split' : 'player'}`); xhr.setRequestHeader('xhr', 'xhr'); xhr.onload = handleBlackjackResponse.bind(null, xhr); xhr.blackjackAction = action; @@ -37,7 +37,8 @@ function handleBlackjackResponse(xhr) { hit: "Unable to hit.", stay: "Unable to stay.", "double-down": "Unable to double down.", - "buy-insurance": "Unable to buy insurance." + "buy-insurance": "Unable to buy insurance.", + "split": "Unable to split" }; result = results[xhr.blackjackAction]; @@ -51,11 +52,14 @@ function handleBlackjackResponse(xhr) { function updateBlackjackActions(state) { const actions = Array.from(document.querySelectorAll('.twentyone-btn')); + document.getElementById(`twentyone-SPLIT_ACTIONS`).style.display = 'none' // Hide all actions. actions.forEach(action => action.style.display = 'none'); if (state) { + if(state.actions.some((action) => action === 'HIT_SPLIT')) state.actions.push('SPLIT_ACTIONS'); + // Show the correct ones. state.actions.forEach(action => document.getElementById(`twentyone-${action}`).style.display = 'inline-block'); } else { @@ -95,6 +99,7 @@ function updateBlackjackTable(state) { `; const dealerCards = makeCardset(state.dealer, 'Dealer', state.dealer_value); const playerCards = makeCardset(state.player, 'Player', state.player_value); + const playerSplitCards = state.has_player_split ? makeCardset(state.player_split, 'Player', state.player_split_value) : ''; updateBlackjackActions(state); @@ -103,47 +108,92 @@ function updateBlackjackTable(state) { ${dealerCards} ${playerCards} + ${playerSplitCards} `; const currency = state.wager.currency === 'coins' ? 'coins' : 'marseybux'; - switch (state.status) { - case 'BLACKJACK': - updateResult(`Blackjack: Received ${state.payout} ${currency}`, "warning"); - break; - case 'WON': - updateResult(`Won: Received ${state.payout} ${currency}`, "success"); - break; - case 'PUSHED': - updateResult(`Pushed: Received ${state.wager.amount} ${currency}`, "success"); - break; - case 'LOST': - let lost = state.wager.amount; - if (state.player_doubled_down) { - lost *= 2; - } - updateResult(`Lost ${lost} ${currency}`, "danger"); - break; - default: - break; + const gameCompleted = ['BLACKJACK', 'WON', 'PUSHED', 'LOST'].indexOf(state.status) !== -1 && (!state.has_player_split || ['WON', 'PUSHED', 'LOST'].indexOf(state.status_split) !== -1); + + if(gameCompleted) { + switch (state.status) { + case 'BLACKJACK': + updateResult(`Blackjack: Received ${state.payout} ${currency}`, "warning"); + break; + case 'WON': + if(state.status_split === 'LOST') { + updateResult(`Won and Lost: Received 0 ${currency}`, "success"); + } + else if(state.status_split === 'PUSHED') { + updateResult(`Won and PUSHED: Received ${state.payout} ${currency}`, "success"); + } + else { + updateResult(`Won: Received ${state.payout} ${currency}`, "success"); + } + break; + case 'PUSHED': + if(state.status_split === 'WON') { + updateResult(`Won and PUSHED: Received ${state.payout} ${currency}`, "success"); + } + else if(state.status_split === 'LOST') { + updateResult(`Lost and Pushed: Lost ${state.wager.amount} ${currency}`, "danger"); + } + else { + updateResult(`Pushed: Received ${state.wager.amount} ${currency}`, "success"); + } + + break; + case 'LOST': + if(state.status_split === 'WON') { + updateResult(`Won and Lost: Received 0 ${currency}`, "success"); + } + else if(state.status_split === 'PUSHED') { + updateResult(`Lost and Pushed: Lost ${state.wager.amount} ${currency}`, "danger"); + } + else { + let lost = state.wager.amount; + if (state.player_doubled_down || state.has_player_split) { + lost *= 2; + } + updateResult(`Lost ${lost} ${currency}`, "danger"); + } + + break; + default: + break; + } + + updateCardsetBackgrounds(state, true); + } + else { + updateCardsetBackgrounds(state); } - updateCardsetBackgrounds(state); - if (state.status === 'PLAYING') { + if (state.status === 'PLAYING' || (state.has_player_split && state.status_split === 'PLAYING')) { updateResult(`${state.wager.amount} ${currency} are at stake`, "success"); } else { enableWager(); } } -function updateCardsetBackgrounds(state) { +function updateCardsetBackgrounds(state, complete = false) { const cardsets = Array.from(document.querySelectorAll('.blackjack-cardset')); for (const cardset of cardsets) { ['PLAYING', 'LOST', 'PUSHED', 'WON', 'BLACKJACK'].forEach(status => cardset.classList.remove(`blackjack-cardset__${status}`)); - cardset.classList.add(`blackjack-cardset__${state.status}`) } + if(complete){ + const wager = state.has_player_split ? state?.wager?.amount * 2 : state?.wager?.amount; + let dealerShows = state.payout > wager ? 'WON': 'LOST'; + if(state.payout === wager) dealerShows = 'PUSHED' + cardsets[0]?.classList.add(`blackjack-cardset__${dealerShows}`) + } + else { + cardsets[0]?.classList.add(`blackjack-cardset__PLAYING`) + } + cardsets[1]?.classList.add(`blackjack-cardset__${state.status}`) + cardsets[2]?.classList.add(`blackjack-cardset__${state.status_split}`) } function deal() { @@ -162,8 +212,8 @@ function deal() { drawFromDeck(); } -function hit() { - const request = makeBlackjackRequest('hit'); +function hit(split = false) { + const request = makeBlackjackRequest('hit', split); const form = new FormData(); form.append("formkey", formkey()); request.send(form); @@ -171,13 +221,21 @@ function hit() { drawFromDeck(); } -function stay() { - const request = makeBlackjackRequest('stay'); +function hitSplit() { + hit(true); +} + +function stay(split = false) { + const request = makeBlackjackRequest('stay', split); const form = new FormData(); form.append("formkey", formkey()); request.send(form); } +function staySplit() { + stay(true); +} + function doubleDown() { const request = makeBlackjackRequest('double-down'); const form = new FormData(); @@ -194,6 +252,13 @@ function buyInsurance() { request.send(form); } +function split() { + const request = makeBlackjackRequest('split'); + const form = new FormData(); + form.append("formkey", formkey()); + request.send(form); +} + function buildBlackjackDeck() { document.getElementById('blackjack-table-deck').innerHTML = `
diff --git a/files/helpers/twentyone.py b/files/helpers/twentyone.py index 85a25b35e..fb14acb88 100644 --- a/files/helpers/twentyone.py +++ b/files/helpers/twentyone.py @@ -25,6 +25,9 @@ class BlackjackAction(str, Enum): STAY = "STAY" DOUBLE_DOWN = "DOUBLE_DOWN" BUY_INSURANCE = "BUY_INSURANCE" + SPLIT = 'SPLIT' + HIT_SPLIT = 'HIT_SPLIT' + STAY_SPLIT = 'STAY_SPLIT' ranks = ("2", "3", "4", "5", "6", "7", "8", "9", "X", "J", "Q", "K", "A") @@ -37,18 +40,22 @@ minimum_bet = 5 def get_initial_state(): return { "player": [], + "player_split": [], + "player_split_value": 0, "player_value": 0, "dealer": [], "dealer_value": 0, + "has_player_split": False, "player_bought_insurance": False, "player_doubled_down": False, "status": BlackjackStatus.PLAYING, + "status_split": BlackjackStatus.PLAYING, "actions": [], "wager": { "amount": 0, "currency": "coins" }, - "payout": 0 + "payout": 0, } @@ -119,7 +126,7 @@ def create_new_game(gambler, wager, currency): raise Exception(f"Gambler cannot afford to bet {wager} {currency}.") -def handle_blackjack_deal(state): +def handle_blackjack_deal(state, split): deck = build_deck(state) first = deck.pop() second = deck.pop() @@ -131,21 +138,28 @@ def handle_blackjack_deal(state): return state -def handle_blackjack_hit(state): +def handle_blackjack_hit(state, split = False): deck = build_deck(state) - next_card = deck.pop() - state['player'].append(next_card) + if(split and state['has_player_split'] and state['status_split'] != BlackjackStatus.LOST): + next_card = deck.pop() + state['player_split'].append(next_card) + elif(state['status'] != BlackjackStatus.LOST): + next_card = deck.pop() + state['player'].append(next_card) return state -def handle_blackjack_stay(state): - state['status'] = BlackjackStatus.STAYED +def handle_blackjack_stay(state, split = False): + if(split and state['has_player_split'] and state['status_split'] != BlackjackStatus.LOST): + state['status_split'] = BlackjackStatus.STAYED + elif(state['status'] != BlackjackStatus.LOST): + state['status'] = BlackjackStatus.STAYED return state -def handle_blackjack_double_down(state): +def handle_blackjack_double_down(state, split): state['player_doubled_down'] = True state = handle_blackjack_hit(state) state = handle_blackjack_stay(state) @@ -153,16 +167,27 @@ def handle_blackjack_double_down(state): return state -def handle_blackjack_buy_insurance(state): +def handle_blackjack_buy_insurance(state, split): state['player_bought_insurance'] = True return state +def handle_split(state, split): + state['has_player_split'] = True + state['player_split'] = [state['player'].pop()] + + state = handle_blackjack_hit(state) + state = handle_blackjack_hit(state, True) + + return state + def check_for_completion(state): + has_split = state['has_player_split'] after_initial_deal = len( - state['player']) == 2 and len(state['dealer']) == 2 + state['player']) == 2 and len(state['dealer']) == 2 and not has_split player_hand_value = get_value_of_hand(state['player']) + player_split_hand_value = get_value_of_hand(state['player_split']) dealer_hand_value = get_value_of_hand(state['dealer']) # Both player and dealer were initially dealt 21: Push. @@ -176,12 +201,22 @@ def check_for_completion(state): return True, state # Player went bust: Lost. - if player_hand_value == -1: + if player_hand_value == -1 and state['status'] != BlackjackStatus.LOST: state['status'] = BlackjackStatus.LOST - return True, state + if(not has_split or state['status_split'] == BlackjackStatus.LOST): + return True, state + + # Player went bust: Lost. + if player_split_hand_value == -1 and state['status_split'] != BlackjackStatus.LOST: + state['status_split'] = BlackjackStatus.LOST + if state['status'] == BlackjackStatus.LOST: + return True, state + + hand_terminal_status = state['status'] in [BlackjackStatus.LOST, BlackjackStatus.STAYED] + hand_split_terminal_status = not has_split or state['status_split'] in [BlackjackStatus.LOST, BlackjackStatus.STAYED] # Player chose to stay: Deal rest for dealer then determine winner. - if state['status'] == BlackjackStatus.STAYED: + if hand_split_terminal_status and hand_terminal_status: deck = build_deck(state) while dealer_hand_value < 17 and dealer_hand_value != -1: @@ -189,16 +224,27 @@ def check_for_completion(state): state['dealer'].append(next_card) dealer_hand_value = get_value_of_hand(state['dealer']) - if player_hand_value > dealer_hand_value or dealer_hand_value == -1: - state['status'] = BlackjackStatus.WON - elif dealer_hand_value > player_hand_value: - state['status'] = BlackjackStatus.LOST - else: - state['status'] = BlackjackStatus.PUSHED - + if((not has_split) or state['status'] != BlackjackStatus.LOST): + if player_hand_value > dealer_hand_value or dealer_hand_value == -1: + state['status'] = BlackjackStatus.WON + elif dealer_hand_value > player_hand_value: + state['status'] = BlackjackStatus.LOST + else: + state['status'] = BlackjackStatus.PUSHED + state['player_value'] = get_value_of_hand(state['player']) state['dealer_value'] = get_value_of_hand(state['dealer']) + if has_split and state['status_split'] != BlackjackStatus.LOST: + if player_split_hand_value > dealer_hand_value or dealer_hand_value == -1: + state['status_split'] = BlackjackStatus.WON + elif dealer_hand_value > player_split_hand_value: + state['status_split'] = BlackjackStatus.LOST + else: + state['status_split'] = BlackjackStatus.PUSHED + + state['player_split_value'] = get_value_of_hand(state['player_split']) + return True, state return False, state @@ -209,25 +255,33 @@ def does_insurance_apply(state): dealer_hand_value = get_value_of_hand(dealer) dealer_first_card_ace = dealer[0][0] == 'A' dealer_never_hit = len(dealer) == 2 - return dealer_hand_value == 21 and dealer_first_card_ace and dealer_never_hit + return not state['has_player_split'] and dealer_hand_value == 21 and dealer_first_card_ace and dealer_never_hit def can_purchase_insurance(state): dealer = state['dealer'] dealer_first_card_ace = dealer[0][0] == 'A' dealer_never_hit = len(dealer) == 2 - return dealer_first_card_ace and dealer_never_hit and not state['player_bought_insurance'] + return not state['has_player_split'] and dealer_first_card_ace and dealer_never_hit and not state['player_bought_insurance'] def can_double_down(state): player = state['player'] player_hand_value = get_value_of_hand(player) player_never_hit = len(player) == 2 - return player_hand_value in (10, 11) and player_never_hit + return not state['has_player_split'] and player_hand_value in (10, 11) and player_never_hit + +def can_split(state): + player = state['player'] + player_never_hit = len(player) == 2 + hand_can_split = player[0][0] == player[1][0] + player_has_split = state['has_player_split'] + return hand_can_split and player_never_hit and not player_has_split def handle_payout(gambler, state, game): status = state['status'] + split_status = state['status_split'] payout = 0 if status == BlackjackStatus.BLACKJACK: @@ -254,12 +308,21 @@ def handle_payout(gambler, state, game): payout = game.wager else: raise Exception("Attempted to payout a game that has not finished.") + + if split_status == BlackjackStatus.WON: + game.winnings += game.wager + payout += game.wager * 2 + elif split_status == BlackjackStatus.LOST: + game.winnings += -game.wager + elif status == BlackjackStatus.PUSHED: + payout += game.wager + gambler.pay_account(game.currency, payout) - if status in {BlackjackStatus.BLACKJACK, BlackjackStatus.WON}: + if status in {BlackjackStatus.BLACKJACK, BlackjackStatus.WON} or split_status in {BlackjackStatus.WON}: distribute_wager_badges(gambler, game.wager, won=True) - elif status == BlackjackStatus.LOST: + elif status == BlackjackStatus.LOST or split_status == BlackjackStatus.LOST: distribute_wager_badges(gambler, game.wager, won=False) game.active = False @@ -284,10 +347,11 @@ action_handlers = { BlackjackAction.STAY: handle_blackjack_stay, BlackjackAction.DOUBLE_DOWN: handle_blackjack_double_down, BlackjackAction.BUY_INSURANCE: handle_blackjack_buy_insurance, + BlackjackAction.SPLIT: handle_split, } -def dispatch_action(gambler, action): +def dispatch_action(gambler, action, is_split = False): game = get_active_twentyone_game(gambler) handler = action_handlers[action] @@ -312,16 +376,24 @@ def dispatch_action(gambler, action): charge_gambler(gambler, game.wager, game.currency) game.wager *= 2 - new_state = handler(state) + if action == BlackjackAction.SPLIT: + if not can_split(state): + raise Exception("Cannot split") + + charge_gambler(gambler, game.wager, game.currency) + + new_state = handler(state, is_split) new_state['player_value'] = get_value_of_hand(new_state['player']) new_state['dealer_value'] = get_value_of_hand(new_state['dealer']) - new_state['actions'] = get_available_actions(new_state) + new_state['player_split_value'] = get_value_of_hand(new_state['player_split']) - game.game_state = json.dumps(new_state) - g.db.add(game) game_over, final_state = check_for_completion(new_state) + new_state['actions'] = get_available_actions(new_state) + game.game_state = json.dumps(new_state) + g.db.add(game) + if game_over: payout = handle_payout(gambler, final_state, game) final_state['actions'] = [BlackjackAction.DEAL] @@ -344,6 +416,7 @@ def build_deck(state): card_counts[card] = deck_count cards_already_dealt = state['player'].copy() + cards_already_dealt.extend(state['player_split'].copy()) cards_already_dealt.extend(state['dealer'].copy()) for card in cards_already_dealt: @@ -384,10 +457,17 @@ def get_available_actions(state): actions.append(BlackjackAction.HIT) actions.append(BlackjackAction.STAY) + if state['has_player_split'] and state['status_split'] == BlackjackStatus.PLAYING: + actions.append(BlackjackAction.HIT_SPLIT) + actions.append(BlackjackAction.STAY_SPLIT) + if can_double_down(state): actions.append(BlackjackAction.DOUBLE_DOWN) if can_purchase_insurance(state): actions.append(BlackjackAction.BUY_INSURANCE) + + if can_split(state): + actions.append(BlackjackAction.SPLIT) return actions diff --git a/files/routes/casino.py b/files/routes/casino.py index efd0a85be..3a9249e7a 100644 --- a/files/routes/casino.py +++ b/files/routes/casino.py @@ -146,7 +146,8 @@ def blackjack_player_hit(v): abort(403, "You are under Rehab award effect!") try: - state = dispatch_action(v, BlackjackAction.HIT) + hand = request.args.get('hand') + state = dispatch_action(v, BlackjackAction.HIT, True if hand == 'split' else False) feed = get_game_feed('blackjack') return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "marseybux": v.marseybux}} except: @@ -164,7 +165,8 @@ def blackjack_player_stay(v): abort(403, "You are under Rehab award effect!") try: - state = dispatch_action(v, BlackjackAction.STAY) + hand = request.args.get('hand') + state = dispatch_action(v, BlackjackAction.STAY, True if hand == 'split' else False) feed = get_game_feed('blackjack') return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "marseybux": v.marseybux}} except: @@ -206,6 +208,23 @@ def blackjack_player_bought_insurance(v): except: abort(403, "Unable to buy insurance!") +@app.post("/casino/twentyone/split") +@limiter.limit('1/second', scope=rpath) +@limiter.limit('1/second', scope=rpath, key_func=get_ID) +@limiter.limit(CASINO_RATELIMIT, deduct_when=lambda response: response.status_code < 400) +@limiter.limit(CASINO_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID) +@auth_required +def split(v): + if v.rehab: + abort(403, "You are under Rehab award effect!") + + try: + state = dispatch_action(v, BlackjackAction.SPLIT) + feed = get_game_feed('blackjack') + return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "marseybux": v.marseybux}} + except: + abort(403, "Unable to split!") + # Roulette @app.get("/casino/roulette/bets") @limiter.limit(CASINO_RATELIMIT, deduct_when=lambda response: response.status_code < 400) diff --git a/files/templates/casino/blackjack_screen.html b/files/templates/casino/blackjack_screen.html index 353ee3249..fc1a20d5b 100644 --- a/files/templates/casino/blackjack_screen.html +++ b/files/templates/casino/blackjack_screen.html @@ -15,6 +15,8 @@
+ @@ -23,6 +25,15 @@ + +
+ + {% endblock %}