fix: change to 7x6 boards, terminate games on error, example_client.py keep alive
This commit is contained in:
@@ -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
|
||||||
|
|||||||
69
src/main.rs
69
src/main.rs
@@ -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(¤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 the move to viewers
|
||||||
broadcast_message(
|
broadcast_message(
|
||||||
¤t_match.viewers,
|
¤t_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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user