add ship wreck detection

This commit is contained in:
sparshg
2024-09-19 21:05:25 +05:30
parent 0242a92ab2
commit 68764fc461
9 changed files with 244 additions and 151 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target /target
.vscode .vscode
.env .env
test.sql

View File

@@ -5,23 +5,40 @@
let { board, callback }: { board: Board; callback: (i: number, j: number) => void } = $props(); let { board, callback }: { board: Board; callback: (i: number, j: number) => void } = $props();
</script> </script>
<div class="grid grid-cols-10 gap-1 bg-primary p-2 rounded-lg"> <div class="grid grid-cols-10 ml-4">
{#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as i}
<div class="text-center">{i}</div>
{/each}
</div>
<div class="flex flex-row">
<div class="grid grid-rows-10 items-center mr-1">
{#each ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'] as i}
<div class="text">{i}</div>
{/each}
</div>
<div
class="grid grid-cols-10 gap-0.5 lg:gap-1 bg-primary-content p-1 lg:p-1.5 rounded-lg size-full"
>
{#each board.board as row, i} {#each board.board as row, i}
{#each row as cell, j} {#each row as cell, j}
<button <button
class="aspect-square bg-blue-950 flex items-center justify-center {!board.isOpponent class="aspect-square {cell === 'm'
? 'bg-secondary'
: cell === 'h'
? 'bg-accent'
: 'bg-primary'} flex items-center justify-center {!board.isOpponent
? 'cursor-default' ? 'cursor-default'
: ''}" : ''}"
onclick={() => callback(i, j)} onclick={() => callback(i, j)}
> >
{#if cell === 's'} {#if cell === 's'}
<Ship class="size-3/5 text-blue-500" /> <Ship class="size-3/5 text-primary-content" />
{:else if cell === 'h'} {:else if cell === 'h'}
<Crosshair class="size-3/5 text-red-500" /> <Crosshair class="size-3/5 text-accent-content" />
{:else if cell === 'm'}
<div class="size-3/5 bg-blue-400 rounded-full"></div>
{/if} {/if}
</button> </button>
{/each} {/each}
{/each} {/each}
</div> </div>
</div>

View File

@@ -9,35 +9,53 @@ export class State {
opponentBoard = $state(new Board(true)); opponentBoard = $state(new Board(true));
room = $state(''); room = $state('');
turn = $state(false); turn = $state(false);
socket = io('ws://127.0.0.1:3000/', { socket: Socket;
constructor(hostname: string) {
this.socket = io(`ws://${hostname}:3000/`, {
transports: ['websocket'] transports: ['websocket']
}); });
constructor() {
this.socket.on('created-room', (room: string) => { this.socket.on('created-room', (room: string) => {
this.room = room; this.room = room;
}); });
this.socket.on('upload', (_, callback) => { this.socket.on('upload', (_, callback) => {
callback(this.playerBoard.board); callback(this.playerBoard.board);
}) });
this.socket.on('turnover', (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 }) => { this.socket.on('attacked', ({ by, at, hit, sunk }) => {
let [i, j] = at; const [i, j]: [number, number] = at;
let board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
if (by == this.socket.id) { if (by == this.socket.id) {
this.opponentBoard.board[i][j] = res ? 'h' : 'm'; this.turn = hit;
this.turn = false;
} else { } else {
this.playerBoard.board[i][j] = res ? 'h' : 'm'; this.turn = !hit;
this.turn = true;
} }
}) board.board[i][j] = hit ? 'h' : 'm';
if (sunk) {
const [[minx, miny], [maxx, maxy]] = sunk;
const x1 = Math.max(0, minx - 1);
const y1 = Math.max(0, miny - 1);
const x2 = Math.min(9, maxx + 1);
const y2 = Math.min(9, maxy + 1);
for (let x = x1; x <= x2; x++) {
for (let y = y1; y <= y2; y++) {
if (board.board[x][y] == 'e') {
board.board[x][y] = 'm';
}
}
}
}
});
} }
attack(i: number, j: number) { attack(i: number, j: number) {
if (!this.turn) return; if (!this.turn) return;
if (this.opponentBoard.board[i][j] != 'e') return;
this.turn = false; this.turn = false;
this.socket.emit('attack', [i, j]); this.socket.emit('attack', [i, j]);
} }

View File

@@ -3,7 +3,8 @@
import Header from '$lib/header.svelte'; import Header from '$lib/header.svelte';
import { State } from '$lib/state.svelte'; import { State } from '$lib/state.svelte';
let gameState = new State(); const hostname = window.location.hostname;
let gameState = new State(hostname);
</script> </script>
<div class="min-h-screen bg-base-300 py-8 px-4 sm:px-6 lg:px-8"> <div class="min-h-screen bg-base-300 py-8 px-4 sm:px-6 lg:px-8">

View File

@@ -9,8 +9,8 @@ export default {
], ],
daisyui: { daisyui: {
themes: ["night"], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] themes: ["cupcake", "night"], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
darkTheme: "night", // name of one of the included themes for dark mode darkTheme: "cupcake", // name of one of the included themes for dark mode
base: true, // applies background color and foreground color for root element by default base: true, // applies background color and foreground color for root element by default
styled: true, // include daisyUI colors and design decisions for all components styled: true, // include daisyUI colors and design decisions for all components
utils: true, // adds responsive and modifier utility classes utils: true, // adds responsive and modifier utility classes

View File

@@ -3,7 +3,7 @@ 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) NOT NULL room_code CHAR(4) NOT NULL
); );

122
src/board.rs Normal file
View File

@@ -0,0 +1,122 @@
use std::ops::{Deref, DerefMut};
use axum::Json;
use rand::Rng;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Board(pub [[char; 10]; 10]);
impl From<Board> for Vec<String> {
fn from(board: Board) -> Self {
board.iter().map(|row| row.iter().collect()).collect()
}
}
impl From<Vec<String>> for Board {
fn from(board: Vec<String>) -> Self {
let mut arr = [['e'; 10]; 10];
for (i, row) in board.iter().enumerate() {
for (j, cell) in row.chars().enumerate() {
arr[i][j] = cell;
}
}
Board(arr)
}
}
impl Deref for Board {
type Target = [[char; 10]; 10];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Board {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Board {
const SHIPS: [i32; 5] = [5, 4, 3, 3, 2];
pub fn from_json(Json(board): Json<Board>) -> Self {
board
}
pub fn randomize() -> Self {
let mut board = Board([['e'; 10]; 10]);
for &length in Self::SHIPS.iter() {
loop {
let dir = rand::thread_rng().gen_bool(0.5);
let x = rand::thread_rng().gen_range(0..(if dir { 10 } else { 11 - length }));
let y = rand::thread_rng().gen_range(0..(if dir { 11 - length } else { 10 }));
if board.is_overlapping(x, y, length, dir) {
continue;
}
for i in 0..length {
let (tx, ty) = if dir { (x, y + i) } else { (x + i, y) };
board[tx as usize][ty as usize] = 's';
}
break;
}
}
board
}
fn is_overlapping(&self, x: i32, y: i32, length: i32, dir: bool) -> bool {
for i in -1..2 {
for j in -1..=length {
let (tx, ty) = if dir { (x + i, y + j) } else { (x + j, y + i) };
if !(0..10).contains(&tx) || !(0..10).contains(&ty) {
continue;
}
if self[tx as usize][ty as usize] != 'e' {
return true;
}
}
}
false
}
pub fn has_sunk(&self, (i, j): (usize, usize)) -> Option<[(usize, usize); 2]> {
let mut queue = vec![(i, j)];
let mut visited = vec![vec![false; 10]; 10];
let mut bounds = [(i, j), (i, j)];
visited[i][j] = true;
while let Some((x, y)) = queue.pop() {
if self[x][y] == 's' {
return None;
}
bounds[0].0 = bounds[0].0.min(x);
bounds[0].1 = bounds[0].1.min(y);
bounds[1].0 = bounds[1].0.max(x);
bounds[1].1 = bounds[1].1.max(y);
for (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)].iter() {
let (tx, ty) = ((x as i32 + dx) as usize, (y as i32 + dy) as usize);
if (0..10).contains(&tx)
&& (0..10).contains(&ty)
&& !visited[tx][ty]
&& matches!(self[tx][ty], 'h' | 's')
{
visited[tx][ty] = true;
queue.push((tx, ty));
}
}
}
Some(bounds)
}
// fn validate_syntax(&self) -> bool {
// self
// .iter()
// .all(|row| row.iter().all(|cell| matches!(cell, 'e' | 'h' | 'm' | 's')))
// }
}
// pub async fn create_board_route(board: Json<Board>) -> Json<String> {
// let board = Board::from_json(board).await;
// Json(format!("{:?}", board))
// }

View File

@@ -1,11 +1,8 @@
use std::convert::Infallible;
use axum::Json;
use rand::Rng;
use serde::Deserialize;
use socketioxide::socket::Sid; use socketioxide::socket::Sid;
use thiserror::Error; use thiserror::Error;
use crate::board::Board;
pub const ROOM_CODE_LENGTH: usize = 4; pub const ROOM_CODE_LENGTH: usize = 4;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@@ -20,6 +17,8 @@ pub enum Error {
AlreadyInRoom, AlreadyInRoom,
#[error("Not in room")] #[error("Not in room")]
NotInRoom, NotInRoom,
#[error("Invalid Move")]
InvalidMove,
#[error("SQL Error\n{0:?}")] #[error("SQL Error\n{0:?}")]
Sqlx(#[from] sqlx::Error), Sqlx(#[from] sqlx::Error),
} }
@@ -85,25 +84,14 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
} }
pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()> { pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()> {
let query = format!( let board: Vec<String> = board.into();
"UPDATE players SET board = ARRAY[{}] WHERE id = '{}'", sqlx::query!(
board "UPDATE players SET board = $1 WHERE id = $2",
.0 &board,
.map(|row| {
format!(
"ARRAY[{}]",
row.map(|x| format!("'{x}'"))
.into_iter()
.collect::<Vec<_>>()
.join(",")
)
})
.into_iter()
.collect::<Vec<String>>()
.join(","),
sid.as_str() sid.as_str()
); )
sqlx::query(&query).execute(pool).await?; .execute(pool)
.await?;
Ok(()) Ok(())
} }
@@ -137,7 +125,11 @@ pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn attack(sid: Sid, (i, j): (usize, usize), pool: &sqlx::PgPool) -> Result<bool> { pub async fn attack(
sid: Sid,
(i, j): (usize, usize),
pool: &sqlx::PgPool,
) -> Result<(bool, Option<[(usize, usize); 2]>)> {
let player = sqlx::query!(r"SELECT room_code FROM players WHERE id = $1", sid.as_str()) let player = sqlx::query!(r"SELECT room_code FROM players WHERE id = $1", sid.as_str())
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
@@ -159,32 +151,31 @@ pub async fn attack(sid: Sid, (i, j): (usize, usize), pool: &sqlx::PgPool) -> Re
_ => return Err(Error::RoomNotFull), // room not full _ => return Err(Error::RoomNotFull), // room not full
}; };
let mut board: Board = sqlx::query!(r"SELECT board FROM players WHERE id = $1", other)
.fetch_one(pool)
.await?
.board
.unwrap()
.into();
let hit = match board[i][j] {
's' => true,
'e' => false,
_ => return Err(Error::InvalidMove),
};
board[i][j] = if hit { 'h' } else { 'm' };
let mut txn = pool.begin().await?; let mut txn = pool.begin().await?;
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!( sqlx::query!(
r#"UPDATE players r#"UPDATE players SET board[$1] = $2 WHERE id = $3"#,
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, i as i32 + 1,
j as i32 + 1, board[i].iter().collect::<String>(),
other other
) )
.execute(&mut *txn) .execute(&mut *txn)
.await?; .await?;
if !hit {
sqlx::query!( sqlx::query!(
r#"UPDATE rooms SET stat = $1 WHERE code = $2"#, r#"UPDATE rooms SET stat = $1 WHERE code = $2"#,
to_status as Status, to_status as Status,
@@ -192,9 +183,10 @@ pub async fn attack(sid: Sid, (i, j): (usize, usize), pool: &sqlx::PgPool) -> Re
) )
.execute(&mut *txn) .execute(&mut *txn)
.await?; .await?;
}
txn.commit().await?; txn.commit().await?;
Ok(turn.hit.unwrap() == "s") Ok((hit, if hit { board.has_sunk((i, j)) } else { None }))
} }
pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<()> { pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<()> {
@@ -211,60 +203,3 @@ enum Status {
P1Turn, P1Turn,
P2Turn, P2Turn,
} }
#[derive(Debug, Deserialize)]
pub struct Board(pub [[char; 10]; 10]);
impl Board {
const SHIPS: [i32; 5] = [5, 4, 3, 3, 2];
pub fn from_json(Json(board): Json<Board>) -> Self {
board
}
pub fn randomize() -> Self {
let mut board = Board([['e'; 10]; 10]);
for &length in Self::SHIPS.iter() {
loop {
let dir = rand::thread_rng().gen_bool(0.5);
let x = rand::thread_rng().gen_range(0..(if dir { 10 } else { 11 - length }));
let y = rand::thread_rng().gen_range(0..(if dir { 11 - length } else { 10 }));
if board.is_overlapping(x, y, length, dir) {
continue;
}
for i in 0..length {
let (tx, ty) = if dir { (x, y + i) } else { (x + i, y) };
board.0[tx as usize][ty as usize] = 's';
}
break;
}
}
board
}
fn is_overlapping(&self, x: i32, y: i32, length: i32, dir: bool) -> bool {
for i in -1..2 {
for j in -1..=length {
let (tx, ty) = if dir { (x + i, y + j) } else { (x + j, y + i) };
if !(0..10).contains(&tx) || !(0..10).contains(&ty) {
continue;
}
if self.0[tx as usize][ty as usize] != 'e' {
return true;
}
}
}
false
}
// fn validate_syntax(&self) -> bool {
// self.0
// .iter()
// .all(|row| row.iter().all(|cell| matches!(cell, 'e' | 'h' | 'm' | 's')))
// }
}
// pub async fn create_board_route(board: Json<Board>) -> Json<String> {
// let board = Board::from_json(board).await;
// Json(format!("{:?}", board.0))
// }

View File

@@ -1,9 +1,10 @@
mod board;
mod game; mod game;
use axum::Router; use axum::Router;
use board::Board;
use dotenv::dotenv; use dotenv::dotenv;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use game::{add_board, add_room, attack, disconnect, join_room, start, Board, ROOM_CODE_LENGTH}; use game::{add_board, add_room, attack, disconnect, join_room, start, ROOM_CODE_LENGTH};
use rand::Rng; use rand::Rng;
use socketioxide::{ use socketioxide::{
extract::{Data, SocketRef, State}, extract::{Data, SocketRef, State},
@@ -33,7 +34,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.layer(layer) .layer(layer)
.layer(CorsLayer::very_permissive()); .layer(CorsLayer::very_permissive());
let listener = TcpListener::bind("127.0.0.1:3000").await?; let listener = TcpListener::bind("0.0.0.0:3000").await?;
println!("listening on {}", listener.local_addr()?); println!("listening on {}", listener.local_addr()?);
axum::serve(listener, app).await?; axum::serve(listener, app).await?;
Ok(()) Ok(())
@@ -98,7 +99,6 @@ fn on_connect(socket: SocketRef) {
if let Err(e) = add_board(id, ack.data.pop().unwrap(), &pool).await if let Err(e) = add_board(id, ack.data.pop().unwrap(), &pool).await
{ {
tracing::error!("{:?}", e); tracing::error!("{:?}", e);
return;
} }
} }
Err(err) => tracing::error!("Ack error, {}", err), Err(err) => tracing::error!("Ack error, {}", err),
@@ -121,19 +121,19 @@ fn on_connect(socket: SocketRef) {
socket.on( socket.on(
"attack", "attack",
|socket: SocketRef, Data::<[usize; 2]>([i, j]), pool: State<PgPool>| async move { |socket: SocketRef, Data::<[usize; 2]>([i, j]), pool: State<PgPool>| async move {
let res = match attack(socket.id, (i, j), &pool).await { let (hit, sunk) = match attack(socket.id, (i, j), &pool).await {
Ok(res) => res, Ok(res) => res,
Err(e) => { Err(e) => {
tracing::error!("{:?}", e); tracing::error!("{:?}", e);
return; return;
} }
}; };
tracing::info!("Attacking at: ({}, {}), result: {}", i, j, res); tracing::info!("Attacking at: ({}, {}), result: {:?}", i, j, hit);
socket socket
.within(socket.rooms().unwrap().first().unwrap().clone()) .within(socket.rooms().unwrap().first().unwrap().clone())
.emit( .emit(
"attacked", "attacked",
serde_json::json!({"by": socket.id.as_str(), "at": [i, j], "res": res}), serde_json::json!({"by": socket.id.as_str(), "at": [i, j], "hit": hit, "sunk": sunk}),
) )
.unwrap(); .unwrap();
}, },
@@ -144,7 +144,6 @@ fn on_connect(socket: SocketRef) {
socket.leave_all().unwrap(); socket.leave_all().unwrap();
if let Err(e) = disconnect(socket.id, &pool).await { if let Err(e) = disconnect(socket.id, &pool).await {
tracing::error!("{:?}", e); tracing::error!("{:?}", e);
return;
} }
}); });
} }