Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1d61f8fc32 | ||
|
9e86d40ca8 | ||
|
cec5d58937 | ||
|
14e0b47596 | ||
|
a7ae0ea4ff | ||
|
3db83c03fd | ||
|
0b5f513520 | ||
|
e285fa4801 | ||
|
db4a58c3e6 | ||
|
a9ef92721f | ||
|
8a1b9bf603 | ||
|
1916ad332b | ||
|
1c4663d753 | ||
|
f5f3f29595 | ||
|
36d2bcaf01 | ||
|
26e3d3db20 | ||
|
e49e5e086b | ||
|
3e5fdf4615 |
30
.github/workflows/cd-backend.yml
vendored
30
.github/workflows/cd-backend.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Deploy
|
||||
name: Deploy backend
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,9 +7,12 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
docker-azure:
|
||||
runs-on: ubuntu-latest
|
||||
environment: battleship
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
@@ -25,6 +28,23 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_IMAGE_PATH }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ secrets.DOCKER_IMAGE_PATH }}:${{ github.sha }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_IMAGE_PATH }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_IMAGE_PATH }}:buildcache,mode=max
|
||||
|
||||
-
|
||||
name: Azure login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
-
|
||||
name: Deploy Container
|
||||
uses: azure/container-apps-deploy-action@v1
|
||||
with:
|
||||
registryUrl: docker.io
|
||||
containerAppName: battleship
|
||||
resourceGroup: Battleship
|
||||
imageToDeploy: docker.io/${{ secrets.DOCKER_IMAGE_PATH }}:${{ github.sha }}
|
||||
|
||||
|
14
.github/workflows/cd-frontend.yml
vendored
14
.github/workflows/cd-frontend.yml
vendored
@@ -1,11 +1,13 @@
|
||||
name: Deploy
|
||||
name: Deploy frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
tags:
|
||||
- v**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_site:
|
||||
build_frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -34,8 +36,8 @@ jobs:
|
||||
# this should match the `pages` option in your adapter-static options
|
||||
path: 'app/build/'
|
||||
|
||||
deploy-site:
|
||||
needs: build_site
|
||||
deploy-frontend:
|
||||
needs: build_frontend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
|
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -11,14 +11,30 @@ env:
|
||||
SQLX_OFFLINE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Check
|
||||
- name: cargo-check
|
||||
run: cargo check
|
||||
- name: Clippy
|
||||
- name: cargo-clippy
|
||||
run: cargo clippy
|
||||
- name: Format
|
||||
run: cargo fmt --all --check
|
||||
- name: cargo-fmt
|
||||
run: cargo fmt --all --check
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: 'app/package-lock.json'
|
||||
- name: Install dependencies
|
||||
working-directory: app
|
||||
run: npm install
|
||||
- name: lint
|
||||
working-directory: app
|
||||
run: npm run lint
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "battleship"
|
||||
version = "0.1.0"
|
||||
version = "1.1.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dotenv",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "battleship"
|
||||
version = "0.1.0"
|
||||
version = "1.1.2"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@@ -45,11 +45,7 @@ cp ./target/release/$APP_NAME /bin/server
|
||||
# runtime dependencies for the application. This often uses a different base
|
||||
# image from the build stage where the necessary files are copied from the build
|
||||
# stage.
|
||||
#
|
||||
# The example below uses the alpine image as the foundation for running the app.
|
||||
# By specifying the "3.18" tag, it will use version 3.18 of alpine. If
|
||||
# reproducability is important, consider using a digest
|
||||
# (e.g., alpine@sha256:664888ac9cfd28068e062c991ebcff4b4c7307dc8dd4df9e728bedde5c449d91).
|
||||
|
||||
FROM alpine AS final
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
|
@@ -1,22 +0,0 @@
|
||||
### Building and running your application
|
||||
|
||||
When you're ready, start your application by running:
|
||||
`docker compose up --build`.
|
||||
|
||||
Your application will be available at http://localhost:3000.
|
||||
|
||||
### Deploying your application to the cloud
|
||||
|
||||
First, build your image, e.g.: `docker build -t myapp .`.
|
||||
If your cloud uses a different CPU architecture than your development
|
||||
machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
|
||||
you'll want to build the image for that platform, e.g.:
|
||||
`docker build --platform=linux/amd64 -t myapp .`.
|
||||
|
||||
Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
|
||||
|
||||
Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
|
||||
docs for more detail on building and pushing.
|
||||
|
||||
### References
|
||||
* [Docker's Rust guide](https://docs.docker.com/language/rust/)
|
25
README.md
Normal file
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Battleship Online
|
||||
|
||||
Play the classic game of Battleship against your friends online! Each player will take turns guessing the location of the other player's ships. The first player to sink all of the other player's ships wins!
|
||||
|
||||
Dark mode | Light mode
|
||||
:-------------------------:|:-------------------------:
|
||||
 | 
|
||||
|
||||
## Development Guide
|
||||
|
||||
The client is built using SvelteKit (static site) and the server uses Axum framework (Rust). The client and server communicate using Socket.io (WebSockets). PostgreSQL is used as a database to store the game state.
|
||||
|
||||
The client can be started using `npm run dev` inside `app` directory.
|
||||
|
||||
The server and the database services are containerized. Just run `docker compose up` to start the server and database services if you are working on the frontend.
|
||||
Make sure to make a `.env` file with these parameters:
|
||||
```
|
||||
DATABASE_PASSWORD=db_password
|
||||
DATABASE_NAME=db_name
|
||||
DATABASE_URL=postgres://postgres:db_password@localhost:5432/db_name
|
||||
```
|
||||
|
||||
If you are working on the server, you can run `cargo watch -i app -x run` to automatically restart the server when the source code changes, and `docker compose up -d db` to start the database service in the background.
|
||||
|
||||
SQLx is used as the database driver for Rust. The driver automatically tests the SQL query macros at compile time. This can fail the rust-analyzer or `cargo build` if the database isn't setup/running. You can run `docker compose up db` to start the database service. To disable this check altogether, set the `SQLX_OFFLINE` environment variable to `true`.
|
@@ -8,7 +8,7 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,4 +38,4 @@
|
||||
"lucide-svelte": "^0.441.0",
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,11 +7,13 @@
|
||||
class: className = '',
|
||||
roomCode,
|
||||
createRoom,
|
||||
joinRoom
|
||||
joinRoom,
|
||||
leaveRoom
|
||||
}: {
|
||||
roomCode: string;
|
||||
createRoom: () => void;
|
||||
joinRoom: (code: string) => void;
|
||||
leaveRoom: () => void;
|
||||
class: string;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -21,6 +23,7 @@
|
||||
>
|
||||
<div class="space-y-4 max-w-[70%]">
|
||||
{#if roomCode}
|
||||
<div class="text-center text-lg text-primary-content">Share this room code</div>
|
||||
<div class="space-x-2 flex flex-row justify-center items-center">
|
||||
<div
|
||||
class="text-3xl font-bold tracking-widest text-secondary-content font-mono bg-secondary py-3 rounded-full px-12"
|
||||
@@ -41,20 +44,31 @@
|
||||
</button>
|
||||
{/if}
|
||||
<div class="text-center text-lg text-primary-content">OR</div>
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter code"
|
||||
maxlength="4"
|
||||
bind:value={joinCode}
|
||||
class="input input-bordered input-primary uppercase tracking-widest placeholder-primary text-neutral text-center font-bold text-xl lg:text-3xl w-full glass"
|
||||
/>
|
||||
<button
|
||||
onclick={() => joinRoom(joinCode)}
|
||||
class="w-full btn btn-outline btn-neutral text-neutral hover:border-neutral hover:bg-transparent text-xl"
|
||||
>
|
||||
Join Room
|
||||
</button>
|
||||
</div>
|
||||
{#if !roomCode}
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter code"
|
||||
maxlength="4"
|
||||
bind:value={joinCode}
|
||||
class="input input-bordered input-primary uppercase tracking-widest placeholder-primary text-neutral text-center font-bold text-xl lg:text-3xl w-full glass"
|
||||
/>
|
||||
<button
|
||||
onclick={() => joinRoom(joinCode)}
|
||||
class="w-full btn btn-outline btn-neutral text-neutral hover:border-neutral hover:bg-transparent text-xl"
|
||||
>
|
||||
Join Room
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-x-2 flex flex-row justify-center items-center">
|
||||
<button
|
||||
class="w-full btn btn-outline btn-neutral text-neutral hover:border-neutral hover:bg-transparent text-xl"
|
||||
onclick={leaveRoom}
|
||||
>
|
||||
Leave room
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,7 +13,8 @@ export class State {
|
||||
socket: Socket;
|
||||
|
||||
constructor() {
|
||||
this.socket = io(`wss://battleship.icyground-d91964e0.centralindia.azurecontainerapps.io`, {
|
||||
const url = import.meta.env.DEV ? 'ws://localhost:3000' : 'wss://battleship.icyground-d91964e0.centralindia.azurecontainerapps.io';
|
||||
this.socket = io(url, {
|
||||
transports: ['websocket'],
|
||||
auth: { session: sessionStorage.getItem('session') }
|
||||
});
|
||||
@@ -38,7 +39,7 @@ export class State {
|
||||
});
|
||||
this.socket.on('attacked', ({ by, at, hit, sunk }) => {
|
||||
const [i, j]: [number, number] = at;
|
||||
let board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
|
||||
const board = by == this.socket.id ? this.opponentBoard : this.playerBoard;
|
||||
if (by == this.socket.id) {
|
||||
this.turn = (hit) ? 1 : -1;
|
||||
} else {
|
||||
@@ -46,7 +47,7 @@ export class State {
|
||||
}
|
||||
if (hit) {
|
||||
board.board[i][j] = 'h';
|
||||
for (let [x, y] of [[-1, -1], [1, 1], [1, -1], [-1, 1]]) {
|
||||
for (const [x, y] of [[-1, -1], [1, 1], [1, -1], [-1, 1]]) {
|
||||
const [tx, ty] = [i + x, j + y];
|
||||
if (tx < 0 || tx >= 10 || ty < 0 || ty >= 10) continue;
|
||||
if (board.board[tx][ty] == 'e')
|
||||
@@ -132,7 +133,7 @@ export class Board {
|
||||
isOverlapping(x: number, y: number, length: number, dir: number): boolean {
|
||||
for (let i = -1; i < 2; i++) {
|
||||
for (let j = -1; j < length + 1; j++) {
|
||||
let [tx, ty] = [x + (dir ? i : j), y + (dir ? j : i)];
|
||||
const [tx, ty] = [x + (dir ? i : j), y + (dir ? j : i)];
|
||||
if (tx < 0 || tx >= 10 || ty < 0 || ty >= 10) continue;
|
||||
if (this.board[tx][ty] != 'e') return true;
|
||||
}
|
||||
|
@@ -6,6 +6,11 @@
|
||||
import { Users } from 'lucide-svelte';
|
||||
|
||||
let gameState = new State();
|
||||
|
||||
function leaveRoom() {
|
||||
gameState.socket.emit('leave');
|
||||
gameState = new State();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-base-300 py-8 px-4 sm:px-6 lg:px-8">
|
||||
@@ -40,10 +45,7 @@
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-error text-xl"
|
||||
onclick={() => {
|
||||
gameState.socket.emit('leave');
|
||||
gameState = new State();
|
||||
}}>Leave</button
|
||||
onclick={leaveRoom}>Leave</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -73,6 +75,7 @@
|
||||
roomCode={gameState.room}
|
||||
createRoom={() => gameState.createRoom()}
|
||||
joinRoom={(code) => gameState.joinRoom(code)}
|
||||
leaveRoom={leaveRoom}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@@ -1,22 +1,22 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui'),
|
||||
],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui'), // eslint-disable-line
|
||||
],
|
||||
|
||||
daisyui: {
|
||||
themes: true, // 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
|
||||
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
|
||||
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
|
||||
themeRoot: ":root", // The element that receives theme color CSS variables
|
||||
},
|
||||
daisyui: {
|
||||
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
|
||||
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
|
||||
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
|
||||
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
|
||||
themeRoot: ":root", // The element that receives theme color CSS variables
|
||||
},
|
||||
}
|
||||
|
||||
|
21
compose.yaml
21
compose.yaml
@@ -1,15 +1,5 @@
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Docker Compose reference guide at
|
||||
# https://docs.docker.com/go/compose-spec-reference/
|
||||
|
||||
# Here the instructions define your application as a service called "server".
|
||||
# This service is built from the Dockerfile in the current directory.
|
||||
# You can add other services your application may depend on here, such as a
|
||||
# database or a cache. For examples, see the Awesome Compose repository:
|
||||
# https://github.com/docker/awesome-compose
|
||||
services:
|
||||
server:
|
||||
image: ${DOCKER_IMAGE_PATH}
|
||||
build:
|
||||
context: .
|
||||
target: final
|
||||
@@ -21,15 +11,6 @@ services:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# The commented out section below is an example of how to define a PostgreSQL
|
||||
# database that your application can use. `depends_on` tells Docker Compose to
|
||||
# start the database before your application. The `db-data` volume persists the
|
||||
# database data between container restarts. The `db-password` secret is used
|
||||
# to set the database password. You must create `db/password.txt` and add
|
||||
# a password of your choosing to it before running `docker compose up`.
|
||||
# depends_on:
|
||||
# db:
|
||||
# condition: service_healthy
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
@@ -39,6 +20,8 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
ports:
|
||||
- 5432:5432
|
||||
expose:
|
||||
- 5432
|
||||
healthcheck:
|
||||
|
BIN
demo/1.png
Normal file
BIN
demo/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
BIN
demo/2.png
Normal file
BIN
demo/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
@@ -1,5 +1,6 @@
|
||||
mod board;
|
||||
mod game;
|
||||
|
||||
use axum::Router;
|
||||
use board::Board;
|
||||
use dotenv::dotenv;
|
||||
@@ -16,7 +17,6 @@ use socketioxide::{
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -34,9 +34,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (layer, io) = SocketIo::builder().with_state(pool).build_layer();
|
||||
|
||||
io.ns("/", on_connect);
|
||||
let app = Router::new()
|
||||
.layer(layer)
|
||||
.layer(CorsLayer::very_permissive());
|
||||
|
||||
let app = Router::new().layer(layer);
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:3000").await?;
|
||||
println!("listening on {}", listener.local_addr()?);
|
||||
|
Reference in New Issue
Block a user