diff --git a/example_client.py b/example_client.py index b086b6e..6ef0308 100644 --- a/example_client.py +++ b/example_client.py @@ -1,22 +1,18 @@ import asyncio -import itertools import websockets -from enum import Enum -from websockets import ConnectionClosed - - -def calculate_move(opponent_move, board): +async def calculate_move(opponent_move, board): if opponent_move is not None: print(f"Opponent played column {opponent_move}") # TODO: Implement your move calculation logic here instead # Use the board variable to see and set the current state of the board - return int(input("Column: ")) + loop = asyncio.get_running_loop() + return int(await loop.run_in_executor(None, input, "Column: ")) async def gameloop(socket): - board = [[None] * 5 for _ in range(6)] + 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 @@ -27,41 +23,26 @@ async def gameloop(socket): 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 + col = await 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] * 5 for _ in range(6)] - our_color = None - opponent_color = None + 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 + col = await 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:])}") - break - + await socket.close() -async def keepalive(websocket, ping_interval=30): - for ping in itertools.count(): - await asyncio.sleep(ping_interval) - try: - await websocket.send("PING") - except ConnectionClosed: - break - async def join_server(username): - async with websockets.connect(f'wss://connect4.abunchofknowitalls.com') as socket: # Establish websocket connection - keepalive_task = asyncio.create_task(keepalive(socket)) - try: - await socket.send(f'CONNECT:{username}') - await gameloop(socket) - finally: - keepalive_task.cancel() + 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) if __name__ == '__main__': # Program entrypoint diff --git a/src/main.rs b/src/main.rs index 3c420c2..93d373d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,8 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::RwLock; -use tokio_tungstenite::{accept_async, tungstenite::Message}; use tokio_tungstenite::tungstenite::Utf8Bytes; +use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info, warn}; use types::Client; @@ -170,8 +170,7 @@ async fn handle_connection( let client_option = clients_guard.get(&addr); // Check if client is valid - if client_option.is_none() - || client_option.unwrap().read().await.current_match.is_none() + if client_option.is_none() || client_option.unwrap().read().await.current_match.is_none() { let _ = send(&tx, "ERROR:INVALID:MOVE"); continue; @@ -194,9 +193,8 @@ async fn handle_connection( }; // Check if it's their move - if (current_match.ledger.is_empty() && current_match.first != addr) - || (current_match.ledger.last().is_some() - && current_match.ledger.last().unwrap().0 == client.color) + if (current_match.ledger.is_empty() && current_match.first != addr) || + (current_match.ledger.last().is_some() && current_match.ledger.last().unwrap().0 == client.color) { let _ = send(&tx, "ERROR:INVALID:MOVE"); continue; @@ -215,24 +213,57 @@ async fn handle_connection( .await; // Check if valid move + let mut invalid = false; if let Ok(column) = column_parse { - if column >= 6 { + if column >= 7 { let _ = send(&tx, "ERROR:INVALID:MOVE"); - continue; + invalid = true; } - if current_match.board[column][4] != Color::None { + if current_match.board[column][5] != Color::None && !invalid { let _ = send(&tx, "ERROR:INVALID:MOVE"); - continue; + invalid = true; } - // Place it - current_match.place_token(client.color.clone(), column) + } else { let _ = send(&tx, "ERROR:INVALID:MOVE"); - continue; + invalid = true; } + // Terminate games if a player makes an invalid move + if invalid { + let opponent_addr = opponent.addr; + let current_match_id = current_match.id; + + broadcast_message(¤t_match.viewers, &observers, "GAME:TERMINATED").await; + + drop(client); + drop(opponent); + drop(current_match); + drop(clients_guard); + + let mut clients_guard = clients.write().await; + let mut client = clients_guard.get_mut(&addr).unwrap().write().await; + client.current_match = None; + let _ = send(&tx, "GAME:TERMINATED"); + drop(client); + + let mut opponent = + clients_guard.get_mut(&opponent_addr).unwrap().write().await; + + if !demo_mode { + opponent.current_match = None; + let _ = send(&opponent.connection, "GAME:TERMINATED"); + } + matches_guard.remove(¤t_match_id).unwrap(); + + continue; + } else { + // Place it + current_match.place_token(client.color.clone(), column_parse.clone()?); + } + // broadcast the move to viewers broadcast_message( ¤t_match.viewers, @@ -248,8 +279,8 @@ async fn handle_connection( let mut result = (Color::None, false); let mut any_empty = true; - for x in 0..6 { - for y in 0..5 { + for x in 0..7 { + for y in 0..6 { let color = current_match.board[x][y].clone(); let mut horizontal_end = true; let mut vertical_end = true; @@ -260,20 +291,20 @@ async fn handle_connection( } for i in 0..4 { - if x + i >= 6 + if x + i >= 7 || current_match.board[x + i][y] != color && horizontal_end { horizontal_end = false; } - if y + i >= 5 + if y + i >= 6 || current_match.board[x][y + i] != color && vertical_end { vertical_end = false; } - if x + i >= 6 - || y + i >= 5 + if x + i >= 7 + || y + i >= 6 || current_match.board[x + i][y + i] != color && diagonal_end { diff --git a/src/random_ai.rs b/src/random_ai.rs index 2ff920e..d192e2d 100644 --- a/src/random_ai.rs +++ b/src/random_ai.rs @@ -2,9 +2,9 @@ use crate::types::Color; use rand::Rng; pub fn random_move(board: &[Vec]) -> usize { - let mut random = rand::rng().random_range(0..6); - while board[random][4] != Color::None { - random = rand::rng().random_range(0..6); + let mut random = rand::rng().random_range(0..7); + while board[random][5] != Color::None { + random = rand::rng().random_range(0..7); } random diff --git a/src/types.rs b/src/types.rs index 5d3a22d..15452b1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -61,7 +61,7 @@ impl Match { // TODO: make player1 in Match always first Match { id, - board: vec![vec![Color::None; 5]; 6], + board: vec![vec![Color::None; 6]; 7], viewers: Vec::new(), ledger: Vec::new(), first, @@ -71,7 +71,7 @@ impl Match { } pub fn place_token(&mut self, color: Color, column: usize) { - for i in 0..5 { + for i in 0..6 { if self.board[column][i] == Color::None { self.board[column][i] = color; break;