mirror of
https://github.com/ghndrx/shellmate.git
synced 2026-02-10 06:45:02 +00:00
Fix terminal resize crash, improve TUI scaling & styling
- Add TerminalSession class with proper resize handler - Fix crash when terminal is resized - Add ASCII art title for wide terminals - Better centered panels with Rich - Green chess board theme with proper piece colors - Stockfish AI integration (falls back to random moves) - Move history display - Responsive layout that adapts to terminal size - Hide/show cursor appropriately
This commit is contained in:
@@ -44,17 +44,59 @@ class ShellMateSSHServer(asyncssh.SSHServer):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalSession:
|
||||||
|
"""Manages terminal state for an SSH session."""
|
||||||
|
|
||||||
|
def __init__(self, process: asyncssh.SSHServerProcess):
|
||||||
|
self.process = process
|
||||||
|
self.width = 80
|
||||||
|
self.height = 24
|
||||||
|
self._update_size()
|
||||||
|
self._resize_event = asyncio.Event()
|
||||||
|
|
||||||
|
def _update_size(self):
|
||||||
|
"""Update terminal size from process."""
|
||||||
|
size = self.process.get_terminal_size()
|
||||||
|
if size:
|
||||||
|
self.width = max(size[0], 40)
|
||||||
|
self.height = max(size[1], 10)
|
||||||
|
|
||||||
|
def handle_resize(self, width, height, pixwidth, pixheight):
|
||||||
|
"""Handle terminal resize event."""
|
||||||
|
self.width = max(width, 40)
|
||||||
|
self.height = max(height, 10)
|
||||||
|
self._resize_event.set()
|
||||||
|
self._resize_event.clear()
|
||||||
|
|
||||||
|
def write(self, data: str):
|
||||||
|
"""Write string data to terminal."""
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
self.process.stdout.write(data)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clear screen and move cursor home."""
|
||||||
|
self.write("\033[2J\033[H")
|
||||||
|
|
||||||
|
def hide_cursor(self):
|
||||||
|
self.write("\033[?25l")
|
||||||
|
|
||||||
|
def show_cursor(self):
|
||||||
|
self.write("\033[?25h")
|
||||||
|
|
||||||
|
|
||||||
async def handle_client(process: asyncssh.SSHServerProcess) -> None:
|
async def handle_client(process: asyncssh.SSHServerProcess) -> None:
|
||||||
"""Handle an SSH client session."""
|
"""Handle an SSH client session."""
|
||||||
username = process.get_extra_info("username") or "guest"
|
username = process.get_extra_info("username") or "guest"
|
||||||
|
|
||||||
# Get terminal info
|
# Create terminal session
|
||||||
term_type = process.get_terminal_type() or "xterm-256color"
|
session = TerminalSession(process)
|
||||||
term_size = process.get_terminal_size()
|
|
||||||
width = term_size[0] if term_size else 80
|
|
||||||
height = term_size[1] if term_size else 24
|
|
||||||
|
|
||||||
logger.info(f"Client {username}: term={term_type}, size={width}x{height}")
|
# Set up resize handler
|
||||||
|
process.channel.set_terminal_size_handler(session.handle_resize)
|
||||||
|
|
||||||
|
term_type = process.get_terminal_type() or "xterm-256color"
|
||||||
|
logger.info(f"Client {username}: term={term_type}, size={session.width}x{session.height}")
|
||||||
|
|
||||||
# Determine mode
|
# Determine mode
|
||||||
if username == "learn":
|
if username == "learn":
|
||||||
@@ -65,145 +107,268 @@ async def handle_client(process: asyncssh.SSHServerProcess) -> None:
|
|||||||
mode = "play"
|
mode = "play"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use simple Rich-based menu (more reliable over SSH)
|
session.hide_cursor()
|
||||||
await run_simple_menu(process, username, mode, width, height)
|
await run_simple_menu(process, session, username, mode)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in ShellMate session: {e}")
|
logger.exception(f"Error in ShellMate session: {e}")
|
||||||
try:
|
try:
|
||||||
process.stdout.write(f"\r\n\033[31mError: {e}\033[0m\r\n")
|
session.write(f"\r\n\033[31mError: {e}\033[0m\r\n")
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
session.show_cursor()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|
||||||
|
|
||||||
async def run_simple_menu(process, username: str, mode: str, width: int, height: int) -> None:
|
async def run_simple_menu(process, session: TerminalSession, username: str, mode: str) -> None:
|
||||||
"""Simple Rich-based fallback menu."""
|
"""Beautiful Rich-based menu that scales to terminal size."""
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
|
from rich.box import DOUBLE, ROUNDED
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.layout import Layout
|
||||||
import io
|
import io
|
||||||
|
|
||||||
class ProcessWriter:
|
class ProcessWriter:
|
||||||
def __init__(self, proc):
|
def __init__(self, sess):
|
||||||
self._proc = proc
|
self._session = sess
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
# Encode string to bytes for binary mode SSH
|
self._session.write(data)
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode('utf-8')
|
|
||||||
self._proc.stdout.write(data)
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
writer = ProcessWriter(process)
|
def render_menu():
|
||||||
console = Console(file=writer, width=width, height=height, force_terminal=True)
|
"""Render the main menu centered on screen."""
|
||||||
|
writer = ProcessWriter(session)
|
||||||
|
console = Console(file=writer, width=session.width, height=session.height, force_terminal=True)
|
||||||
|
|
||||||
|
session.clear()
|
||||||
|
|
||||||
|
# Calculate vertical padding for centering
|
||||||
|
menu_height = 18
|
||||||
|
top_pad = max(0, (session.height - menu_height) // 2)
|
||||||
|
|
||||||
|
for _ in range(top_pad):
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# ASCII art title
|
||||||
|
if session.width >= 60:
|
||||||
|
title = """
|
||||||
|
███████╗██╗ ██╗███████╗██╗ ██╗ ███╗ ███╗ █████╗ ████████╗███████╗
|
||||||
|
██╔════╝██║ ██║██╔════╝██║ ██║ ████╗ ████║██╔══██╗╚══██╔══╝██╔════╝
|
||||||
|
███████╗███████║█████╗ ██║ ██║ ██╔████╔██║███████║ ██║ █████╗
|
||||||
|
╚════██║██╔══██║██╔══╝ ██║ ██║ ██║╚██╔╝██║██╔══██║ ██║ ██╔══╝
|
||||||
|
███████║██║ ██║███████╗███████╗███████╗██║ ╚═╝ ██║██║ ██║ ██║ ███████╗
|
||||||
|
╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
||||||
|
"""
|
||||||
|
console.print(Text(title, style="bold green"))
|
||||||
|
else:
|
||||||
|
console.print(Align.center(Text("♟️ SHELLMATE ♟️", style="bold green")))
|
||||||
|
|
||||||
|
console.print(Align.center(Text("SSH into Chess Mastery", style="dim italic")))
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Menu panel
|
||||||
|
menu_content = f"""
|
||||||
|
[cyan]Welcome, [bold]{username}[/bold]![/cyan]
|
||||||
|
|
||||||
|
[bold white][[1]][/] ⚔️ Play vs AI
|
||||||
|
[bold white][[2]][/] 👥 Play vs Human
|
||||||
|
[bold white][[3]][/] 📚 Learn & Practice
|
||||||
|
[bold white][[q]][/] 🚪 Quit
|
||||||
|
|
||||||
|
[dim]Press a key to select...[/dim]
|
||||||
|
"""
|
||||||
|
panel_width = min(50, session.width - 4)
|
||||||
|
panel = Panel(
|
||||||
|
Align.center(menu_content),
|
||||||
|
box=ROUNDED,
|
||||||
|
border_style="bright_blue",
|
||||||
|
width=panel_width,
|
||||||
|
title="[bold white]♔ Main Menu ♔[/]",
|
||||||
|
subtitle=f"[dim]{session.width}x{session.height}[/dim]"
|
||||||
|
)
|
||||||
|
console.print(Align.center(panel))
|
||||||
|
|
||||||
# Clear screen
|
render_menu()
|
||||||
console.print("\033[2J\033[H", end="")
|
|
||||||
|
|
||||||
# Welcome banner
|
# Wait for input with resize handling
|
||||||
banner = """
|
|
||||||
╔═══════════════════════════════════════╗
|
|
||||||
║ ♟️ S H E L L M A T E ♟️ ║
|
|
||||||
║ SSH into Chess Mastery ║
|
|
||||||
╚═══════════════════════════════════════╝
|
|
||||||
"""
|
|
||||||
console.print(Align.center(Text(banner, style="bold green")))
|
|
||||||
console.print()
|
|
||||||
console.print(Align.center(f"[cyan]Welcome, {username}![/cyan]"))
|
|
||||||
console.print()
|
|
||||||
console.print(Align.center("[bold]Select mode:[/bold]"))
|
|
||||||
console.print()
|
|
||||||
console.print(Align.center(" [1] ⚔️ Play vs AI"))
|
|
||||||
console.print(Align.center(" [2] 👥 Play vs Human"))
|
|
||||||
console.print(Align.center(" [3] 📚 Learn"))
|
|
||||||
console.print(Align.center(" [q] Quit"))
|
|
||||||
console.print()
|
|
||||||
console.print(Align.center("[dim]Press a key...[/dim]"))
|
|
||||||
|
|
||||||
# Wait for input
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = await process.stdin.read(1)
|
# Use asyncio.wait with timeout to check for resize
|
||||||
|
read_task = asyncio.create_task(process.stdin.read(1))
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[read_task],
|
||||||
|
timeout=0.5,
|
||||||
|
return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if resize happened
|
||||||
|
if session._resize_event.is_set():
|
||||||
|
render_menu()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = read_task.result()
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
|
|
||||||
char = data.decode() if isinstance(data, bytes) else data
|
char = data.decode() if isinstance(data, bytes) else data
|
||||||
|
|
||||||
if char in ('q', 'Q', '\x03', '\x04'): # q, Ctrl+C, Ctrl+D
|
if char in ('q', 'Q', '\x03', '\x04'): # q, Ctrl+C, Ctrl+D
|
||||||
console.print("\r\n[yellow]Goodbye![/yellow]\r\n")
|
session.clear()
|
||||||
|
session.write("\r\n\033[33mGoodbye! Thanks for playing!\033[0m\r\n\r\n")
|
||||||
break
|
break
|
||||||
elif char == '1':
|
elif char == '1':
|
||||||
console.print("\r\n[green]Starting game vs AI...[/green]")
|
await run_chess_game(process, session, username, "ai")
|
||||||
await run_chess_game(process, console, username, "ai")
|
|
||||||
break
|
break
|
||||||
elif char == '2':
|
elif char == '2':
|
||||||
console.print("\r\n[yellow]Matchmaking coming soon![/yellow]")
|
session.clear()
|
||||||
|
session.write("\r\n\033[33mMatchmaking coming soon! Try playing vs AI.\033[0m\r\n")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
render_menu()
|
||||||
elif char == '3':
|
elif char == '3':
|
||||||
console.print("\r\n[yellow]Tutorials coming soon![/yellow]")
|
session.clear()
|
||||||
|
session.write("\r\n\033[33mTutorials coming soon! Try playing vs AI.\033[0m\r\n")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
render_menu()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Input error: {e}")
|
logger.error(f"Input error: {e}")
|
||||||
break
|
continue # Don't break on errors, try to continue
|
||||||
|
|
||||||
|
|
||||||
async def run_chess_game(process, console, username: str, opponent: str) -> None:
|
async def run_chess_game(process, session: TerminalSession, username: str, opponent: str) -> None:
|
||||||
"""Run a chess game session."""
|
"""Run a beautiful chess game session with Stockfish AI."""
|
||||||
import chess
|
import chess
|
||||||
from rich.table import Table
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.box import ROUNDED
|
||||||
|
|
||||||
|
class ProcessWriter:
|
||||||
|
def __init__(self, sess):
|
||||||
|
self._session = sess
|
||||||
|
def write(self, data):
|
||||||
|
self._session.write(data)
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
board = chess.Board()
|
board = chess.Board()
|
||||||
|
move_history = []
|
||||||
|
status_msg = ""
|
||||||
|
|
||||||
# Unicode pieces
|
# Try to use Stockfish
|
||||||
|
stockfish_engine = None
|
||||||
|
try:
|
||||||
|
from stockfish import Stockfish
|
||||||
|
stockfish_engine = Stockfish(path="/usr/games/stockfish", depth=10)
|
||||||
|
stockfish_engine.set_skill_level(5) # Medium difficulty
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Stockfish not available: {e}")
|
||||||
|
|
||||||
|
# Unicode pieces with better styling
|
||||||
PIECES = {
|
PIECES = {
|
||||||
'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙',
|
'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙',
|
||||||
'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟',
|
'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟',
|
||||||
}
|
}
|
||||||
|
|
||||||
def render_board():
|
def render_board():
|
||||||
console.print("\033[2J\033[H", end="")
|
nonlocal status_msg
|
||||||
|
writer = ProcessWriter(session)
|
||||||
|
console = Console(file=writer, width=session.width, height=session.height, force_terminal=True)
|
||||||
|
|
||||||
# File labels
|
session.clear()
|
||||||
console.print(" a b c d e f g h")
|
|
||||||
console.print(" ╔═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╗")
|
# Calculate centering
|
||||||
|
board_width = 42
|
||||||
|
left_pad = max(0, (session.width - board_width) // 2)
|
||||||
|
pad = " " * left_pad
|
||||||
|
|
||||||
|
# Title
|
||||||
|
console.print()
|
||||||
|
console.print(Align.center(Text("♔ SHELLMATE CHESS ♔", style="bold cyan")))
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Board with better colors
|
||||||
|
console.print(f"{pad} a b c d e f g h")
|
||||||
|
console.print(f"{pad} ┌───┬───┬───┬───┬───┬───┬───┬───┐")
|
||||||
|
|
||||||
for rank in range(7, -1, -1):
|
for rank in range(7, -1, -1):
|
||||||
row = f" {rank + 1} ║"
|
row = f"{pad}{rank + 1} │"
|
||||||
for file in range(8):
|
for file in range(8):
|
||||||
square = chess.square(file, rank)
|
square = chess.square(file, rank)
|
||||||
piece = board.piece_at(square)
|
piece = board.piece_at(square)
|
||||||
|
|
||||||
is_light = (rank + file) % 2 == 1
|
is_light = (rank + file) % 2 == 1
|
||||||
bg = "on #6d8b5e" if is_light else "on #3d5a45"
|
# Green theme for board
|
||||||
|
if is_light:
|
||||||
|
bg = "on #769656" # Light green
|
||||||
|
else:
|
||||||
|
bg = "on #4a7c3f" # Dark green
|
||||||
|
|
||||||
if piece:
|
if piece:
|
||||||
char = PIECES.get(piece.symbol(), '?')
|
char = PIECES.get(piece.symbol(), '?')
|
||||||
fg = "white" if piece.color == chess.WHITE else "black"
|
# White pieces in white/cream, black pieces in dark
|
||||||
row += f"[{fg} {bg}] {char} [/]"
|
if piece.color == chess.WHITE:
|
||||||
|
fg = "#ffffff bold"
|
||||||
|
else:
|
||||||
|
fg = "#1a1a1a bold"
|
||||||
|
row += f"[{fg} {bg}] {char} [/]│"
|
||||||
else:
|
else:
|
||||||
row += f"[{bg}] [/]"
|
row += f"[{bg}] [/]│"
|
||||||
|
|
||||||
if file < 7:
|
|
||||||
row += "│"
|
|
||||||
|
|
||||||
row += f"║ {rank + 1}"
|
row += f" {rank + 1}"
|
||||||
console.print(row)
|
console.print(row)
|
||||||
|
|
||||||
if rank > 0:
|
if rank > 0:
|
||||||
console.print(" ╟───┼───┼───┼───┼───┼───┼───┼───╢")
|
console.print(f"{pad} ├───┼───┼───┼───┼───┼───┼───┼───┤")
|
||||||
|
|
||||||
console.print(" ╚═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╝")
|
console.print(f"{pad} └───┴───┴───┴───┴───┴───┴───┴───┘")
|
||||||
console.print(" a b c d e f g h")
|
console.print(f"{pad} a b c d e f g h")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
turn = "White" if board.turn == chess.WHITE else "Black"
|
# Status panel
|
||||||
console.print(f"[bold]{turn} to move[/bold]")
|
turn = "White ♔" if board.turn == chess.WHITE else "Black ♚"
|
||||||
|
turn_style = "bold white" if board.turn == chess.WHITE else "bold"
|
||||||
|
|
||||||
|
status_lines = []
|
||||||
|
status_lines.append(f"[{turn_style}]{turn} to move[/]")
|
||||||
|
|
||||||
if board.is_check():
|
if board.is_check():
|
||||||
console.print("[red bold]CHECK![/red bold]")
|
status_lines.append("[red bold]⚠️ CHECK![/red bold]")
|
||||||
|
|
||||||
console.print("\r\n[dim]Enter move (e.g., e2e4) or 'q' to quit:[/dim] ", end="")
|
if move_history:
|
||||||
|
last_moves = move_history[-3:]
|
||||||
|
status_lines.append(f"[dim]Moves: {' '.join(last_moves)}[/dim]")
|
||||||
|
|
||||||
|
if status_msg:
|
||||||
|
status_lines.append(f"[yellow]{status_msg}[/yellow]")
|
||||||
|
status_msg = ""
|
||||||
|
|
||||||
|
status_lines.append("")
|
||||||
|
status_lines.append("[dim]Enter move (e.g. e2e4) │ [q]uit │ [r]esign[/dim]")
|
||||||
|
|
||||||
|
status_panel = Panel(
|
||||||
|
"\n".join(status_lines),
|
||||||
|
box=ROUNDED,
|
||||||
|
border_style="blue",
|
||||||
|
width=min(50, session.width - 4),
|
||||||
|
title="[bold]Game Status[/bold]"
|
||||||
|
)
|
||||||
|
console.print(Align.center(status_panel))
|
||||||
|
|
||||||
|
# Show cursor for input
|
||||||
|
session.show_cursor()
|
||||||
|
console.print(Align.center("[cyan]Move: [/cyan]"), end="")
|
||||||
|
|
||||||
render_board()
|
render_board()
|
||||||
|
|
||||||
@@ -211,7 +376,24 @@ async def run_chess_game(process, console, username: str, opponent: str) -> None
|
|||||||
|
|
||||||
while not board.is_game_over():
|
while not board.is_game_over():
|
||||||
try:
|
try:
|
||||||
data = await process.stdin.read(1)
|
read_task = asyncio.create_task(process.stdin.read(1))
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[read_task],
|
||||||
|
timeout=0.5,
|
||||||
|
return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle resize
|
||||||
|
if session._resize_event.is_set():
|
||||||
|
render_board()
|
||||||
|
session.write(move_buffer)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = read_task.result()
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -219,58 +401,92 @@ async def run_chess_game(process, console, username: str, opponent: str) -> None
|
|||||||
|
|
||||||
if char in ('\x03', '\x04'): # Ctrl+C/D
|
if char in ('\x03', '\x04'): # Ctrl+C/D
|
||||||
break
|
break
|
||||||
elif char == 'q':
|
elif char in ('q', 'r'):
|
||||||
console.print("\r\n[yellow]Game ended.[/yellow]")
|
status_msg = "Game resigned. Thanks for playing!"
|
||||||
|
render_board()
|
||||||
|
await asyncio.sleep(2)
|
||||||
break
|
break
|
||||||
elif char == '\r' or char == '\n':
|
elif char == '\r' or char == '\n':
|
||||||
# Process move
|
|
||||||
if move_buffer:
|
if move_buffer:
|
||||||
try:
|
try:
|
||||||
move = chess.Move.from_uci(move_buffer.strip())
|
move = chess.Move.from_uci(move_buffer.strip().lower())
|
||||||
if move in board.legal_moves:
|
if move in board.legal_moves:
|
||||||
board.push(move)
|
board.push(move)
|
||||||
|
move_history.append(move_buffer.lower())
|
||||||
move_buffer = ""
|
move_buffer = ""
|
||||||
render_board()
|
render_board()
|
||||||
|
|
||||||
# AI response
|
# AI response
|
||||||
if opponent == "ai" and not board.is_game_over():
|
if opponent == "ai" and not board.is_game_over():
|
||||||
console.print("[cyan]AI thinking...[/cyan]")
|
session.hide_cursor()
|
||||||
await asyncio.sleep(0.5)
|
session.write("\r\n\033[36m AI thinking...\033[0m")
|
||||||
|
|
||||||
|
if stockfish_engine:
|
||||||
|
try:
|
||||||
|
stockfish_engine.set_fen_position(board.fen())
|
||||||
|
best_move = stockfish_engine.get_best_move()
|
||||||
|
ai_move = chess.Move.from_uci(best_move)
|
||||||
|
except:
|
||||||
|
import random
|
||||||
|
ai_move = random.choice(list(board.legal_moves))
|
||||||
|
else:
|
||||||
|
import random
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
ai_move = random.choice(list(board.legal_moves))
|
||||||
|
|
||||||
# Simple random legal move for now
|
|
||||||
import random
|
|
||||||
ai_move = random.choice(list(board.legal_moves))
|
|
||||||
board.push(ai_move)
|
board.push(ai_move)
|
||||||
|
move_history.append(ai_move.uci())
|
||||||
render_board()
|
render_board()
|
||||||
else:
|
else:
|
||||||
console.print(f"\r\n[red]Illegal move: {move_buffer}[/red]")
|
status_msg = f"Illegal move: {move_buffer}"
|
||||||
console.print("[dim]Enter move:[/dim] ", end="")
|
|
||||||
move_buffer = ""
|
move_buffer = ""
|
||||||
|
render_board()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"\r\n[red]Invalid: {move_buffer}[/red]")
|
status_msg = f"Invalid move format: {move_buffer}"
|
||||||
console.print("[dim]Enter move:[/dim] ", end="")
|
|
||||||
move_buffer = ""
|
move_buffer = ""
|
||||||
|
render_board()
|
||||||
elif char == '\x7f' or char == '\b': # Backspace
|
elif char == '\x7f' or char == '\b': # Backspace
|
||||||
if move_buffer:
|
if move_buffer:
|
||||||
move_buffer = move_buffer[:-1]
|
move_buffer = move_buffer[:-1]
|
||||||
process.stdout.write(b'\b \b')
|
session.write('\b \b')
|
||||||
elif char.isprintable():
|
elif char.isprintable() and len(move_buffer) < 5:
|
||||||
move_buffer += char
|
move_buffer += char
|
||||||
process.stdout.write(char.encode('utf-8'))
|
session.write(char)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Game input error: {e}")
|
logger.error(f"Game input error: {e}")
|
||||||
break
|
continue
|
||||||
|
|
||||||
if board.is_game_over():
|
if board.is_game_over():
|
||||||
|
session.clear()
|
||||||
|
writer = ProcessWriter(session)
|
||||||
|
console = Console(file=writer, width=session.width, height=session.height, force_terminal=True)
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
if board.is_checkmate():
|
if board.is_checkmate():
|
||||||
winner = "Black" if board.turn == chess.WHITE else "White"
|
winner = "Black ♚" if board.turn == chess.WHITE else "White ♔"
|
||||||
console.print(f"[bold green]Checkmate! {winner} wins![/bold green]")
|
console.print(Align.center(Panel(
|
||||||
|
f"[bold green]🏆 CHECKMATE! 🏆\n\n{winner} wins![/bold green]",
|
||||||
|
box=ROUNDED,
|
||||||
|
border_style="green",
|
||||||
|
width=40
|
||||||
|
)))
|
||||||
elif board.is_stalemate():
|
elif board.is_stalemate():
|
||||||
console.print("[yellow]Stalemate! Draw.[/yellow]")
|
console.print(Align.center(Panel(
|
||||||
|
"[yellow]Stalemate!\n\nThe game is a draw.[/yellow]",
|
||||||
|
box=ROUNDED,
|
||||||
|
border_style="yellow",
|
||||||
|
width=40
|
||||||
|
)))
|
||||||
else:
|
else:
|
||||||
console.print("[yellow]Game over - Draw.[/yellow]")
|
console.print(Align.center(Panel(
|
||||||
|
"[yellow]Game Over\n\nDraw by repetition or insufficient material.[/yellow]",
|
||||||
|
box=ROUNDED,
|
||||||
|
border_style="yellow",
|
||||||
|
width=40
|
||||||
|
)))
|
||||||
|
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user