forked from eros/curltastic
refactor(backend): allow multi-client rooms and drop room-full
Anyone may join and watch/throw; game auto-starts on first connection. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
0e08e21c9b
commit
a4e848c849
@ -50,7 +50,6 @@ impl AppState {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct RoomQuery {
|
struct RoomQuery {
|
||||||
room: String,
|
room: String,
|
||||||
team: Option<Team>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -95,74 +94,43 @@ async fn ws_handler(
|
|||||||
Query(query): Query<RoomQuery>,
|
Query(query): Query<RoomQuery>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, state, query.room, query.team))
|
ws.on_upgrade(move |socket| handle_socket(socket, state, query.room))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_socket(
|
async fn handle_socket(
|
||||||
socket: WebSocket,
|
socket: WebSocket,
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
room_id: String,
|
room_id: String,
|
||||||
preferred_team: Option<Team>,
|
|
||||||
) {
|
) {
|
||||||
let room = state.get_or_create_room(&room_id).await;
|
let room = state.get_or_create_room(&room_id).await;
|
||||||
let (mut sender, receiver) = socket.split();
|
let (mut sender, receiver) = socket.split();
|
||||||
|
|
||||||
if try_join_room(&room).await.is_err() {
|
{
|
||||||
let err = serde_json::to_string(&ServerMessage::Error {
|
let mut room_guard = room.lock().await;
|
||||||
message: "Room is full".to_string(),
|
if matches!(room_guard.game.game_state_message(), ServerMessage::GameState { phase: Phase::Waiting, .. }) {
|
||||||
})
|
room_guard.game.start();
|
||||||
.unwrap();
|
}
|
||||||
let _ = sender.send(Message::Text(Utf8Bytes::from(err))).await;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let player_id = Uuid::new_v4().to_string();
|
|
||||||
let team = register_player(&room, &player_id, preferred_team).await;
|
|
||||||
|
|
||||||
let joined = serde_json::to_string(&ServerMessage::Joined {
|
let joined = serde_json::to_string(&ServerMessage::Joined {
|
||||||
room: room_id.clone(),
|
room: room_id.clone(),
|
||||||
team,
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = sender.send(Message::Text(Utf8Bytes::from(joined))).await;
|
if sender.send(Message::Text(Utf8Bytes::from(joined))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let tx = { room.lock().await.tx.clone() };
|
let tx = { room.lock().await.tx.clone() };
|
||||||
|
|
||||||
let send_task = spawn_forwarder(sender, tx.subscribe());
|
let send_task = spawn_forwarder(sender, tx.subscribe());
|
||||||
broadcast_room_state(&room, &tx).await;
|
broadcast_room_state(&room, &tx).await;
|
||||||
|
|
||||||
let recv_task = spawn_message_handler(room.clone(), player_id.clone(), tx, receiver);
|
let recv_task = spawn_message_handler(room.clone(), tx, receiver);
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = send_task => {}
|
_ = send_task => {}
|
||||||
_ = recv_task => {}
|
_ = recv_task => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_player(&room, &player_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_join_room(room: &Arc<Mutex<Room>>) -> Result<(), ()> {
|
|
||||||
let room_guard = room.lock().await;
|
|
||||||
if room_guard.game.can_start() {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn register_player(
|
|
||||||
room: &Arc<Mutex<Room>>,
|
|
||||||
player_id: &str,
|
|
||||||
preferred_team: Option<Team>,
|
|
||||||
) -> Team {
|
|
||||||
let mut room_guard = room.lock().await;
|
|
||||||
let team = room_guard
|
|
||||||
.game
|
|
||||||
.add_player(player_id.to_string(), preferred_team)
|
|
||||||
.unwrap_or(Team::Red);
|
|
||||||
if room_guard.game.can_start() {
|
|
||||||
room_guard.game.start();
|
|
||||||
}
|
|
||||||
team
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_forwarder(
|
fn spawn_forwarder(
|
||||||
@ -192,33 +160,25 @@ async fn broadcast_room_state(
|
|||||||
tx: &tokio::sync::broadcast::Sender<ServerMessage>,
|
tx: &tokio::sync::broadcast::Sender<ServerMessage>,
|
||||||
) {
|
) {
|
||||||
let room_guard = room.lock().await;
|
let room_guard = room.lock().await;
|
||||||
let msg = if room_guard.game.can_start() {
|
let _ = tx.send(room_guard.game.game_state_message());
|
||||||
room_guard.game.game_state_message()
|
|
||||||
} else {
|
|
||||||
ServerMessage::Waiting {
|
|
||||||
message: "Waiting for other player".to_string(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let _ = tx.send(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_message_handler(
|
fn spawn_message_handler(
|
||||||
room: Arc<Mutex<Room>>,
|
room: Arc<Mutex<Room>>,
|
||||||
player_id: String,
|
|
||||||
tx: tokio::sync::broadcast::Sender<ServerMessage>,
|
tx: tokio::sync::broadcast::Sender<ServerMessage>,
|
||||||
mut receiver: WsReceiver,
|
mut receiver: WsReceiver,
|
||||||
) -> tokio::task::JoinHandle<()> {
|
) -> tokio::task::JoinHandle<()> {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(Ok(msg)) = receiver.next().await {
|
while let Some(Ok(msg)) = receiver.next().await {
|
||||||
let Message::Text(text) = msg else { continue; };
|
let Message::Text(text) = msg else { continue };
|
||||||
let text_ref = text.as_str();
|
let text_ref = text.as_str();
|
||||||
let parsed: Result<ClientMessage, _> = serde_json::from_str(text_ref);
|
let parsed: Result<ClientMessage, _> = serde_json::from_str(text_ref);
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(ClientMessage::Throw { broom_x, broom_y, weight, curl, friction }) => {
|
Ok(ClientMessage::Throw { team, broom_x, broom_y, weight, curl, friction }) => {
|
||||||
let mut room_guard = room.lock().await;
|
let mut room_guard = room.lock().await;
|
||||||
match room_guard
|
match room_guard
|
||||||
.game
|
.game
|
||||||
.process_throw(&player_id, broom_x, broom_y, weight, curl, friction)
|
.process_throw(team, broom_x, broom_y, weight, curl, friction)
|
||||||
{
|
{
|
||||||
Ok(ThrowOutcome {
|
Ok(ThrowOutcome {
|
||||||
trajectory,
|
trajectory,
|
||||||
@ -226,7 +186,7 @@ fn spawn_message_handler(
|
|||||||
state_message,
|
state_message,
|
||||||
game_over,
|
game_over,
|
||||||
}) => {
|
}) => {
|
||||||
let _ = tx.send(ServerMessage::Trajectory { path: trajectory });
|
let _ = tx.send(ServerMessage::Trajectory { paths: trajectory });
|
||||||
if let Some(scored) = end_scored {
|
if let Some(scored) = end_scored {
|
||||||
let _ = tx.send(scored);
|
let _ = tx.send(scored);
|
||||||
}
|
}
|
||||||
@ -247,8 +207,3 @@ fn spawn_message_handler(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_player(room: &Arc<Mutex<Room>>, player_id: &str) {
|
|
||||||
let mut room_guard = room.lock().await;
|
|
||||||
room_guard.game.remove_player(player_id);
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user