diff --git a/config.template b/config.template index 88e9de7..09c8862 100644 --- a/config.template +++ b/config.template @@ -465,6 +465,7 @@ hamtest = True tictactoe = True wordOfTheDay = True battleShip = True +potatogunner = True # enable or disable the quiz game module questions are in data/quiz.json quiz = False diff --git a/mesh_bot.py b/mesh_bot.py index e5470e3..a0d5fff 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -16,7 +16,7 @@ import modules.settings as my_settings from modules.system import * # list of commands to remove from the default list for DM only -restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe", "tic-tac-toe", "quiz", "q:", "survey", "s:", "battleship"] +restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe", "tic-tac-toe", "quiz", "q:", "survey", "s:", "battleship", "spudgun", "spudgunner"] restrictedResponse = "πŸ€–only available in a Direct MessageπŸ“΅" # "" for none blackhole_mode = False @@ -111,6 +111,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM), "sms:": lambda: handle_sms(message_from_id, message), "solar": lambda: drap_xray_conditions() + "\n" + solar_conditions() + "\n" + get_noaa_scales_summary(), + "spudgun": lambda: handlePotatoGunner(message, message_from_id, deviceID), + "spudgunner": lambda: handlePotatoGunner(message, message_from_id, deviceID), "sun": lambda: handle_sun(message_from_id, deviceID, channel_number), "survey": lambda: surveyHandler(message, message_from_id, deviceID), "s:": lambda: surveyHandler(message, message_from_id, deviceID), @@ -1135,6 +1137,34 @@ def handleTicTacToe(message, nodeID, deviceID): return msg +def handlePotatoGunner(message, nodeID, deviceID): + global potatogunnerTracker + + tracker_entry = next((entry for entry in potatogunnerTracker if entry['nodeID'] == nodeID), None) + + # Handle end/exit command + if message.lower().startswith('e'): + if tracker_entry: + potatogunner.end(nodeID) + potatogunnerTracker.remove(tracker_entry) + return "πŸ₯” Thanks for playing! πŸ₯”" + + # If not found, create new tracker entry + if not tracker_entry: + potatogunnerTracker.append({ + "nodeID": nodeID, + "last_played": time.time() + }) + msg = "πŸ₯” POTATO GUNNER πŸ₯” Let's plant some spuds!\n" + msg += potatogunner.new_game(nodeID) + return msg + else: + tracker_entry["last_played"] = time.time() + + msg = potatogunner.play(nodeID, message) + return msg + + def handleBattleship(message, nodeID, deviceID): global battleshipTracker from modules.games import battleship @@ -2327,6 +2357,7 @@ gameTrackers = [ (tictactoeTracker, "TicTacToe", handleTicTacToe), (surveyTracker, "Survey", surveyHandler), (battleshipTracker, "Battleship", handleBattleship), + (potatogunnerTracker, "PotatoGunner", handlePotatoGunner), # quiz does not use a tracker (quizGamePlayer) always active ] diff --git a/modules/games/README.md b/modules/games/README.md index ec4ee73..2b27efb 100644 --- a/modules/games/README.md +++ b/modules/games/README.md @@ -9,6 +9,7 @@ - [Tic-Tac-Toe (2D/3D)](#tic-tac-toe-game-module) - [MasterMind](#mastermind-game-module) - [Battleship](#battleship-game-module) +- [Potato Gunner](#potato-gunner-game-module) - [Video Poker](#video-poker-game-module) - [Hangman](#hangman-game-module) - [Quiz](#quiz-game-module) @@ -444,6 +445,121 @@ Would you like to play again? (N)ormal, (H)ard, or e(X)pert? - Ported from [pwdkramer/pythonMastermind](https://github.com/pwdkramer/pythonMastermind) - Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024 +# Potato Gunner Game Module + +A silly, potato-themed artillery game for the Meshtastic mesh-bot. Fire spuds at moving targets, navigate wind and chaos events, and earn powerups to become the ultimate potato sharpshooter! + +## How to Play + +- **Start the Game:** + Send the command `spudgun`, or `spudgunner` via DM to the bot to start a new tournament. + +- **Gameplay:** + Each round, you'll see the target distance and wind conditions. Enter an elevation angle (0-90Β°) AND hairspray PSI pressure (0-100) to control your shot's velocity. + - **Input format:** `angle,power` (e.g., `45,60` for 45Β° at 60 PSI) + - **Quick hints:** `low,50` | `mid,50` | `high,50` (uses default 50 PSI) + - **Backward compatible:** Just enter angle (e.g., `45`) for default 50 PSI + - **Power mechanics:** + - 10 PSI: Short range, easy to control + - 50 PSI: Medium range, balanced (default) + - 100 PSI: Maximum range, harder to control + - **Accuracy radius:** Shots within the accuracy range hit the target (base 150 yards) + - **Wind effects:** Wind can increase or decrease your effective distance + - **Chaos events:** Random silly events like earthquakes, meteor showers, alien invasions, and more make aiming tricky! + +- **Commands:** + - `,` β€” Fire spud at that angle with that PSI power (e.g., `45,60`) + - `` β€” Fire at default 50 PSI (backward compatible, e.g., `45`) + - `low`, `mid`, `high` β€” Use quick elevation hints at 50 PSI + - `stats` β€” Display session statistics + - `new` β€” Advance to next round + - `e` β€” End tournament + +- **Scoring:** + - Base score: 100 points per hit + - Accuracy bonus: Up to +100 points (closer hits = more points) + - Streak bonus: +15 points for each consecutive hit + - Chaos survival bonus: +60 points for hitting during chaos events + - Round multiplier: Score multiplies by round number + +- **Powerups (Earned on Good Shots):** + - 🎯 **Accuracy β‰₯75%**: Homing Spud (auto-corrects aim by 15%) + - ⚑ **Accuracy β‰₯50%**: Super Spud (+60 yard accuracy radius) + - πŸ”₯ **3+ hit streak**: Potato Shield (free miss, one-time use) + - πŸ€ **Chaos survival**: Lucky Potato (auto-negates next chaos event) + +- **Difficulty Levels:** + - Target distance expands each round + - Wind intensity increases progressively + - Chaos events become more frequent and intense + - Round multiplier increases with score + +- **High Scores:** + The module tracks your best tournament score. Beat it to earn a "NEW HIGH SCORE!" message and lock in your achievement! + +- **Example Game Flow:** + +``` +πŸ₯” **Welcome to POTATO GUNNER!** πŸ₯” +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +πŸ† **High Score: 1250 pts** πŸ† + +πŸ₯” **ROUND 1 - POTATO ARTILLERY RANGE** πŸ₯” +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🎯 **Target Distance:** ~28425yd +πŸ’¨ **Wind:** +3yd (slight breeze) +πŸŒͺ️ **Difficulty:** GENTLE + +> 45 + +🎯 **Target Distance:** ~28425yd +πŸ’¨ **Wind:** +3yd (slight breeze) +πŸŒͺ️ **Difficulty:** GENTLE + +> 45,60 + +πŸ’£ SPUD FIRED AT 45.0Β° with 60 PSI +Velocity: 120m/s | Wind effect: +2yd +**Actual distance: 28510yd** | Target: 28425yd +Spread: 85yd (hit zone: Β±150yd) + +βœ… **HIT!** 🎯 +Excellent accuracy! +165 pts earned! + +πŸ“Š Session: 165 pts | Accuracy: 1/1 (100%) +Type 'new' for next round or 'e' to end: +``` + +## Notes + +- Single session per player (new game cancels previous session) +- Game sessions can span multiple rounds (10+ recommended for full experience) +- Each round becomes progressively harder with increased distance and wind +- Power affects velocity: higher PSI = longer range, more damage potential +- Chaos events add fun disruption - sometimes hitting during chaos is worth more! +- Play via DM for best experience and to avoid interference with other players +- High scores are stored persistently in `data/potatogunner_hs.pkl` + +## Tips for Success + +- Start with medium power (50 PSI) for balanced range, adjust based on target distance +- Use low power (10-30 PSI) for close targets or high precision needs +- Use high power (70-100 PSI) for distant targets or when you need maximum range +- Watch the wind direction and adjust your aim accordingly +- Land consecutive hits to build streaks and earn powerups +- Use Lucky Potato to negate difficult chaos events +- Push for high accuracy (75%+) to earn the "SHARPSHOOTER" rating +- Experiment with power/angle combinations to find what works best! + +## Chaos Events (12 types) + +Wind Gust, Earthquake, Rain Storm, Meteor Shower, Alien Invasion, Mischievous Squirrel, Bird Swarm, Angry Farmer, Gravity Flip, Inverse Wind, Ricochet Zone, Slow Motion β€” each with escalating intensity as you progress! + +## Credits + +- Based on the classic [Gunner Artillery Game](https://github.com/coding-horror/basic-computer-games/blob/main/42_Gunner/python/gunner.py) +- Adapted and enhanced with chaos system, powerups, and potato theming for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025 + # Video Poker Game Module A text-based Video Poker game for the Meshtastic mesh-bot. Play classic five-card draw poker, place your bets, and try to build your bankroll! diff --git a/modules/games/potatogunner.py b/modules/games/potatogunner.py new file mode 100644 index 0000000..66f0af9 --- /dev/null +++ b/modules/games/potatogunner.py @@ -0,0 +1,755 @@ +#!/usr/bin/env python3 +# Potato Gunner - Potato-Themed Artillery Game for MeshBot +# Based on: https://github.com/coding-horror/basic-computer-games/blob/main/42_Gunner/python/gunner.py +# 2026 Kelly Keeton K7MHI + +from math import sin, cos, radians +from random import random, randint, choice +import time +import pickle +from modules.log import logger + + +def saveHSPotato(nodeID, highScore): + """Save the game high_score to pickle.""" + highScore = {'nodeID': nodeID, 'highScore': highScore} + try: + with open('data/potatogunner_hs.pkl', 'wb') as file: + pickle.dump(highScore, file) + except FileNotFoundError: + logger.debug("System: PotatoGunner: Creating new data/potatogunner_hs.pkl file") + with open('data/potatogunner_hs.pkl', 'wb') as file: + pickle.dump(highScore, file) + + +def loadHSPotato(): + """Load the game high_score from pickle.""" + try: + with open('data/potatogunner_hs.pkl', 'rb') as file: + highScore = pickle.load(file) + return highScore + except FileNotFoundError: + logger.debug("System: PotatoGunner: Creating new data/potatogunner_hs.pkl file") + highScore = {'nodeID': 0, 'highScore': 0} + with open('data/potatogunner_hs.pkl', 'wb') as file: + pickle.dump(highScore, file) + return highScore + + +class PotatoGunner: + """ + Silly potato-themed artillery game. Players fire spuds at moving targets + with wind, chaos events, and powerups. Multi-round tournament format. + """ + + def __init__(self): + self.games = {} # {nodeID: game_state_dict} + + @staticmethod + def format_yards(value): + """Format yardage with comma separators for readability.""" + return f"{int(value):,d}" + + def new_game(self, nodeID, difficulty='normal'): + """Start a new tournament session.""" + # Load existing highscore + hs_data = loadHSPotato() + high_score = hs_data.get('highScore', 0) + + self.games[nodeID] = { + 'round': 1, + 'score': 0, + 'rounds_won': 0, + 'total_shots': 0, + 'total_hits': 0, + 'accuracy_streak': 0, + 'powerups': { + 'super_spud': 0, + 'homing_spud': 0, + 'lucky_potato': 0, + 'potato_shield': 0 + }, + 'powerups_used': { + 'super_spud': 0, + 'homing_spud': 0, + 'lucky_potato': 0, + 'potato_shield': 0 + }, + 'difficulty': difficulty, + 'chaos_event': None, + 'last_accuracy': 0.0, + 'start_time': time.time(), + 'high_score': high_score + } + + # intro + intro = f"πŸ† High Score: {high_score} pts\n" + intro += "πŸ’£ Fire spuds at targets!\n" + intro += " Angle: 0-90Β° | Power: 0-100 PSI\n" + intro += " Commands: hint, stats, exit\n" + + return intro + self._display_round_intro(nodeID) + + def play(self, nodeID, input_msg): + """Process a single shot/action in the game.""" + try: + if nodeID not in self.games: + return self.new_game(nodeID) + + g = self.games[nodeID] + input_str = input_msg.strip().lower() + + # Handle quit commands + if input_str in ('end', 'e', 'quit', 'q', 'exit'): + return self.end(nodeID) + + # Handle stats command + if input_str in ('stats', 's', 'score'): + return self._display_stats(nodeID) + + # Handle hint command + if input_str in ('hint', 'h', 'help', 'examples'): + return self._show_launch_hints(nodeID) + elevation = None + power = 50 # Default power if not specified + + hints = { + 'low': 15, 'min': 10, 'ground': 5, + 'mid': 45, 'medium': 45, 'normal': 45, + 'high': 70, 'max': 80, 'steep': 85 + } + + try: + # Check if format is "angle,power" (numeric or hint word) + if ',' in input_str: + parts = input_str.split(',') + if len(parts) != 2: + return "⚠️ Format: elevation,power (e.g., 45,60 or high,30)" + + # Try numeric first, then hint words + elevation_str = parts[0].strip() + try: + elevation = float(elevation_str) + except ValueError: + # Check if it's a hint word + if elevation_str in hints: + elevation = hints[elevation_str] + else: + return f"⚠️ '{elevation_str}' not recognized. Use angle (0-90) or hint: {', '.join(hints.keys())}" + + try: + power = float(parts[1].strip()) + except ValueError: + return "⚠️ Power must be 0-100 PSI (e.g., high,30)" + else: + # Just elevation (no power specified) + try: + elevation = float(input_str) + except ValueError: + # Check if it's a hint word + if input_str in hints: + elevation = hints[input_str] + power = 50 # Default power + else: + return "⚠️ Enter: angle (0-90Β°) or angle,power (e.g., 45,60)\nHints: low, mid, high, max\nCommands: hint, stats, e" + else: + power = 50 # Default power if numeric angle only + + # Validate elevation + if elevation < 0 or elevation > 90: + return "⚠️ Elevation must be 0-90 degrees. Try again!" + + # Validate power + if power < 0 or power > 100: + return "⚠️ Power must be 0-100 PSI. Try again!" + + except ValueError: + return "⚠️ Error parsing input. Use: angle,power (e.g., 45,60 or high,30)" + + # Process the shot + return self._fire_spud(nodeID, elevation, power) + + except Exception as e: + return f"⚠️ Error: {e}" + + def _fire_spud(self, nodeID, elevation, power=50): + """Fire a spud at the target and calculate hit.""" + g = self.games[nodeID] + + # Get/create round state if needed + if 'current_round_state' not in g: + g['current_round_state'] = self._generate_round_state(nodeID) + + rs = g['current_round_state'] + g['total_shots'] += 1 + + # Enhanced physics: calculate spud distance with better ballistics + # Using realistic projectile motion: range = (vβ‚€Β² * sin(2ΞΈ)) / g + # Power scales from 0-100 PSI, affecting velocity + elevation_rad = radians(elevation) + + # Base distance calculation with power scaling + gravity = 9.81 + # Scale velocity based on power (0-100 PSI) where 50 PSI = base 100 m/s + # Formula: velocity = (power / 50) * 100, so 50 PSI = 100 m/s, 100 PSI = 200 m/s, 10 PSI = 20 m/s + base_velocity = (power / 50.0) * 100 # m/s (scales with PSI) + base_distance = (base_velocity ** 2 * sin(2 * elevation_rad)) / gravity + base_distance = base_distance * 100 # Convert to yards (roughly) + + # Apply chaos effects to the trajectory + wind_factor = rs.get('wind', 0) + + # Check if Lucky Potato should be used (automatic on chaos survival) + lucky_active = False + if rs['chaos_event'] and g['powerups']['lucky_potato'] > 0: + # Auto-use lucky potato to skip chaos effect + g['powerups']['lucky_potato'] -= 1 + g['powerups_used']['lucky_potato'] += 1 + lucky_active = True + wind_factor = 0 # Nullify chaos effect + else: + # Handle specific chaos events with their effects + if rs['chaos_event'] == 'earthquake': + wind_factor += randint(-100, 100) # Adds randomness + elif rs['chaos_event'] == 'inverse_wind': + wind_factor = -wind_factor + elif rs['chaos_event'] == 'ricochet': + # Spud might bounce unexpectedly + ricochet_chance = random() + if ricochet_chance > 0.7: # 30% chance of crazy ricochet + base_distance += randint(-500, 500) + elif rs['chaos_event'] == 'gravity_flip': + # Inverted gravity means opposite trajectory behavior + base_distance = base_distance * -0.5 + + actual_distance = base_distance + wind_factor + actual_distance = max(0, actual_distance) # Can't be negative + + # Apply powerup: homing spud (auto-correction toward target) + powerup_correction = 0 + if g['powerups']['homing_spud'] > 0: + powerup_correction = (rs['target_distance'] - actual_distance) * 0.15 # 15% auto-correct + actual_distance += powerup_correction + + # Calculate proximity and accuracy + proximity = abs(actual_distance - rs['target_distance']) + + # Accuracy radius scales with powerups and difficulty + base_accuracy_radius = 670 # Yards + if g['powerups']['super_spud'] > 0: + base_accuracy_radius += 60 # +60 accuracy improvement + if rs['chaos_event'] and not lucky_active: + base_accuracy_radius -= 30 # Chaos makes it harder (unless Lucky saved you) + + accuracy_radius = max(50, base_accuracy_radius) # Min 50 yard radius + + # Build response message + msg = f"πŸ₯”FIRED ATπŸš€ {elevation}Β° with {int(power)} PSI\n" + msg += f"Distance calc: {self.format_yards(base_distance)}yd" + if powerup_correction != 0: + msg += f" (homing: {int(powerup_correction):+d}yd)" + msg += "\n" + msg += f"Velocity: {int(base_velocity)}m/s | Wind effect: {int(wind_factor):+d}yd\n" + msg += f"Actual distance: {self.format_yards(actual_distance)}yd | Target: {self.format_yards(rs['target_distance'])}yd\n" + msg += f"Missed by: {self.format_yards(proximity)}yd\n" + + # Add chaos event narrative if present + if rs['chaos_event']: + chaos_info = self._get_chaos_description(rs['chaos_event']) + msg += f"\n{chaos_info['title']} {chaos_info['flavor']}\n" + if lucky_active: + msg += "πŸ€ Lucky Potato activated! Chaos effect negated!\n" + + msg += "\n" + + # Determine hit or miss + if proximity <= accuracy_radius: + # HIT! + msg += "🎯 DIRECT HIT! πŸ₯”πŸ’₯\n" + g['total_hits'] += 1 + g['rounds_won'] += 1 + g['accuracy_streak'] += 1 + + # Calculate points with multiple modifiers + accuracy_bonus = max(0, int((accuracy_radius - proximity) / accuracy_radius * 100)) + combo_bonus = min(75, g['accuracy_streak'] * 15) # +15 per streak, max 75 + round_multiplier = 1.0 + (g['round'] - 1) * 0.15 # 15% per round + + powerup_bonus = 0 + if g['powerups']['super_spud'] > 0: + powerup_bonus += 50 # Increased from 25 + g['powerups']['super_spud'] -= 1 + g['powerups_used']['super_spud'] += 1 + msg += "⚑ Super Spud activated!\n" + + if g['powerups']['homing_spud'] > 0: + powerup_bonus += 30 + g['powerups']['homing_spud'] -= 1 + g['powerups_used']['homing_spud'] += 1 + msg += "🎯 Homing Spud guided you to victory!\n" + + chaos_bonus = 0 + if rs['chaos_event']: + chaos_bonus = 60 # Higher bonus for chaos survival + chaos_name = rs['chaos_event'].replace('_', ' ').title() + msg += f"🎊 Survived chaos ({chaos_name}): +{chaos_bonus} bonus!\n" + + # Calculate total points + base_points = 100 + total_points = int((base_points + accuracy_bonus + combo_bonus + powerup_bonus + chaos_bonus) * round_multiplier) + + g['score'] += total_points + g['last_accuracy'] = min(100, (accuracy_radius - proximity)) / accuracy_radius + + msg += f"\n✨ +{total_points} points!\n" + msg += f" Base: {base_points} | Accuracy: +{accuracy_bonus} | " + msg += f"Streak: +{combo_bonus} | RoundΓ—{round_multiplier:.2f}\n" + + # Award powerups on successful hit + powerup_msg = self._award_powerups(nodeID) + if powerup_msg: + msg += f"\n{powerup_msg}" + + msg += f"\nπŸ“Š Session: {g['score']} pts | Accuracy: {g['total_hits']}/{g['total_shots']} " + msg += f"({100*g['total_hits']//max(1, g['total_shots'])}%)" + else: + # MISS + msg += "❌ MISS! ⚠️\n" + if actual_distance < rs['target_distance']: + msg += f"Short by {self.format_yards(proximity)} yards\n" + else: + msg += f"Over by {self.format_yards(proximity)} yards\n" + + g['accuracy_streak'] = 0 + + msg += f"\nπŸ“Š Session: {g['score']} pts | Accuracy: {g['total_hits']}/{g['total_shots']} " + msg += f"({100*g['total_hits']//max(1, g['total_shots'])}%)" + + # Clear round state and auto-advance to next round display + del g['current_round_state'] + msg += "\n" + self._display_round_intro(nodeID) + + return msg + + def _award_powerups(self, nodeID): + """Determine and award powerups on successful hit based on performance.""" + g = self.games[nodeID] + msg = "" + awarded = [] + + # Accuracy-based powerups + if g['last_accuracy'] >= 0.75: # 75% accuracy or better + if g['total_hits'] > 0: # Make sure we have actual hits + g['powerups']['homing_spud'] += 1 + awarded.append("🎯 Homing Spud (auto-aim next round)") + + if g['last_accuracy'] >= 0.50: # 50% accuracy or better + if g['total_hits'] > 0: + g['powerups']['super_spud'] += 1 + awarded.append("⚑ Super Spud (+40% accuracy)") + + # Streak-based powerup + if g['accuracy_streak'] >= 3: + g['powerups']['potato_shield'] = min(2, g['powerups'].get('potato_shield', 0) + 1) + awarded.append("πŸ›‘οΈ Potato Shield (free miss next round)") + + # Chaos survival powerup + if g.get('current_round_state', {}).get('chaos_event'): + # 20% chance to get lucky potato on chaos survival + if random() < 0.2: + g['powerups']['lucky_potato'] = min(1, g['powerups'].get('lucky_potato', 0) + 1) + awarded.append("πŸ€ Lucky Potato (skip chaos)") + + if awarded: + msg = "\n🎁 Powerups Earned:\n" + for pup in awarded: + msg += f" β€’ {pup}\n" + + return msg + + def _get_chaos_description(self, chaos_event): + """Get detailed silly description for chaos events.""" + descriptions = { + 'wind_gust': { + 'title': 'πŸŒͺ️ WIND GUST!', + 'flavor': 'A sudden gust catches your spud mid-flight!', + 'effect': 'Trajectory unpredictable!', + }, + 'earthquake': { + 'title': '🌍 EARTHQUAKE!', + 'flavor': 'The ground shakes violently beneath the artillery range!', + 'effect': 'Everything shifts unexpectedly!', + }, + 'rain': { + 'title': '🌧️ HEAVY RAIN!', + 'flavor': 'Torrential downpour obscures the target!', + 'effect': 'Visibility severely reduced!', + }, + 'meteor': { + 'title': 'β˜„οΈ METEOR SHOWER!', + 'flavor': 'Meteorites crash nearby, creating shockwaves!', + 'effect': 'Field distorted by impacts!', + }, + 'alien': { + 'title': 'πŸ‘½ ALIEN ABDUCTION!', + 'flavor': 'A UFO hovers overhead, messing with gravity!', + 'effect': 'Physics go haywire!', + }, + 'squirrel': { + 'title': '🐿️ SQUIRREL ATTACK!', + 'flavor': 'A mischievous squirrel steals a spud from the launcher!', + 'effect': 'Target distracted but launcher disrupted!', + }, + 'birds': { + 'title': 'πŸ¦… BIRD STRIKE!', + 'flavor': 'A flock of birds interferes with your trajectory!', + 'effect': 'Unpredictable interference patterns!', + }, + 'farmer': { + 'title': 'πŸ‘¨β€πŸŒΎ FARMER IN FIELD!', + 'flavor': 'The farmer wanders into the firing zone!', + 'effect': 'Must avoid hitting him! (Or get penalty)', + }, + 'gravity_flip': { + 'title': '⬆️ GRAVITY FLIPPED!', + 'flavor': 'A mysterious force inverts gravity in the field!', + 'effect': 'Spuds fly in weird trajectories!', + }, + 'inverse_wind': { + 'title': 'πŸ’¨ WIND REVERSAL!', + 'flavor': 'The wind suddenly switches direction!', + 'effect': 'Expect opposite effects!', + }, + 'ricochet': { + 'title': 'πŸ”€ RICOCHET MODE!', + 'flavor': 'Mysterious bouncy energy permeates the range!', + 'effect': 'Spuds might bounce unpredictably!', + }, + 'slowmo': { + 'title': '🐌 SLOW-MOTION!', + 'flavor': 'Time slows down mysteriously...', + 'effect': 'Everything moves in slow-mo!', + }, + } + + return descriptions.get(chaos_event, { + 'title': '⚑ CHAOS!', + 'flavor': 'Something weird is happening!', + 'effect': 'Unknown chaos detected!', + }) + + def _generate_round_state(self, nodeID): + """Generate round difficulty, chaos event, target, etc. with scaling difficulty.""" + g = self.games[nodeID] + round_num = g['round'] + score = g['score'] + + # Dynamic difficulty based on round AND score + # Early rounds are easier, later rounds harder + if round_num <= 2: + base_distance_range = (28000, 38000) + wind_max = 15 + chaos_chance = 0.20 + chaos_intensity = 'gentle' + elif round_num <= 4: + base_distance_range = (22000, 38000) + wind_max = 30 + chaos_chance = 0.35 + chaos_intensity = 'moderate' + elif round_num <= 6: + base_distance_range = (18000, 40000) + wind_max = 40 + chaos_chance = 0.45 + chaos_intensity = 'wild' + else: + base_distance_range = (15000, 45000) + wind_max = 50 + (round_num - 6) * 5 # Escalate further + chaos_chance = 0.55 + (round_num - 6) * 0.05 + chaos_intensity = 'insane' + + # If player is doing well, bump up difficulty slightly + if score > 500: + wind_max += 10 + chaos_chance += 0.05 + if score > 1500: + wind_max += 15 + chaos_chance += 0.10 + + base_distance = randint(base_distance_range[0], base_distance_range[1]) + + # More realistic wind: comes in gusts (can be positive or negative) + wind_bias = (random() - 0.5) * 2 # Bias wind -1 to +1 + wind_magnitude = random() * wind_max + wind_factor = wind_bias * wind_magnitude + + # Chaos event selection - weighted based on intensity + chaos_event = None + if random() < chaos_chance: + if chaos_intensity == 'gentle': + # Mostly environmental, not too harsh + chaos_event = choice(['wind_gust', 'rain', 'birds']) + elif chaos_intensity == 'moderate': + # Mix of environmental and character + chaos_event = choice([ + 'wind_gust', 'rain', 'birds', 'squirrel', 'earthquake', 'farmer' + ]) + elif chaos_intensity == 'wild': + # Include rule mutations + chaos_event = choice([ + 'wind_gust', 'earthquake', 'rain', 'alien', + 'squirrel', 'birds', 'farmer', + 'gravity_flip', 'inverse_wind', 'ricochet' + ]) + else: # insane + # All chaos types available + chaos_event = choice([ + 'wind_gust', 'earthquake', 'rain', 'meteor', 'alien', + 'squirrel', 'birds', 'farmer', + 'gravity_flip', 'inverse_wind', 'ricochet', 'slowmo' + ]) + + # Apply chaos event effects + if chaos_event == 'wind_gust': + wind_factor *= 2.5 # Sudden wind increase + elif chaos_event == 'earthquake': + base_distance += randint(-1000, 1000) + wind_factor += randint(-50, 50) + elif chaos_event == 'rain': + # Rain adds unpredictability + wind_factor += (random() - 0.5) * 200 + elif chaos_event == 'meteor': + # Meteor strikes can create shockwaves + base_distance += randint(-2000, 2000) + wind_factor *= 1.5 + elif chaos_event == 'inverse_wind': + wind_factor = -wind_factor + elif chaos_event == 'gravity_flip': + # Reduced effectiveness for gravity flip + pass # Applied in _fire_spud + elif chaos_event == 'ricochet': + # Ricochet is handled in _fire_spud + pass + + # Ensure distance remains positive + base_distance = max(5000, base_distance) + + return { + 'target_distance': base_distance, + 'wind': wind_factor, + 'chaos_event': chaos_event, + 'chaos_intensity': chaos_intensity, + 'created_at': time.time() + } + + def _start_round(self, nodeID): + """Start a new round.""" + g = self.games[nodeID] + g['round'] += 1 + if 'current_round_state' in g: + del g['current_round_state'] + return self._display_round_intro(nodeID) + + def _display_round_intro(self, nodeID): + """Display introduction to a round with detailed info.""" + g = self.games[nodeID] + rs = self._generate_round_state(nodeID) + g['current_round_state'] = rs + + msg = f"\nπŸ₯” ROUND {g['round']} πŸ₯”\n" + msg += f"Score: {g['score']} | Wins: {g['rounds_won']}/{g['round']}\n" + + # Calculate and display stats + if g['total_shots'] > 0: + accuracy_pct = 100 * g['total_hits'] // g['total_shots'] + msg += f"Hit Rate: {accuracy_pct}%" + if g['accuracy_streak'] > 0: + msg += f" | πŸ”₯ Streak: {g['accuracy_streak']}" + msg += "\n\n" + + # Target info + msg += f"🎯 Target: ~{self.format_yards(rs['target_distance'])}yd\n" + if rs['wind'] != 0: + msg += f"πŸ’¨ Wind: {int(rs['wind']):+d}yd\n" + + # Chaos event announcement + if rs['chaos_event']: + chaos_info = self._get_chaos_description(rs['chaos_event']) + msg += f"\n⚠️ {chaos_info['title']} ⚠️\n" + msg += f" {chaos_info['flavor']}\n" + msg += f" β†’ {chaos_info['effect']}\n" + + pups = [] + if g['powerups']['super_spud'] > 0: + pups.append(f" ⚑ Super Spud Γ—{g['powerups']['super_spud']}") + if g['powerups']['homing_spud'] > 0: + pups.append(f" 🎯 Homing Γ—{g['powerups']['homing_spud']}") + if g['powerups']['lucky_potato'] > 0: + pups.append(f" πŸ€ Lucky Γ—{g['powerups']['lucky_potato']}") + if g['powerups']['potato_shield'] > 0: + pups.append(f" πŸ›‘οΈ Shield Γ—{g['powerups']['potato_shield']}") + + if pups: + msg += f"\nπŸ’ͺ Powerups:\n" + msg += "\n".join(pups) + "\n" + + msg += "\nπŸ”« Angle,Power?" + return msg + + def _display_stats(self, nodeID): + """Display current session statistics.""" + if nodeID not in self.games: + return "No active game session." + + g = self.games[nodeID] + accuracy_pct = 100 * g['total_hits'] // max(1, g['total_shots']) + + msg = "πŸ“Š STATS πŸ“Š\n" + msg += f"Round: {g['round']} | Score: {g['score']} | Wins: {g['rounds_won']}/{g['round']}\n" + msg += f"Shots: {g['total_shots']} | Hits: {g['total_hits']} ({accuracy_pct}%) | Streak: {g['accuracy_streak']}\n" + + + if (g['powerups']['super_spud'] + g['powerups']['homing_spud'] + + g['powerups']['lucky_potato'] + g['powerups']['potato_shield']) > 0: + msg += "πŸ’ͺ Active Powerups:\n" + if g['powerups']['super_spud'] > 0: + msg += f" ⚑ Super Spud Γ—{g['powerups']['super_spud']}\n" + if g['powerups']['homing_spud'] > 0: + msg += f" 🎯 Homing Γ—{g['powerups']['homing_spud']}\n" + if g['powerups']['lucky_potato'] > 0: + msg += f" πŸ€ Lucky Γ—{g['powerups']['lucky_potato']}\n" + if g['powerups']['potato_shield'] > 0: + msg += f" πŸ›‘οΈ Shield Γ—{g['powerups']['potato_shield']}\n" + + msg += "\nFire away! Angle,Power (e.g., 45,60)" + return msg + + def _show_launch_hints(self, nodeID): + """Show example launches with different angle/power combinations.""" + if nodeID not in self.games: + return "No active game session." + + g = self.games[nodeID] + rs = g.get('current_round_state') + if not rs: + rs = self._generate_round_state(nodeID) + + target = rs['target_distance'] + + msg = "🎯 **LAUNCH EXAMPLES** 🎯\n" + if rs['wind'] != 0: + msg += f"Target: {self.format_yards(target)}yd | Wind: {int(rs['wind']):+d}yd\n\n" + else: + msg += f"Target: {self.format_yards(target)}yd\n\n" + + # Calculate 3 example launches with different strategies + examples = [ + ("Low & Slow", 15, 30), + ("Mid Balanced", 45, 50), + ("High & Fast", 70, 80), + ] + + for name, angle, power in examples: + # Quick physics calculation + elevation_rad = radians(angle) + gravity = 9.81 + base_velocity = (power / 50.0) * 100 + base_distance = (base_velocity ** 2 * sin(2 * elevation_rad)) / gravity + base_distance = base_distance * 100 + wind_factor = rs.get('wind', 0) + actual_distance = base_distance + wind_factor + actual_distance = max(0, actual_distance) + proximity = abs(actual_distance - target) + + # Show result + accuracy_emoji = "🎯" if proximity < 500 else "⚑" if proximity < 1000 else "πŸ“" + msg += f"{accuracy_emoji} {name}: {angle}Β°,{power}PSI β†’ {self.format_yards(actual_distance)}yd" + if proximity < 500: + msg += " (Close!)" + msg += "\n" + + msg += "\nTry: 45,60 or high,40 or just low" + return msg + + def end(self, nodeID): + """End the game and display final score.""" + if nodeID not in self.games: + return "No active game." + + g = self.games[nodeID] + + # Calculate performance rating + accuracy_pct = (100 * g['total_hits']) // max(1, g['total_shots']) + if accuracy_pct >= 75: + rating = "πŸ† SHARPSHOOTER πŸ†" + elif accuracy_pct >= 60: + rating = "⭐ EXPERT ⭐" + elif accuracy_pct >= 45: + rating = "πŸ‘ DECENT πŸ‘" + elif accuracy_pct >= 30: + rating = "πŸ₯” SPUD-SLINGER πŸ₯”" + else: + rating = "πŸŽͺ CHAOS CREATOR πŸŽͺ" + + # Check if new highscore + is_high_score = g['score'] > g['high_score'] + if is_high_score: + saveHSPotato(nodeID, g['score']) + + msg = "\n🏁 TOURNAMENT COMPLETE! 🏁\n" + msg += f"Rating: {rating}\n\n" + msg += "πŸ“Š FINAL STATISTICS:\n" + msg += f" Final Score: {g['score']} pts" + if is_high_score: + msg += " πŸŽ‰ NEW HIGH SCORE! πŸŽ‰" + elif g['high_score'] > 0: + msg += f" (High Score: {g['high_score']})" + msg += "\n" + msg += f" Rounds: {g['round']} completed\n" + msg += f" Wins: {g['rounds_won']}/{g['round']} rounds ({100*g['rounds_won']//max(1, g['round'])}%)\n" + msg += f" Accuracy: {g['total_hits']}/{g['total_shots']} ({accuracy_pct}%)\n" + msg += f" Longest Streak: {g['accuracy_streak']} hits\n\n" + + # Powerup usage stats + used_any = any(g['powerups_used'].values()) + if used_any: + msg += "πŸ’ͺ POWERUPS USED:\n" + if g['powerups_used']['super_spud'] > 0: + msg += f" ⚑ Super Spuds: {g['powerups_used']['super_spud']}x\n" + if g['powerups_used']['homing_spud'] > 0: + msg += f" 🎯 Homing Spuds: {g['powerups_used']['homing_spud']}x\n" + if g['powerups_used']['lucky_potato'] > 0: + msg += f" πŸ€ Lucky Potatoes: {g['powerups_used']['lucky_potato']}x\n" + if g['powerups_used']['potato_shield'] > 0: + msg += f" πŸ›‘οΈ Shields: {g['powerups_used']['potato_shield']}x\n" + msg += "\n" + + # Fun facts + msg += "πŸ“ˆ SESSION HIGHLIGHTS:\n" + if g['score'] > 2000: + msg += " 🌟 LEGENDARY PERFORMANCE!\n" + elif g['score'] > 1500: + msg += " 🎊 IMPRESSIVE WORK!\n" + elif g['score'] > 1000: + msg += " πŸ‘ SOLID EFFORT!\n" + else: + msg += " 🌱 PRACTICE MAKES PERFECT!\n" + + if accuracy_pct >= 75: + msg += " πŸ’― EXCEPTIONAL ACCURACY!\n" + if g['accuracy_streak'] >= 5: + msg += f" πŸ”₯ HOT HAND: {g['accuracy_streak']} HIT STREAK!\n" + if g['round'] >= 10: + msg += " 🎯 WENT THE DISTANCE!\n" + + msg += "\nπŸ₯” Thanks for playing! Type 'potato' to play again." + + # Clean up + if nodeID in self.games: + del self.games[nodeID] + + return msg + + +# Singleton instance for bot integration +potatogunner = PotatoGunner() + diff --git a/modules/settings.py b/modules/settings.py index 27ad3d8..44b2d96 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -51,6 +51,7 @@ dwPlayerTracker = [] # DopeWars player tracker jackTracker = [] # Jack game tracker mindTracker = [] # Mastermind (mmind) game tracker battleshipTracker = [] # Battleship game tracker +potatogunnerTracker = [] # Potato Gunner game tracker # Memory Management Constants MAX_MSG_HISTORY = 250 @@ -501,6 +502,7 @@ try: surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True) wordOfTheDay = config['games'].getboolean('wordOfTheDay', True) battleship_enabled = config['games'].getboolean('battleShip', True) + potatogunner_enabled = config['games'].getboolean('potatogunner', True) # messaging settings responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7 diff --git a/modules/system.py b/modules/system.py index 7403b88..e15af9d 100644 --- a/modules/system.py +++ b/modules/system.py @@ -223,6 +223,12 @@ if tictactoe_enabled: from modules.games.tictactoe import TicTacToe # from the spudgunman/meshing-around repo tictactoe = TicTacToe(display_module=None) trap_list = trap_list + ("tictactoe","tic-tac-toe",) + games_enabled = True + +if potatogunner_enabled: + from modules.games.potatogunner import potatogunner # from the spudgunman/meshing-around repo + trap_list = trap_list + ("spudgunner","spudgun",) + games_enabled = True if quiz_enabled: from modules.games.quiz import * # from the spudgunman/meshing-around repo @@ -275,6 +281,8 @@ if games_enabled is True: gamesCmdList += "ticTacToe, " if battleship_enabled: gamesCmdList += "battleship, " + if potatogunner_enabled: + gamesCmdList += "spudgunner, " gamesCmdList = gamesCmdList[:-2] # remove the last comma else: gamesCmdList = "" @@ -524,7 +532,7 @@ def cleanup_game_trackers(current_time): tracker_names = [ 'dwPlayerTracker', 'lemonadeTracker', 'jackTracker', 'vpTracker', 'mindTracker', 'golfTracker', - 'hangmanTracker', 'hamtestTracker', 'tictactoeTracker', 'surveyTracker', 'battleshipTracker' + 'hangmanTracker', 'hamtestTracker', 'tictactoeTracker', 'surveyTracker', 'battleshipTracker', 'potatogunnerTracker' ] for tracker_name in tracker_names: