diff --git a/.gitignore b/.gitignore
index b5e703e..ca10f2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/target
.vscode
-.env
\ No newline at end of file
+.env
+test.sql
\ No newline at end of file
diff --git a/app/src/lib/board.svelte b/app/src/lib/board.svelte
index 7273966..4efdcf6 100644
--- a/app/src/lib/board.svelte
+++ b/app/src/lib/board.svelte
@@ -5,23 +5,40 @@
let { board, callback }: { board: Board; callback: (i: number, j: number) => void } = $props();
-
- {#each board.board as row, i}
- {#each row as cell, j}
-
- {/each}
+
+ {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as i}
+
{i}
{/each}
+
+
+
+ {#each ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'] as i}
+
{i}
+ {/each}
+
+
+ {#each board.board as row, i}
+ {#each row as cell, j}
+
+ {/each}
+ {/each}
+
+
diff --git a/app/src/lib/state.svelte.ts b/app/src/lib/state.svelte.ts
index 0a4ed74..f7f7981 100644
--- a/app/src/lib/state.svelte.ts
+++ b/app/src/lib/state.svelte.ts
@@ -9,35 +9,53 @@ export class State {
opponentBoard = $state(new Board(true));
room = $state('');
turn = $state(false);
- socket = io('ws://127.0.0.1:3000/', {
- transports: ['websocket']
- });
+ socket: Socket;
+
+ constructor(hostname: string) {
+ this.socket = io(`ws://${hostname}:3000/`, {
+ transports: ['websocket']
+ });
- constructor() {
this.socket.on('created-room', (room: string) => {
this.room = room;
});
this.socket.on('upload', (_, callback) => {
callback(this.playerBoard.board);
- })
+ });
this.socket.on('turnover', (id) => {
this.turn = id != this.socket.id;
- })
- this.socket.on('attacked', ({ by, at, res }) => {
- let [i, j] = at;
+ });
+ this.socket.on('attacked', ({ by, at, hit, sunk }) => {
+ const [i, j]: [number, number] = at;
+ let board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
if (by == this.socket.id) {
- this.opponentBoard.board[i][j] = res ? 'h' : 'm';
- this.turn = false;
+ this.turn = hit;
} else {
- this.playerBoard.board[i][j] = res ? 'h' : 'm';
- this.turn = true;
+ this.turn = !hit;
}
- })
+ 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) {
if (!this.turn) return;
+ if (this.opponentBoard.board[i][j] != 'e') return;
this.turn = false;
+
this.socket.emit('attack', [i, j]);
}
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte
index 8403baf..ce4632e 100644
--- a/app/src/routes/+page.svelte
+++ b/app/src/routes/+page.svelte
@@ -3,7 +3,8 @@
import Header from '$lib/header.svelte';
import { State } from '$lib/state.svelte';
- let gameState = new State();
+ const hostname = window.location.hostname;
+ let gameState = new State(hostname);
diff --git a/app/tailwind.config.js b/app/tailwind.config.js
index 41ab34d..f666207 100644
--- a/app/tailwind.config.js
+++ b/app/tailwind.config.js
@@ -9,8 +9,8 @@ export default {
],
daisyui: {
- themes: ["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
+ themes: ["cupcake", "night"], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
+ 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
styled: true, // include daisyUI colors and design decisions for all components
utils: true, // adds responsive and modifier utility classes
diff --git a/migrations/0001_battleship.sql b/migrations/0001_battleship.sql
index c1339fe..f42da13 100644
--- a/migrations/0001_battleship.sql
+++ b/migrations/0001_battleship.sql
@@ -3,7 +3,7 @@ CREATE TYPE STAT AS ENUM ('waiting', 'p1turn', 'p2turn');
CREATE TABLE IF NOT EXISTS players (
id CHAR(16) PRIMARY KEY,
- board CHAR [10] [10],
+ board CHAR(10) [10],
room_code CHAR(4) NOT NULL
);
diff --git a/src/board.rs b/src/board.rs
new file mode 100644
index 0000000..e47d57f
--- /dev/null
+++ b/src/board.rs
@@ -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
for Vec {
+ fn from(board: Board) -> Self {
+ board.iter().map(|row| row.iter().collect()).collect()
+ }
+}
+
+impl From> for Board {
+ fn from(board: Vec) -> 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) -> 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) -> Json {
+// let board = Board::from_json(board).await;
+// Json(format!("{:?}", board))
+// }
diff --git a/src/game.rs b/src/game.rs
index fd5cbe6..b81c392 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -1,11 +1,8 @@
-use std::convert::Infallible;
-
-use axum::Json;
-use rand::Rng;
-use serde::Deserialize;
use socketioxide::socket::Sid;
use thiserror::Error;
+use crate::board::Board;
+
pub const ROOM_CODE_LENGTH: usize = 4;
pub type Result = std::result::Result;
@@ -20,6 +17,8 @@ pub enum Error {
AlreadyInRoom,
#[error("Not in room")]
NotInRoom,
+ #[error("Invalid Move")]
+ InvalidMove,
#[error("SQL Error\n{0:?}")]
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<()> {
- let query = format!(
- "UPDATE players SET board = ARRAY[{}] WHERE id = '{}'",
- board
- .0
- .map(|row| {
- format!(
- "ARRAY[{}]",
- row.map(|x| format!("'{x}'"))
- .into_iter()
- .collect::>()
- .join(",")
- )
- })
- .into_iter()
- .collect::>()
- .join(","),
+ let board: Vec = board.into();
+ sqlx::query!(
+ "UPDATE players SET board = $1 WHERE id = $2",
+ &board,
sid.as_str()
- );
- sqlx::query(&query).execute(pool).await?;
+ )
+ .execute(pool)
+ .await?;
Ok(())
}
@@ -137,7 +125,11 @@ pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
Ok(())
}
-pub async fn attack(sid: Sid, (i, j): (usize, usize), pool: &sqlx::PgPool) -> Result {
+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())
.fetch_one(pool)
.await?;
@@ -159,42 +151,42 @@ pub async fn attack(sid: Sid, (i, j): (usize, usize), pool: &sqlx::PgPool) -> Re
_ => 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 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"#,
+ r#"UPDATE players SET board[$1] = $2 WHERE id = $3"#,
i as i32 + 1,
- j as i32 + 1,
+ board[i].iter().collect::(),
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?;
+ if !hit {
+ 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")
+ Ok((hit, if hit { board.has_sunk((i, j)) } else { None }))
}
pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<()> {
@@ -211,60 +203,3 @@ enum Status {
P1Turn,
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) -> 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) -> Json {
-// let board = Board::from_json(board).await;
-// Json(format!("{:?}", board.0))
-// }
diff --git a/src/main.rs b/src/main.rs
index 8c71bc2..91e8f64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,10 @@
+mod board;
mod game;
-
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, Board, ROOM_CODE_LENGTH};
+use game::{add_board, add_room, attack, disconnect, join_room, start, ROOM_CODE_LENGTH};
use rand::Rng;
use socketioxide::{
extract::{Data, SocketRef, State},
@@ -33,7 +34,7 @@ async fn main() -> Result<(), Box> {
.layer(layer)
.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()?);
axum::serve(listener, app).await?;
Ok(())
@@ -98,7 +99,6 @@ fn on_connect(socket: SocketRef) {
if let Err(e) = add_board(id, ack.data.pop().unwrap(), &pool).await
{
tracing::error!("{:?}", e);
- return;
}
}
Err(err) => tracing::error!("Ack error, {}", err),
@@ -121,19 +121,19 @@ fn on_connect(socket: SocketRef) {
socket.on(
"attack",
|socket: SocketRef, Data::<[usize; 2]>([i, j]), pool: State| 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,
Err(e) => {
tracing::error!("{:?}", e);
return;
}
};
- tracing::info!("Attacking at: ({}, {}), result: {}", i, j, res);
+ tracing::info!("Attacking at: ({}, {}), result: {:?}", i, j, hit);
socket
.within(socket.rooms().unwrap().first().unwrap().clone())
.emit(
"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();
},
@@ -144,7 +144,6 @@ fn on_connect(socket: SocketRef) {
socket.leave_all().unwrap();
if let Err(e) = disconnect(socket.id, &pool).await {
tracing::error!("{:?}", e);
- return;
}
});
}