restore state on reload/new join

This commit is contained in:
sparshg
2024-09-22 04:41:53 +05:30
parent b994cd7439
commit 44e72d77f2
7 changed files with 333 additions and 48 deletions

View File

@@ -2,9 +2,9 @@ use std::ops::{Deref, DerefMut};
use axum::Json;
use rand::Rng;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
pub struct Board(pub [[char; 10]; 10]);
impl From<Board> for Vec<String> {
@@ -109,6 +109,31 @@ impl Board {
Some(bounds)
}
pub fn mark_redundant(mut self) -> Self {
for i in 0..10 {
for j in 0..10 {
if self[i][j] == 'h' {
for (dx, dy) in [(-1, -1), (1, 1), (1, -1), (-1, 1)].iter() {
let (tx, ty) = ((i as i32 + dx) as usize, (j as i32 + dy) as usize);
if (0..10).contains(&tx) && (0..10).contains(&ty) {
self[tx][ty] = 'm';
}
}
if self.has_sunk((i, j)).is_some() {
for (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)].iter() {
let (tx, ty) = ((i as i32 + dx) as usize, (j as i32 + dy) as usize);
if (0..10).contains(&tx) && (0..10).contains(&ty) && self[tx][ty] == 'e'
{
self[tx][ty] = 'm';
}
}
}
}
}
}
self
}
// fn validate_syntax(&self) -> bool {
// self
// .iter()

View File

@@ -1,3 +1,4 @@
use serde::Serialize;
use socketioxide::socket::Sid;
use thiserror::Error;
@@ -9,8 +10,8 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("Room full")]
RoomFull,
#[error("Room full, potential replacement {0:?}")]
RoomFull(Option<String>),
#[error("Room not full")]
RoomNotFull,
#[error("Already in room")]
@@ -23,7 +24,25 @@ pub enum Error {
Sqlx(#[from] sqlx::Error),
}
#[derive(Debug, sqlx::Type, PartialEq, Serialize)]
#[sqlx(type_name = "STAT", rename_all = "lowercase")]
pub enum Status {
Waiting,
P1Turn,
P2Turn,
}
pub async fn room_if_player_exists(sid: &str, pool: &sqlx::PgPool) -> Result<Option<String>> {
Ok(
sqlx::query!("SELECT room_code FROM players WHERE id = $1", sid)
.fetch_optional(pool)
.await?
.map(|player| player.room_code),
)
}
pub async fn add_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
delete_sid(sid.as_str(), pool).await?;
sqlx::query!(
r"WITH new_user AS (INSERT INTO players (id, room_code) VALUES ($1, $2) RETURNING id) INSERT INTO rooms (player1_id, code) SELECT $1, $2 FROM new_user",
sid.as_str(),
@@ -45,8 +64,15 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
let sid = sid.as_str();
if room.player1_id.is_some() && room.player2_id.is_some() {
return Err(Error::RoomFull);
if let (Some(p1), Some(p2)) = (room.player1_id.as_ref(), room.player2_id.as_ref()) {
if in_delete_sid(&p1, &pool).await? {
update_sid(&p1, sid, pool).await?;
return Err(Error::RoomFull(Some(p1.to_string())));
} else if in_delete_sid(&p2, &pool).await? {
update_sid(&p2, sid, pool).await?;
return Err(Error::RoomFull(Some(p2.to_string())));
}
return Err(Error::RoomFull(None));
}
if let Some(id) = room.player1_id.as_ref() {
if id == sid {
@@ -58,7 +84,7 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
return Err(Error::AlreadyInRoom);
}
}
delete_sid(sid, pool).await?;
let mut txn = pool.begin().await?;
// create/update player
@@ -84,6 +110,15 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
Ok(())
}
pub async fn get_room(sid: Sid, pool: &sqlx::PgPool) -> Result<Option<String>> {
Ok(
sqlx::query!("SELECT room_code FROM players WHERE id = $1", sid.as_str())
.fetch_optional(pool)
.await?
.map(|r| r.room_code),
)
}
pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()> {
let board: Vec<String> = board.into();
sqlx::query!(
@@ -96,6 +131,66 @@ pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()
Ok(())
}
pub async fn get_game_state(
sid: &str,
room: &str,
pool: &sqlx::PgPool,
) -> Result<(bool, Vec<String>, Vec<String>)> {
let room_details = sqlx::query!(
r#"SELECT player1_id, player2_id, stat AS "stat: Status" FROM rooms WHERE code = $1"#,
room
)
.fetch_one(pool)
.await?;
let turn = match room_details.stat {
Status::P1Turn if room_details.player1_id == Some(sid.to_string()) => true,
Status::P2Turn if room_details.player2_id == Some(sid.to_string()) => true,
_ => false,
};
let oid = match (room_details.player1_id, room_details.player2_id) {
(Some(p1), Some(p2)) if p1 == sid => p2,
(Some(p1), Some(p2)) if p2 == sid => p1,
_ => return Err(Error::NotInRoom),
};
let player_board: Board = sqlx::query!(
r#"SELECT board FROM players WHERE id = $1 AND room_code = $2"#,
sid,
room
)
.fetch_one(pool)
.await?
.board
.unwrap()
.into();
let player_board: Vec<String> = player_board.mark_redundant().into();
let opponent_board: Board = sqlx::query!(
r#"SELECT board FROM players WHERE id = $1 AND room_code = $2"#,
oid,
room
)
.fetch_one(pool)
.await?
.board
.unwrap()
.into();
let opponent_board: Vec<String> = opponent_board.mark_redundant().into();
let opponent_board: Vec<String> = opponent_board
.into_iter()
.map(|row| {
row.chars()
.into_iter()
.map(|x| if x == 's' { 'e' } else { x })
.collect()
})
.collect::<Vec<_>>();
Ok((turn, player_board, opponent_board))
}
pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
let room = sqlx::query!(
r"SELECT player1_id, player2_id FROM rooms WHERE code = $1",
@@ -190,17 +285,47 @@ pub async fn attack(
Ok((hit, if hit { board.has_sunk((i, j)) } else { None }))
}
pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<()> {
sqlx::query!(r"DELETE FROM players WHERE id = $1", sid.as_str())
pub async fn update_sid(oldsid: &str, newsid: &str, pool: &sqlx::PgPool) -> Result<()> {
sqlx::query!(r"UPDATE players SET id = $1 WHERE id = $2", newsid, oldsid)
.execute(pool)
.await?;
Ok(())
}
#[derive(Debug, sqlx::Type, PartialEq)]
#[sqlx(type_name = "STAT", rename_all = "lowercase")]
enum Status {
Waiting,
P1Turn,
P2Turn,
pub async fn delete_sid(sid: &str, pool: &sqlx::PgPool) -> Result<()> {
sqlx::query!(r"DELETE FROM players WHERE id = $1", sid)
.execute(pool)
.await?;
Ok(())
}
pub async fn to_delete_sid(sid: &str, pool: &sqlx::PgPool) -> Result<()> {
sqlx::query!(
r"INSERT INTO abandoned_players (time, id) VALUES (NOW(), $1)",
sid
)
.execute(pool)
.await?;
Ok(())
}
pub async fn in_delete_sid(sid: &str, pool: &sqlx::PgPool) -> Result<bool> {
Ok(
sqlx::query!(r"SELECT id FROM abandoned_players WHERE id = $1", sid)
.fetch_optional(pool)
.await?
.is_some(),
)
}
pub async fn delete_abandoned(pool: &sqlx::PgPool) -> Result<()> {
sqlx::query!(
r"DELETE FROM players
WHERE id IN (SELECT id FROM abandoned_players
ORDER BY time DESC
OFFSET 1000)"
)
.execute(pool)
.await?;
Ok(()) // TODO: REMOVE duliplcates id from abandoned
}

View File

@@ -4,8 +4,13 @@ use axum::Router;
use board::Board;
use dotenv::dotenv;
use futures_util::stream::StreamExt;
use game::{add_board, add_room, attack, disconnect, join_room, start, ROOM_CODE_LENGTH};
use game::{
add_board, add_room, attack, delete_sid, get_game_state, get_room, join_room,
room_if_player_exists, start, to_delete_sid, update_sid, Error, ROOM_CODE_LENGTH,
};
use rand::Rng;
use serde::Deserialize;
use socketioxide::{
extract::{Data, SocketRef, State},
SocketIo,
@@ -26,6 +31,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("DATABASE_URL")?;
let pool = sqlx::postgres::PgPool::connect(&url).await?;
sqlx::migrate!("./migrations").run(&pool).await?;
sqlx::query("DELETE FROM players").execute(&pool).await?;
sqlx::query("DELETE FROM abandoned_players")
.execute(&pool)
.await?;
sqlx::query("DELETE FROM rooms").execute(&pool).await?;
let (layer, io) = SocketIo::builder().with_state(pool).build_layer();
io.ns("/", on_connect);
@@ -40,8 +50,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn on_connect(socket: SocketRef) {
#[derive(Debug, Deserialize)]
struct AuthPayload {
pub session: Option<String>,
}
async fn on_connect(socket: SocketRef, Data(auth): Data<AuthPayload>, pool: State<PgPool>) {
tracing::info!("Connected: {:?}", socket.id);
tracing::info!("Connected: {:?}", auth.session);
if let Some(sid) = auth.session {
update_sid(&sid, socket.id.as_str(), &pool).await.unwrap();
let sid = socket.id.as_str();
if let Some(room) = room_if_player_exists(&sid, &pool).await.unwrap() {
let data = get_game_state(&sid, &room, &pool).await.unwrap();
socket
.emit(
"restore",
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2}),
)
.unwrap();
socket.join(room.clone()).unwrap();
emit_update_room(
&socket,
&room,
socket.within(room.clone()).sockets().unwrap().len(),
);
}
}
socket.on(
"create",
|socket: SocketRef, pool: State<PgPool>| async move {
@@ -59,6 +96,7 @@ fn on_connect(socket: SocketRef) {
.map(|x| char::to_ascii_uppercase(&(x as char)))
.collect();
tracing::info!("Creating room: {:?}", room);
// TODO: Handle duplicates
if let Err(e) = add_room(socket.id, room.clone(), &pool).await {
tracing::error!("{:?}", e);
@@ -66,7 +104,11 @@ fn on_connect(socket: SocketRef) {
}
socket.leave_all().unwrap();
socket.join(room.clone()).unwrap();
socket.emit("joined-room", &room).unwrap();
emit_update_room(
&socket,
&room,
socket.within(room.clone()).sockets().unwrap().len(),
);
},
);
@@ -77,15 +119,30 @@ fn on_connect(socket: SocketRef) {
return;
}
tracing::info!("Joining room: {:?}", room);
if let Err(e) = join_room(socket.id, room.clone(), &pool).await {
tracing::error!("{:?}", e);
return;
let room_error = join_room(socket.id, room.clone(), &pool).await;
if let Err(e) = &room_error {
if let Error::RoomFull(Some(player)) = &e {
tracing::warn!("{:?}", e);
update_sid(&player, socket.id.as_str(), &pool).await.unwrap();
let data = get_game_state(socket.id.as_str(), &room, &pool).await.unwrap();
socket
.emit(
"restore",
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2}),
)
.unwrap();
} else {
tracing::error!("{:?}", e);
return;
}
}
socket.leave_all().unwrap();
socket.join(room.clone()).unwrap();
socket.emit("joined-room", &room).unwrap();
if socket.within(room.clone()).sockets().unwrap().len() != 2 {
let users = socket.within(room.clone()).sockets().unwrap().len();
emit_update_room(&socket, &room, users);
if users != 2 || room_error.is_err() {
return;
}
let ack_stream = socket
@@ -141,11 +198,44 @@ fn on_connect(socket: SocketRef) {
},
);
socket.on(
"leave",
|socket: SocketRef, pool: State<PgPool>| async move {
tracing::info!("Leaving Rooms: {:?}", socket.id);
leave_and_inform(&socket, &pool).await;
},
);
socket.on_disconnect(|socket: SocketRef, pool: State<PgPool>| async move {
tracing::info!("Disconnecting: {:?}", socket.id);
socket.leave_all().unwrap();
if let Err(e) = disconnect(socket.id, &pool).await {
tracing::error!("{:?}", e);
}
leave_and_inform(&socket, &pool).await;
});
}
async fn leave_and_inform(socket: &SocketRef, pool: &PgPool) {
let room = socket
.rooms()
.unwrap()
.first()
.map(|s| s.to_string())
.or(get_room(socket.id, pool).await.unwrap());
let Some(room) = room else {
return;
};
let ops = socket.within(room.clone());
socket.leave_all().unwrap();
emit_update_room(socket, &room.to_string(), ops.sockets().unwrap().len());
if let Err(e) = to_delete_sid(socket.id.as_str(), pool).await {
tracing::error!("{:?}", e);
}
}
fn emit_update_room(socket: &SocketRef, room: &String, users: usize) {
socket
.within(room.clone())
.emit(
"update-room",
serde_json::json!({"room": &room, "users": users}),
)
.unwrap();
}