restore state on reload/new join
This commit is contained in:
29
src/board.rs
29
src/board.rs
@@ -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()
|
||||
|
151
src/game.rs
151
src/game.rs
@@ -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
|
||||
}
|
||||
|
114
src/main.rs
114
src/main.rs
@@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user