diff --git a/config.template b/config.template index 764c46e..5b374f0 100644 --- a/config.template +++ b/config.template @@ -53,9 +53,10 @@ LogMessagesToFile = False SyslogToFile = False [games] -# enable or disable the games module +# enable or disable the games module(s) dopeWars = True lemonade = True +blackjack = True [sentry] # detect anyone close to the bot diff --git a/mesh_bot.py b/mesh_bot.py index 8610d15..9460def 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -25,9 +25,10 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi "wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'), "wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'), "wiki:": lambda: handle_wiki(message), - "games": lambda: "CMD: dopewars, lemonstand", + "games": lambda: gamesCmdList, "dopewars": lambda: handleDopeWars(message_from_id, message, deviceID), "lemonstand": lambda: handleLemonade(message_from_id, message), + "blackjack": lambda: handleBlackJack(message_from_id, message), "ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel), "askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel), "joke": tell_joke, @@ -242,6 +243,43 @@ def handleLemonade(nodeID, message): time.sleep(1) return msg +def handleBlackJack(nodeID, message): + global jackTracker + msg = "" + + # if player sends a L for leave table + if message.lower().startswith("l"): + logger.debug(f"System: BlackJack: {nodeID} is leaving the table") + # add 16 hours to the player time to leave the table, this will be detected by bot logic as player leaving + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + jackTracker[i]['leaveTime'] = time.time() + 57600 + jackTracker[i]['cmd'] = "new" + else: + # Play BlackJack + msg = playBlackJack(nodeID=nodeID, message=message) + + # get player's last command from tracker + last_cmd = "" + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + last_cmd = jackTracker[i]['cmd'] + + # find higest dollar amount in tracker for high score + if last_cmd == "new": + high_score = 0 + for i in range(len(jackTracker)): + if jackTracker[i]['cash'] > high_score: + high_score = int(jackTracker[i]['cash']) + user = jackTracker[i]['nodeID'] + if user != 0: + msg += f" Ranking🥇:{get_name_from_number(user)} with {high_score} chips. " + + if last_cmd != "": + logger.debug(f"System: BlackJack: {nodeID} last command: {last_cmd}") + + return msg + def handle_wxc(message_from_id, deviceID, cmd): location = get_node_location(message_from_id, deviceID) if use_meteo_wxApi and not "wxc" in cmd and not use_metric: @@ -508,7 +546,6 @@ def onReceive(packet, interface): # play the game send_message(handleDopeWars(message_from_id, message_string, rxNode), channel_number, message_from_id, rxNode) - for i in range(0, len(lemonadeTracker)): if lemonadeTracker[i].get('nodeID') == message_from_id: # check if the player has played in the last 8 hours @@ -525,6 +562,23 @@ def onReceive(packet, interface): # play the game send_message(handleLemonade(message_from_id, message_string), channel_number, message_from_id, rxNode) + + for i in range(0, len(jackTracker)): + if jackTracker[i].get('nodeID') == message_from_id: + # check if the player has played in the last 8 hours + if jackTracker[i].get('time') > (time.time() - 28800): + playingGame = True + game = "BlackJack" + if llm_enabled: + logger.debug(f"System: LLM Disabled for {message_from_id} for duration of game") + + #if time exceeds 8 hours reset the player + if jackTracker[i].get('time') < (time.time() - 28800): + logger.debug(f"System: BlackJack: Resetting player {message_from_id}") + jackTracker.pop(i) + + # play the game + send_message(handleBlackJack(message_from_id, message_string), channel_number, message_from_id, rxNode) else: playingGame = False diff --git a/modules/blackjack.py b/modules/blackjack.py new file mode 100644 index 0000000..b8b18a8 --- /dev/null +++ b/modules/blackjack.py @@ -0,0 +1,431 @@ +# Port of https://github.com/Himan10/BlackJack +# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024 + +from random import choices, shuffle +from modules.log import * +import time + +jack_starting_cash = 100 # Replace 100 with your desired starting cash value +jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\ + 'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}] + +SUITS = ("♥️", "♦️", "♠️", "♣️") +RANKS = ( + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "J", + "Q", + "K", + "A", +) +VALUES = { + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "10": 10, + "J": 10, + "Q": 10, + "K": 10, + "A": 11, +} + +class Card: + def __init__(self, suit, rank): + self.suit = suit + self.rank = rank + + def __str__(self): + return self.rank + " of " + self.suit + +class Deck: + """ Creating a Deck of cards and Deal two cards to both player and dealer. """ + + def __init__(self): + self.deck = [] + self.player = [] + self.dealer = [] + for suit in SUITS: + for rank in RANKS: + self.deck.append((suit, rank)) + + def shuffle(self): + shuffle(self.deck) + + def deal_cards(self): + self.player = choices(self.deck, k=2) + self.delete_cards(self.player) + self.dealer = choices(self.deck, k=2) + self.delete_cards(self.dealer) # Delete Drawn Cards + return self.player, self.dealer + + def delete_cards(self, total_drawn): + """ Delete Drawn cards from the Decks """ + try: + for i in total_drawn: + self.deck.remove(i) + except ValueError: + pass + +class Hand: + """ Adding the values of player/dealer cards and change the values of Aces acc. to situation. """ + def __init__(self): + self.cards = [] + self.value = 0 + self.aces = 0 + + def add_cards(self, card): + self.cards.extend(card) + for count, ele in enumerate(card, 0): + if ele[1] == "A": + self.aces += 1 + self.value += VALUES[ele[1]] + self.adjust_for_ace() + + def adjust_for_ace(self): + while self.aces > 0 and self.value > 21: + self.value -= 10 + self.aces -= 1 + +class Chips: + """ Player/dealer chips for making bets and Adding/Deducting amount in/from Player's total. """ + def __init__(self): + self.total = jack_starting_cash + self.bet = 0 + self.winnings = 0 + + def win_bet(self): + self.total += self.bet + self.winnings += 1 + + def loss_bet(self): + self.total -= self.bet + self.winnings -= 1 + +def take_bet(bet_amount, player_money): + try: + while bet_amount > player_money or bet_amount <= 0: + return f"Enter a bet amount between 1 and {player_money}" + return bet_amount + + except TypeError: + return "Invalid bet amount" + +def success_rate(card, obj_h): + """ Calculate Success rate of 'HIT' new cards """ + msg = "" + rate = 0 + diff = 21 - obj_h.value + if diff != 0: + rate = (VALUES[card[0][1]] / diff) * 100 + + if rate < 100: + msg += f"If Hit, chance {100-int(rate)}% success, {int(rate)}% failure." + elif rate > 100: + l_rate = int(rate - (rate - 99)) # Round to 99 + if card[0][1] == "A": + l_rate -= 99 + msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success." + else: + msg += f"If Hit, a low chance of success." + return msg + +def hits(obj_de): + new_card = [obj_de.deal_cards()[0][0]] + # obj_h.add_cards(new_card) + return new_card + +def display_hand(hand): + # Display the cards in the hand nicely + d = "" # display + for card in hand: + d += f"{card[1]}{card[0]}" + if card != hand[-1]: + d += ", " + return d + +def show_some(player_cards, dealer_cards, obj_h): + msg = f"PLAYER[{obj_h.value}] {display_hand(player_cards)} " + msg += f"DEALER[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} " + return msg + +def show_all(player_cards, dealer_cards, obj_h, obj_d): + msg = f"PLAYER_CARDS [{obj_h.value}] {display_hand(player_cards)} " + msg += f"DEALER_CARDS [{obj_d.value}] {display_hand(dealer_cards)}" + return msg + +def player_bust(obj_h, obj_c): + if obj_h.value > 21: + obj_c.loss_bet() + return True + return False + +def player_wins(obj_h, obj_d, obj_c): + if any((obj_h.value == 21, obj_h.value > obj_d.value and obj_h.value < 21)): + obj_c.win_bet() + return True + return False + +def dealer_bust(obj_d, obj_h, obj_c): + if obj_d.value > 21: + if obj_h.value < 21: + obj_c.win_bet() + return True + return False + +def dealer_wins(obj_h, obj_d, obj_c): + if any((obj_d.value == 21, obj_d.value > obj_h.value and obj_d.value < 21)): + obj_c.loss_bet() + return True + return False + +def push(obj_h, obj_d): + if obj_h.value == obj_d.value: + return True + return False + +def player_surrender(obj_c): + obj_c.loss_bet() + return True + +def gameStats(p_count, d_count, draw_c): + msg = f"\n📊WINS:{p_count},DEALER:{d_count},DRAW:{draw_c}" + return msg + +def getLastCmdJack(nodeID): + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + return jackTracker[i]['cmd'] + return None + +def setLastCmdJack(nodeID, cmd): + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + jackTracker[i]['cmd'] = cmd + return True + return False + +def playBlackJack(nodeID, message): + # Initalize the Game + msg, last_cmd = '', None + p_win, d_win, draw = 0, 0, 0 + p_chips = Chips() + p_hand = Hand() + d_hand = Hand() + p_cards, d_cards = [], [] + bet_money = 0 + # Initalize the Cards + cards_deck = Deck() + cards_deck.shuffle() + p_cards, d_cards = cards_deck.deal_cards() + # Deal the cards to player and dealer + p_hand.add_cards(p_cards) + d_hand.add_cards(d_cards) + next_card = hits(cards_deck) + + # Check if player, use tracking + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + last_cmd = jackTracker[i]['cmd'] + p_chips.total = jackTracker[i]['cash'] + p_win = jackTracker[i]['gameStats']['p_win'] + d_win = jackTracker[i]['gameStats']['d_win'] + draw = jackTracker[i]['gameStats']['draw'] + bet_money = jackTracker[i]['bet'] + p_chips.bet = bet_money + if last_cmd == "playing": + p_cards = jackTracker[i]['p_cards'] + d_cards = jackTracker[i]['d_cards'] + p_hand = jackTracker[i]['p_hand'] + d_hand = jackTracker[i]['d_hand'] + next_card = jackTracker[i]['next_card'] + + if last_cmd is None: + # create new player if not in tracker + logger.debug(f"System: BlackJack: New Player {nodeID}") + jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\ + 'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card}) + return f"Welcome to BlackJack! you have {p_chips.total} chips, Whats your bet?" + + if getLastCmdJack(nodeID) == "new": + # Place Bet + try: + if message == "b": + if bet_money == 0: + bet_money = 5 + else: + bet_money = bet_money + + if bet_money != 0: + bet_money = int(bet_money) + else: + bet_money = int(message) + + if bet_money < p_chips.total or bet_money <= 0: + p_chips.bet = take_bet(bet_money, p_chips.total) + else: + return f"Invalid Bet, the maximum bet you can place is {p_chips.total}" + except ValueError: + p_chips.bet = 5 + + # Show the cards + msg += show_some(p_cards, d_cards, p_hand) + + # check for blackjack 21 and only two cards + if p_hand.value == 21 and len(p_hand.cards) == 2: + msg += "Player 🎰 BLAAAACKJACKKKK 💰" + p_chips.total += round(p_chips.bet * 1.5) + setLastCmdJack(nodeID, "dealerTurn") + # Save the game state + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + jackTracker[i]['cash'] = p_chips.total + break + else: + # Display the statistics + stats = success_rate(next_card, p_hand) + msg += stats + setLastCmdJack(nodeID, "betPlaced") + + + if getLastCmdJack(nodeID) == "betPlaced": + setLastCmdJack(nodeID, "playing") + msg += "(H)it,(S)tand,(F)orfit,(D)ouble" + + # save the game state + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + jackTracker[i]['cash'] = p_chips.total + jackTracker[i]['bet'] = p_chips.bet + jackTracker[i]['p_cards'] = p_cards + jackTracker[i]['d_cards'] = d_cards + jackTracker[i]['p_hand'] = p_hand + jackTracker[i]['d_hand'] = d_hand + jackTracker[i]['next_card'] = next_card + return msg + + + while getLastCmdJack(nodeID) == "playing": # Recall var. from hit and stand function + next_card = hits(cards_deck) + + # Get the statistics + stats = success_rate(next_card, p_hand) + + # Player's Turn + choice = message.lower() + + if choice == "hit" or choice == "h": + # hits(obj_de, p_hand) + p_hand.add_cards(next_card) + msg += show_some(p_hand.cards, d_cards, p_hand) + elif choice == "stand" or choice == "s": + setLastCmdJack(nodeID, "dealerTurn") + elif choice == "forfit" or choice == "f": + p_chips.bet = p_chips.bet / 2 + setLastCmdJack(nodeID, "dealerTurn") + p_hand.value += 21 + elif choice == "double" or choice == "d": + if p_chips.bet * 2 <= p_chips.total: + p_chips.bet *= 2 + next_d_card = hits(cards_deck) + p_hand.add_cards(next_d_card) + setLastCmdJack(nodeID, "dealerTurn") + else: + return "You can't Double Down, dont have enough chips" + else: + return "Invalid Choice" + + # Check if player bust + if player_bust(p_hand, p_chips): + d_win += 1 + msg += "Player BUUUSSTTT💥" + setLastCmdJack(nodeID, "dealerTurn") + + if getLastCmdJack(nodeID) == "playing": + msg += stats + msg += "[H,S,F,D,L]" + + # Save the game state + for i in range(len(jackTracker)): + if jackTracker[i]['nodeID'] == nodeID: + jackTracker[i]['cash'] = p_chips.total + jackTracker[i]['bet'] = p_chips.bet + jackTracker[i]['gameStats']['p_win'] = p_win + jackTracker[i]['gameStats']['d_win'] = d_win + jackTracker[i]['gameStats']['draw'] = draw + jackTracker[i]['p_cards'] = p_cards + jackTracker[i]['d_cards'] = d_cards + jackTracker[i]['p_hand'] = p_hand + jackTracker[i]['d_hand'] = d_hand + break + + if getLastCmdJack(nodeID) == "dealerTurn": + break + + return msg + + if getLastCmdJack(nodeID) == "dealerTurn": + if p_hand.value <= 21: + # Dealer's Turn + while d_hand.value < 17: + d_card = hits(cards_deck) + d_hand.add_cards(d_card) + if dealer_bust(d_hand, p_hand, p_chips): + p_win += 1 + msg += "Dealer BUUUSSTTT💥" + break + # Show all cards + msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand) + + # Check who wins + if push(p_hand, d_hand): + draw += 1 + msg += f"👌PUSH" + elif player_wins(p_hand, d_hand, p_chips): + p_win += 1 + msg += f"🎉PLAYER WINS🎰" + elif dealer_wins(p_hand, d_hand, p_chips): + d_win += 1 + msg += f"👎DEALER WINS" + else: + msg += f"👎DEALER WINS" + + # Display the Game Stats + msg += gameStats(str(p_win), str(d_win), str(draw)) + + # Display the chips left + if p_chips.total < 1: + if p_chips.total > 0: + msg += f"🪙Keep the change you filthy animal!" + else: + msg += "💸NO MORE MONEY! Game Over!" + p_chips.total = jack_starting_cash + else: + msg += f"💰You have {p_chips.total} chips left" + + msg += "(B)et or (L)eave table." + + # Reset the game + setLastCmdJack(nodeID, "new") + jackTracker[i]['cash'] = p_chips.total + jackTracker[i]['gameStats']['p_win'] = p_win + jackTracker[i]['gameStats']['d_win'] = d_win + jackTracker[i]['gameStats']['draw'] = draw + jackTracker[i]['p_cards'] = [] + jackTracker[i]['d_cards'] = [] + jackTracker[i]['p_hand'] = [] + jackTracker[i]['d_hand'] = [] + jackTracker[i]['time'] = time.time() + + return msg diff --git a/modules/settings.py b/modules/settings.py index 906241f..6796f25 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -86,6 +86,7 @@ else: # variables try: + # general useDMForResponse = config['general'].getboolean('respond_by_dm_only', True) publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time @@ -104,12 +105,14 @@ try: llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b + # sentry sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2 sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9 sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',') sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters + # location location_enabled = config['location'].getboolean('enabled', True) latitudeValue = config['location'].getfloat('lat', 48.50) longitudeValue = config['location'].getfloat('lon', -123.0) @@ -119,14 +122,17 @@ try: numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet + # bbs bbs_enabled = config['bbs'].getboolean('enabled', False) bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl') bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',') bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',') + # repeater repeater_enabled = config['repeater'].getboolean('enabled', False) repeater_channels = config['repeater'].get('repeater_channels', '').split(',') + # radio monitoring radio_detection_enabled = config['radioMon'].getboolean('enabled', False) rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532 sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2 @@ -134,9 +140,11 @@ try: signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN - + + # games dopewars_enabled = config['games'].getboolean('dopeWars', True) lemonade_enabled = config['games'].getboolean('lemonade', True) + blackjack_enabled = config['games'].getboolean('blackjack', True) except KeyError as e: print(f"System: Error reading config file: {e}") diff --git a/modules/system.py b/modules/system.py index ad3a5a6..8f0147a 100644 --- a/modules/system.py +++ b/modules/system.py @@ -88,11 +88,25 @@ if lemonade_enabled: from modules.lemonade import * # from the spudgunman/meshing-around repo trap_list = trap_list + ("lemonstand",) games_enabled = True + +# BlackJack Configuration +if blackjack_enabled: + from modules.blackjack import * # from the spudgunman/meshing-around repo + trap_list = trap_list + ("blackjack",) + games_enabled = True # Games Configuration if games_enabled is True: help_message = help_message + ", games" trap_list = trap_list + ("games",) + gamesCmdList = "CMD: " + if dopewars_enabled: + gamesCmdList += "DopeWars, " + if lemonade_enabled: + gamesCmdList += "LemonStand, " + if blackjack_enabled: + gamesCmdList += "BlackJack, " + gamesCmdList = gamesCmdList[:-2] # remove the last comma # Scheduled Broadcast Configuration if scheduler_enabled: