Added game system. [G]ames are available in the Utilities menu. The readme was updated. A gamefile syntax readme was added.

This commit is contained in:
Jonathan Hite 2025-02-06 22:33:14 -05:00
parent 27279577dd
commit 73fd2d67f4
11 changed files with 537 additions and 106 deletions

View file

@ -2,6 +2,7 @@ import configparser
import logging
import random
import time
import os
from meshtastic import BROADCAST_NUM
@ -43,6 +44,8 @@ def build_menu(items, menu_name):
menu_str += "[C]hannel Dir\n"
elif item.strip() == 'J':
menu_str += "[J]S8CALL\n"
elif item.strip() == 'G':
menu_str += "[G]ames\n"
elif item.strip() == 'S':
menu_str += "[S]tats\n"
elif item.strip() == 'F':
@ -666,3 +669,288 @@ def handle_quick_help_command(sender_id, interface):
response = ("QUICK COMMANDS✈\nSend command below for usage info:\nSM,, - Send "
"Mail\nCM - Check Mail\nPB,, - Post Bulletin\nCB,, - Check Bulletins\n")
send_message(response, sender_id, interface)
def get_games_available(game_files):
"""Returns a dictionary of available games with their filenames and titles.
- If the first line contains `title="Game Title"`, it uses that as the display name.
- Otherwise, it uses the filename (without extension).
"""
games = {}
for file in game_files:
try:
file_path = os.path.join('./games', file)
with open(file_path, 'r', encoding='utf-8') as fp:
first_line = fp.readline().strip()
# Check if the first line has a title definition
if first_line.lower().startswith("title="):
game_title = first_line.split("=", 1)[1].strip().strip('"')
else:
game_title = file # Use the filename as the title
games[game_title] = file # Store the title with its correct filename
except Exception as e:
print(f"Error loading game {file}: {e}")
return games # Return a dictionary {Title: Filename}
def handle_games_command(sender_id, interface):
"""Handles the Games Menu and lists available text-based games."""
# Find files in ./games that:
# - Have a .txt or .csv extension
# - OR have no extension
game_files = [
f for f in os.listdir('./games')
if os.path.isfile(os.path.join('./games', f)) and (f.endswith('.txt') or f.endswith('.csv') or '.' not in f)
]
games_available = get_games_available(game_files)
if not games_available:
send_message("No games available yet. Come back soon.", sender_id, interface)
update_user_state(sender_id, {'command': 'UTILITIES', 'step': 1})
return None
# Store game filenames in state to avoid title-related issues
game_titles = list(games_available.keys()) # Display titles
game_filenames = list(games_available.values()) # Actual filenames
# Include exit option
numbered_games = "\n".join(f"{i+1}. {title}" for i, title in enumerate(game_titles))
numbered_games += "\n[X] Exit"
response = f"🎮 Games Menu 🎮\nWhich game would you like to play?\n{numbered_games}"
send_message(response, sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1, 'games': game_filenames, 'titles': game_titles})
return response
def handle_game_menu_selection(sender_id, message, step, interface, state):
"""Handles the user's selection of a game from the Games Menu, allowing exit with 'X' and starting immediately."""
# Allow users to exit with "X" like other menus
if message.lower() == "x":
handle_help_command(sender_id, interface) # Return to main menu
return
games_available = state.get('games', [])
try:
game_index = int(message) - 1 # Convert user input to zero-based index
if 0 <= game_index < len(games_available):
selected_game = games_available[game_index]
# Update state to indicate the user is now in-game
update_user_state(sender_id, {'command': 'IN_GAME', 'step': 3, 'game': selected_game})
# Start the game immediately
start_selected_game(sender_id, interface, {'game': selected_game})
else:
send_message("Invalid selection. Please enter a valid game number or 'X' to exit.", sender_id, interface)
except ValueError:
send_message("Invalid input. Please enter a number corresponding to a game or 'X' to exit.", sender_id, interface)
def start_selected_game(sender_id, interface, state):
"""Starts the game selected by the user and ensures title detection."""
game_name = state.get('game', None)
if not game_name:
send_message("Unexpected error: No game found. Returning to game menu.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
# Construct the game file path
game_file_path = os.path.join('./games', game_name)
# Final check if the file exists
if not os.path.exists(game_file_path):
send_message(f"Error: The game '{game_name}' could not be loaded.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
# Load the game map with title handling
try:
game_title, game_map = load_game_map(game_file_path)
except Exception as e:
send_message(f"Error loading game: {e}", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
if not game_map:
send_message(f"Error: The game '{game_name}' could not be loaded.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
# Set up the user state for playing (ENSURE game_title is included)
new_state = {
'command': 'IN_GAME',
'step': 3,
'game': game_name,
'game_title': game_title, # ✅ Ensure title is stored
'game_map': game_map,
'game_position': 1
}
update_user_state(sender_id, new_state)
# Present the first segment
present_story_segment(sender_id, interface, new_state) # ✅ Pass updated state
def load_game_map(file_path):
"""Loads a game map from a CSV file and returns its structured format."""
print(f"DEBUG: Inside load_game_map(), trying to open {file_path}")
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
print(f"DEBUG: Read {len(lines)} lines from file.")
if not lines:
print("❌ ERROR: File is empty!")
return None
# Check if the first line contains a title
first_line = lines[0].strip()
if first_line.lower().startswith("title="):
game_title = first_line.split("=", 1)[1].strip().strip('"')
game_lines = lines[1:] # Skip title
else:
game_title = file_path # Use filename if no title
game_lines = lines
print(f"DEBUG: Game title detected -> {game_title}")
# Parse game map
game_map = {}
for i, line in enumerate(game_lines, start=1):
game_map[i] = line.strip().split(",")
print(f"DEBUG: Successfully loaded game map with {len(game_map)} entries.")
return game_title, game_map
except Exception as e:
print(f"❌ ERROR inside load_game_map(): {e}")
return None
def present_story_segment(sender_id, interface, state):
"""Presents the current segment of the game and available choices."""
game_name = state.get('game')
game_title = state.get('game_title', "Unknown Game") # ✅ Prevent KeyError
game_map = state.get('game_map', {})
game_position = state.get('game_position', 1)
if game_position not in game_map:
send_message("Error: Invalid game state.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
# Retrieve the current story segment
segment = game_map[game_position]
storyline = segment[0]
choices = segment[1:]
# Build response message
response = f"🎮 {game_title} 🎮\n\n{storyline}\n\n"
for i in range(0, len(choices), 2): # Display numbered choices
response += f"{(i//2)+1}. {choices[i]}\n"
response += "\n[X] Exit"
send_message(response, sender_id, interface)
# Update user state to track the current game progress
update_user_state(sender_id, {
'command': 'IN_GAME',
'step': 3,
'game': game_name,
'game_title': game_title, # ✅ Ensure it stays in state
'game_map': game_map,
'game_position': game_position
})
def process_game_choice(sender_id, message, interface, state):
"""Processes the player's choice and advances the game."""
game_map = state.get('game_map', {})
game_position = state.get('game_position', 1)
if game_position not in game_map:
send_message("Error: Invalid game state.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
segment = game_map[game_position]
# Extract the storyline and choices
storyline = segment[0] # First element is the story text
choices = segment[1:] # Remaining elements are choices
# Ensure choices are properly formatted (must be in pairs)
if len(choices) % 2 != 0:
send_message("Error: Game data is corrupted.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
return
# Handle Exit
if message.lower() == "x":
send_message(f"Exiting '{state['game_title']}'... Returning to Games Menu.", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
handle_games_command(sender_id, interface) # Immediately display the game menu
return
try:
# Convert user input to index (1-based to 0-based)
choice_index = int(message) - 1
# Validate choice selection
if choice_index < 0 or choice_index * 2 + 1 >= len(choices):
send_message("Invalid selection. Please enter a valid number.", sender_id, interface)
return
# Retrieve the target position for the chosen option
target_position = int(choices[choice_index * 2 + 1])
# Check if the target position exists
if target_position not in game_map:
send_message("💀 Game Over! You fell into an abyss. 💀", sender_id, interface)
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
handle_games_command(sender_id, interface) # Return to game menu
return
# Update state with the new game position
update_user_state(sender_id, {
'command': 'IN_GAME',
'step': 3,
'game': state['game'],
'game_title': state['game_title'],
'game_map': game_map,
'game_position': target_position
})
# Present the new story segment
present_story_segment(sender_id, interface, {
'command': 'IN_GAME',
'step': 3,
'game': state['game'],
'game_title': state['game_title'],
'game_map': game_map,
'game_position': target_position
})
except (ValueError, IndexError):
send_message("Invalid selection. Please enter a valid number.", sender_id, interface)