hyperchess 0.1.1

A fast, terminal-based multiplayer chess game built with Rust.
Documentation
mod game;
mod ui;

use std::io;
use std::env;
use tokio::sync::mpsc;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use ratatui::{backend::CrosstermBackend, Terminal};
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use chess::{File, Rank, Square, Color as ChessColor};
use game::AppState;
use hyperchess::protocol::GameMessage;
use serde_json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Argument Parsing
    let args: Vec<String> = env::args().collect();
    let is_multiplayer = args.contains(&String::from("--multiplayer"));

    // 2. Setup Terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // 3. Init State
    let mut app = AppState::new();
    
    // 4. Networking Setup (Channels)
    let (tx_net_out, mut rx_net_out) = mpsc::channel::<GameMessage>(10); // UI -> Network
    let (tx_net_in, mut rx_net_in) = mpsc::channel::<GameMessage>(10);   // Network -> UI

    if is_multiplayer {
        app.debug_msg = String::from("Connecting to server...");
        terminal.draw(|f| ui::draw_ui(f, &app))?;

        // Spawn Network Task
        tokio::spawn(async move {
            match TcpStream::connect("0.tcp.in.ngrok.io:13295").await {
                Ok(mut socket) => {
                    let (mut reader, mut writer) = socket.split();
                    let mut buf = [0; 1024];

                    loop {
                        tokio::select! {
                            // Read from Server
                            result = reader.read(&mut buf) => {
                                match result {
                                    Ok(0) => break, // Disconnected
                                    Ok(n) => {
                                        if let Ok(msg) = serde_json::from_slice::<GameMessage>(&buf[..n]) {
                                            let _ = tx_net_in.send(msg).await;
                                        }
                                    }
                                    Err(_) => break,
                                }
                            }
                            // Write to Server (Move sent from UI)
                            Some(msg) = rx_net_out.recv() => {
                                let json = serde_json::to_string(&msg).unwrap();
                                let _ = writer.write_all(json.as_bytes()).await;
                            }
                        }
                    }
                }
                Err(e) => {
                    // In a real app, handle error gracefully
                    eprintln!("Failed to connect: {}", e);
                }
            }
        });
    }

    // 5. Main Game Loop
    loop {
        // Draw UI
        terminal.draw(|f| ui::draw_ui(f, &app))?;

        // A. Handle Network Messages (Incoming)
        if is_multiplayer {
            while let Ok(msg) = rx_net_in.try_recv() {
                match msg {
                    GameMessage::GameStart(color_str) => {
                        let color = if color_str == "white" { ChessColor::White } else { ChessColor::Black };
                        app.set_player_color(color);
                    }
                    GameMessage::OpponentMove { from, to } => {
                        app.network_apply_move(from, to);
                    }
                    _ => {}
                }
            }
        }

        // B. Handle Input (Mouse/Keyboard)
        if event::poll(std::time::Duration::from_millis(50))? {
            match event::read()? {
                Event::Key(key) => {
                    if key.code == KeyCode::Char('q') {
                        break;
                    }
                }
                Event::Mouse(mouse) => {
                    if let MouseEventKind::Down(crossterm::event::MouseButton::Left) = mouse.kind {
                        let term_size = terminal.size()?;
                        
                        // Layout math (matches ui.rs)
                        let cell_width = 5;
                        let cell_height = 3;
                        let board_pixel_width = 8 * cell_width;
                        let start_x = term_size.x + (term_size.width.saturating_sub(board_pixel_width)) / 2;
                        let start_y = term_size.y + 6; // Margin(1) + Status(4) + Padding(1)

                        let relative_x = mouse.column.saturating_sub(start_x);
                        let relative_y = mouse.row.saturating_sub(start_y);
                        let col_idx = relative_x / cell_width;
                        let row_idx = relative_y / cell_height;

                        if col_idx < 8 && row_idx < 8 {
                            let current_turn = app.board.side_to_move();
                            
                            // View Rotation Logic
                            // If Multiplayer: Always rotate so MY color is at bottom
                            // If Hotseat: Rotate based on whose turn it is
                            
                            let view_orientation = if let Some(my_c) = app.my_color {
                                my_c // In multiplayer, board is fixed to my perspective
                            } else {
                                current_turn // In hotseat, board flips
                            };

                            let (rank, file) = match view_orientation {
                                ChessColor::White => (Rank::from_index(7 - row_idx as usize), File::from_index(col_idx as usize)),
                                ChessColor::Black => (Rank::from_index(row_idx as usize), File::from_index(7 - col_idx as usize)),
                            };

                            let sq = Square::make_square(rank, file);
                            
                            // Handle Click & Get Move (if any)
                            if let Some((from, to)) = app.handle_click(sq) {
                                // If a move was made and we are online, send it!
                                if is_multiplayer {
                                    let msg = GameMessage::MakeMove { from, to };
                                    let _ = tx_net_out.send(msg).await; // Use blocking send in sync context
                                }
                            }
                        }
                    }
                }
                _ => {}
            }
        }
    }

    // Cleanup
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
    terminal.show_cursor()?;

    Ok(())
}