mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-08-07 17:32:52 +02:00
Merge pull request #198 from martinbogo/feature/tictactoe-game
Feature/tictactoe game
This commit is contained in:
+16
-7
@@ -26,18 +26,27 @@ def load_bbsdb():
|
||||
# if the message is not a duplicate, add it to bbs_messages Maintain the message ID sequence
|
||||
new_id = len(bbs_messages) + 1
|
||||
bbs_messages.append([new_id, msg[1], msg[2], msg[3]])
|
||||
except Exception as e:
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: bbsdb.pkl not found, creating new one")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error creating bbsdb.pkl: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error loading bbsdb.pkl: {e}")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
logger.debug("System: Creating new data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
def save_bbsdb():
|
||||
global bbs_messages
|
||||
# save the bbs messages to the database file
|
||||
logger.debug("System: Saving data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
try:
|
||||
logger.debug("System: Saving data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error saving bbsdb: {e}")
|
||||
|
||||
def bbs_help():
|
||||
# help message
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Tic-Tac-Toe game for Meshtastic mesh-bot
|
||||
# Board positions chosen by numbers 1-9
|
||||
# 2025
|
||||
import random
|
||||
# to molly and jake, I miss you both so much.
|
||||
|
||||
class TicTacToe:
|
||||
def __init__(self):
|
||||
self.game = {}
|
||||
|
||||
def new_game(self, id):
|
||||
"""Start a new game"""
|
||||
games = won = 0
|
||||
ret = ""
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
ret += f"Games:{games} Won:{won}\n"
|
||||
|
||||
self.game[id] = {
|
||||
"board": [" "] * 9, # 3x3 board as flat list
|
||||
"player": "X", # Human is X, bot is O
|
||||
"games": games + 1,
|
||||
"won": won,
|
||||
"turn": "human" # whose turn it is
|
||||
}
|
||||
ret += self.show_board(id)
|
||||
ret += "Pick 1-9:"
|
||||
return ret
|
||||
|
||||
def show_board(self, id):
|
||||
"""Display compact board with move numbers"""
|
||||
g = self.game[id]
|
||||
b = g["board"]
|
||||
|
||||
# Show board with positions
|
||||
board_str = ""
|
||||
for i in range(3):
|
||||
row = ""
|
||||
for j in range(3):
|
||||
pos = i * 3 + j
|
||||
cell = b[pos] if b[pos] != " " else str(pos + 1)
|
||||
row += cell
|
||||
if j < 2:
|
||||
row += "|"
|
||||
board_str += row
|
||||
if i < 2:
|
||||
board_str += "\n-+-+-\n"
|
||||
|
||||
return board_str + "\n"
|
||||
|
||||
def make_move(self, id, position):
|
||||
"""Make a move for the current player"""
|
||||
g = self.game[id]
|
||||
|
||||
# Validate position
|
||||
if position < 1 or position > 9:
|
||||
return False
|
||||
|
||||
pos = position - 1
|
||||
if g["board"][pos] != " ":
|
||||
return False
|
||||
|
||||
# Make human move
|
||||
g["board"][pos] = "X"
|
||||
return True
|
||||
|
||||
def bot_move(self, id):
|
||||
"""AI makes a move"""
|
||||
g = self.game[id]
|
||||
|
||||
# Simple AI: Try to win, block, or pick random
|
||||
move = self.find_winning_move(id, "O") # Try to win
|
||||
if move == -1:
|
||||
move = self.find_winning_move(id, "X") # Block player
|
||||
if move == -1:
|
||||
move = self.find_random_move(id) # Random move
|
||||
|
||||
if move != -1:
|
||||
g["board"][move] = "O"
|
||||
return move
|
||||
|
||||
def find_winning_move(self, id, player):
|
||||
"""Find a winning move for the given player"""
|
||||
g = self.game[id]
|
||||
board = g["board"][:]
|
||||
|
||||
# Check all empty positions
|
||||
for i in range(9):
|
||||
if board[i] == " ":
|
||||
board[i] = player
|
||||
if self.check_winner_on_board(board) == player:
|
||||
return i
|
||||
board[i] = " "
|
||||
return -1
|
||||
|
||||
def find_random_move(self, id):
|
||||
"""Find a random empty position"""
|
||||
g = self.game[id]
|
||||
empty = [i for i in range(9) if g["board"][i] == " "]
|
||||
return random.choice(empty) if empty else -1
|
||||
|
||||
def check_winner_on_board(self, board):
|
||||
"""Check winner on given board state"""
|
||||
# Winning combinations
|
||||
wins = [
|
||||
[0,1,2], [3,4,5], [6,7,8], # Rows
|
||||
[0,3,6], [1,4,7], [2,5,8], # Columns
|
||||
[0,4,8], [2,4,6] # Diagonals
|
||||
]
|
||||
|
||||
for combo in wins:
|
||||
if board[combo[0]] == board[combo[1]] == board[combo[2]] != " ":
|
||||
return board[combo[0]]
|
||||
return None
|
||||
|
||||
def check_winner(self, id):
|
||||
"""Check if there's a winner"""
|
||||
g = self.game[id]
|
||||
return self.check_winner_on_board(g["board"])
|
||||
|
||||
def is_board_full(self, id):
|
||||
"""Check if board is full"""
|
||||
g = self.game[id]
|
||||
return " " not in g["board"]
|
||||
|
||||
def game_over_msg(self, id):
|
||||
"""Generate game over message"""
|
||||
g = self.game[id]
|
||||
winner = self.check_winner(id)
|
||||
|
||||
if winner == "X":
|
||||
g["won"] += 1
|
||||
return "🎉You won! (n)ew (e)nd"
|
||||
elif winner == "O":
|
||||
return "🤖Bot wins! (n)ew (e)nd"
|
||||
else:
|
||||
return "🤝Tie game! (n)ew (e)nd"
|
||||
|
||||
def play(self, id, input_msg):
|
||||
"""Main game play function"""
|
||||
if id not in self.game:
|
||||
return self.new_game(id)
|
||||
|
||||
# If input is just "tictactoe", show current board
|
||||
if input_msg.lower().strip() == "tictactoe":
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
|
||||
g = self.game[id]
|
||||
|
||||
# Parse player move
|
||||
try:
|
||||
# Extract just the number from the input
|
||||
numbers = [char for char in input_msg if char.isdigit()]
|
||||
if not numbers:
|
||||
if input_msg.lower().startswith('q'):
|
||||
self.end_game(id)
|
||||
return "Game ended. To start a new game, type 'tictactoe'."
|
||||
elif input_msg.lower().startswith('n'):
|
||||
return self.new_game(id)
|
||||
elif input_msg.lower().startswith('b'):
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
position = int(numbers[0])
|
||||
except (ValueError, IndexError):
|
||||
return "Enter 1-9, or (e)nd (n)ew game, send (b)oard to see board🧩"
|
||||
|
||||
# Make player move
|
||||
if not self.make_move(id, position):
|
||||
return "Invalid move! Pick 1-9:"
|
||||
|
||||
# Check if player won
|
||||
if self.check_winner(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Check for tie
|
||||
if self.is_board_full(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Bot's turn
|
||||
bot_pos = self.bot_move(id)
|
||||
|
||||
# Check if bot won
|
||||
if self.check_winner(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Check for tie after bot move
|
||||
if self.is_board_full(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Continue game
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
|
||||
def end_game(self, id):
|
||||
"""Clean up finished game but keep stats"""
|
||||
if id in self.game:
|
||||
# Remove game but we'll create new one on next play
|
||||
del self.game[id]
|
||||
|
||||
def end(self, id):
|
||||
"""End game completely (called by 'end' command)"""
|
||||
if id in self.game:
|
||||
del self.game[id]
|
||||
|
||||
|
||||
# Global instances for the bot system
|
||||
tictactoeTracker = []
|
||||
tictactoe = TicTacToe()
|
||||
@@ -371,6 +371,7 @@ try:
|
||||
golfSim_enabled = config['games'].getboolean('golfSim', True)
|
||||
hangman_enabled = config['games'].getboolean('hangman', True)
|
||||
hamtest_enabled = config['games'].getboolean('hamtest', True)
|
||||
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
|
||||
|
||||
# messaging settings
|
||||
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
|
||||
|
||||
@@ -152,6 +152,12 @@ def store_sms(nodeID, sms):
|
||||
global sms_db
|
||||
try:
|
||||
logger.debug("System: Setting SMS for " + str(nodeID))
|
||||
# if the nodeID has over 5 sms addresses warn and return
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
if len(item['sms']) >= 5:
|
||||
logger.warning("System: 📵SMS limit reached for " + str(nodeID))
|
||||
return False
|
||||
# if not in db, add it
|
||||
if nodeID not in sms_db:
|
||||
sms_db.append({'nodeID': nodeID, 'sms': sms})
|
||||
|
||||
@@ -7,8 +7,10 @@ import meshtastic.ble_interface
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
# not ideal but needed?
|
||||
import contextlib # for suppressing output on watchdog
|
||||
import io # for suppressing output on watchdog
|
||||
# homebrew 'modules'
|
||||
from modules.log import *
|
||||
|
||||
# Global Variables
|
||||
@@ -19,6 +21,71 @@ games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
interface_retry_count = 3
|
||||
|
||||
# Memory Management Constants
|
||||
MAX_MSG_HISTORY = 100
|
||||
MAX_CMD_HISTORY = 200
|
||||
MAX_SEEN_NODES = 200
|
||||
CLEANUP_INTERVAL = 86400 # 24 hours in seconds
|
||||
GAMEDELAY = CLEANUP_INTERVAL # the age of game entries in seconds before they are cleaned up
|
||||
|
||||
def cleanup_memory():
|
||||
"""Clean up memory by limiting list sizes and removing stale entries"""
|
||||
global cmdHistory, seenNodes, multiPingList
|
||||
current_time = time.time()
|
||||
|
||||
try:
|
||||
# Limit cmdHistory size
|
||||
if 'cmdHistory' in globals() and len(cmdHistory) > MAX_CMD_HISTORY:
|
||||
cmdHistory = cmdHistory[-(MAX_CMD_HISTORY - 50):] # keep the most recent 50 entries
|
||||
logger.debug(f"System: Trimmed cmdHistory to {len(cmdHistory)} entries")
|
||||
|
||||
# Clean up old seenNodes entries (older than 24 hours)
|
||||
if 'seenNodes' in globals():
|
||||
initial_count = len(seenNodes)
|
||||
seenNodes = [node for node in seenNodes
|
||||
if current_time - node.get('lastSeen', 0) < 86400]
|
||||
if len(seenNodes) < initial_count:
|
||||
logger.debug(f"System: Cleaned up {initial_count - len(seenNodes)} old seenNodes entries")
|
||||
|
||||
# Clean up stale game tracker entries
|
||||
cleanup_game_trackers(current_time)
|
||||
|
||||
# Clean up multiPingList of completed or stale entries
|
||||
if 'multiPingList' in globals():
|
||||
multiPingList[:] = [ping for ping in multiPingList
|
||||
if ping.get('message_from_id', 0) != 0 and
|
||||
ping.get('count', 0) > 0]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error during memory cleanup: {e}")
|
||||
|
||||
def cleanup_game_trackers(current_time):
|
||||
"""Clean up all game tracker lists of stale entries"""
|
||||
try:
|
||||
# List of game tracker global variable names
|
||||
tracker_names = [
|
||||
'dwPlayerTracker', 'lemonadeTracker', 'jackTracker',
|
||||
'vpTracker', 'mindTracker', 'golfTracker',
|
||||
'hangmanTracker', 'hamtestTracker', 'tictactoeTracker'
|
||||
]
|
||||
|
||||
for tracker_name in tracker_names:
|
||||
if tracker_name in globals():
|
||||
tracker = globals()[tracker_name]
|
||||
if isinstance(tracker, list):
|
||||
initial_count = len(tracker)
|
||||
# Remove entries older than GAMEDELAY
|
||||
globals()[tracker_name] = [
|
||||
entry for entry in tracker
|
||||
if current_time - entry.get('last_played', entry.get('time', 0)) < GAMEDELAY
|
||||
]
|
||||
cleaned_count = initial_count - len(globals()[tracker_name])
|
||||
if cleaned_count > 0:
|
||||
logger.debug(f"System: Cleaned up {cleaned_count} stale entries from {tracker_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error cleaning up game trackers: {e}")
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
# ping, pinging, ack, testing, test, pong
|
||||
@@ -193,6 +260,11 @@ if hamtest_enabled:
|
||||
trap_list = trap_list + ("hamtest",)
|
||||
games_enabled = True
|
||||
|
||||
if tictactoe_enabled:
|
||||
from modules.games.tictactoe import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("tictactoe",)
|
||||
games_enabled = True
|
||||
|
||||
# Games Configuration
|
||||
if games_enabled is True:
|
||||
help_message = help_message + ", games"
|
||||
@@ -217,6 +289,8 @@ if games_enabled is True:
|
||||
gamesCmdList += "hangman, "
|
||||
if hamtest_enabled:
|
||||
gamesCmdList += "hamTest, "
|
||||
if tictactoe_enabled:
|
||||
gamesCmdList += "ticTacToe, "
|
||||
gamesCmdList = gamesCmdList[:-2] # remove the last comma
|
||||
else:
|
||||
gamesCmdList = ""
|
||||
@@ -1405,6 +1479,10 @@ async def watchdog():
|
||||
load_bbsdm()
|
||||
load_bbsdb()
|
||||
|
||||
# perform memory cleanup every 10 minutes
|
||||
if datetime.now().minute % 10 == 0:
|
||||
cleanup_memory()
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
|
||||
Reference in New Issue
Block a user