From f524284cb2d82b4821db5a52adae36100f986ee7 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Tue, 2 Dec 2025 13:05:35 -0500 Subject: [PATCH] feat: 1.0 stable api --- debug_client.js | 72 ++++++++++++++++++++++----------------- example_client.py | 87 +++++++++++++++++++++++++++++------------------ src/main.rs | 21 ++++++++++-- src/types.rs | 20 ++++++++--- 4 files changed, 127 insertions(+), 73 deletions(-) diff --git a/debug_client.js b/debug_client.js index 18eeb93..c4146af 100644 --- a/debug_client.js +++ b/debug_client.js @@ -1,56 +1,66 @@ -const WebSocket = require('ws'); -const readline = require('readline'); +const WebSocket = require("ws"); +const readline = require("readline"); -const url = 'wss://connect4.abunchofknowitalls.com'; -console.log(`Connecting to ${url}...`); -const ws = new WebSocket(url); +const DEFAULT_URL = "wss://connect4.abunchofknowitalls.com"; +let ws; let pingInterval; -ws.on('open', () => { - console.log('Connected to server!'); - console.log('Type a message and press Enter to send raw text.'); +function startClient(serverUrl) { + console.log(`Connecting to ${serverUrl}...`); + ws = new WebSocket(serverUrl); + + ws.on("open", () => { + console.log("Connected to server!"); + console.log("Type a message and press Enter to send raw text."); // Keep the connection alive by sending a ping every 30 seconds pingInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.ping(); - } + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + } }, 30000); -}); + }); -ws.on('message', (data) => { + ws.on("message", (data) => { // data is usually a Buffer in 'ws', convert to string - console.log('< ' + data.toString()); -}); + console.log("< " + data.toString()); + }); -ws.on('pong', () => { + ws.on("pong", () => { // console.log('Received pong'); -}); + }); -ws.on('close', () => { - console.log('Disconnected from server.'); + ws.on("close", () => { + console.log("Disconnected from server."); if (pingInterval) clearInterval(pingInterval); process.exit(0); -}); + }); -ws.on('error', (err) => { - console.error('WebSocket error:', err.message); + ws.on("error", (err) => { + console.error("WebSocket error:", err.message); if (pingInterval) clearInterval(pingInterval); -}); + }); +} const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: '> ' + input: process.stdin, + output: process.stdout, + prompt: "> ", }); -rl.prompt(); +rl.question(`Enter server address [${DEFAULT_URL}]: `, (answer) => { + const serverUrl = answer.trim() || DEFAULT_URL; + startClient(serverUrl); -rl.on('line', (line) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(line); + rl.on("line", (line) => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(line); } else { - console.log('Socket not open. State:', ws.readyState); + const state = ws ? ws.readyState : "N/A"; + console.log("Socket not open. State:", state); } rl.prompt(); + }); + + rl.prompt(); }); diff --git a/example_client.py b/example_client.py index ff8686d..ab6b2b9 100644 --- a/example_client.py +++ b/example_client.py @@ -2,48 +2,67 @@ import asyncio import websockets +DEFAULT_SERVER_URL = "wss://connect4.abunchofknowitalls.com" + + def calculate_move(opponent_move, board): - if opponent_move is not None: - print(f"Opponent played column {opponent_move}") - # TODO: Use the board variable to see and set the current state of the board - # TODO: Implement your move calculation logic here instead - return 0 + if opponent_move is not None: + print(f"Opponent played column {opponent_move}") + # TODO: Use the board variable to see and set the current state of the board + # TODO: Implement your move calculation logic here instead + return 0 async def gameloop(socket): - board = [[None] * 6 for _ in range(7)] - while True: # While game is active, continually anticipate messages - message = (await socket.recv()).split(':') # Receive message from server + board = [[None] * 6 for _ in range(7)] + while True: # While game is active, continually anticipate messages + message = (await socket.recv()).split(":") # Receive message from server - match message[0]: - case 'CONNECT': - await socket.send('READY') + match message[0]: + case "CONNECT": + await socket.send("READY") - case 'GAME': - if message[1] == 'START': - if message[2] == '1': - col = calculate_move(None, board) # calculate_move is some arbitrary function you have created to figure out the next move - await socket.send(f'PLAY:{col}') # Send your move to the sever - if (message[1] == 'WINS') | (message[1] == 'LOSS') | (message[1] == 'DRAW') | (message[1] == 'TERMINATED'): # Game has ended - print(message[0]+":"+message[1]) - board = [[None] * 6 for _ in range(7)] - await socket.send('READY') + case "GAME": + if message[1] == "START": + if message[2] == "1": + col = calculate_move( + None, board + ) # calculate_move is some arbitrary function you have created to figure out the next move + await socket.send(f"PLAY:{col}") # Send your move to the sever + if ( + (message[1] == "WINS") + | (message[1] == "LOSS") + | (message[1] == "DRAW") + | (message[1] == "TERMINATED") + ): # Game has ended + print(message[0] + ":" + message[1]) + board = [[None] * 6 for _ in range(7)] + await socket.send("READY") - case 'OPPONENT': # Opponent has gone; calculate next move - col = calculate_move(message[1], board) # Give your function your opponent's move - await socket.send(f'PLAY:{col}') # Send your move to the sever + case "OPPONENT": # Opponent has gone; calculate next move + col = calculate_move( + message[1], board + ) # Give your function your opponent's move + await socket.send(f"PLAY:{col}") # Send your move to the sever - case 'ERROR': - print(f"{message[0]}: {':'.join(message[1:])}") + case "ERROR": + print(f"{message[0]}: {':'.join(message[1:])}") - await socket.close() - -async def join_server(username): - async with websockets.connect(f'wss://connect4.abunchofknowitalls.com', ping_interval=30, ping_timeout=30) as socket: # Establish websocket connection - await socket.send(f'CONNECT:{username}') - await gameloop(socket) + await socket.close() -if __name__ == '__main__': # Program entrypoint - username = input("Enter username: ") - asyncio.run(join_server(username)) +async def join_server(username, server_url): + async with websockets.connect( + server_url, ping_interval=30, ping_timeout=30 + ) as socket: # Establish websocket connection + await socket.send(f"CONNECT:{username}") + await gameloop(socket) + + +if __name__ == "__main__": # Program entrypoint + server_url = ( + input(f"Enter server address [{DEFAULT_SERVER_URL}]: ").strip() + or DEFAULT_SERVER_URL + ) + username = input("Enter username: ") + asyncio.run(join_server(username, server_url)) diff --git a/src/main.rs b/src/main.rs index b7b0ab6..375994c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -423,9 +423,15 @@ async fn handle_connection( broadcast_message_all_observers(&observers, &message).await; } else { // Create next matches + // TODO: Make this a function for (i, id) in tourney.top_half.iter().enumerate() { let player1_addr = tourney.players.get(id).unwrap(); - let player2_addr = tourney.players.get(tourney.bottom_half.get(i).unwrap()).unwrap(); + let player2_addr = tourney.players.get(tourney.bottom_half.get(i).unwrap()); + + if player2_addr.is_none() { continue; } + let player2_addr = player2_addr.unwrap(); + + // TODO: gen without collisions let match_id: u32 = rand::rng().random_range(100000..=999999); let new_match = Arc::new(RwLock::new(Match::new( match_id, @@ -475,11 +481,11 @@ async fn handle_connection( if !demo_mode { let connection = opponent.connection.clone(); let column = column_parse.clone()?; - let waiting = *waiting_timeout.read().await * 1000 + (rand::rng().random_range(0..=500) - 250); + let waiting = *waiting_timeout.read().await as i64 * 1000 + (rand::rng().random_range(0..=500) - 250); let matches_move = matches.clone(); let match_id_move = current_match.id; current_match.wait_thread = Some(tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_millis(waiting)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(waiting as u64)).await; let mut matches_guard = matches_move.write().await; let mut current_match = matches_guard.get_mut(&match_id_move).unwrap().write().await; @@ -708,6 +714,15 @@ async fn handle_connection( matches.write().await.insert(match_id, new_match.clone()); } } + else if text.starts_with("TOURNAMENT:WAIT:") { + if admin.read().await.is_none() || admin.read().await.unwrap() != addr { + let _ = send(&tx, "ERROR:INVALID:AUTH"); + continue; + } + + let new_timeout = text.split(":").collect::>()[2].parse::()?; + *waiting_timeout.write().await = (new_timeout * 1000.0) as u64; + } else { let _ = send(&tx, "ERROR:UNKNOWN"); diff --git a/src/types.rs b/src/types.rs index 1950fa5..9d7634b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -74,13 +74,23 @@ impl Tournament { } pub fn next(&mut self) { - let first = *self.bottom_half.last().unwrap(); - let last = *self.top_half.last().unwrap(); + if self.is_completed { + return; + } - self.top_half[0] = first; - self.bottom_half[0] = last; + if self.top_half.len() <= 1 || self.bottom_half.is_empty() { + self.is_completed = true; + return; + } - if self.top_half[0] == 0 { + let last_from_top = self.top_half.pop().unwrap(); + let first_from_bottom = self.bottom_half.remove(0); + + self.top_half.insert(1, first_from_bottom); + self.bottom_half.push(last_from_top); + + let expected_bottom_start = self.top_half.len() as u32; + if self.top_half[1] == 1 && self.bottom_half[0] == expected_bottom_start { self.is_completed = true; } }