like soccer but with more stuff to do
This commit is contained in:
Kelly committed 2026-08-25 18:10:40 -07:00
1 parent 000c30a3c8
commit e6a7f97ee4
6 files changed
+1170 -2

No files matched your search

+38 -1
View File
@@ -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", "football"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
blackhole_mode = False
@@ -60,6 +60,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
"earthquake": lambda: handleEarthquake(message, message_from_id, deviceID),
"email:": lambda: handle_email(message_from_id, message),
"football": lambda: handleFootball(message, message_from_id, deviceID),
"games": lambda: gamesCmdList,
"globalthermonuclearwar": lambda: handle_gTnW(),
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
@@ -1134,6 +1135,41 @@ def handleTicTacToe(message, nodeID, deviceID):
return msg
def handleFootball(message, nodeID, deviceID):
"""Handle Football game commands.
User vs Bot football game with natural language commands.
"""
global footballTracker
msg_lower = message.lower().strip()
tracker_entry = next((entry for entry in footballTracker if entry['nodeID'] == nodeID), None)
# End/exit command
if msg_lower.startswith('end') or msg_lower.startswith('exit') or msg_lower.startswith('quit'):
if tracker_entry:
footballTracker.remove(tracker_entry)
return "Thanks for playing Football! 🏈"
# New game command
if msg_lower.startswith('new') or not tracker_entry:
if not tracker_entry:
footballTracker.append({
"nodeID": nodeID,
"last_played": time.time(),
})
msg = football.new_game(nodeID, winning_score=20)
return msg
# Update last played time
if tracker_entry:
tracker_entry["last_played"] = time.time()
# Play command
msg = football.play(nodeID, message)
return msg
def handleBattleship(message, nodeID, deviceID):
global battleshipTracker
from modules.games import battleship
@@ -2326,6 +2362,7 @@ gameTrackers = [
(tictactoeTracker, "TicTacToe", handleTicTacToe),
(surveyTracker, "Survey", surveyHandler),
(battleshipTracker, "Battleship", handleBattleship),
(footballTracker, "Football", handleFootball),
# quiz does not use a tracker (quizGamePlayer) always active
]
+162
View File
@@ -9,6 +9,7 @@
- [Tic-Tac-Toe (2D/3D)](#tic-tac-toe-game-module)
- [MasterMind](#mastermind-game-module)
- [Battleship](#battleship-game-module)
- [Football](#football-game-module)
- [Video Poker](#video-poker-game-module)
- [Hangman](#hangman-game-module)
- [Quiz](#quiz-game-module)
@@ -592,6 +593,167 @@ Your ships: 5/5 afloat
- Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025
# Football Game Module
A classic N.F.U. Football game adapted for Meshtastic mesh-bot. Play against the bot (or another human in the future) using natural language commands!
## How to Play
- **Start a New Game:**
Send `football` or `fb` via DM to the bot to start a new game.
- **Gameplay:**
You always play Team 1 (offense), starting on your own 20-yard line.
The bot plays Team 2 (defense).
**Offensive Plays (Natural Language):**
- `run` — Running plays (gains 0-20 yards)
- `pass` — Passing plays (gains 5-30 yards, high variance)
- `bomb` — Bomb pass (high risk, high reward)
- `sweep` — Sweep around the edge
- `option` — Option play (run or pass)
- `screen` — Screen pass (short, safe)
**Special Plays (4th Down Only):**
- `punt` — Punt the ball away
- `field goal` or `fg` — Attempt a field goal
**Game Commands:**
- `score` or `s` — Show current score and field
- `help` or `?` — Show available plays and tips
- `new` or `n` — Start a new game
- `end` or `exit` — End the current game
- **Game Objective:**
Score points by moving the ball toward the opponent's endzone and reach the winning score (default: 20 points).
- **Scoring:**
- **Touchdown:** 6 points (ball reaches opponent's endzone)
- **Extra Point:** 1 point (automatic conversion, 90% success rate)
- **Field Goal:** 3 points (attempt on any down)
- **Safety:** 2 points (awarded to defending team)
- **Game Flow:**
- Each drive consists of 4 downs to gain 10 yards (1st down).
- Turnover on downs if you don't gain 10 yards in 4 plays.
- Possession changes after touchdowns, safeties, turnovers, or punts.
- Game ends when either team reaches the winning score.
## Bot Strategy
The bot uses a **biased random strategy**:
- 70% random play selection
- 20% counter-play (if you run repeatedly, bot defends against passes)
- 10% aggressive defensive plays
This makes the bot both unpredictable and challenging!
## Example Session
```
🏈 **FOOTBALL** 🏈
User vs Bot · Win at 20 pts
**Coin Flip**: Team 2 receives kickoff
🏟️ **FIELD**
[0 10 20 30 40 50 60 70 80 90 100]
●
Down: 1/4 | Yard Line: 20
**Commands**: run, pass, sweep, bomb, punt, field goal
Or type: score, help, end
**🤖 Bot's Turn (Team 2)** 🤖
**Bot Play**: LEFT SWEEP
**Yards Gained**: 8
📍 **Bot First Down!**
🏟️ **FIELD**
[0 10 20 30 40 50 60 70 80 90 100]
●
Down: 1/4 | Yard Line: 28
Your play: run, pass, sweep, bomb, punt, field goal?
> pass
**User (Team 1)**: SCREEN PASS
**Bot Defense**: QB SNEAK
**Yards Gained**: 12
🏈 **TOUCHDOWN!** 🏈
Extra point **GOOD**. 7 points!
📊 **SCORE**
User (Team 1): 7
Bot (Team 2): 8
🏟️ **FIELD**
[0 10 20 30 40 50 60 70 80 90 100]
●
Down: 1/4 | Yard Line: 80
Your play: run, pass, sweep, bomb, punt, field goal?
> help
🏈 **FOOTBALL HELP** 🏈
**Offensive Plays:**
run - Running plays (gains 0-20 yards)
pass - Passing plays (gains 5-30 yards, high variance)
bomb - Bomb pass (high risk/high reward)
sweep - Sweep around the edge
option - Option play (run or pass)
screen - Screen pass (short, safe)
**Special Plays (4th Down Only):**
punt - Punt the ball away
fg / field goal - Attempt a field goal
**Commands:**
score - Show current score
help - This help text
new - Start a new game
end - End the current game
```
## Rules & Features
- 100-yard field (0–100 yard line)
- Classic football rules: 4 downs to gain 10 yards
- Touchdown endzone at opponent's 100-yard line
- Turnover on downs if 10 yards not gained
- 2.5% chance of fumble per play (loses possession)
- Safeties occur if ball pushed back past own endzone
- Game state stored per player (nodeID)
- Supports concurrent games from multiple users
## Notes
- Each player starts at their own 20-yard line after scoring
- Initial possession determined by coin flip
- Only one game session per player at a time
- Play via DM to avoid interfering with other users
- Natural language input allows flexible phrasing (e.g., "run", "rushing", "carry" all work)
- Game data is stored in-memory; restarting the bot resets all games
## Future Enhancements
- Two-player mode (user vs. user) similar to Battleship
- Play statistics and leaderboards
- Playbook customization
- Multi-message game display for visual improvements
- Field position tracking across possessions
## Credits
- Ported from the classic BASIC N.F.U. Football game
- Original Python port by Martin Thoma (2022)
- Original JavaScript version by Oscar Toledo G. (nanochess)
- Refactored for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025
# Word of the Day Game — Rules & Features
- **Word of the Day:**
+877
View File
@@ -0,0 +1,877 @@
"""
Football Game Module for Mesh-Bot DE K7MHI 2026
Refactored from the classic BASIC N.F.U. Football game.
User plays against Bot with random team names and natural language command input.
Original credits:
- Ported to Python by Martin Thoma in 2022
- JavaScript version by Oscar Toledo G. (nanochess)
- https://github.com/coding-horror/basic-computer-games/tree/main/37_Football/python
"""
import random
import time
from typing import Dict, Optional, Tuple
from math import floor
from modules.log import logger
class Football:
"""Football game class for Mesh-Bot.
User plays against Bot with random team names selected from PLAY_DATA.
Natural language commands: run, pass, punt, field goal, score, help, end, new
"""
# Play data: maps play number to offensive/defensive effectiveness
PLAY_DATA = {
"players": [17, 8, 4, 14, 19, 3, 10, 1, 7, 11, 15, 9, 5, 20, 13, 18, 16, 2, 12, 6,
20, 2, 17, 5, 8, 18, 12, 11, 1, 4, 19, 14, 10, 7, 9, 15, 6, 13, 16, 3],
"actions": [
"PITCHOUT", "TRIPLE REVERSE", "DRAW", "QB SNEAK", "END AROUND",
"DOUBLE REVERSE", "LEFT SWEEP", "RIGHT SWEEP", "OFF TACKLE", "WISHBONE OPTION",
"FLARE PASS", "SCREEN PASS", "ROLL OUT OPTION", "RIGHT CURL", "LEFT CURL",
"WISHBONE OPTION", "SIDELINE PASS", "HALF-BACK OPTION", "RAZZLE-DAZZLE", "BOMB!!!!"
],
"teams": ["Chirps", "Burps", "Quacks", "Barks", "Meows", "Hisses", "Roars", "Growls"]
}
# Natural language → play type mapping
PLAY_COMMANDS = {
"run": [0, 1, 2, 3, 4, 5, 6, 7, 8], # Run plays (positions 1-9)
"pass": [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # Pass plays (positions 11-20)
"bomb": [19], # Bomb play
"sweep": [6, 7], # Sweep plays
"option": [9, 15], # Option plays
"screen": [11], # Screen pass
"curl": [13, 14], # Curl plays
}
def __init__(self, display_module=None):
"""Initialize Football game.
Args:
display_module: Optional display module for visual output (future use)
"""
self.display_module = display_module
self.game = {} # game[nodeID] = game state dict
self.winning_score = 20 # Default winning score
def new_game(self, nodeID: int, winning_score: int = 20) -> str:
"""Start a new football game.
Args:
nodeID: User's mesh node ID
winning_score: Points needed to win
Returns:
Welcome message with game info and initial prompt
"""
self.winning_score = winning_score
# Initialize play scramble (aa, ba, ca arrays from original)
player_data = [num - 1 for num in self.PLAY_DATA["players"]]
aa = [-100] * 20 # User team play indices
ba = [-100] * 20 # Bot team play indices
ca = [-100] * 40 # Combined scramble
for i in range(40):
index = player_data[i - 1]
if i < 20:
aa[index] = i
else:
ba[index] = i - 20
ca[i] = index
# Random coin flip for initial possession
initial_possession = random.randint(0, 1) # 0 = user, 1 = bot
# Select random team names from PLAY_DATA
team_names = random.sample(self.PLAY_DATA["teams"], 2)
user_team = team_names[0]
bot_team = team_names[1]
self.game[nodeID] = {
"nodeID": nodeID,
"user_team": user_team,
"bot_team": bot_team,
"score": [0, 0], # [User team, Bot team]
"possession": initial_possession, # 0=user, 1=bot
"position": 20 if initial_possession == 0 else 50, # Ball position (0-100), 50=midfield after kickoff
"down": 1,
"yards_to_go": 10,
"position_at_drive_start": 20 if initial_possession == 0 else 50,
"games": 1,
"aa": aa, # User team play indices
"ba": ba, # Bot team play indices
"ca": ca, # Combined scramble
"turn": "user" if initial_possession == 0 else "bot",
"last_played": time.time(),
"game_over": False,
"last_play_result": "",
"waiting_for_conversion": False, # Flag to wait for conversion type choice
}
msg = "🏈 LET'S PLAY 🏈\n"
msg += f"{user_team} vs 🤖 {bot_team} winner at {self.winning_score} pts\n\n"
msg += f"🪙 {user_team if initial_possession == 0 else bot_team} receives kickoff\n"
msg += self._get_field_display(nodeID)
msg += "\nPlay Commands: run, pass, sweep, bomb, punt, or field goal, or help.\n\n"
if initial_possession == 1: # Bot receives
msg += self._handle_bot_kickoff(nodeID)
return msg
def play(self, nodeID: int, command: str) -> str:
"""Process user's play command.
Args:
nodeID: User's mesh node ID
command: Natural language command (e.g., "run", "pass")
Returns:
Message with play result, field update, and next prompt
"""
if nodeID not in self.game:
return "Game not started. Type 'football new' to begin."
game = self.game[nodeID]
command = command.strip().lower()
# Check if waiting for conversion type choice after TD
if game.get("waiting_for_conversion", False):
if command in ("kick", "1"):
msg = self._score_touchdown(nodeID, "", conversion_type="kick")
elif command in ("2pt", "2"):
msg = self._score_touchdown(nodeID, "", conversion_type="2pt")
else:
return "After touchdown, choose: 'kick' for 1-point or '2pt' for 2-point conversion"
game["waiting_for_conversion"] = False
# Check for game over
if self._check_game_over(nodeID):
return msg + "\n\n" + self._end_game(nodeID)
# Bot now has possession - play full drive automatically
msg += "\n\n🔄 POSSESSION CHANGE - 🤖\n\n" + self._play_bot_full_drive(nodeID)
# Check for game over again
if self._check_game_over(nodeID):
return msg + "\n\n" + self._end_game(nodeID)
game["last_played"] = time.time()
return msg
# Handle special commands
if command in ("end", "e", "quit", "q"):
return self._end_game(nodeID)
if command in ("score", "s"):
msg = self._get_scores(nodeID)
msg += "\n" + self._get_field_display(nodeID)
msg += "\nCall Play"
return msg
if command in ("help", "h", "?"):
return self._get_help_text(nodeID)
if command in ("new", "n"):
return self.new_game(nodeID, self.winning_score)
# If bot's turn, play entire bot drive automatically
if game["possession"] == 1: # Bot possession
play_result = self._play_bot_full_drive(nodeID)
# Check for game over
if self._check_game_over(nodeID):
return play_result + "\n\n" + self._end_game(nodeID)
# After bot drive, prompt user for their turn
play_result += "\n" + self._get_field_display(nodeID)
play_result += "\n\n📍 Your Play?"
game["last_played"] = time.time()
return play_result
# Parse user's play command
play_result = self._execute_user_play(nodeID, command)
game["last_play_result"] = play_result
# Check for game over
if self._check_game_over(nodeID):
return play_result + "\n\n" + self._end_game(nodeID)
# If still user possession (didn't lose it), prompt for next play
if game["possession"] == 0:
play_result += "\n" + self._get_field_display(nodeID)
play_result += "\n\n📍 Next Play?"
else:
# Bot gained possession, play entire bot drive automatically
play_result += "\n\n🔄 POSSESSION CHANGE - 🤖\n\n" + self._play_bot_full_drive(nodeID)
# Check for game over
if self._check_game_over(nodeID):
return play_result + "\n\n" + self._end_game(nodeID)
# After bot drive completes, show field and prompt user
play_result += "\n" + self._get_field_display(nodeID)
play_result += "\n\n📍 Your Play?"
game["last_played"] = time.time()
return play_result
def _execute_user_play(self, nodeID: int, command: str) -> str:
"""Execute user's offensive play.
Returns play result message and updates game state.
"""
game = self.game[nodeID]
msg = f"\n({game['user_team']})\n\n"
# Handle special 4th down scenarios
if game["down"] == 4:
if "punt" in command:
return self._execute_punt(nodeID)
elif "field" in command or "goal" in command or "fg" in command:
return self._execute_field_goal(nodeID)
elif "go" in command or "try" in command or "gain" in command:
pass # Fall through to normal play
else:
# Default: punt on 4th down
return self._execute_punt(nodeID)
# Parse natural language to play type
play_type = self._parse_play_command(command)
if play_type is None:
return f"❓ Unknown command: '{command}'\nTry: run, pass, bomb, sweep, option, screen, or punt/field goal"
# Get user's play number
user_play_num = random.choice(play_type)
# Get bot's defensive play (biased random)
bot_play_num = self._get_bot_play(nodeID, user_play_type=play_type)
# Calculate yards gained
aa = game["aa"]
ba = game["ba"]
yards = self._calculate_yards(nodeID, user_play_num, bot_play_num)
# Build play description
play_name = self.PLAY_DATA["actions"][user_play_num]
bot_defense = self.PLAY_DATA["actions"][bot_play_num]
msg += f"Offense: {play_name}\n"
msg += f"Defense (🤖 {game['bot_team']}): {bot_defense}\n"
# Check for penalties before play
penalty_result = self._check_penalties(nodeID, is_pass_play=any(p in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] for p in play_type))
if penalty_result:
msg += penalty_result + "\n\n"
# Replay the down
msg += "🔄 Same down, replay\n"
return msg
# Check for interception on pass plays (8% chance)
is_pass_play = any(play in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] for play in play_type)
if is_pass_play and random.random() < 0.08:
msg += f"🔴 INTERCEPTION! {game['bot_team']} ball!\n\n"
game["possession"] = 1
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
# Check for sack on pass plays (negative yards)
if is_pass_play and yards < 0:
# Check for intentional grounding (QB throws away under pressure)
if random.random() < 0.15: # 15% chance of grounding call
msg += f"Result: 🚫 SACK! {-yards} yard loss\n"
msg += f"🚩 INTENTIONAL GROUNDING - 5 yard penalty from line of scrimmage\n\n"
# Reset position and add penalty
game["position"] = max(0, game["position"] - 5)
game["down"] += 1
return msg
else:
msg += f"Result: 🚫 SACK! {-yards} yard loss\n\n"
else:
msg += f"Result: +{yards}yd\n\n"
# Update position
game["position"] += yards
# Check for fumble/turnover (2.5% chance)
if random.random() < 0.025:
msg += f"🔴 FUMBLE! {game['bot_team']} ball!\n"
game["possession"] = 1
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
# Check for touchdown
if game["position"] >= 100:
msg += "🏈 TOUCHDOWN! 🏈\n"
msg += "Choose conversion: 'kick' for 1-point (95% success, 7 pts) or '2pt' for 2-point (50% success, 8 pts)"
game["waiting_for_conversion"] = True
return msg
# Check for safety (ball at or behind own goal)
if game["position"] <= 0:
msg += f"⚠️ SAFETY! {game['bot_team']} scores 2 points.\n"
game["score"][1] += 2
game["possession"] = 1
game["position"] = 50
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = 50
return msg
# Update down and yards to go
yards_gained_since_drive_start = game["position"] - game["position_at_drive_start"]
if yards_gained_since_drive_start >= 10:
msg += "📍 FIRST DOWN!\n"
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
else:
game["down"] += 1
game["yards_to_go"] = 10 - yards_gained_since_drive_start
# Check for turnover on downs
if game["down"] > 4:
msg += "🔴 TURNOVER ON DOWNS!\n"
game["possession"] = 1
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
def _execute_punt(self, nodeID: int) -> str:
"""Execute a punt play.
Returns punt result message.
"""
game = self.game[nodeID]
msg = f"📍 {game['user_team'].upper()} PUNTS\n"
# Punt distance 25-60 yards (25% chance of bad punt)
if random.random() < 0.25:
punt_dist = random.randint(10, 25)
msg += f"Bad punt! Only {punt_dist} yards.\n"
else:
punt_dist = random.randint(25, 60)
msg += f"Punt: {punt_dist} yards.\n"
# Move ball
game["position"] += punt_dist
# Check for out of bounds past endzone
if game["position"] > 100:
game["position"] = 100
msg += "Punt went into endzone. Touchback.\n"
# Possession change
game["possession"] = 1
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
def _execute_field_goal(self, nodeID: int) -> str:
"""Execute a field goal attempt.
Returns field goal result message.
"""
game = self.game[nodeID]
msg = "🎯 Field Goal Attempt\n"
# Field goal range depends on distance
distance = 100 - game["position"]
success_rate = max(0.05, 0.95 - (distance / 100))
if random.random() < success_rate:
msg += f"✅ GOOD! 3 points!\n"
game["score"][0] += 3
else:
msg += f"❌ Ball went wide.\n"
# Possession change
game["possession"] = 1
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"] if game["position"] < 20 else 20
game["position"] = game["position_at_drive_start"]
return msg
def _play_bot_full_drive(self, nodeID: int) -> str:
"""Execute bot's full possession drive until they lose the ball.
Loops through bot's plays until possession changes back to user (0),
or game ends. This plays out the entire bot drive automatically.
Returns accumulated message with all plays in the drive.
"""
game = self.game[nodeID]
full_msg = ""
# Loop until bot loses possession
while game["possession"] == 1:
full_msg += self._play_bot_possession(nodeID)
# Check if game is over (prevent infinite loops on score checks)
if self._check_game_over(nodeID):
break
return full_msg
def _play_bot_possession(self, nodeID: int) -> str:
"""Execute bot's possession turn (single play).
Returns message with bot's play and any results.
"""
game = self.game[nodeID]
msg = f"\n🤖 {game['bot_team']}'s Turn 🤖\n\n"
# Bot decides to play or kick on 4th down
if game["down"] == 4:
rand = random.random()
if rand < 0.5:
return msg + self._execute_bot_punt(nodeID)
else:
return msg + self._execute_bot_field_goal(nodeID)
# Bot selects a play
bot_play_num = random.randint(0, 19)
user_defensive_play = random.randint(0, 19)
# Check for bot penalties
is_bot_pass = bot_play_num in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
penalty_result = self._check_penalties(nodeID, is_pass_play=is_bot_pass)
if penalty_result:
msg += f"{game['bot_team']} Play: {self.PLAY_DATA['actions'][bot_play_num]}\n"
msg += penalty_result + "\n\n"
msg += "🔄 Same down, replay\n"
return msg
# Calculate yards
yards = self._calculate_yards(nodeID, bot_play_num, user_defensive_play)
play_name = self.PLAY_DATA["actions"][bot_play_num]
msg += f"{game['bot_team']} Play: {play_name}\n"
msg += f"Yards Gained: {yards}\n\n"
# Update position (bot moving toward 0 yardline)
game["position"] -= yards
# Check for fumble
if random.random() < 0.025:
msg += f"🔴 FUMBLE! {game['user_team']} recovers!\n"
game["possession"] = 0
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
# Check for touchdown (bot endzone is position 0)
if game["position"] <= 0:
return self._score_bot_touchdown(nodeID, msg)
# Check for safety
if game["position"] >= 100:
msg += f"⚠️ SAFETY! {game['user_team']} scores 2 points.\n"
game["score"][0] += 2
game["possession"] = 0
game["position"] = 50
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = 50
return msg
# Update down and yards to go
yards_gained_since_drive_start = game["position_at_drive_start"] - game["position"]
if yards_gained_since_drive_start >= 10:
msg += f"📍 {game['bot_team'].upper()} FIRST DOWN!\n"
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
else:
game["down"] += 1
game["yards_to_go"] = 10 - yards_gained_since_drive_start
# Check for turnover on downs
if game["down"] > 4:
msg += f"🔴 {game['bot_team'].upper()} TURNOVER ON DOWNS! {game['user_team']}'s ball.\n"
game["possession"] = 0
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
return msg
def _execute_bot_punt(self, nodeID: int) -> str:
"""Bot executes a punt.
Returns punt result message.
"""
game = self.game[nodeID]
msg = f"📍 {game['bot_team'].upper()} PUNTS\n"
if random.random() < 0.25:
punt_dist = random.randint(10, 25)
msg += f"Bad punt! Only {punt_dist} yards.\n"
else:
punt_dist = random.randint(25, 60)
msg += f"Punt: {punt_dist} yards.\n"
game["position"] -= punt_dist
if game["position"] < 0:
game["position"] = 0
msg += "Punt rolled into endzone. Touchback.\n"
game["possession"] = 0
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"]
msg += f"\n{self._get_field_display(nodeID)}\n"
msg += "Your play?"
return msg
def _execute_bot_field_goal(self, nodeID: int) -> str:
"""Bot attempts a field goal.
Returns field goal result message.
"""
game = self.game[nodeID]
msg = f"🎯 {game['bot_team'].upper()} FIELD GOAL ATTEMPT\n"
distance = game["position"]
success_rate = max(0.05, 0.95 - (distance / 100))
if random.random() < success_rate:
msg += f"✅ GOOD! Bot scores 3!\n"
game["score"][1] += 3
else:
msg += f"❌ Ball went wide.\n"
game["possession"] = 0
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = game["position"] if game["position"] < 80 else 80
game["position"] = game["position_at_drive_start"]
msg += f"\n{self._get_field_display(nodeID)}\n"
msg += "Your play?"
return msg
def _handle_bot_kickoff(self, nodeID: int) -> str:
"""Bot receives kickoff at start of game.
Returns kickoff and full bot drive result.
"""
game = self.game[nodeID]
msg = f"🤖 {game['bot_team']} receives kickoff at midfield (50 yard line).\n\n"
return msg + self._play_bot_full_drive(nodeID)
def _score_touchdown(self, nodeID: int, msg_prefix: str, conversion_type: str = "auto") -> str:
"""Process user touchdown.
Args:
nodeID: User's node ID
msg_prefix: Message prefix from play result
conversion_type: "auto" for AI choice, "kick" for 1-point, "2pt" for 2-point
Returns touchdown message.
"""
game = self.game[nodeID]
msg = msg_prefix
msg += "🏈 TOUCHDOWN! 🏈\n"
# Let user choose conversion type on their TD
if conversion_type == "auto":
# For now, auto to kick (95% good, 5% blocked)
if random.random() < 0.05:
msg += "Extra point (1-point kick) BLOCKED. 6 points.\n"
game["score"][0] += 6
else:
msg += "Extra point (1-point kick) GOOD. 7 points!\n"
game["score"][0] += 7
elif conversion_type == "kick":
# 1-point extra point (95% success)
if random.random() < 0.05:
msg += "1-Point Kick BLOCKED. 6 points.\n"
game["score"][0] += 6
else:
msg += "1-Point Kick GOOD. 7 points!\n"
game["score"][0] += 7
elif conversion_type == "2pt":
# 2-point conversion (50% success)
if random.random() < 0.5:
msg += "2-Point Conversion GOOD. 8 points!\n"
game["score"][0] += 8
else:
msg += "2-Point Conversion FAILED. 6 points.\n"
game["score"][0] += 6
# Change possession and handle kickoff
msg += f"\n📍 KICKOFF - {game['bot_team']} receives at the 20 yard line\n"
# Check for kickoff return TD (8% chance)
if random.random() < 0.08:
msg += f"🏈 KICKOFF RETURN FOR TOUCHDOWN! 🏈 {game['bot_team'].upper()}!\n"
msg += "2-Point conversion attempt after return TD...\n"
if random.random() < 0.5:
msg += "2-Point Conversion GOOD. 8 points!\n"
game["score"][1] += 8
else:
msg += "2-Point Conversion FAILED. 6 points.\n"
game["score"][1] += 6
# Keep possession with bot
game["possession"] = 1
game["position"] = 50
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = 20
else:
# Normal kickoff
game["possession"] = 1
game["position"] = 20 # Receiving team starts at 20 yard line
game["position_at_drive_start"] = 20
game["down"] = 1
game["yards_to_go"] = 10
return msg
def _score_bot_touchdown(self, nodeID: int, msg_prefix: str) -> str:
"""Process bot touchdown.
Returns touchdown message. Bot randomly chooses conversion type.
"""
game = self.game[nodeID]
msg = msg_prefix
msg += f"🏈 {game['bot_team'].upper()} TOUCHDOWN! 🏈\n"
# Bot decides conversion type: 70% kick, 30% 2-point
if random.random() < 0.7:
# 1-point kick (95% success)
if random.random() < 0.05:
msg += "Extra point (1-point kick) BLOCKED. 6 points.\n"
game["score"][1] += 6
else:
msg += "Extra point (1-point kick) GOOD. 7 points!\n"
game["score"][1] += 7
else:
# 2-point conversion (50% success)
if random.random() < 0.5:
msg += "2-Point Conversion GOOD. 8 points!\n"
game["score"][1] += 8
else:
msg += "2-Point Conversion FAILED. 6 points.\n"
game["score"][1] += 6
# Kickoff to user team
msg += f"\n📍 KICKOFF - {game['user_team']} receives at the 20 yard line\n"
# Check for kickoff return TD (8% chance)
if random.random() < 0.08:
msg += f"🏈 KICKOFF RETURN FOR TOUCHDOWN! 🏈 {game['user_team'].upper()}!\n"
msg += "2-Point conversion attempt after return TD...\n"
if random.random() < 0.5:
msg += "2-Point Conversion GOOD. 8 points!\n"
game["score"][0] += 8
else:
msg += "2-Point Conversion FAILED. 6 points.\n"
game["score"][0] += 6
# Keep possession with user
game["possession"] = 0
game["position"] = 50
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = 50
else:
# Normal kickoff
game["possession"] = 0
game["position"] = 20 # User receives at 20 yard line
game["down"] = 1
game["yards_to_go"] = 10
game["position_at_drive_start"] = 20
return msg
def _calculate_yards(self, nodeID: int, offensive_play: int, defensive_play: int) -> int:
"""Calculate yards gained based on play matchup.
Yards depend on how different the plays are (classic football game logic).
Can be negative (sack) or positive depending on matchup.
"""
game = self.game[nodeID]
aa = game["aa"]
ba = game["ba"]
# Get play effectiveness values
off_val = aa[offensive_play]
def_val = ba[defensive_play]
# Calculate yards: difference in play values affects outcome
diff = abs(off_val - def_val)
base_yards = floor(diff / 19 * ((100 - game["position"] + 25) * random.random() - 15))
# Randomize with small variance
# Allow negative yards (sacks) now - don't cap at 0
yards = base_yards + random.randint(-2, 2)
return yards
def _parse_play_command(self, command: str) -> Optional[list]:
"""Parse natural language play command to play indices.
Args:
command: Natural language (e.g., "run", "pass")
Returns:
List of valid play indices, or None if unrecognized
"""
for key, plays in self.PLAY_COMMANDS.items():
if key in command:
return plays
return None
def _check_penalties(self, nodeID: int, is_pass_play: bool = False) -> Optional[str]:
"""Check for penalties on offense (pre-play).
Returns penalty description if penalty occurred, None otherwise.
Penalties:
- False start (2% base)
- Holding on offense (3% base, +2% if pass play)
- Pass interference (0% on run plays, 4% on pass plays)
"""
# False start
if random.random() < 0.02:
return "🚩 FALSE START on offense - 5 yard penalty"
# Holding on offense
if random.random() < 0.03 + (0.02 if is_pass_play else 0):
return "🚩 HOLDING on offense - 10 yard penalty"
# Pass interference (defense, but we'll check it here)
if is_pass_play and random.random() < 0.04:
return "🚩 PASS INTERFERENCE on defense - Automatic first down"
return None
def _get_bot_play(self, nodeID: int, user_play_type: list) -> int:
"""Get bot's defensive play number using biased random strategy.
Strategy:
- 70% random from all plays
- 20% counter-play (if user ran, play pass defense)
- 10% aggressive shutdown
Returns play number (0-19)
"""
rand = random.random()
if rand < 0.70:
# Random play
return random.randint(0, 19)
elif rand < 0.90:
# Counter-play: if user played running plays, bot plays pass defense
if any(play in [0, 1, 2, 3, 4, 5, 6, 7, 8] for play in user_play_type):
# Play pass defense
return random.choice([10, 12, 14, 16])
else:
# Play run defense
return random.choice([1, 3, 5, 7])
else:
# Aggressive play
return random.choice([19, 18, 17]) # Aggressive plays
def _get_field_display(self, nodeID: int) -> str:
"""Get status tracker with down, yards to score, and field position.
Returns formatted status string.
"""
game = self.game[nodeID]
pos = game["position"]
yards_to_go = game["yards_to_go"]
# Calculate yards to score (touchdown)
if game["possession"] == 0: # User offensive
yards_to_score = 100 - pos
else: # Bot offensive
yards_to_score = pos
status = f"📊Down {game['down']}/4 | To Score: {yards_to_score}yd | To 1st: {yards_to_go}yd | Pos: {pos}"
return status
def _get_scores(self, nodeID: int) -> str:
"""Get current score display.
Returns formatted score string.
"""
game = self.game[nodeID]
msg = "\n📊 SCORE\n"
msg += f"You ({game['user_team']}): {game['score'][0]}\n"
msg += f"🤖 Bot ({game['bot_team']}): {game['score'][1]}\n"
return msg
def _check_game_over(self, nodeID: int) -> bool:
"""Check if game is over (someone reached winning score).
Returns True if game over, False otherwise.
"""
game = self.game[nodeID]
return game["score"][0] >= self.winning_score or game["score"][1] >= self.winning_score
def _end_game(self, nodeID: int) -> str:
"""End the game and return final result.
Returns end-game message with stats.
"""
game = self.game[nodeID]
msg = "\n🏁 GAME OVER 🏁\n"
msg += f"Final Score:\n"
msg += f" You ({game['user_team']}): {game['score'][0]}\n"
msg += f" 🤖 Bot ({game['bot_team']}): {game['score'][1]}\n\n"
if game['score'][0] > game['score'][1]:
msg += f"🎉 YOU WIN! ({game['user_team'].upper()}) Great game!\n"
elif game['score'][1] > game['score'][0]:
msg += f"🤖 BOT WINS ({game['bot_team'].upper()}) Better luck next time!\n"
else:
msg += "🤝 TIE GAME! Well played!\n"
msg += "\nType 'football new' to play again."
game["game_over"] = True
return msg
def _get_help_text(self, nodeID: int) -> str:
"""Get help text for game commands.
Returns help message.
"""
msg = "🏈 FOOTBALL HELP 🏈\n\n"
msg += "Offensive Plays:\n"
msg += " run - Running plays (gains 0-20 yards)\n"
msg += " pass - Passing plays (8% interception risk, can get sacked)\n"
msg += " bomb - Bomb pass (high risk/high reward, 8% INT)\n"
msg += " sweep - Sweep around the edge\n"
msg += " option - Option play (run or pass)\n"
msg += " screen - Screen pass (short, safe, 8% INT)\n\n"
msg += "Special Plays (4th Down Only):\n"
msg += " punt - Punt the ball away\n"
msg += " fg / field goal - Attempt a field goal\n"
msg += " kick - Go for 1-point after TD\n"
msg += " 2pt - Go for 2-point conversion after TD\n\n"
msg += "Commands:\n"
msg += " score - Show current score\n"
msg += " help - This help text\n"
msg += " new - Start a new game\n"
msg += " end - End the current game\n\n"
return msg
+2
View File
@@ -51,6 +51,7 @@ dwPlayerTracker = [] # DopeWars player tracker
jackTracker = [] # Jack game tracker
mindTracker = [] # Mastermind (mmind) game tracker
battleshipTracker = [] # Battleship game tracker
footballTracker = [] # Football 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)
football_enabled = config['games'].getboolean('football', True)
# messaging settings
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
+9 -1
View File
@@ -240,6 +240,12 @@ if battleship_enabled:
trap_list = trap_list + ("battleship",)
games_enabled = True
if football_enabled:
from modules.games.football import Football # from the spudgunman/meshing-around repo
football = Football(display_module=None)
trap_list = trap_list + ("football",)
games_enabled = True
# Games Configuration
if games_enabled is True:
help_message = help_message + ", games"
@@ -269,6 +275,8 @@ if games_enabled is True:
gamesCmdList += "ticTacToe, "
if battleship_enabled:
gamesCmdList += "battleship, "
if football_enabled:
gamesCmdList += "football, "
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', 'footballTracker'
]
for tracker_name in tracker_names:
+82
View File
@@ -337,6 +337,88 @@ class TestBot(unittest.TestCase):
self.assertIsInstance(initial, str)
self.assertIsInstance(answer_msg, str)
def test_football_new_game(self):
from games.football import Football
football = Football(display_module=None)
user_id = 12345
# Start a new game
initial = football.new_game(user_id, winning_score=20)
print("Initial game:", initial[:100]) # Print first 100 chars
self.assertIsInstance(initial, str)
self.assertIn("FOOTBALL", initial)
self.assertIn("Coin Flip", initial)
# Verify game state was created
self.assertIn(user_id, football.game)
self.assertIn("score", football.game[user_id])
self.assertEqual(football.game[user_id]["score"], [0, 0])
def test_football_user_play(self):
from games.football import Football
football = Football(display_module=None)
user_id = 12346
# Start a new game
football.new_game(user_id, winning_score=20)
# Make sure user has possession (or get one)
if football.game[user_id]["possession"] == 1: # Bot has possession
football.game[user_id]["possession"] = 0 # Give to user
football.game[user_id]["position"] = 20
# User executes a running play
result = football.play(user_id, "run")
print("User play result:", result[:100])
self.assertIsInstance(result, str)
self.assertIn("Yards Gained", result)
def test_football_scoring(self):
from games.football import Football
football = Football(display_module=None)
user_id = 12347
# Start a new game
football.new_game(user_id, winning_score=20)
# Set user at near opponent endzone (for easy TD)
football.game[user_id]["possession"] = 0
football.game[user_id]["position"] = 98
football.game[user_id]["down"] = 1
# Execute play that should result in TD
result = football._score_touchdown(user_id, "")
print("Touchdown result:", result[:100])
self.assertIsInstance(result, str)
self.assertIn("TOUCHDOWN", result)
# Verify score increased
self.assertGreater(football.game[user_id]["score"][0], 0)
def test_football_bot_strategy(self):
from games.football import Football
football = Football(display_module=None)
user_id = 12348
# Test bot move selection with biased random
user_play_type = [0, 1, 2, 3, 4] # Running plays
# Get multiple bot plays to verify strategy works
bot_plays = []
for _ in range(10):
bot_play = football._get_bot_play(user_id, user_play_type)
bot_plays.append(bot_play)
self.assertIsInstance(bot_play, int)
self.assertGreaterEqual(bot_play, 0)
self.assertLess(bot_play, 20)
# Verify we got some variety (not always the same play)
self.assertGreater(len(set(bot_plays)), 1)
def test_football_end_game(self):
from games.football import Football
football = Football(display_module=None)
user_id = 12349
# Start a new game
football.new_game(user_id, winning_score=3) # Low winning score
# Manually set score to trigger game over
football.game[user_id]["score"] = [3, 1]
# End the game
result = football._end_game(user_id)
print("End game result:", result[:100])
self.assertIsInstance(result, str)
self.assertIn("GAME OVER", result)
self.assertIn("YOU WIN", result)
self.assertTrue(football.game[user_id]["game_over"])
##### API Tests - Extended tests run only if CHECKALL is True #####