mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-10-30 22:01:57 +01:00
1162 lines
46 KiB
Python
1162 lines
46 KiB
Python
"""
|
|
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"],
|
|
"star_players": [
|
|
"Gronk the Destroyer", "Captain Clumsy", "Lightning McLegs", "The Refrigerator",
|
|
"Dizzy McFumble", "Super Sack", "Turbo Legs", "The Magician", "Crash Bandicoot",
|
|
"Speedy Gonzales", "The Tackler", "Brick Wall", "The Interceptor", "Boom Boom",
|
|
"Ninja Turtle", "Laser Arm", "The Breaker", "Rocket Man", "Flash Gordon", "King Kong", "The Hammer"
|
|
],
|
|
"chaos_events": [
|
|
"🌪️ TORNADO on field! Ball blown 20 yards!",
|
|
"🔥 FIRE on sideline! Fire truck blocking view!",
|
|
"🍔 BURGER fell from sky onto field!",
|
|
"🚁 HELICOPTER flies overhead - crowd distracted!",
|
|
"🐓 WILD CHICKEN runs across field!",
|
|
"⚡️ LIGHTNING strikes goal post!",
|
|
"🎺 MARCHING BAND invades field accidentally!",
|
|
"🎪 STREAKER on field! Security chasing!",
|
|
"🤡 CLOWN car drives onto field somehow!",
|
|
"😴 Coach fell asleep on sideline! Time out!",
|
|
"📢 Ref's microphone stuck - everyone hears his lunch order!",
|
|
"🎉 Confetti cannon goes off for wrong team!",
|
|
"🐘 An actual elephant walked onto field!"
|
|
]
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# Direct play name → index mapping
|
|
PLAY_NAMES = {
|
|
"pitchout": 0,
|
|
"triple reverse": 1,
|
|
"draw": 2,
|
|
"qb sneak": 3,
|
|
"end around": 4,
|
|
"double reverse": 5,
|
|
"left sweep": 6,
|
|
"right sweep": 7,
|
|
"off tackle": 8,
|
|
"wishbone option": 9,
|
|
"flare pass": 10,
|
|
"screen pass": 11,
|
|
"roll out option": 12,
|
|
"right curl": 13,
|
|
"left curl": 14,
|
|
"sideline pass": 16,
|
|
"half-back option": 17,
|
|
"razzle-dazzle": 18,
|
|
"bomb": 19,
|
|
}
|
|
|
|
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
|
|
"star_player_user": random.choice(self.PLAY_DATA["star_players"]), # User's star player
|
|
"star_player_bot": random.choice(self.PLAY_DATA["star_players"]), # Bot's star player
|
|
}
|
|
|
|
msg = "🏈 LET'S PLAY 🏈\n"
|
|
msg += f"{user_team} vs 🤖 {bot_team} winner at {self.winning_score} pts\n\n"
|
|
msg += f"⭐ {user_team}'s Star: {self.game[nodeID]['star_player_user']}\n"
|
|
msg += f"⭐ {bot_team}'s Star: {self.game[nodeID]['star_player_bot']}\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)
|
|
msg += "\n" + self._get_field_display(nodeID)
|
|
msg += "\n\n📍 Your Play?"
|
|
|
|
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()
|
|
|
|
# DEBUG: Log incoming command
|
|
logger.debug(f"Football.play() - NodeID {nodeID}, Command: '{command}'")
|
|
|
|
# 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"({game['user_team']})"
|
|
|
|
# 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:
|
|
msg = f"❓ Command not recognized: '{command}'\n\n"
|
|
msg += f"Try: run, pass, bomb, sweep, option, screen, or punt/field goal"
|
|
return msg
|
|
|
|
# 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 chaos event (5% chance)
|
|
chaos = self._get_chaos_event()
|
|
if chaos:
|
|
msg += f"\n{chaos}\n"
|
|
|
|
# Check for penalties before play
|
|
is_pass_play = any(p in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] for p in play_type)
|
|
penalty_result, penalty_yards = self._check_penalties(nodeID, is_pass_play=is_pass_play)
|
|
if penalty_result:
|
|
msg += penalty_result + "\n\n"
|
|
|
|
# Apply penalty yards to position (move backward)
|
|
if penalty_yards > 0:
|
|
game["position"] = max(0, game["position"] - penalty_yards)
|
|
msg += f"Ball moved back {penalty_yards} to the {game['position']}.\n"
|
|
|
|
# Check for pass interference - automatic first down
|
|
if "PASS INTERFERENCE" in penalty_result:
|
|
msg += "📍 AUTOMATIC FIRST DOWN!\n"
|
|
game["down"] = 1
|
|
game["yards_to_go"] = 10
|
|
game["position_at_drive_start"] = game["position"]
|
|
else:
|
|
# Regular penalty - replay the same down, not the next one.
|
|
# Example: 3rd & 5 becomes 3rd & 5 again after the penalty.
|
|
# Recalculate yards to go using the current drive position.
|
|
yards_gained_since_drive_start = game["position"] - game["position_at_drive_start"]
|
|
game["yards_to_go"] = max(0, 10 - yards_gained_since_drive_start)
|
|
|
|
if game["down"] <= 4:
|
|
msg += "🔄 Same down, replay\n"
|
|
else:
|
|
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
|
|
|
|
# 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"Loss: 🚫 SACK {-yards}yd\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"Loss: 🚫 SACK {-yards}yd\n\n"
|
|
else:
|
|
msg += f"Gain: +{yards}yd\n\n"
|
|
|
|
# Check for star player moments
|
|
boost = self._star_player_boost(nodeID, is_user=True)
|
|
if boost:
|
|
msg += f"{boost}\n"
|
|
yards += 5 # Boost adds 5 yards
|
|
|
|
fail = self._star_player_fail(nodeID, is_user=True)
|
|
if fail and not boost: # Don't penalize if already boosted
|
|
msg += f"{fail}\n"
|
|
yards -= 3 # Fail costs 3 yards
|
|
|
|
msg += "\n"
|
|
|
|
# Update position (clamp to valid field range 0-100)
|
|
game["position"] += yards
|
|
game["position"] = max(0, min(100, game["position"]))
|
|
|
|
# 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
|
|
# yards_to_go should be positive: how many more yards needed for first down
|
|
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 toward opponent's endzone (position decreases for user punting)
|
|
game["position"] -= punt_dist
|
|
|
|
# Check for out of bounds past endzone (touchback)
|
|
if game["position"] < 0:
|
|
game["position"] = 20 # Receiving team gets ball at 20-yard line
|
|
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)
|
|
# Get user's intelligent defensive play (reads bot's play tendency)
|
|
user_defensive_play = self._get_user_defense_play(nodeID, bot_offensive_play=bot_play_num)
|
|
|
|
# Check for bot penalties
|
|
is_bot_pass = bot_play_num in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
|
|
penalty_result, penalty_yards = 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"
|
|
|
|
# Apply penalty yards (move forward for bot)
|
|
if penalty_yards > 0:
|
|
game["position"] = min(100, game["position"] + penalty_yards)
|
|
msg += f"Ball moved forward {penalty_yards} to the {game['position']}.\n"
|
|
|
|
# Check for pass interference - automatic first down
|
|
if "PASS INTERFERENCE" in penalty_result:
|
|
msg += "📍 AUTOMATIC FIRST DOWN!\n"
|
|
game["down"] = 1
|
|
game["yards_to_go"] = 10
|
|
game["position_at_drive_start"] = game["position"]
|
|
else:
|
|
# Regular penalty - replay the same down for the bot as well.
|
|
# Example: 3rd & 5 remains 3rd & 5 after the penalty.
|
|
yards_gained_since_drive_start = game["position_at_drive_start"] - game["position"]
|
|
game["yards_to_go"] = max(0, 10 - yards_gained_since_drive_start)
|
|
|
|
if game["down"] <= 4:
|
|
msg += "🔄 Same down, replay\n"
|
|
else:
|
|
msg += "🔴 TURNOVER ON DOWNS!\n"
|
|
game["possession"] = 0
|
|
game["down"] = 1
|
|
game["yards_to_go"] = 10
|
|
game["position_at_drive_start"] = game["position"]
|
|
|
|
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"
|
|
|
|
# Format yard gain/loss clearly
|
|
if yards >= 0:
|
|
msg += f"Gain: +{yards}yd\n\n"
|
|
else:
|
|
msg += f"Loss: {yards}yd\n\n"
|
|
|
|
# Check for chaos event (5% chance)
|
|
chaos = self._get_chaos_event()
|
|
if chaos:
|
|
msg += f"{chaos}\n"
|
|
|
|
# Check for star player moments
|
|
boost = self._star_player_boost(nodeID, is_user=False)
|
|
if boost:
|
|
msg += f"{boost}\n"
|
|
yards += 5 # Boost adds 5 yards
|
|
|
|
fail = self._star_player_fail(nodeID, is_user=False)
|
|
if fail and not boost: # Don't penalize if already boosted
|
|
msg += f"{fail}\n"
|
|
yards -= 3 # Fail costs 3 yards
|
|
|
|
msg += "\n"
|
|
|
|
# Update position (clamp to valid field range 0-100)
|
|
game["position"] -= yards
|
|
game["position"] = max(0, min(100, game["position"]))
|
|
|
|
# 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
|
|
# yards_to_go should be positive: how many more yards needed for first down
|
|
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"] > 100:
|
|
game["position"] = 80 # Receiving team gets ball at 20-yard line (100 - 20 = 80)
|
|
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"]
|
|
|
|
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.
|
|
|
|
How it works:
|
|
1. Compare offensive vs defensive play effectiveness (0-19 scale)
|
|
2. Better matchup = larger yard gain/loss
|
|
3. Randomness + field position affect the outcome
|
|
|
|
Returns yards (negative = loss/sack, positive = gain)
|
|
"""
|
|
game = self.game[nodeID]
|
|
aa = game["aa"]
|
|
ba = game["ba"]
|
|
|
|
# Step 1: Get play effectiveness (0-19)
|
|
off_effectiveness = aa[offensive_play]
|
|
def_effectiveness = ba[defensive_play]
|
|
|
|
# Step 2: How much of a mismatch? (0-19 scale)
|
|
play_difference = abs(off_effectiveness - def_effectiveness)
|
|
|
|
# Step 3: Base calculation
|
|
# - Normalize play difference (0-1)
|
|
# - Distance to endzone affects potential yards
|
|
# - Random factor (0-1) adds variance
|
|
# - Offset (-15) allows negative results (sacks)
|
|
yards_to_endzone = 100 - game["position"] + 25 # Extra buffer beyond endzone
|
|
randomness = random.random() # 0.0 to 1.0
|
|
base_yards = floor(
|
|
(play_difference / 19) * # Play matchup factor (0-1)
|
|
(yards_to_endzone * randomness - 15) # Distance & randomness - variance
|
|
)
|
|
|
|
# Step 4: Add random variance (±2 yards)
|
|
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.
|
|
|
|
Supports:
|
|
- Category commands: "run", "pass", "bomb", "sweep", etc.
|
|
- Direct play numbers: "6" or "play 6"
|
|
- Play names: "left sweep", "right sweep", etc.
|
|
|
|
Args:
|
|
command: Natural language (e.g., "run", "play 6", "left sweep")
|
|
|
|
Returns:
|
|
List of valid play indices, or None if unrecognized
|
|
"""
|
|
import re
|
|
|
|
command = command.strip().lower()
|
|
|
|
# Try direct play number: "6" or "play 6"
|
|
match = re.search(r'play\s+(\d+)|^(\d+)$', command)
|
|
if match:
|
|
play_num = int(match.group(1) or match.group(2))
|
|
if 0 <= play_num <= 19:
|
|
return [play_num]
|
|
else:
|
|
return None
|
|
|
|
# Try specific play name
|
|
for play_name, play_idx in self.PLAY_NAMES.items():
|
|
if play_name in command:
|
|
return [play_idx]
|
|
|
|
# Try category command
|
|
for key, plays in self.PLAY_COMMANDS.items():
|
|
if key in command:
|
|
return plays
|
|
|
|
logger.debug(f"_parse_play_command() - No match found for '{command}'")
|
|
return None
|
|
|
|
def _check_penalties(self, nodeID: int, is_pass_play: bool = False) -> Tuple[Optional[str], int]:
|
|
"""Check for penalties on offense (pre-play).
|
|
|
|
Returns tuple of (penalty description if occurred, yards penalty applied).
|
|
|
|
Penalties:
|
|
- False start (2% base, 5 yard penalty)
|
|
- Holding on offense (3% base, +2% if pass play, 10 yard penalty)
|
|
- Pass interference (0% on run plays, 4% on pass plays, auto first down)
|
|
"""
|
|
# False start
|
|
if random.random() < 0.02:
|
|
return ("🚩 FALSE START on offense - 5 yard penalty", 5)
|
|
|
|
# Holding on offense
|
|
if random.random() < 0.03 + (0.02 if is_pass_play else 0):
|
|
return ("🚩 HOLDING on offense - 10 yard penalty", 10)
|
|
|
|
# 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", 0)
|
|
|
|
return (None, 0)
|
|
|
|
def _get_bot_play(self, nodeID: int, user_play_type: list) -> int:
|
|
"""Get bot's defensive play number using intelligent strategy.
|
|
|
|
Strategy (Medium-High Intelligence, scales up when behind):
|
|
- Base: 50% intelligent counter-play, 25% aggressive, 25% random
|
|
- If bot is behind in score: increase aggressive play to 50%, reduce random to 10%
|
|
|
|
Returns play number (0-19)
|
|
"""
|
|
game = self.game[nodeID]
|
|
|
|
# Check if bot is behind
|
|
is_behind = game["score"][1] < game["score"][0]
|
|
|
|
if is_behind:
|
|
# Bot is behind - increase aggression (40% counter, 50% aggressive, 10% random)
|
|
rand = random.random()
|
|
if rand < 0.40:
|
|
# Intelligent counter-play: read the offense
|
|
if any(play in [0, 1, 2, 3, 4, 5, 6, 7, 8] for play in user_play_type):
|
|
return random.choice([1, 3, 5, 7, 8])
|
|
else:
|
|
return random.choice([10, 12, 13, 14, 16])
|
|
elif rand < 0.90:
|
|
# Aggressive play - blitz/shutdown
|
|
return random.choice([19, 18, 17, 9, 15])
|
|
else:
|
|
# Random variation
|
|
return random.randint(0, 19)
|
|
else:
|
|
# Bot is tied or ahead - normal strategy (50% counter, 25% aggressive, 25% random)
|
|
rand = random.random()
|
|
if rand < 0.50:
|
|
# Intelligent counter-play: read the offense
|
|
if any(play in [0, 1, 2, 3, 4, 5, 6, 7, 8] for play in user_play_type):
|
|
return random.choice([1, 3, 5, 7, 8])
|
|
else:
|
|
return random.choice([10, 12, 13, 14, 16])
|
|
elif rand < 0.75:
|
|
# Aggressive play - blitz/shutdown
|
|
return random.choice([19, 18, 17, 9, 15])
|
|
else:
|
|
# Random variation
|
|
return random.randint(0, 19)
|
|
|
|
def _get_user_defense_play(self, nodeID: int, bot_offensive_play: int) -> int:
|
|
"""Get user's defensive play number using intelligent strategy.
|
|
|
|
Strategy (Medium-High Intelligence):
|
|
- 50% intelligent counter-play (reads bot's play type)
|
|
- 25% aggressive shutdown (blitz/stack the box)
|
|
- 25% random variation (to keep unpredictable)
|
|
|
|
Returns play number (0-19)
|
|
"""
|
|
# Determine if bot's play is run or pass
|
|
is_bot_pass = bot_offensive_play in [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
|
|
|
|
rand = random.random()
|
|
|
|
if rand < 0.50:
|
|
# Intelligent counter-play: read the offense
|
|
if is_bot_pass:
|
|
# Bot passing - play pass defense (secondary coverage)
|
|
return random.choice([10, 12, 13, 14, 16])
|
|
else:
|
|
# Bot running - play run defense (stack the box)
|
|
return random.choice([1, 3, 5, 7, 8])
|
|
elif rand < 0.75:
|
|
# Aggressive play - blitz/shutdown
|
|
return random.choice([19, 18, 17, 9, 15])
|
|
else:
|
|
# Random variation
|
|
return random.randint(0, 19)
|
|
|
|
def _get_chaos_event(self) -> Optional[str]:
|
|
"""Get a random chaos event (5% chance).
|
|
|
|
Returns chaos event message or None if no event.
|
|
"""
|
|
if random.random() < 0.05: # 5% chance
|
|
return random.choice(self.PLAY_DATA["chaos_events"])
|
|
return None
|
|
|
|
def _star_player_boost(self, nodeID: int, is_user: bool) -> Optional[str]:
|
|
"""Star player makes a clutch play (8% chance during big moments).
|
|
|
|
Returns boost message or None if no boost.
|
|
"""
|
|
if random.random() < 0.08:
|
|
game = self.game[nodeID]
|
|
star = game["star_player_user"] if is_user else game["star_player_bot"]
|
|
messages = [
|
|
f"⭐ {star} makes an INCREDIBLE play!",
|
|
f"🔥 {star} is ON FIRE right now!",
|
|
f"💪 {star} muscles through for extra yards!",
|
|
f"✨ {star} shows why they're a STAR!"
|
|
]
|
|
return random.choice(messages)
|
|
return None
|
|
|
|
def _star_player_fail(self, nodeID: int, is_user: bool) -> Optional[str]:
|
|
"""Star player has an embarrassing moment (5% chance).
|
|
|
|
Returns fail message or None if no fail.
|
|
"""
|
|
if random.random() < 0.05:
|
|
game = self.game[nodeID]
|
|
star = game["star_player_user"] if is_user else game["star_player_bot"]
|
|
messages = [
|
|
f"😬 {star} tripped over their own feet!",
|
|
f"🤦 {star} dropped it like it's hot... literally!",
|
|
f"💀 {star} ran the WRONG direction!",
|
|
f"🙈 {star} forgot which team they're on!"
|
|
]
|
|
return random.choice(messages)
|
|
return None
|
|
|
|
|
|
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
|
|
|
|
score_text = f"{game['score'][0]}:{game['score'][1]}"
|
|
status = f"📊 {score_text} | Down {game['down']}/4 | Goal: {yards_to_score}yd | 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
|
|
|