fix: change to 7x6 boards, terminate games on error, example_client.py keep alive

This commit is contained in:
2025-11-20 16:10:57 -05:00
Unverified
parent 348e8ae786
commit 3669a54eea
4 changed files with 67 additions and 55 deletions

View File

@@ -1,22 +1,18 @@
import asyncio import asyncio
import itertools
import websockets import websockets
from enum import Enum
from websockets import ConnectionClosed async def calculate_move(opponent_move, board):
def calculate_move(opponent_move, board):
if opponent_move is not None: if opponent_move is not None:
print(f"Opponent played column {opponent_move}") print(f"Opponent played column {opponent_move}")
# TODO: Implement your move calculation logic here instead # TODO: Implement your move calculation logic here instead
# Use the board variable to see and set the current state of the board # 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): 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 while True: # While game is active, continually anticipate messages
message = (await socket.recv()).split(':') # Receive message from server message = (await socket.recv()).split(':') # Receive message from server
@@ -27,41 +23,26 @@ async def gameloop(socket):
case 'GAME': case 'GAME':
if message[1] == 'START': if message[1] == 'START':
if message[2] == '1': 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 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 if (message[1] == 'WINS') | (message[1] == 'LOSS') | (message[1] == 'DRAW') | (message[1] == 'TERMINATED'): # Game has ended
print(message[0]+message[1]) print(message[0]+":"+message[1])
board = [[None] * 5 for _ in range(6)] board = [[None] * 6 for _ in range(7)]
our_color = None
opponent_color = None
await socket.send('READY') await socket.send('READY')
case 'OPPONENT': # Opponent has gone; calculate next move 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 await socket.send(f'PLAY:{col}') # Send your move to the sever
case 'ERROR': case 'ERROR':
print(f"{message[0]}: {':'.join(message[1:])}") print(f"{message[0]}: {':'.join(message[1:])}")
break
await socket.close() 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 def join_server(username):
async with websockets.connect(f'wss://connect4.abunchofknowitalls.com') as socket: # Establish websocket connection async with websockets.connect(f'wss://connect4.abunchofknowitalls.com', ping_interval=30, ping_timeout=30) as socket: # Establish websocket connection
keepalive_task = asyncio.create_task(keepalive(socket)) await socket.send(f'CONNECT:{username}')
try: await gameloop(socket)
await socket.send(f'CONNECT:{username}')
await gameloop(socket)
finally:
keepalive_task.cancel()
if __name__ == '__main__': # Program entrypoint if __name__ == '__main__': # Program entrypoint

View File

@@ -16,8 +16,8 @@ use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::error::SendError;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio_tungstenite::{accept_async, tungstenite::Message};
use tokio_tungstenite::tungstenite::Utf8Bytes; use tokio_tungstenite::tungstenite::Utf8Bytes;
use tokio_tungstenite::{accept_async, tungstenite::Message};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use types::Client; use types::Client;
@@ -170,8 +170,7 @@ async fn handle_connection(
let client_option = clients_guard.get(&addr); let client_option = clients_guard.get(&addr);
// Check if client is valid // Check if client is valid
if client_option.is_none() if client_option.is_none() || client_option.unwrap().read().await.current_match.is_none()
|| client_option.unwrap().read().await.current_match.is_none()
{ {
let _ = send(&tx, "ERROR:INVALID:MOVE"); let _ = send(&tx, "ERROR:INVALID:MOVE");
continue; continue;
@@ -194,9 +193,8 @@ async fn handle_connection(
}; };
// Check if it's their move // Check if it's their move
if (current_match.ledger.is_empty() && current_match.first != addr) if (current_match.ledger.is_empty() && current_match.first != addr) ||
|| (current_match.ledger.last().is_some() (current_match.ledger.last().is_some() && current_match.ledger.last().unwrap().0 == client.color)
&& current_match.ledger.last().unwrap().0 == client.color)
{ {
let _ = send(&tx, "ERROR:INVALID:MOVE"); let _ = send(&tx, "ERROR:INVALID:MOVE");
continue; continue;
@@ -215,24 +213,57 @@ async fn handle_connection(
.await; .await;
// Check if valid move // Check if valid move
let mut invalid = false;
if let Ok(column) = column_parse { if let Ok(column) = column_parse {
if column >= 6 { if column >= 7 {
let _ = send(&tx, "ERROR:INVALID:MOVE"); 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"); let _ = send(&tx, "ERROR:INVALID:MOVE");
continue; invalid = true;
} }
// Place it
current_match.place_token(client.color.clone(), column)
} else { } else {
let _ = send(&tx, "ERROR:INVALID:MOVE"); 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(&current_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(&current_match_id).unwrap();
continue;
} else {
// Place it
current_match.place_token(client.color.clone(), column_parse.clone()?);
}
// broadcast the move to viewers // broadcast the move to viewers
broadcast_message( broadcast_message(
&current_match.viewers, &current_match.viewers,
@@ -248,8 +279,8 @@ async fn handle_connection(
let mut result = (Color::None, false); let mut result = (Color::None, false);
let mut any_empty = true; let mut any_empty = true;
for x in 0..6 { for x in 0..7 {
for y in 0..5 { for y in 0..6 {
let color = current_match.board[x][y].clone(); let color = current_match.board[x][y].clone();
let mut horizontal_end = true; let mut horizontal_end = true;
let mut vertical_end = true; let mut vertical_end = true;
@@ -260,20 +291,20 @@ async fn handle_connection(
} }
for i in 0..4 { for i in 0..4 {
if x + i >= 6 if x + i >= 7
|| current_match.board[x + i][y] != color && horizontal_end || current_match.board[x + i][y] != color && horizontal_end
{ {
horizontal_end = false; horizontal_end = false;
} }
if y + i >= 5 if y + i >= 6
|| current_match.board[x][y + i] != color && vertical_end || current_match.board[x][y + i] != color && vertical_end
{ {
vertical_end = false; vertical_end = false;
} }
if x + i >= 6 if x + i >= 7
|| y + i >= 5 || y + i >= 6
|| current_match.board[x + i][y + i] != color || current_match.board[x + i][y + i] != color
&& diagonal_end && diagonal_end
{ {

View File

@@ -2,9 +2,9 @@ use crate::types::Color;
use rand::Rng; use rand::Rng;
pub fn random_move(board: &[Vec<Color>]) -> usize { pub fn random_move(board: &[Vec<Color>]) -> usize {
let mut random = rand::rng().random_range(0..6); let mut random = rand::rng().random_range(0..7);
while board[random][4] != Color::None { while board[random][5] != Color::None {
random = rand::rng().random_range(0..6); random = rand::rng().random_range(0..7);
} }
random random

View File

@@ -61,7 +61,7 @@ impl Match {
// TODO: make player1 in Match always first // TODO: make player1 in Match always first
Match { Match {
id, id,
board: vec![vec![Color::None; 5]; 6], board: vec![vec![Color::None; 6]; 7],
viewers: Vec::new(), viewers: Vec::new(),
ledger: Vec::new(), ledger: Vec::new(),
first, first,
@@ -71,7 +71,7 @@ impl Match {
} }
pub fn place_token(&mut self, color: Color, column: usize) { 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 { if self.board[column][i] == Color::None {
self.board[column][i] = color; self.board[column][i] = color;
break; break;