restore state on reload/new join
This commit is contained in:
@@ -23,13 +23,13 @@
|
|||||||
{#if roomCode}
|
{#if roomCode}
|
||||||
<div class="space-x-2 flex flex-row justify-center items-center">
|
<div class="space-x-2 flex flex-row justify-center items-center">
|
||||||
<div
|
<div
|
||||||
class="text-3xl font-bold tracking-widest text-accent-content font-mono bg-accent py-3 rounded-full px-12"
|
class="text-3xl font-bold tracking-widest text-secondary-content font-mono bg-secondary py-3 rounded-full px-12"
|
||||||
>
|
>
|
||||||
{roomCode}
|
{roomCode}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-accent btn-circle size-16"
|
class="btn btn-secondary btn-circle size-16"
|
||||||
onclick={() => navigator.clipboard.writeText(roomCode)}
|
onclick={() => navigator.clipboard.writeText(roomCode)}
|
||||||
>
|
>
|
||||||
<ClipboardCopy />
|
<ClipboardCopy />
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onclick={() => joinRoom(joinCode)}
|
onclick={() => joinRoom(joinCode)}
|
||||||
class="w-full btn btn-outline btn-neutral text-neutral hover:text-neutral hover:bg-transparent text-xl"
|
class="w-full btn btn-outline btn-neutral text-neutral hover:border-neutral hover:bg-transparent text-xl"
|
||||||
>
|
>
|
||||||
Join Room
|
Join Room
|
||||||
</button>
|
</button>
|
||||||
|
@@ -7,34 +7,43 @@ export class State {
|
|||||||
phase: Phase = $state('placement');
|
phase: Phase = $state('placement');
|
||||||
playerBoard = $state(new Board(false));
|
playerBoard = $state(new Board(false));
|
||||||
opponentBoard = $state(new Board(true));
|
opponentBoard = $state(new Board(true));
|
||||||
|
users = $state(0);
|
||||||
room = $state('');
|
room = $state('');
|
||||||
turn = $state(false);
|
turn = $state(-1); // -1 not my turn, 0 might be, 1 is
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
||||||
constructor(hostname: string) {
|
constructor(hostname: string) {
|
||||||
|
let session = sessionStorage.getItem('session');
|
||||||
|
|
||||||
this.socket = io(`ws://${hostname}:3000/`, {
|
this.socket = io(`ws://${hostname}:3000/`, {
|
||||||
transports: ['websocket']
|
transports: ['websocket'],
|
||||||
|
auth: { session }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('joined-room', (room: string) => {
|
this.socket.on('connect', () => {
|
||||||
this.phase = 'waiting';
|
sessionStorage.setItem('session', this.socket.id!);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('update-room', ({ room, users }) => {
|
||||||
|
if (this.phase == 'placement') this.phase = 'waiting';
|
||||||
this.room = room;
|
this.room = room;
|
||||||
|
this.users = users;
|
||||||
});
|
});
|
||||||
|
|
||||||
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) ? 1 : -1;
|
||||||
this.phase = this.turn ? 'selfturn' : 'otherturn';
|
this.phase = this.turn ? 'selfturn' : 'otherturn';
|
||||||
});
|
});
|
||||||
this.socket.on('attacked', ({ by, at, hit, sunk }) => {
|
this.socket.on('attacked', ({ by, at, hit, sunk }) => {
|
||||||
const [i, j]: [number, number] = at;
|
const [i, j]: [number, number] = at;
|
||||||
let board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
|
let board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
|
||||||
if (by == this.socket.id) {
|
if (by == this.socket.id) {
|
||||||
this.turn = hit;
|
this.turn = (hit) ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
this.turn = !hit;
|
this.turn = (!hit) ? 1 : -1;
|
||||||
}
|
}
|
||||||
if (hit) {
|
if (hit) {
|
||||||
board.board[i][j] = 'h';
|
board.board[i][j] = 'h';
|
||||||
@@ -62,12 +71,19 @@ export class State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on('restore', ({ turn, player, opponent }: { turn: boolean, player: string[], opponent: string[] }) => {
|
||||||
|
this.turn = turn ? 1 : -1;
|
||||||
|
this.phase = this.turn ? 'selfturn' : 'otherturn';
|
||||||
|
this.playerBoard.board = player.map((s) => s.split('').map(c => c as CellType));
|
||||||
|
this.opponentBoard.board = opponent.map((s) => s.split('').map(c => c as CellType));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
attack(i: number, j: number) {
|
attack(i: number, j: number) {
|
||||||
if (!this.turn) return;
|
if (this.turn != 1) return;
|
||||||
if (this.opponentBoard.board[i][j] != 'e') return;
|
if (this.opponentBoard.board[i][j] != 'e') return;
|
||||||
this.turn = false;
|
this.turn = 0;
|
||||||
|
|
||||||
this.socket.emit('attack', [i, j]);
|
this.socket.emit('attack', [i, j]);
|
||||||
}
|
}
|
||||||
@@ -87,6 +103,7 @@ export class State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
static shipTypes = [5, 4, 3, 3, 2];
|
static shipTypes = [5, 4, 3, 3, 2];
|
||||||
board: Array<Array<CellType>> = $state(Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => 'e')));
|
board: Array<Array<CellType>> = $state(Array.from({ length: 10 }, () => Array.from({ length: 10 }, () => 'e')));
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
import Header from '$lib/header.svelte';
|
import Header from '$lib/header.svelte';
|
||||||
import Join from '$lib/join.svelte';
|
import Join from '$lib/join.svelte';
|
||||||
import { State } from '$lib/state.svelte';
|
import { State } from '$lib/state.svelte';
|
||||||
|
import { Users } from 'lucide-svelte';
|
||||||
|
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
let gameState = new State(hostname);
|
let gameState = new State(hostname);
|
||||||
@@ -18,14 +19,35 @@
|
|||||||
<h2 class="text-2xl font-semibold rounded-full bg-base-300 py-3 px-6">
|
<h2 class="text-2xl font-semibold rounded-full bg-base-300 py-3 px-6">
|
||||||
{gameState.hasNotStarted()
|
{gameState.hasNotStarted()
|
||||||
? 'Place your ships'
|
? 'Place your ships'
|
||||||
: gameState.turn
|
: gameState.turn >= 0
|
||||||
? 'Make a guess'
|
? 'Make a guess'
|
||||||
: 'Waiting for opponent'}
|
: 'Waiting for opponent'}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex space-x-4">
|
{#if gameState.room}
|
||||||
<div class="text-blue-600">Your Ships: {5}</div>
|
<div class="flex flex-row h-full items-center space-x-4">
|
||||||
<div class="text-red-600">Enemy Ships: {5}</div>
|
<button
|
||||||
</div>
|
class="rounded-full bg-base-300 px-4 uppercase font-mono font-bold tracking-wide text-xl py-2.5 tooltip tooltip-bottom"
|
||||||
|
data-tip="Copy"
|
||||||
|
onclick={() => navigator.clipboard.writeText(gameState.room)}
|
||||||
|
>
|
||||||
|
{gameState.room}
|
||||||
|
</button>
|
||||||
|
<div class="rounded-full bg-base-300 px-4 flex items-center space-x-2 py-3">
|
||||||
|
<div
|
||||||
|
class="size-3 bg-green-500 rounded-full shadow-[0_0_10px] shadow-green-500"
|
||||||
|
></div>
|
||||||
|
<div class="font-mono font-bold">{gameState.users}</div>
|
||||||
|
<Users />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-error text-xl"
|
||||||
|
onclick={() => {
|
||||||
|
gameState.socket.emit('leave');
|
||||||
|
gameState = new State(window.location.hostname);
|
||||||
|
}}>Leave</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-8">
|
<div class="grid md:grid-cols-2 gap-8">
|
||||||
@@ -33,7 +55,7 @@
|
|||||||
<h3 class="text-lg font-medium mb-2">Your Board</h3>
|
<h3 class="text-lg font-medium mb-2">Your Board</h3>
|
||||||
|
|
||||||
<Board
|
<Board
|
||||||
class={!gameState.turn ? 'scale-[1.01]' : 'opacity-60'}
|
class={gameState.turn < 0 ? 'scale-[1.01]' : 'opacity-60'}
|
||||||
board={gameState.playerBoard}
|
board={gameState.playerBoard}
|
||||||
callback={() => {}}
|
callback={() => {}}
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +64,7 @@
|
|||||||
<h3 class="text-lg font-medium mb-2">Opponent's Board</h3>
|
<h3 class="text-lg font-medium mb-2">Opponent's Board</h3>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Board
|
<Board
|
||||||
class={gameState.turn ? 'scale-[1.01]' : 'opacity-60'}
|
class={gameState.turn >= 0 ? 'scale-[1.01]' : 'opacity-60'}
|
||||||
board={gameState.opponentBoard}
|
board={gameState.opponentBoard}
|
||||||
callback={(i, j) => gameState.attack(i, j)}
|
callback={(i, j) => gameState.attack(i, j)}
|
||||||
/>
|
/>
|
||||||
|
@@ -24,15 +24,21 @@ CREATE TABLE IF NOT EXISTS rooms (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS abandoned_players (
|
||||||
|
time TIMESTAMP PRIMARY KEY,
|
||||||
|
id CHAR(16) NOT NULL,
|
||||||
|
CONSTRAINT fk_player_id FOREIGN KEY (id) REFERENCES players (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
ALTER TABLE players
|
ALTER TABLE players
|
||||||
ADD CONSTRAINT fk_room_code FOREIGN KEY (room_code) REFERENCES rooms (code) ON DELETE
|
ADD CONSTRAINT fk_room_code FOREIGN KEY (room_code) REFERENCES rooms (code) ON DELETE
|
||||||
SET NULL;
|
SET NULL;
|
||||||
|
|
||||||
ALTER TABLE rooms
|
ALTER TABLE rooms
|
||||||
ADD CONSTRAINT fk_player1 FOREIGN KEY (player1_id) REFERENCES players (id) ON DELETE
|
ADD CONSTRAINT fk_player1 FOREIGN KEY (player1_id) REFERENCES players (id) ON DELETE
|
||||||
SET NULL,
|
SET NULL ON UPDATE CASCADE,
|
||||||
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 ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- delete room if both players are null
|
-- delete room if both players are null
|
||||||
CREATE OR REPLACE FUNCTION delete_room() RETURNS TRIGGER AS $$ BEGIN IF (
|
CREATE OR REPLACE FUNCTION delete_room() RETURNS TRIGGER AS $$ BEGIN IF (
|
||||||
|
29
src/board.rs
29
src/board.rs
@@ -2,9 +2,9 @@ use std::ops::{Deref, DerefMut};
|
|||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Board(pub [[char; 10]; 10]);
|
pub struct Board(pub [[char; 10]; 10]);
|
||||||
|
|
||||||
impl From<Board> for Vec<String> {
|
impl From<Board> for Vec<String> {
|
||||||
@@ -109,6 +109,31 @@ impl Board {
|
|||||||
Some(bounds)
|
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 {
|
// fn validate_syntax(&self) -> bool {
|
||||||
// self
|
// self
|
||||||
// .iter()
|
// .iter()
|
||||||
|
151
src/game.rs
151
src/game.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use serde::Serialize;
|
||||||
use socketioxide::socket::Sid;
|
use socketioxide::socket::Sid;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Room full")]
|
#[error("Room full, potential replacement {0:?}")]
|
||||||
RoomFull,
|
RoomFull(Option<String>),
|
||||||
#[error("Room not full")]
|
#[error("Room not full")]
|
||||||
RoomNotFull,
|
RoomNotFull,
|
||||||
#[error("Already in room")]
|
#[error("Already in room")]
|
||||||
@@ -23,7 +24,25 @@ pub enum Error {
|
|||||||
Sqlx(#[from] sqlx::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<()> {
|
pub async fn add_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
|
||||||
|
delete_sid(sid.as_str(), pool).await?;
|
||||||
sqlx::query!(
|
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",
|
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(),
|
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();
|
let sid = sid.as_str();
|
||||||
|
|
||||||
if room.player1_id.is_some() && room.player2_id.is_some() {
|
if let (Some(p1), Some(p2)) = (room.player1_id.as_ref(), room.player2_id.as_ref()) {
|
||||||
return Err(Error::RoomFull);
|
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 let Some(id) = room.player1_id.as_ref() {
|
||||||
if id == sid {
|
if id == sid {
|
||||||
@@ -58,7 +84,7 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
|
|||||||
return Err(Error::AlreadyInRoom);
|
return Err(Error::AlreadyInRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delete_sid(sid, pool).await?;
|
||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
// create/update player
|
// create/update player
|
||||||
@@ -84,6 +110,15 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
|
|||||||
Ok(())
|
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<()> {
|
pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()> {
|
||||||
let board: Vec<String> = board.into();
|
let board: Vec<String> = board.into();
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -96,6 +131,66 @@ pub async fn add_board(sid: Sid, board: Board, pool: &sqlx::PgPool) -> Result<()
|
|||||||
Ok(())
|
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<()> {
|
pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
|
||||||
let room = sqlx::query!(
|
let room = sqlx::query!(
|
||||||
r"SELECT player1_id, player2_id FROM rooms WHERE code = $1",
|
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 }))
|
Ok((hit, if hit { board.has_sunk((i, j)) } else { None }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn disconnect(sid: Sid, pool: &sqlx::PgPool) -> Result<()> {
|
pub async fn update_sid(oldsid: &str, newsid: &str, pool: &sqlx::PgPool) -> Result<()> {
|
||||||
sqlx::query!(r"DELETE FROM players WHERE id = $1", sid.as_str())
|
sqlx::query!(r"UPDATE players SET id = $1 WHERE id = $2", newsid, oldsid)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, sqlx::Type, PartialEq)]
|
pub async fn delete_sid(sid: &str, pool: &sqlx::PgPool) -> Result<()> {
|
||||||
#[sqlx(type_name = "STAT", rename_all = "lowercase")]
|
sqlx::query!(r"DELETE FROM players WHERE id = $1", sid)
|
||||||
enum Status {
|
.execute(pool)
|
||||||
Waiting,
|
.await?;
|
||||||
P1Turn,
|
Ok(())
|
||||||
P2Turn,
|
}
|
||||||
|
|
||||||
|
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 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, 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 rand::Rng;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
use socketioxide::{
|
use socketioxide::{
|
||||||
extract::{Data, SocketRef, State},
|
extract::{Data, SocketRef, State},
|
||||||
SocketIo,
|
SocketIo,
|
||||||
@@ -26,6 +31,11 @@ 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?;
|
||||||
|
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();
|
let (layer, io) = SocketIo::builder().with_state(pool).build_layer();
|
||||||
|
|
||||||
io.ns("/", on_connect);
|
io.ns("/", on_connect);
|
||||||
@@ -40,8 +50,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
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: {:?}", 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(
|
socket.on(
|
||||||
"create",
|
"create",
|
||||||
|socket: SocketRef, pool: State<PgPool>| async move {
|
|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)))
|
.map(|x| char::to_ascii_uppercase(&(x as char)))
|
||||||
.collect();
|
.collect();
|
||||||
tracing::info!("Creating room: {:?}", room);
|
tracing::info!("Creating room: {:?}", room);
|
||||||
|
// TODO: Handle duplicates
|
||||||
|
|
||||||
if let Err(e) = add_room(socket.id, room.clone(), &pool).await {
|
if let Err(e) = add_room(socket.id, room.clone(), &pool).await {
|
||||||
tracing::error!("{:?}", e);
|
tracing::error!("{:?}", e);
|
||||||
@@ -66,7 +104,11 @@ fn on_connect(socket: SocketRef) {
|
|||||||
}
|
}
|
||||||
socket.leave_all().unwrap();
|
socket.leave_all().unwrap();
|
||||||
socket.join(room.clone()).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;
|
return;
|
||||||
}
|
}
|
||||||
tracing::info!("Joining room: {:?}", room);
|
tracing::info!("Joining room: {:?}", room);
|
||||||
if let Err(e) = join_room(socket.id, room.clone(), &pool).await {
|
let room_error = join_room(socket.id, room.clone(), &pool).await;
|
||||||
tracing::error!("{:?}", e);
|
if let Err(e) = &room_error {
|
||||||
return;
|
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.leave_all().unwrap();
|
||||||
socket.join(room.clone()).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;
|
return;
|
||||||
}
|
}
|
||||||
let ack_stream = socket
|
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 {
|
socket.on_disconnect(|socket: SocketRef, pool: State<PgPool>| async move {
|
||||||
tracing::info!("Disconnecting: {:?}", socket.id);
|
tracing::info!("Disconnecting: {:?}", socket.id);
|
||||||
socket.leave_all().unwrap();
|
leave_and_inform(&socket, &pool).await;
|
||||||
if let Err(e) = disconnect(socket.id, &pool).await {
|
|
||||||
tracing::error!("{:?}", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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