mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2025-04-20 07:16:06 -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
@ -221,6 +221,8 @@ The following device roles have been working:
|
||||
- **Statistics**: View statistics about nodes, hardware, and roles.
|
||||
- **Wall of Shame**: View devices with low battery levels.
|
||||
- **Fortune Teller**: Get a random fortune. Pulls from the fortunes.txt file. Feel free to edit this file remove or add more if you like.
|
||||
- **GAMES**: GAMES!!! BBSs have to have games. You can make and add your own! Games should be written in python. The Games Menu gets the names from the GameTitle variable. (e.g. GameTitle="A Brave Adventure") in the game.py file.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -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)
|
||||
|
@ -1,102 +0,0 @@
|
||||
###############################
|
||||
#### Select Interface type ####
|
||||
###############################
|
||||
# [type = serial] for USB connected devices
|
||||
#If there are multiple serial devices connected, be sure to use the "port" option and specify a port
|
||||
# Linux Example:
|
||||
# port = /dev/ttyUSB0
|
||||
#
|
||||
# Windows Example:
|
||||
# port = COM3
|
||||
# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock)
|
||||
# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device
|
||||
# Example:
|
||||
# [interface]
|
||||
# type = tcp
|
||||
# hostname = 192.168.1.100
|
||||
|
||||
[interface]
|
||||
type = serial
|
||||
# port = /dev/ttyACM0
|
||||
# hostname = 192.168.x.x
|
||||
|
||||
|
||||
############################
|
||||
#### BBS NODE SYNC LIST ####
|
||||
############################
|
||||
# Provide a list of other nodes running TC²-BBS to sync mail messages and bulletins with
|
||||
# Enter in a list of other BBS Nodes by their nodeID separated by commas (no spaces)
|
||||
# Example:
|
||||
# [sync]
|
||||
# bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d
|
||||
|
||||
# [sync]
|
||||
# bbs_nodes = !17d7e4b7
|
||||
|
||||
|
||||
############################
|
||||
#### Allowed Node IDs ####
|
||||
############################
|
||||
# Provide a list of node IDs that are allowed to post to the urgent board.
|
||||
# If this section is commented out, anyone can post to the urgent board.
|
||||
# Example:
|
||||
# [allow_list]
|
||||
# allowed_nodes = 12345678,87654321
|
||||
#
|
||||
# [allow_list]
|
||||
# allowed_nodes = !17d7e4b7
|
||||
|
||||
|
||||
####################
|
||||
#### Menu Items ####
|
||||
####################
|
||||
# Remove any menu items you don't plan on using with your BBS below
|
||||
#
|
||||
[menu]
|
||||
# Default Main Menu options for reference
|
||||
# [Q]uick Commands
|
||||
# [B]BS
|
||||
# [U]tilities
|
||||
# E[X]IT
|
||||
#
|
||||
# Remove any menu items from the list below that you want to exclude from the main menu
|
||||
main_menu_items = Q, B, U, X
|
||||
|
||||
# Default BBS Menu options for reference
|
||||
# [M]ail
|
||||
# [B]ulletins
|
||||
# [C]hannel Dir
|
||||
# [J]S8CALL
|
||||
# E[X]IT
|
||||
#
|
||||
# Remove any menu items from the list below that you want to exclude from the BBS menu
|
||||
bbs_menu_items = M, B, C, J, X
|
||||
|
||||
# Default Utilities Menu option for reference
|
||||
# [S]tats
|
||||
# [F]ortune
|
||||
# [W]all of Shame
|
||||
# E[X]IT
|
||||
#
|
||||
# Remove any menu items from the list below that you want to exclude from the utilities menu
|
||||
utilities_menu_items = S, F, W, X
|
||||
|
||||
|
||||
##########################
|
||||
#### JS8Call Settings ####
|
||||
##########################
|
||||
# If you would like messages from JS8Call to go into the BBS, uncomment and enter in info below:
|
||||
# host = the IP address for your system running JS8Call
|
||||
# port = TCP API port for JS8CALL - Default is 2442
|
||||
# db_file = this can be left as the default "js8call.db" unless you need to change for some reason
|
||||
# js8groups = the JS8Call groups you're interested in receiving into the BBS
|
||||
# store_messages = "true" will send messages that arent part of a group into the BBS (can be noisy). "false" will ignore these
|
||||
# js8urgent = the JS8Call groups you consider to be urgent - anything sent to these will have a notice sent to the
|
||||
# group chat (similar to how the urgent bulletin board works
|
||||
# [js8call]
|
||||
# host = 192.168.1.100
|
||||
# port = 2442
|
||||
# db_file = js8call.db
|
||||
# js8groups = @GRP1,@GRP2,@GRP3
|
||||
# store_messages = True
|
||||
# js8urgent = @URGNT
|
66
gamefile_syntax.md
Normal file
66
gamefile_syntax.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 📜 How to Format Game CSV Files
|
||||
|
||||
This guide explains how to structure game files for the **text-based game system** using **CSV format**.
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Basic Structure**
|
||||
Each line in the CSV file represents a **story segment** with:
|
||||
- **Storyline text** (always first column)
|
||||
- **Choices** (paired as `Choice Text, Target Line`)
|
||||
- If no choices exist, it signals **GAME OVER**
|
||||
|
||||
### **Example Format:**
|
||||
```csv
|
||||
What is your first choice?, East, 2, West, 3, North, 4, South, 5
|
||||
You chose East. A dense forest appears., Run forward, 6, Look around, 7
|
||||
You chose West. A river blocks your path., Try to swim, 8, Walk along the bank, 9
|
||||
You chose North. The path is blocked by rocks.
|
||||
You chose South. A cave entrance looms ahead., Enter the cave, 10, Turn back, 11
|
||||
You went East and chose to run. The ground gives way and you fall. GAME OVER.
|
||||
You went East and looked around. You see a hidden path., Follow it, 12, Stay put, 13
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Title Handling**
|
||||
The **first line** of the file may contain a title in the format:
|
||||
```csv
|
||||
title="The Mysterious Forest"
|
||||
```
|
||||
- If present, the game uses this as the **title**.
|
||||
- If absent, the **filename** is used as the title.
|
||||
- The second line will then be the **first story segment**.
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Rules for Formatting**
|
||||
✅ **Each row starts with story text**
|
||||
✅ **Choices appear as pairs** (`Choice Text, Target Line`)
|
||||
✅ **Target Line must reference an existing line number**
|
||||
✅ **A line without choices = GAME OVER**
|
||||
✅ **Title (optional) must be in the first line as `title="Game Name"`**
|
||||
✅ **File should be saved as `.csv` with UTF-8 encoding**
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Understanding the Flow**
|
||||
1️⃣ The game **starts at line 1**.
|
||||
2️⃣ The user selects a **numbered choice**.
|
||||
3️⃣ The game jumps to the **corresponding line number**.
|
||||
4️⃣ The game continues until:
|
||||
- **No choices remain (GAME OVER)**
|
||||
- **The player exits** (`X` command).
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Edge Cases & Notes**
|
||||
⚠ **If an invalid line number is referenced**, the game may crash.
|
||||
⚠ **A story segment must always be in column 1**, even if no choices follow.
|
||||
⚠ **Extra spaces around text are automatically trimmed.**
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Final Notes**
|
||||
By following this format, you can create **interactive, branching text adventures** without modifying code! 🎮🚀
|
||||
|
8
games/lost_forest.csv
Normal file
8
games/lost_forest.csv
Normal file
@ -0,0 +1,8 @@
|
||||
title="The Lost Forest"
|
||||
What is your first choice?, East, 2, West, 3, North, 4, South, 5
|
||||
You chose East. A dense forest appears., Run forward, 6, Look around, 7
|
||||
You chose West. A river blocks your path., Try to swim, 8, Walk along the bank, 9
|
||||
You chose North. The path is blocked by rocks.
|
||||
You chose South. A cave entrance looms ahead., Enter the cave, 10, Turn back, 11
|
||||
You went East and chose to run. The ground gives way and you fall. GAME OVER.
|
||||
You went East and looked around. You see a hidden path., Follow it, 12, Stay put, 13
|
Can't render this file because it contains an unexpected character in line 1 and column 7.
|
1
games/testing.txt
Normal file
1
games/testing.txt
Normal file
@ -0,0 +1 @@
|
||||
title="Test Game 9000"
|
0
games/testnoname
Normal file
0
games/testnoname
Normal file
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from meshtastic import BROADCAST_NUM
|
||||
|
||||
@ -8,11 +9,17 @@ from command_handlers import (
|
||||
handle_channel_directory_command, handle_channel_directory_steps, handle_send_mail_command,
|
||||
handle_read_mail_command, handle_check_mail_command, handle_delete_mail_confirmation, handle_post_bulletin_command,
|
||||
handle_check_bulletin_command, handle_read_bulletin_command, handle_read_channel_command,
|
||||
handle_post_channel_command, handle_list_channels_command, handle_quick_help_command
|
||||
)
|
||||
handle_post_channel_command, handle_list_channels_command, handle_quick_help_command, handle_games_command, process_game_choice,
|
||||
start_selected_game, handle_game_menu_selection
|
||||
)
|
||||
|
||||
import command_handlers
|
||||
|
||||
games_available = command_handlers.get_games_available(os.listdir('./games'))
|
||||
|
||||
from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection, add_channel
|
||||
from js8call_integration import handle_js8call_command, handle_js8call_steps, handle_group_message_selection
|
||||
from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message
|
||||
from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message, update_user_state
|
||||
|
||||
main_menu_handlers = {
|
||||
"q": handle_quick_help_command,
|
||||
@ -31,6 +38,7 @@ bbs_menu_handlers = {
|
||||
|
||||
|
||||
utilities_menu_handlers = {
|
||||
"g": handle_games_command,
|
||||
"s": handle_stats_command,
|
||||
"f": handle_fortune_command,
|
||||
"w": handle_wall_of_shame_command,
|
||||
@ -53,6 +61,12 @@ board_action_handlers = {
|
||||
"x": handle_help_command
|
||||
}
|
||||
|
||||
games_menu_handlers = {
|
||||
"x": handle_help_command,
|
||||
}
|
||||
for i in range(1, len(games_available) + 1):
|
||||
games_menu_handlers[str(i)] = lambda sender_id, interface, i=i: handle_game_menu_selection(sender_id, str(i), 1, interface, None)
|
||||
|
||||
def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
state = get_user_state(sender_id)
|
||||
message_lower = message.lower().strip()
|
||||
@ -90,6 +104,21 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
channel_name, channel_url = parts[1], parts[2]
|
||||
add_channel(channel_name, channel_url)
|
||||
else:
|
||||
# ✅ **Corrected IN_GAME Handling**
|
||||
if state and state['command'] == 'IN_GAME':
|
||||
message_lower = message.lower().strip()
|
||||
|
||||
# Always check if the user wants to exit
|
||||
if message_lower == "x":
|
||||
send_message(f"Exiting '{state['game']}'... Returning to Games Menu.", sender_id, interface)
|
||||
update_user_state(sender_id, {'command': 'GAMES', 'step': 1})
|
||||
return
|
||||
|
||||
# Otherwise, process the game choice
|
||||
process_game_choice(sender_id, message, interface, state)
|
||||
return
|
||||
|
||||
# 📌 Other menu processing remains unchanged
|
||||
if message_lower.startswith("sm,,"):
|
||||
handle_send_mail_command(sender_id, message_strip, interface, bbs_nodes)
|
||||
elif message_lower.startswith("cm"):
|
||||
@ -109,6 +138,8 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
handlers = bbs_menu_handlers
|
||||
elif menu_name == 'utilities':
|
||||
handlers = utilities_menu_handlers
|
||||
elif menu_name == 'games':
|
||||
handlers = games_menu_handlers
|
||||
else:
|
||||
handlers = main_menu_handlers
|
||||
elif state and state['command'] == 'BULLETIN_MENU':
|
||||
@ -121,6 +152,9 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
elif state and state['command'] == 'GROUP_MESSAGES':
|
||||
handle_group_message_selection(sender_id, message, state['step'], state, interface)
|
||||
return
|
||||
elif state and state['command'] == 'GAMES':
|
||||
handle_game_menu_selection(sender_id, message, state['step'], interface, state)
|
||||
return
|
||||
else:
|
||||
handlers = main_menu_handlers
|
||||
|
||||
@ -176,6 +210,8 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
handle_help_command(sender_id, interface)
|
||||
|
||||
|
||||
|
||||
|
||||
def on_receive(packet, interface):
|
||||
try:
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
|
@ -45,7 +45,7 @@ def display_banner():
|
||||
██║ ██║ ██╔═══╝ ╚════╝██╔══██╗██╔══██╗╚════██║
|
||||
██║ ╚██████╗███████╗ ██████╔╝██████╔╝███████║
|
||||
╚═╝ ╚═════╝╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||
Meshtastic Version
|
||||
Meshtastic Version WITH GAMES
|
||||
"""
|
||||
print(banner)
|
||||
|
||||
|
10
text.py
Normal file
10
text.py
Normal file
@ -0,0 +1,10 @@
|
||||
import os
|
||||
|
||||
print("Current Working Directory:", os.getcwd())
|
||||
file_path = "./games/lost_forest.csv"
|
||||
|
||||
if os.path.exists(file_path):
|
||||
print("✅ File exists! Python can detect it.")
|
||||
else:
|
||||
print("❌ File does NOT exist according to Python.")
|
||||
|
122
validate_game.py
Normal file
122
validate_game.py
Normal file
@ -0,0 +1,122 @@
|
||||
import os
|
||||
|
||||
def list_game_files():
|
||||
"""Lists all game files in the ./games directory."""
|
||||
game_dir = "./games"
|
||||
if not os.path.exists(game_dir):
|
||||
print("❌ ERROR: 'games' directory does not exist.")
|
||||
return []
|
||||
|
||||
game_files = [f for f in os.listdir(game_dir) if os.path.isfile(os.path.join(game_dir, f))]
|
||||
|
||||
if not game_files:
|
||||
print("❌ ERROR: No game files found in the './games' directory.")
|
||||
return []
|
||||
|
||||
return game_files
|
||||
|
||||
|
||||
def validate_game_file(file_path):
|
||||
"""Validates the format of a game CSV file."""
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"❌ ERROR: File '{file_path}' does not exist.")
|
||||
return False
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
if not lines:
|
||||
print(f"❌ ERROR: File '{file_path}' is empty.")
|
||||
return False
|
||||
|
||||
# Check title format
|
||||
first_line = lines[0].strip()
|
||||
if first_line.lower().startswith("title="):
|
||||
title = first_line.split("=", 1)[1].strip().strip('"')
|
||||
print(f"✅ Title detected: {title}")
|
||||
game_lines = lines[1:] # Skip title line
|
||||
else:
|
||||
print(f"⚠️ WARNING: No title detected. Using filename instead.")
|
||||
game_lines = lines
|
||||
|
||||
game_map = {}
|
||||
valid_lines = set()
|
||||
|
||||
for index, line in enumerate(game_lines, start=1):
|
||||
parts = [p.strip() for p in line.strip().split(",")]
|
||||
|
||||
if not parts or len(parts) < 1:
|
||||
print(f"❌ ERROR: Line {index} is empty or improperly formatted.")
|
||||
return False
|
||||
|
||||
# First element is the story text
|
||||
storyline = parts[0]
|
||||
choices = parts[1:]
|
||||
|
||||
# Validate choice pairs
|
||||
if len(choices) % 2 != 0:
|
||||
print(f"❌ ERROR: Line {index} has an uneven number of choices. Choices must be in pairs.")
|
||||
return False
|
||||
|
||||
# Validate choices mapping
|
||||
for i in range(1, len(choices), 2):
|
||||
choice_text = choices[i - 1]
|
||||
try:
|
||||
target_line = int(choices[i])
|
||||
valid_lines.add(target_line)
|
||||
except ValueError:
|
||||
print(f"❌ ERROR: Invalid mapping in line {index} ('{choice_text}' does not map to a valid number).")
|
||||
return False
|
||||
|
||||
# Store story segment
|
||||
game_map[index] = (storyline, choices)
|
||||
|
||||
# Validate that all mapped lines exist
|
||||
missing_lines = valid_lines - set(game_map.keys())
|
||||
if missing_lines:
|
||||
print(f"❌ ERROR: The following mapped lines do not exist: {sorted(missing_lines)}")
|
||||
return False
|
||||
|
||||
print(f"✅ Validation passed for '{file_path}'. No errors detected!")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Lists games and asks user which to validate."""
|
||||
game_files = list_game_files()
|
||||
|
||||
if not game_files:
|
||||
return
|
||||
|
||||
print("\nAvailable games for validation:")
|
||||
for i, game in enumerate(game_files, start=1):
|
||||
print(f"{i}. {game}")
|
||||
print("A. Validate ALL games")
|
||||
print("X. Exit")
|
||||
|
||||
choice = input("\nSelect a game to validate (or 'A' for all, 'X' to exit): ").strip().lower()
|
||||
|
||||
if choice == "x":
|
||||
print("Exiting...")
|
||||
return
|
||||
elif choice == "a":
|
||||
print("\n🔍 Validating all games...")
|
||||
for game in game_files:
|
||||
print(f"\n🔎 Validating {game}...")
|
||||
validate_game_file(os.path.join("./games", game))
|
||||
else:
|
||||
try:
|
||||
game_index = int(choice) - 1
|
||||
if 0 <= game_index < len(game_files):
|
||||
game_path = os.path.join("./games", game_files[game_index])
|
||||
print(f"\n🔎 Validating {game_files[game_index]}...")
|
||||
validate_game_file(game_path)
|
||||
else:
|
||||
print("❌ ERROR: Invalid selection.")
|
||||
except ValueError:
|
||||
print("❌ ERROR: Invalid input. Please enter a number or 'A'/'X'.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user