diff --git a/src/shellmate/ai/engine.py b/src/shellmate/ai/engine.py index 0da91ab..b528a29 100644 --- a/src/shellmate/ai/engine.py +++ b/src/shellmate/ai/engine.py @@ -1,8 +1,7 @@ """Stockfish AI engine wrapper with move explanations.""" -import asyncio from dataclasses import dataclass -from typing import Optional + import chess from stockfish import Stockfish diff --git a/src/shellmate/core/game.py b/src/shellmate/core/game.py index 18b2850..9233015 100644 --- a/src/shellmate/core/game.py +++ b/src/shellmate/core/game.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum -from typing import Optional + import chess import chess.pgn @@ -25,8 +25,8 @@ class Move: fen_after: str timestamp: datetime = field(default_factory=datetime.utcnow) think_time_ms: int = 0 - evaluation: Optional[float] = None - explanation: Optional[str] = None + evaluation: float | None = None + explanation: str | None = None @dataclass @@ -51,7 +51,7 @@ class ChessGame: """Return the ID of the player whose turn it is.""" return self.white_player_id if self.board.turn == chess.WHITE else self.black_player_id - def make_move(self, uci: str) -> Optional[Move]: + def make_move(self, uci: str) -> Move | None: """Make a move and return Move object if valid.""" try: chess_move = chess.Move.from_uci(uci) @@ -86,7 +86,10 @@ class ChessGame: def _check_game_end(self) -> None: """Check if the game has ended and set result.""" if self.board.is_checkmate(): - self.result = GameResult.BLACK_WINS if self.board.turn == chess.WHITE else GameResult.WHITE_WINS + if self.board.turn == chess.WHITE: + self.result = GameResult.BLACK_WINS + else: + self.result = GameResult.WHITE_WINS elif self.board.is_stalemate() or self.board.is_insufficient_material(): self.result = GameResult.DRAW elif self.board.can_claim_draw(): diff --git a/src/shellmate/core/player.py b/src/shellmate/core/player.py index 06d937f..a0e6bf6 100644 --- a/src/shellmate/core/player.py +++ b/src/shellmate/core/player.py @@ -3,7 +3,6 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum -from typing import Optional class PlayerType(Enum): @@ -25,7 +24,7 @@ class Player: losses: int = 0 draws: int = 0 created_at: datetime = field(default_factory=datetime.utcnow) - last_seen: Optional[datetime] = None + last_seen: datetime | None = None @property def winrate(self) -> float: diff --git a/src/shellmate/ssh/server.py b/src/shellmate/ssh/server.py index e47db85..afd7f09 100644 --- a/src/shellmate/ssh/server.py +++ b/src/shellmate/ssh/server.py @@ -3,11 +3,8 @@ import asyncio import logging import os -import sys -from typing import Optional -import asyncssh -from shellmate.tui.app import ShellMateApp +import asyncssh logger = logging.getLogger(__name__) @@ -16,13 +13,13 @@ class ShellMateSSHServer(asyncssh.SSHServer): """SSH server that launches ShellMate TUI for each connection.""" def __init__(self): - self._username: Optional[str] = None + self._username: str | None = None def connection_made(self, conn: asyncssh.SSHServerConnection) -> None: peername = conn.get_extra_info('peername') logger.info(f"SSH connection from {peername}") - def connection_lost(self, exc: Optional[Exception]) -> None: + def connection_lost(self, exc: Exception | None) -> None: if exc: logger.error(f"SSH connection error: {exc}") else: @@ -130,15 +127,13 @@ async def handle_client(process: asyncssh.SSHServerProcess) -> None: async def run_simple_menu(process, session: TerminalSession, username: str, mode: str) -> None: """Beautiful Rich-based menu that scales to terminal size.""" + + from rich.align import Align + from rich.box import ROUNDED from rich.console import Console from rich.panel import Panel - from rich.text import Text - from rich.align import Align - from rich.box import HEAVY, ROUNDED, DOUBLE from rich.table import Table - from rich.layout import Layout - from rich.style import Style - import io + from rich.text import Text class ProcessWriter: def __init__(self, sess): @@ -154,7 +149,10 @@ async def run_simple_menu(process, session: TerminalSession, username: str, mode session._update_size() writer = ProcessWriter(session) - console = Console(file=writer, width=session.width, height=session.height, force_terminal=True, color_system="truecolor") + console = Console( + file=writer, width=session.width, height=session.height, + force_terminal=True, color_system="truecolor" + ) session.clear() @@ -186,10 +184,10 @@ async def run_simple_menu(process, session: TerminalSession, username: str, mode menu_table.add_column(justify="center") menu_table.add_row(Text(f"Welcome, {username}!", style="cyan")) menu_table.add_row("") - menu_table.add_row("[bright_white on blue] 1 [/bright_white on blue] Play vs AI [dim]♔ vs ♚[/dim]") - menu_table.add_row("[bright_white on magenta] 2 [/bright_white on magenta] Play vs Human [dim]♔ vs ♔[/dim]") - menu_table.add_row("[bright_white on green] 3 [/bright_white on green] Learn & Practice [dim]📖[/dim]") - menu_table.add_row("[bright_white on red] q [/bright_white on red] Quit [dim]👋[/dim]") + menu_table.add_row("[bright_white on blue] 1 [/] Play vs AI [dim]♔ vs ♚[/]") + menu_table.add_row("[bright_white on magenta] 2 [/] Play vs Human [dim]♔ vs ♔[/]") + menu_table.add_row("[bright_white on green] 3 [/] Learn & Practice [dim]📖[/]") + menu_table.add_row("[bright_white on red] q [/] Quit [dim]👋[/]") menu_table.add_row("") menu_table.add_row(Text("Press a key to select...", style="dim italic")) @@ -205,7 +203,8 @@ async def run_simple_menu(process, session: TerminalSession, username: str, mode # Footer console.print() - console.print(Align.center(Text(f"Terminal: {session.width}×{session.height}", style="dim"))) + term_info = f"Terminal: {session.width}×{session.height}" + console.print(Align.center(Text(term_info, style="dim"))) render_menu() @@ -248,11 +247,10 @@ async def run_simple_menu(process, session: TerminalSession, username: str, mode async def run_chess_game(process, session: TerminalSession, username: str, opponent: str) -> None: """Run a beautiful chess game session with Stockfish AI.""" import chess + from rich.align import Align + from rich.box import ROUNDED from rich.console import Console from rich.panel import Panel - from rich.align import Align - from rich.text import Text - from rich.box import ROUNDED class ProcessWriter: def __init__(self, sess): @@ -276,7 +274,7 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon logger.warning(f"Stockfish not available: {e}") # Unicode pieces - PIECES = { + piece_chars = { 'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙', 'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟', } @@ -331,7 +329,7 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon bg, bg_end = get_cell_style(square, piece, is_light) if piece: - char = PIECES.get(piece.symbol(), '?') + char = piece_chars.get(piece.symbol(), '?') if piece.color == chess.WHITE: piece_row += f"{bg} \033[1;97m{char}\033[0m{bg} {bg_end}" else: @@ -378,9 +376,9 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon # Instructions based on state if selected_square is not None: sq_name = chess.square_name(selected_square).upper() - lines.append(f" \033[36mSelected: {sq_name}\033[0m → Type destination (or ESC to cancel)") + lines.append(f" \033[36mSelected: {sq_name}\033[0m → Type dest (ESC cancel)") else: - lines.append(" Type \033[36msquare\033[0m (e.g. E2) to select │ \033[31mQ\033[0m = quit") + lines.append(" Type \033[36msquare\033[0m (e.g. E2) │ \033[31mQ\033[0m quit") lines.append("") @@ -398,9 +396,9 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon # Input prompt if selected_square is not None: - session.write(pad + f" \033[32m→ \033[0m") + session.write(pad + " \033[32m→ \033[0m") else: - session.write(pad + f" \033[36m> \033[0m") + session.write(pad + " \033[36m> \033[0m") session.show_cursor() def parse_square(text): @@ -497,9 +495,14 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon # Check for promotion piece = board.piece_at(selected_square) if piece and piece.piece_type == chess.PAWN: - if (piece.color == chess.WHITE and chess.square_rank(sq) == 7) or \ - (piece.color == chess.BLACK and chess.square_rank(sq) == 0): - move = chess.Move(selected_square, sq, promotion=chess.QUEEN) + is_white_promo = (piece.color == chess.WHITE and + chess.square_rank(sq) == 7) + is_black_promo = (piece.color == chess.BLACK and + chess.square_rank(sq) == 0) + if is_white_promo or is_black_promo: + move = chess.Move( + selected_square, sq, promotion=chess.QUEEN + ) board.push(move) move_history.append(move.uci()) @@ -516,7 +519,7 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon stockfish_engine.set_fen_position(board.fen()) best_move = stockfish_engine.get_best_move() ai_move = chess.Move.from_uci(best_move) - except: + except Exception: import random ai_move = random.choice(list(board.legal_moves)) else: @@ -560,7 +563,10 @@ async def run_chess_game(process, session: TerminalSession, username: str, oppon if board.is_game_over(): session.clear() writer = ProcessWriter(session) - console = Console(file=writer, width=session.width, height=session.height, force_terminal=True) + console = Console( + file=writer, width=session.width, height=session.height, + force_terminal=True + ) console.print() if board.is_checkmate(): diff --git a/src/shellmate/tui/app.py b/src/shellmate/tui/app.py index 7b266d4..cac752b 100644 --- a/src/shellmate/tui/app.py +++ b/src/shellmate/tui/app.py @@ -1,19 +1,18 @@ """Main TUI application.""" -from typing import Any -from textual.app import App, ComposeResult -from textual.containers import Container, Horizontal, Vertical -from textual.widgets import Footer, Header, Static -from textual.binding import Binding -from textual.screen import Screen import chess +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Footer, Header from shellmate.tui.widgets import ( ChessBoardWidget, - MoveInput, - MoveListWidget, GameStatusWidget, MainMenu, + MoveInput, + MoveListWidget, ) from shellmate.tui.widgets.move_input import parse_move diff --git a/src/shellmate/tui/widgets/__init__.py b/src/shellmate/tui/widgets/__init__.py index 149a017..3ea1416 100644 --- a/src/shellmate/tui/widgets/__init__.py +++ b/src/shellmate/tui/widgets/__init__.py @@ -1,10 +1,10 @@ """TUI widgets for ShellMate.""" from .board import ChessBoardWidget +from .menu import MainMenu from .move_input import MoveInput from .move_list import MoveListWidget from .status import GameStatusWidget -from .menu import MainMenu __all__ = [ "ChessBoardWidget", diff --git a/src/shellmate/tui/widgets/board.py b/src/shellmate/tui/widgets/board.py index ad38569..d11d1f3 100644 --- a/src/shellmate/tui/widgets/board.py +++ b/src/shellmate/tui/widgets/board.py @@ -1,13 +1,12 @@ """Chess board widget with Unicode pieces - Hyper-polished version.""" -from textual.widget import Widget -from textual.reactive import reactive -from textual.geometry import Size -from rich.text import Text -from rich.style import Style -from rich.console import RenderableType import chess - +from rich.console import RenderableType +from rich.style import Style +from rich.text import Text +from textual.geometry import Size +from textual.reactive import reactive +from textual.widget import Widget # Unicode chess pieces - using filled variants for better visibility PIECES_FILLED = { @@ -120,7 +119,6 @@ class ChessBoardWidget(Widget): def _render_compact(self) -> RenderableType: """Compact board for small terminals.""" text = Text() - cw = 3 # Fixed cell width for compact files = "abcdefgh" if not self.flipped else "hgfedcba" diff --git a/src/shellmate/tui/widgets/menu.py b/src/shellmate/tui/widgets/menu.py index 3d8cfbc..29a1ac1 100644 --- a/src/shellmate/tui/widgets/menu.py +++ b/src/shellmate/tui/widgets/menu.py @@ -1,11 +1,9 @@ """Main menu widget.""" +from textual.containers import Vertical +from textual.message import Message from textual.widget import Widget from textual.widgets import Button, Static -from textual.containers import Vertical, Center -from textual.message import Message -from rich.text import Text -from rich.console import RenderableType class MainMenu(Widget): diff --git a/src/shellmate/tui/widgets/move_input.py b/src/shellmate/tui/widgets/move_input.py index cf7ac5e..5eb9b08 100644 --- a/src/shellmate/tui/widgets/move_input.py +++ b/src/shellmate/tui/widgets/move_input.py @@ -1,8 +1,8 @@ """Move input widget.""" -from textual.widgets import Input -from textual.message import Message import chess +from textual.message import Message +from textual.widgets import Input class MoveInput(Input): diff --git a/src/shellmate/tui/widgets/move_list.py b/src/shellmate/tui/widgets/move_list.py index ac328e4..b93b018 100644 --- a/src/shellmate/tui/widgets/move_list.py +++ b/src/shellmate/tui/widgets/move_list.py @@ -1,10 +1,9 @@ """Move list/history widget.""" -from textual.widget import Widget -from textual.reactive import reactive -from rich.text import Text from rich.console import RenderableType -import chess +from rich.text import Text +from textual.reactive import reactive +from textual.widget import Widget class MoveListWidget(Widget): diff --git a/src/shellmate/tui/widgets/status.py b/src/shellmate/tui/widgets/status.py index 812e3d2..db3b581 100644 --- a/src/shellmate/tui/widgets/status.py +++ b/src/shellmate/tui/widgets/status.py @@ -1,11 +1,9 @@ """Game status widget.""" -from textual.widget import Widget -from textual.reactive import reactive -from rich.text import Text -from rich.panel import Panel -from rich.console import RenderableType import chess +from rich.console import RenderableType +from rich.text import Text +from textual.widget import Widget class GameStatusWidget(Widget): @@ -43,7 +41,7 @@ class GameStatusWidget(Widget): if self._is_checkmate: winner = self._black_name if self._turn == chess.WHITE else self._white_name - text.append(f"♚ CHECKMATE!\n", style="bold red") + text.append("♚ CHECKMATE!\n", style="bold red") text.append(f"{winner} wins!", style="bold yellow") elif self._is_stalemate: text.append("½ STALEMATE\n", style="bold yellow") @@ -70,9 +68,13 @@ class GameStatusWidget(Widget): text.append(f"⚫ {black_time_str}", style=black_style) # Evaluation (if available) - if self._evaluation is not None and not (self._is_checkmate or self._is_stalemate or self._is_draw): + game_ended = self._is_checkmate or self._is_stalemate or self._is_draw + if self._evaluation is not None and not game_ended: text.append("\n\n") - eval_str = f"+{self._evaluation:.1f}" if self._evaluation > 0 else f"{self._evaluation:.1f}" + if self._evaluation > 0: + eval_str = f"+{self._evaluation:.1f}" + else: + eval_str = f"{self._evaluation:.1f}" bar = self._eval_bar(self._evaluation) text.append(f"Eval: {eval_str}\n", style="dim") text.append(bar) diff --git a/tests/test_ssh_server.py b/tests/test_ssh_server.py index afec699..2a94bd0 100644 --- a/tests/test_ssh_server.py +++ b/tests/test_ssh_server.py @@ -1,9 +1,9 @@ """Tests for SSH server functionality.""" -import asyncio -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + class TestShellMateSSHServer: """Test SSH server authentication.""" @@ -113,9 +113,10 @@ class TestChessBoard: def test_board_initialization(self): """Board should initialize with standard position.""" - from shellmate.tui.widgets.board import ChessBoardWidget import chess + from shellmate.tui.widgets.board import ChessBoardWidget + widget = ChessBoardWidget() assert widget.board.fen() == chess.STARTING_FEN @@ -136,9 +137,10 @@ class TestChessBoard: def test_square_selection(self): """Selecting a square should show legal moves.""" - from shellmate.tui.widgets.board import ChessBoardWidget import chess + from shellmate.tui.widgets.board import ChessBoardWidget + widget = ChessBoardWidget() # Select e2 pawn @@ -152,9 +154,10 @@ class TestChessBoard: def test_square_deselection(self): """Deselecting should clear legal moves.""" - from shellmate.tui.widgets.board import ChessBoardWidget import chess + from shellmate.tui.widgets.board import ChessBoardWidget + widget = ChessBoardWidget() widget.select_square(chess.E2) widget.select_square(None) @@ -251,9 +254,10 @@ class TestIntegration: @pytest.mark.asyncio async def test_server_starts(self): """Server should start without errors.""" - from shellmate.ssh.server import start_server - import tempfile import os + import tempfile + + from shellmate.ssh.server import start_server # Create a temporary host key with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/test_ui_render.py b/tests/test_ui_render.py index f6bc02b..777e241 100644 --- a/tests/test_ui_render.py +++ b/tests/test_ui_render.py @@ -1,20 +1,22 @@ """Tests for UI rendering - catches Rich markup errors before deployment.""" -import pytest from io import StringIO + +from rich.align import Align +from rich.box import ROUNDED from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.text import Text -from rich.align import Align -from rich.box import ROUNDED def test_menu_render_no_markup_errors(): """Test that the main menu renders without Rich markup errors.""" # Simulate the menu rendering code output = StringIO() - console = Console(file=output, width=80, height=24, force_terminal=True, color_system="truecolor") + console = Console( + file=output, width=80, height=24, force_terminal=True, color_system="truecolor" + ) username = "testuser" width = 80 @@ -35,10 +37,18 @@ def test_menu_render_no_markup_errors(): menu_table.add_column(justify="center") menu_table.add_row(Text(f"Welcome, {username}!", style="cyan")) menu_table.add_row("") - menu_table.add_row("[bright_white on blue] 1 [/bright_white on blue] Play vs AI [dim]♔ vs ♚[/dim]") - menu_table.add_row("[bright_white on magenta] 2 [/bright_white on magenta] Play vs Human [dim]♔ vs ♔[/dim]") - menu_table.add_row("[bright_white on green] 3 [/bright_white on green] Learn & Practice [dim]📖[/dim]") - menu_table.add_row("[bright_white on red] q [/bright_white on red] Quit [dim]👋[/dim]") + menu_table.add_row( + "[bright_white on blue] 1 [/] Play vs AI [dim]♔ vs ♚[/dim]" + ) + menu_table.add_row( + "[bright_white on magenta] 2 [/] Play vs Human [dim]♔ vs ♔[/dim]" + ) + menu_table.add_row( + "[bright_white on green] 3 [/] Learn & Practice [dim]📖[/dim]" + ) + menu_table.add_row( + "[bright_white on red] q [/] Quit [dim]👋[/dim]" + ) menu_table.add_row("") menu_table.add_row(Text("Press a key to select...", style="dim italic")) @@ -93,7 +103,7 @@ def test_chess_board_render(): """Test that chess board renders without errors.""" output = StringIO() - PIECES = { + piece_map = { 'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙', 'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟', } @@ -106,8 +116,8 @@ def test_chess_board_render(): # Render rank 8 with pieces pieces_row = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'] row = " 8 |" - for file, piece_char in enumerate(pieces_row): - char = PIECES.get(piece_char, '?') + for piece_char in pieces_row: + char = piece_map.get(piece_char, '?') row += f" {char} |" row += " 8" lines.append(row)