1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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(())
}