spudgunner

plant some potatos
This commit is contained in:
Kelly committed 2026-08-25 17:42:00 -07:00
1 parent 000c30a3c8
commit f5a3ac68bd
6 files changed
+915 -2

No files matched your search

+116
View File
@@ -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:**
- `<angle>,<power>` — Fire spud at that angle with that PSI power (e.g., `45,60`)
- `<angle>` — 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!
+755
View File
@@ -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()
+2
View File
@@ -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
@@ -500,6 +501,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
+9 -1
View File
@@ -217,6 +217,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
@@ -269,6 +275,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 = ""
@@ -518,7 +526,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: