MarseyWorld/files/helpers/twentyone.py

394 lines
9.9 KiB
Python

import json
import random
from enum import Enum
from math import floor
from flask import g
from files.classes.casino_game import CasinoGame
from files.helpers.casino import distribute_wager_badges
from sqlalchemy.orm.exc import MultipleResultsFound
class BlackjackStatus(str, Enum):
PLAYING = "PLAYING"
STAYED = "STAYED"
PUSHED = "PUSHED"
WON = "WON"
LOST = "LOST"
BLACKJACK = "BLACKJACK"
class BlackjackAction(str, Enum):
DEAL = "DEAL"
HIT = "HIT"
STAY = "STAY"
DOUBLE_DOWN = "DOUBLE_DOWN"
BUY_INSURANCE = "BUY_INSURANCE"
ranks = ("2", "3", "4", "5", "6", "7", "8", "9", "X", "J", "Q", "K", "A")
suits = ("S", "H", "C", "D")
deck = [rank + suit for rank in ranks for suit in suits]
deck_count = 4
minimum_bet = 5
def get_initial_state():
return {
"player": [],
"player_value": 0,
"dealer": [],
"dealer_value": 0,
"player_bought_insurance": False,
"player_doubled_down": False,
"status": BlackjackStatus.PLAYING,
"actions": [],
"wager": {
"amount": 0,
"currency": "coins"
},
"payout": 0
}
def build_casino_game(gambler, wager, currency):
initial_state = get_initial_state()
initial_state['wager']['amount'] = wager
initial_state['wager']['currency'] = currency
casino_game = CasinoGame()
casino_game.user_id = gambler.id
casino_game.currency = currency
casino_game.wager = wager
casino_game.winnings = 0
casino_game.kind = 'blackjack'
casino_game.game_state = json.dumps(initial_state)
casino_game.active = True
g.db.add(casino_game)
return casino_game
def get_active_twentyone_game(gambler):
try:
return g.db.query(CasinoGame).filter(
CasinoGame.active == True,
CasinoGame.kind == 'blackjack',
CasinoGame.user_id == gambler.id).one_or_none()
except MultipleResultsFound:
games = g.db.query(CasinoGame).filter(
CasinoGame.active == True,
CasinoGame.kind == 'blackjack',
CasinoGame.user_id == gambler.id).all()
for game in games:
g.db.delete(game)
g.db.commit()
return None
def get_active_twentyone_game_state(gambler):
active_game = get_active_twentyone_game(gambler)
full_state = active_game.game_state_json
return remove_exploitable_information(full_state)
def charge_gambler(gambler, amount, currency):
charged = gambler.charge_account(currency, amount)[0]
if not charged:
raise Exception("Gambler cannot afford charge.")
def create_new_game(gambler, wager, currency):
existing_game = get_active_twentyone_game(gambler)
over_minimum_bet = wager >= minimum_bet
if existing_game:
existing_game.active = False
g.db.add(existing_game)
if not over_minimum_bet:
raise Exception(f"Gambler must bet over {minimum_bet} {currency}.")
try:
charge_gambler(gambler, wager, currency)
new_game = build_casino_game(gambler, wager, currency)
g.db.add(new_game)
g.db.flush()
except:
raise Exception(f"Gambler cannot afford to bet {wager} {currency}.")
def handle_blackjack_deal(state):
deck = build_deck(state)
first = deck.pop()
second = deck.pop()
third = deck.pop()
fourth = deck.pop()
state['player'] = [first, third]
state['dealer'] = [second, fourth]
return state
def handle_blackjack_hit(state):
deck = build_deck(state)
next_card = deck.pop()
state['player'].append(next_card)
return state
def handle_blackjack_stay(state):
state['status'] = BlackjackStatus.STAYED
return state
def handle_blackjack_double_down(state):
state['player_doubled_down'] = True
state = handle_blackjack_hit(state)
state = handle_blackjack_stay(state)
return state
def handle_blackjack_buy_insurance(state):
state['player_bought_insurance'] = True
return state
def check_for_completion(state):
after_initial_deal = len(
state['player']) == 2 and len(state['dealer']) == 2
player_hand_value = get_value_of_hand(state['player'])
dealer_hand_value = get_value_of_hand(state['dealer'])
# Both player and dealer were initially dealt 21: Push.
if after_initial_deal and player_hand_value == 21 and dealer_hand_value == 21:
state['status'] = BlackjackStatus.PUSHED
return True, state
# Player was originally dealt 21, dealer was not: Blackjack.
if after_initial_deal and player_hand_value == 21:
state['status'] = BlackjackStatus.BLACKJACK
return True, state
# Player went bust: Lost.
if player_hand_value == -1:
state['status'] = BlackjackStatus.LOST
return True, state
# Player chose to stay: Deal rest for dealer then determine winner.
if state['status'] == BlackjackStatus.STAYED:
deck = build_deck(state)
while dealer_hand_value < 17 and dealer_hand_value != -1:
next_card = deck.pop()
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
state['player_value'] = get_value_of_hand(state['player'])
state['dealer_value'] = get_value_of_hand(state['dealer'])
return True, state
return False, state
def does_insurance_apply(state):
dealer = state['dealer']
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
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']
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
def handle_payout(gambler, state, game):
status = state['status']
payout = 0
if status == BlackjackStatus.BLACKJACK:
game.winnings = floor(game.wager * 3/2)
payout = game.wager + game.winnings
elif status == BlackjackStatus.WON:
game.winnings = game.wager
payout = game.wager * 2
elif status == BlackjackStatus.LOST:
dealer = state['dealer']
dealer_first_card_ace = dealer[0][0] == 'A'
dealer_never_hit = len(dealer) == 2
dealer_hand_value = get_value_of_hand(dealer) == 21
insurance_applies = dealer_hand_value == 21 and dealer_first_card_ace and dealer_never_hit
if insurance_applies and state['player_bought_insurance']:
game.winnings = 0
payout = game.wager
else:
game.winnings = -game.wager
payout = 0
elif status == BlackjackStatus.PUSHED:
game.winnings = 0
payout = game.wager
else:
raise Exception("Attempted to payout a game that has not finished.")
gambler.pay_account(game.currency, payout)
if status in {BlackjackStatus.BLACKJACK, BlackjackStatus.WON}:
distribute_wager_badges(gambler, game.wager, won=True)
elif status == BlackjackStatus.LOST:
distribute_wager_badges(gambler, game.wager, won=False)
game.active = False
g.db.add(game)
return payout
def remove_exploitable_information(state):
safe_state = state
if len(safe_state['dealer']) >= 2:
safe_state['dealer'][1] = '?'
safe_state['dealer_value'] = '?'
return safe_state
action_handlers = {
BlackjackAction.DEAL: handle_blackjack_deal,
BlackjackAction.HIT: handle_blackjack_hit,
BlackjackAction.STAY: handle_blackjack_stay,
BlackjackAction.DOUBLE_DOWN: handle_blackjack_double_down,
BlackjackAction.BUY_INSURANCE: handle_blackjack_buy_insurance,
}
def dispatch_action(gambler, action):
game = get_active_twentyone_game(gambler)
handler = action_handlers[action]
if not game:
raise Exception(
'Gambler has no active blackjack game.')
if not handler:
raise Exception(
f'Illegal action {action} passed to Blackjack#dispatch_action.')
state = game.game_state_json
if action == BlackjackAction.BUY_INSURANCE:
if not can_purchase_insurance(state):
raise Exception("Insurance cannot be purchased.")
charge_gambler(gambler, floor(game.wager / 2), game.currency)
if action == BlackjackAction.DOUBLE_DOWN:
if not can_double_down(state):
raise Exception("Cannot double down.")
charge_gambler(gambler, game.wager, game.currency)
game.wager *= 2
new_state = handler(state)
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)
game.game_state = json.dumps(new_state)
g.db.add(game)
game_over, final_state = check_for_completion(new_state)
if game_over:
payout = handle_payout(gambler, final_state, game)
final_state['actions'] = [BlackjackAction.DEAL]
final_state['payout'] = payout
return final_state
else:
safe_state = remove_exploitable_information(new_state)
return safe_state
def shuffle(collection):
random.shuffle(collection)
return collection
def build_deck(state):
card_counts = {}
for card in deck:
card_counts[card] = deck_count
cards_already_dealt = state['player'].copy()
cards_already_dealt.extend(state['dealer'].copy())
for card in cards_already_dealt:
card_counts[card] = card_counts[card] - 1
deck_without_already_dealt_cards = []
for card in deck:
amount = card_counts[card]
for _ in range(amount):
deck_without_already_dealt_cards.append(card)
return shuffle(deck_without_already_dealt_cards)
def get_value_of_card(card):
rank = card[0]
return 0 if rank == "A" else min(ranks.index(rank) + 2, 10)
def get_value_of_hand(hand):
without_aces = sum(map(get_value_of_card, hand))
ace_count = sum("A" in c for c in hand)
possibilities = []
for i in range(ace_count + 1):
value = without_aces + (ace_count - i) + i * 11
possibilities.append(-1 if value > 21 else value)
return max(possibilities)
def get_available_actions(state):
actions = []
if state['status'] == BlackjackStatus.PLAYING:
actions.append(BlackjackAction.HIT)
actions.append(BlackjackAction.STAY)
if can_double_down(state):
actions.append(BlackjackAction.DOUBLE_DOWN)
if can_purchase_insurance(state):
actions.append(BlackjackAction.BUY_INSURANCE)
return actions