feat: ShellMate - SSH Chess TUI

Play chess in your terminal over SSH. No installs, no accounts.

Features:
- Beautiful terminal-filling chess board with ANSI colors
- Play against Stockfish AI (multiple difficulty levels)
- Two-step move interaction with visual feedback
- Leaderboard with PostgreSQL persistence
- SSH key persistence across restarts

Infrastructure:
- Docker containerized deployment
- CI/CD pipeline for dev/staging/production
- Health checks with auto-rollback
- Landing page at shellmate.sh

Tech: Python 3.12+, asyncssh, python-chess, Stockfish
This commit is contained in:
2026-02-01 20:05:58 +00:00
commit 590fbe045c
33 changed files with 3925 additions and 0 deletions

176
tests/test_ui_render.py Normal file
View File

@@ -0,0 +1,176 @@
"""Tests for UI rendering - catches Rich markup errors before deployment."""
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
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"
)
username = "testuser"
width = 80
height = 24
pieces = "♔ ♕ ♖ ♗ ♘ ♙"
# Title section
console.print(Align.center(Text(pieces, style="dim white")))
console.print()
console.print(Align.center(Text("S H E L L M A T E", style="bold bright_green")))
console.print(Align.center(Text("" * 20, style="green")))
console.print(Align.center(Text("SSH into Chess Mastery", style="italic bright_black")))
console.print(Align.center(Text(pieces[::-1], style="dim white")))
console.print()
# Menu table - this is where markup errors often occur
menu_table = Table(show_header=False, box=None, padding=(0, 2))
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 [/] 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"))
panel_width = min(45, width - 4)
panel = Panel(
Align.center(menu_table),
box=ROUNDED,
border_style="bright_blue",
width=panel_width,
padding=(1, 2),
)
console.print(Align.center(panel))
# Footer
console.print()
console.print(Align.center(Text(f"Terminal: {width}×{height}", style="dim")))
# If we got here without exception, markup is valid
rendered = output.getvalue()
# Check key content is present (content may have ANSI codes)
assert "S H E L L M A T E" in rendered or "SHELLMATE" in rendered
assert "Welcome" in rendered
assert "Play vs AI" in rendered
def test_game_status_render():
"""Test that game status panel renders correctly."""
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True)
status_lines = []
status_lines.append("[bold white]White ♔ to move[/bold white]")
status_lines.append("[dim]Moves: e2e4 e7e5 g1f3[/dim]")
status_lines.append("")
status_lines.append("[dim]Enter move (e.g. e2e4) │ [q]uit │ [r]esign[/dim]")
panel = Panel(
"\n".join(status_lines),
box=ROUNDED,
border_style="blue",
width=50,
title="[bold]Game Status[/bold]"
)
console.print(panel)
rendered = output.getvalue()
assert "White" in rendered
assert "Game Status" in rendered
def test_chess_board_render():
"""Test that chess board renders without errors."""
output = StringIO()
piece_map = {
'K': '', 'Q': '', 'R': '', 'B': '', 'N': '', 'P': '',
'k': '', 'q': '', 'r': '', 'b': '', 'n': '', 'p': '',
}
# Simplified board rendering (plain text)
lines = []
lines.append(" a b c d e f g h")
lines.append(" +---+---+---+---+---+---+---+---+")
# Render rank 8 with pieces
pieces_row = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']
row = " 8 |"
for piece_char in pieces_row:
char = piece_map.get(piece_char, '?')
row += f" {char} |"
row += " 8"
lines.append(row)
lines.append(" +---+---+---+---+---+---+---+---+")
for line in lines:
output.write(line + "\n")
rendered = output.getvalue()
assert "a b c" in rendered
assert "" in rendered # Black rook
assert "+---+" in rendered
def test_narrow_terminal_render():
"""Test that menu renders correctly on narrow terminals."""
output = StringIO()
console = Console(file=output, width=40, height=20, force_terminal=True)
# Small terminal fallback
console.print(Align.center(Text("♟ SHELLMATE ♟", style="bold green")))
console.print()
menu_table = Table(show_header=False, box=None)
menu_table.add_column(justify="center")
menu_table.add_row(Text("Welcome!", style="cyan"))
menu_table.add_row("[dim]1. Play[/dim]")
menu_table.add_row("[dim]q. Quit[/dim]")
console.print(Align.center(menu_table))
rendered = output.getvalue()
assert "SHELLMATE" in rendered
def test_markup_escape_special_chars():
"""Test that usernames with special chars don't break markup."""
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True)
# Usernames that could break markup
test_usernames = [
"normal_user",
"user[with]brackets",
"user<with>angles",
"[admin]",
"test/path",
]
for username in test_usernames:
# Using Text() object safely escapes special characters
console.print(Text(f"Welcome, {username}!", style="cyan"))
rendered = output.getvalue()
assert "normal_user" in rendered
# Text() should have escaped the brackets safely