working state

This commit is contained in:
sparshg
2024-09-19 00:02:24 +05:30
parent 31ff8d0753
commit d38952a628
4 changed files with 280 additions and 240 deletions

View File

@@ -20,40 +20,29 @@ export class State {
this.socket.on('upload', (_, callback) => { this.socket.on('upload', (_, callback) => {
callback(this.playerBoard.board); callback(this.playerBoard.board);
}) })
this.socket.on('turn', (id) => { this.socket.on('turnover', (id) => {
this.turn = id == this.socket.id; this.turn = id != this.socket.id;
})
this.socket.on('attacked', ({ by, at, res }) => {
let [i, j] = at;
if (by == this.socket.id) {
this.opponentBoard.board[i][j] = res ? 'h' : 'm';
this.turn = false;
} else {
this.playerBoard.board[i][j] = res ? 'h' : 'm';
this.turn = true;
}
}) })
} }
async attack(i: number, j: number) { attack(i: number, j: number) {
if (!this.turn) return; if (!this.turn) return;
this.turn = false; this.turn = false;
const res = await this.socket.emitWithAck('attack', [i, j]); this.socket.emit('attack', [i, j]);
if (res) {
this.opponentBoard.board[i][j] = 'h';
} else {
this.opponentBoard.board[i][j] = 'm';
}
} }
async createRoom() { createRoom() {
this.socket.emit('create'); this.socket.emit('create');
// this.socket.emit('upload', this.playerBoard.board);
// send the board to the server
// let api = 'http://127.0.0.1:3000/';
// await fetch(api, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Access-Control-Allow-Origin': '*',
// },
// body: JSON.stringify(this.playerBoard.board),
// }).then((response) => {
// console.log(response);
// response.json().then((data) => {
// console.log(data);
// });
// });
} }
joinRoom() { joinRoom() {
@@ -72,10 +61,6 @@ export class Board {
if (!isOpponent) this.randomize(); if (!isOpponent) this.randomize();
} }
// set(x: number, y: number, type: CellType) {
// this.board[x][y] = type;
// }
randomize() { randomize() {
this.board = Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => 'e')); this.board = Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => 'e'));
for (const shipLength of Board.shipTypes) { for (const shipLength of Board.shipTypes) {

View File

@@ -3,15 +3,25 @@ CREATE TYPE STAT AS ENUM ('waiting', 'p1turn', 'p2turn');
CREATE TABLE IF NOT EXISTS players ( CREATE TABLE IF NOT EXISTS players (
id CHAR(16) PRIMARY KEY, id CHAR(16) PRIMARY KEY,
board CHAR(10) [10], board CHAR [10] [10],
room_code CHAR(4) room_code CHAR(4) NOT NULL
); );
CREATE TABLE IF NOT EXISTS rooms ( CREATE TABLE IF NOT EXISTS rooms (
code CHAR(4) PRIMARY KEY, code CHAR(4) PRIMARY KEY,
player1_id CHAR(16), player1_id CHAR(16),
player2_id CHAR(16), player2_id CHAR(16),
stat STAT DEFAULT 'waiting' stat STAT DEFAULT 'waiting' NOT NULL,
CHECK (
(
player1_id IS DISTINCT
FROM player2_id
)
OR (
player1_id IS NULL
AND player2_id IS NULL
)
)
); );
ALTER TABLE players ALTER TABLE players
@@ -24,5 +34,22 @@ SET NULL,
ADD CONSTRAINT fk_player2 FOREIGN KEY (player2_id) REFERENCES players (id) ON DELETE ADD CONSTRAINT fk_player2 FOREIGN KEY (player2_id) REFERENCES players (id) ON DELETE
SET NULL; SET NULL;
-- delete room if both players are null
CREATE OR REPLACE FUNCTION delete_room() RETURNS TRIGGER AS $$ BEGIN IF (
SELECT player1_id IS NULL
AND player2_id IS NULL
FROM rooms
WHERE code = OLD.room_code
) THEN
DELETE FROM rooms
WHERE code = OLD.room_code;
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER delete_room_trigger
AFTER DELETE ON players FOR EACH ROW EXECUTE FUNCTION delete_room();
CREATE INDEX idx_player_room_code ON players (room_code); CREATE INDEX idx_player_room_code ON players (room_code);
CREATE INDEX idx_room_status ON rooms (stat); CREATE INDEX idx_room_status ON rooms (stat);

View File

@@ -1,34 +1,18 @@
use std::{collections::HashMap, sync::Arc};
use axum::Json; use axum::Json;
use rand::Rng; use rand::Rng;
use serde::Deserialize; use serde::Deserialize;
use socketioxide::socket::Sid; use socketioxide::socket::Sid;
use tokio::sync::RwLock;
pub const ROOM_CODE_LENGTH: usize = 4; pub const ROOM_CODE_LENGTH: usize = 4;
// #[derive(Default, Clone)] pub async fn add_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
// pub struct Store { sqlx::query!(
// rooms: Arc<RwLock<HashMap<String, Room>>>, 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",
// sockets: Arc<RwLock<HashMap<Sid, String>>>, sid.as_str(),
// } code
)
// impl Store { .execute(pool)
// pub async fn add_room(&self, code: String) { .await?;
// let mut store = self.rooms.write().await;
// store.insert(
// code.clone(),
// Room {
// code,
// ..Default::default()
// },
// );
// }
pub async fn add_room(code: String, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO rooms (code) VALUES ($1)", code)
.execute(pool)
.await?;
Ok(()) Ok(())
} }
@@ -40,25 +24,39 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
let sid = sid.as_str();
if room.player1_id.is_some() && room.player2_id.is_some() { if room.player1_id.is_some() && room.player2_id.is_some() {
return Err(sqlx::Error::RowNotFound); // room full return Err(sqlx::Error::RowNotFound); // room full
} }
if let Some(id) = room.player1_id.as_ref() {
if id == sid {
return Err(sqlx::Error::RowNotFound); // already in room
}
}
if let Some(id) = room.player2_id.as_ref() {
if id == sid {
return Err(sqlx::Error::RowNotFound); // already in room
}
}
let mut txn = pool.begin().await?; let mut txn = pool.begin().await?;
// create/update player
sqlx::query!( sqlx::query!(
r#"INSERT INTO players (id, room_code) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET room_code = $2"#, r#"INSERT INTO players (id, room_code) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET room_code = $2"#,
sid.as_str(), sid,
code code
) )
.execute(&mut *txn) .execute(&mut *txn)
.await?; .await?;
// add to room
sqlx::query(&format!( sqlx::query(&format!(
"UPDATE rooms SET player{}_id = $1 WHERE code = $2", "UPDATE rooms SET player{}_id = $1 WHERE code = $2",
if room.player1_id.is_none() { "1" } else { "2" } if room.player1_id.is_none() { "1" } else { "2" }
)) ))
.bind(sid.as_str()) .bind(sid)
.bind(code) .bind(code)
.execute(&mut *txn) .execute(&mut *txn)
.await?; .await?;
@@ -66,102 +64,133 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
txn.commit().await?; txn.commit().await?;
Ok(()) Ok(())
} }
// pub async fn join_room(&self, sid: Sid, code: String) -> Result<(), ()> {
// if self.rooms.read().await.get(&code).is_none() {
// return Err(());
// };
// let mut sockets = self.sockets.write().await;
// let player = sockets
// .entry(sid)
// .and_modify(|p| p.room = Some(code.clone()))
// .or_insert(Arc::new(Player {
// sid,
// room: Some(code.clone()),
// board: None,
// }));
// let mut rooms = self.rooms.write().await;
// let Some(room) = rooms.get_mut(&code) else {
// return Err(());
// };
// if room.player1.is_none() { pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
// room.player1 = Some(Arc::clone(player)); let query = format!(
// } "UPDATE players SET board = ARRAY[{}] WHERE id = '{}'",
// Ok(()) board
// } .0
.map(|row| {
format!(
"ARRAY[{}]",
row.map(|x| format!("'{x}'"))
.into_iter()
.collect::<Vec<_>>()
.join(",")
)
})
.into_iter()
.collect::<Vec<String>>()
.join(","),
sid.as_str()
);
sqlx::query(&query).execute(pool).await.unwrap();
Ok(())
}
// pub async fn add_board(&self, sid: Sid, board: Board) -> Result<(), ()> { pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
// let mut store = self.sockets.write().await; let room = sqlx::query!(
// if let Some(player) = store.get_mut(&sid) { r"SELECT player1_id, player2_id FROM rooms WHERE code = $1",
// player.board = Some(board); code
// } else { )
// return Err(()); .fetch_one(pool)
// } .await?;
// Ok(())
// }
// pub async fn start(&self, code: String, sid: Sid) -> Result<(), ()> { let (Some(player1), Some(player2)) = (room.player1_id, room.player2_id) else {
// let mut store = self.rooms.write().await; return Err(sqlx::Error::RowNotFound); // room not full
// let Some(room) = store.get_mut(&code) else { };
// return Err(());
// };
// dbg!(&room);
// let (Some(player1), Some(player2)) = (room.player1, room.player2) else {
// return Err(());
// };
// if player1.sid == sid { let status = if sid.as_str() == player1 {
// room.status = Status::Player1Turn; Status::P2Turn
// } else if player2.sid == sid { } else if sid.as_str() == player2 {
// room.status = Status::Player2Turn; Status::P1Turn
// } else { } else {
// return Err(()); return Err(sqlx::Error::RowNotFound); // not in room
// } };
// Ok(())
// }
// pub async fn attack(&self, sid: Sid, (i, j): (usize, usize)) -> Result<bool, ()> { sqlx::query!(
// let sockets = self.sockets.read().await; r"UPDATE rooms SET stat = $1 WHERE code = $2",
// let Some(player) = sockets.get(&sid) else { status as Status,
// return Err(()); code
// }; )
// let mut rooms = self.rooms.write().await; .execute(pool)
// let Some(room) = rooms.get_mut(player.room.as_ref().unwrap()) else { .await?;
// return Err(()); Ok(())
// }; }
// match room.status { pub async fn attack(
// Status::Player1Turn if player.sid == room.player1.as_ref().unwrap().sid => { sid: Sid,
// room.status = Status::Player2Turn; (i, j): (usize, usize),
// return Ok(room.player2.as_ref().unwrap().board.as_ref().unwrap().0[i][j] == 's'); pool: &sqlx::PgPool,
// } ) -> Result<bool, sqlx::Error> {
// Status::Player2Turn if player.sid == room.player2.as_ref().unwrap().sid => { let player = sqlx::query!(r"SELECT room_code FROM players WHERE id = $1", sid.as_str())
// room.status = Status::Player1Turn; .fetch_one(pool)
// return Ok(room.player1.as_ref().unwrap().board.as_ref().unwrap().0[i][j] == 's'); .await?;
// }
// _ => return Err(()),
// }
// Err(()) let room = sqlx::query!(
// } r#"SELECT stat AS "stat: Status", player1_id, player2_id FROM rooms WHERE code = $1"#,
// } player.room_code
)
.fetch_one(pool)
.await?;
// #[derive(Default, Debug)] let (_, other, to_status) = match (room.player1_id, room.player2_id) {
// struct Room { (Some(p1), Some(p2)) if p1 == sid.as_str() && room.stat == Status::P1Turn => {
// code: String, (p1, p2, Status::P2Turn)
// player1: Option<Arc<Player>>, }
// player2: Option<Arc<Player>>, (Some(p1), Some(p2)) if p2 == sid.as_str() && room.stat == Status::P2Turn => {
// status: Status, (p2, p1, Status::P1Turn)
// } }
_ => return Err(sqlx::Error::RowNotFound), // room not full
};
// #[derive(Debug)] let mut txn = pool.begin().await?;
// struct Player {
// sid: Sid,
// board: Option<Board>,
// room: Option<String>,
// }
#[derive(Debug, sqlx::Type)] let turn = sqlx::query!(
r"SELECT board[$1][$2] as HIT FROM players WHERE id = $3",
i as i32 + 1,
j as i32 + 1,
other
)
.fetch_one(&mut *txn)
.await?;
sqlx::query!(
r#"UPDATE players
SET board[$1][$2] = CASE
WHEN board[$1][$2] = 's' THEN 'h'
WHEN board[$1][$2] = 'e' THEN 'm'
ELSE board[$1][$2]
END
WHERE id = $3"#,
i as i32 + 1,
j as i32 + 1,
other
)
.execute(&mut *txn)
.await?;
sqlx::query!(
r#"UPDATE rooms SET stat = $1 WHERE code = $2"#,
to_status as Status,
player.room_code
)
.execute(&mut *txn)
.await?;
txn.commit().await?;
Ok(turn.hit.unwrap() == "s")
}
pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
sqlx::query!(r"DELETE FROM players WHERE id = $1", sid.as_str())
.execute(pool)
.await
.unwrap();
Ok(())
}
#[derive(Debug, sqlx::Type, PartialEq)]
#[sqlx(type_name = "STAT", rename_all = "lowercase")] #[sqlx(type_name = "STAT", rename_all = "lowercase")]
enum Status { enum Status {
Waiting, Waiting,
@@ -169,12 +198,6 @@ enum Status {
P2Turn, P2Turn,
} }
// impl Default for Status {
// fn default() -> Self {
// Status::Waiting
// }
// }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Board(pub [[char; 10]; 10]); pub struct Board(pub [[char; 10]; 10]);

View File

@@ -4,7 +4,7 @@ use std::{str::FromStr, sync::Arc};
use axum::Router; use axum::Router;
use dotenv::dotenv; use dotenv::dotenv;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use game::{join_room, Board, ROOM_CODE_LENGTH}; use game::{add_board, add_room, attack, disconnect, join_room, start, Board, ROOM_CODE_LENGTH};
use rand::Rng; use rand::Rng;
use serde_json::Value; use serde_json::Value;
use socketioxide::{ use socketioxide::{
@@ -13,6 +13,7 @@ use socketioxide::{
socket::Sid, socket::Sid,
SocketIo, SocketIo,
}; };
use sqlx::PgPool;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
@@ -28,14 +29,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("DATABASE_URL")?; let url = std::env::var("DATABASE_URL")?;
let pool = sqlx::postgres::PgPool::connect(&url).await?; let pool = sqlx::postgres::PgPool::connect(&url).await?;
sqlx::migrate!("./migrations").run(&pool).await?; sqlx::migrate!("./migrations").run(&pool).await?;
join_room(
Sid::from_str("aaaaaaaaaaaaaaaa").unwrap(),
"AAAB".to_string(),
&pool,
)
.await?;
let (layer, io) = SocketIo::builder().with_state(pool).build_layer(); let (layer, io) = SocketIo::builder().with_state(pool).build_layer();
// io.ns("/", on_connect);
io.ns("/", on_connect);
let app = Router::new() let app = Router::new()
// .route("/", post(game::create_board_route)) // .route("/", post(game::create_board_route))
.layer(layer) .layer(layer)
@@ -47,89 +43,98 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
// fn on_connect(socket: SocketRef, io: SocketIo) { fn on_connect(socket: SocketRef, io: SocketIo) {
// tracing::info!("Connected: {:?}", socket.id); tracing::info!("Connected: {:?}", socket.id);
// // tracing::info!( // tracing::info!(
// // "All rooms and sockets: {:?}", // "All rooms and sockets: {:?}",
// // io.rooms() // io.rooms()
// // .unwrap() // .unwrap()
// // .iter() // .iter()
// // .map(|room| { (room, io.within(room.clone()).sockets().unwrap()) }) // .map(|room| { (room, io.within(room.clone()).sockets().unwrap()) })
// // ); // );
// socket.on( socket.on(
// "create", "create",
// |socket: SocketRef, store: State<Store>| async move { |socket: SocketRef, pool: State<PgPool>| async move {
// if !socket.rooms().unwrap().is_empty() { if !socket.rooms().unwrap().is_empty() {
// socket socket
// .emit("created-room", socket.rooms().unwrap().first()) .emit("created-room", socket.rooms().unwrap().first())
// .unwrap(); .unwrap();
// println!("{} Already in a room", socket.id); println!("{} Already in a room", socket.id);
// return; return;
// } }
// let room: String = rand::thread_rng() let room: String = rand::thread_rng()
// .sample_iter(&rand::distributions::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
// .take(ROOM_CODE_LENGTH) .take(ROOM_CODE_LENGTH)
// .map(|x| char::to_ascii_uppercase(&(x as char))) .map(|x| char::to_ascii_uppercase(&(x as char)))
// .collect(); .collect();
// tracing::info!("Creating room: {:?}", room); tracing::info!("Creating room: {:?}", room);
// store.add_room(room.clone()).await; add_room(socket.id, room.clone(), &pool).await.unwrap();
// store.join_room(socket.id, room.clone()).await.unwrap(); socket.leave_all().unwrap();
// socket.leave_all().unwrap(); socket.join(room.clone()).unwrap();
// socket.join(room.clone()).unwrap(); socket.emit("created-room", &room).unwrap();
// socket.emit("created-room", &room).unwrap(); },
// }, );
// );
// socket.on( socket.on(
// "join", "join",
// |socket: SocketRef, Data::<String>(room), store: State<Store>| async move { |socket: SocketRef, Data::<String>(room), pool: State<PgPool>| async move {
// if room.len() != ROOM_CODE_LENGTH { if room.len() != ROOM_CODE_LENGTH {
// return; return;
// } }
// tracing::info!("Joining room: {:?}", room); tracing::info!("Joining room: {:?}", room);
// store.join_room(socket.id, room.clone()).await.unwrap(); join_room(socket.id, room.clone(), &pool).await.unwrap();
// socket.leave_all().unwrap(); socket.leave_all().unwrap();
// socket.join(room.clone()).unwrap(); socket.join(room.clone()).unwrap();
// if socket.within(room.clone()).sockets().unwrap().len() != 2 { if socket.within(room.clone()).sockets().unwrap().len() != 2 {
// return; return;
// } }
// let ack_stream = socket let ack_stream = socket
// .within(room.clone()) .within(room.clone())
// .emit_with_ack::<Vec<Board>>("upload", ()) .emit_with_ack::<Vec<Board>>("upload", ())
// .unwrap(); .unwrap();
// ack_stream ack_stream
// .for_each(|(id, ack)| { .for_each(|(id, ack)| {
// let store = store.clone(); let pool = pool.clone();
// async move { async move {
// match ack { match ack {
// Ok(mut ack) => { Ok(mut ack) => {
// store.add_board(id, ack.data.pop().unwrap()).await.unwrap(); add_board(id, ack.data.pop().unwrap(), &pool).await.unwrap();
// } }
// Err(err) => tracing::error!("Ack error, {}", err), Err(err) => tracing::error!("Ack error, {}", err),
// } }
// } }
// }) })
// .await; .await;
// store.start(room.clone(), socket.id).await.unwrap(); start(socket.id, room.clone(), &pool).await.unwrap();
// tracing::info!("Game started"); tracing::info!("Game started");
// socket.within(room.clone()).emit("turn", socket.id).unwrap(); socket
// }, .within(room.clone())
// ); .emit("turnover", socket.id)
.unwrap();
},
);
// socket.on( socket.on(
// "attack", "attack",
// |socket: SocketRef, Data::<[usize; 2]>([i, j]), ack: AckSender, store: State<Store>| async move { |socket: SocketRef, Data::<[usize; 2]>([i, j]), pool: State<PgPool>| async move {
// let res = store.attack(socket.id, (i, j)).await.unwrap(); let res = attack(socket.id, (i, j), &pool).await.unwrap();
// tracing::info!("Attacking at: ({}, {}), result: {}", i, j, res); tracing::info!("Attacking at: ({}, {}), result: {}", i, j, res);
// ack.send(res).unwrap(); socket
// }, .within(socket.rooms().unwrap().first().unwrap().clone())
// ); .emit(
"attacked",
serde_json::json!({"by": socket.id.as_str(), "at": [i, j], "res": res}),
)
.unwrap();
},
);
// socket.on_disconnect(|socket: SocketRef, store: State<Store>| { socket.on_disconnect(|socket: SocketRef, pool: State<PgPool>| async move {
// tracing::info!("Disconnecting: {:?}", socket.id); tracing::info!("Disconnecting: {:?}", socket.id);
// socket.leave_all().unwrap(); socket.leave_all().unwrap();
// // TODO: Delete room disconnect(socket.id, &pool).await.unwrap();
// }); // TODO: Delete room
// } });
}