mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2025-08-13 00:45:35 -04:00
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:
parent
27279577dd
commit
73fd2d67f4
11 changed files with 537 additions and 106 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue