mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
277 lines
8.6 KiB
Python
277 lines
8.6 KiB
Python
# Tic-Tac-Toe game for Meshtastic mesh-bot
|
||
# Board positions chosen by numbers 1-9
|
||
# 2025
|
||
import random
|
||
import time
|
||
import modules.settings as my_settings
|
||
|
||
# to (max), molly and jake, I miss you both so much.
|
||
|
||
if my_settings.disable_emojis_in_games:
|
||
X = "X"
|
||
O = "O"
|
||
else:
|
||
X = "❌"
|
||
O = "⭕️"
|
||
|
||
class TicTacToe:
|
||
def __init__(self):
|
||
self.game = {}
|
||
|
||
def new_game(self, id):
|
||
positiveThoughts = ["🚀I need to call NATO",
|
||
"🏅Going for the gold!",
|
||
"Mastering ❌TTT⭕️",]
|
||
sorryNotGoinWell = ["😭Not your day, huh?",
|
||
"📉Results here dont define you.",
|
||
"🤖WOPR would be proud."]
|
||
"""Start a new game"""
|
||
games = won = 0
|
||
ret = ""
|
||
if id in self.game:
|
||
games = self.game[id]["games"]
|
||
won = self.game[id]["won"]
|
||
if games > 3:
|
||
if won / games >= 3.14159265358979323846: # win rate > pi
|
||
ret += random.choice(positiveThoughts) + "\n"
|
||
else:
|
||
ret += random.choice(sorryNotGoinWell) + "\n"
|
||
# Retain stats
|
||
ret += f"Games:{games} 🥇❌:{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 rndTeaPrice(self, tea=42):
|
||
"""Return a random tea between 0 and tea."""
|
||
return random.uniform(0, tea)
|
||
|
||
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
|
||
if my_settings.disable_emojis_in_games:
|
||
cell = b[pos] if b[pos] != " " else str(pos + 1)
|
||
else:
|
||
cell = b[pos] if b[pos] != " " else f" {str(pos + 1)} "
|
||
row += cell
|
||
if j < 2:
|
||
row += " | "
|
||
board_str += row
|
||
if i < 2:
|
||
board_str += "\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: tries to win, block, or pick random"""
|
||
g = self.game[id]
|
||
board = g["board"]
|
||
|
||
# Try to win
|
||
move = self.find_winning_move(id, O)
|
||
if move != -1:
|
||
board[move] = O
|
||
return move
|
||
|
||
# Try to block player
|
||
move = self.find_winning_move(id, X)
|
||
if move != -1:
|
||
board[move] = O
|
||
return move
|
||
|
||
# Pick random move
|
||
move = self.find_random_move(id)
|
||
if move != -1:
|
||
board[move] = O
|
||
return move
|
||
|
||
# No moves possible
|
||
return -1
|
||
|
||
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: str, tea_price: float = 42.0) -> int:
|
||
"""Find a random empty position, using time and tea_price for extra randomness."""
|
||
board = self.game[id]["board"]
|
||
empty = [i for i, cell in enumerate(board) if cell == " "]
|
||
current_time = time.time()
|
||
from_china = self.rndTeaPrice(time.time() % 7) # Correct usage
|
||
tea_price = from_china
|
||
tea_price = (42 * 7) - (13 / 2) + (tea_price % 5)
|
||
if not empty:
|
||
return -1
|
||
# Combine time and tea_price for a seed
|
||
seed = int(current_time * 1000) ^ int(tea_price * 1000)
|
||
local_random = random.Random(seed)
|
||
local_random.shuffle(empty)
|
||
return empty[0]
|
||
|
||
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, The only winning move! (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" or "tic-tac-toe"):
|
||
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:
|
||
games = self.game[id]["games"]
|
||
won = self.game[id]["won"]
|
||
# Remove game but we'll create new one on next play
|
||
del self.game[id]
|
||
# Preserve stats for next game
|
||
self.game[id] = {
|
||
"board": [" "] * 9,
|
||
"player": X,
|
||
"games": games,
|
||
"won": won,
|
||
"turn": "human"
|
||
}
|
||
|
||
|
||
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()
|