Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
8a6fb7ccdb
|
|||
|
bc4bf2c696 | ||
|
2b19134d88 | ||
|
2bb5efd9bb | ||
|
5fee374ac9 | ||
|
cd1a156d3e | ||
|
1d61f8fc32 | ||
|
9e86d40ca8 | ||
|
cec5d58937 | ||
|
14e0b47596 | ||
|
a7ae0ea4ff | ||
|
3db83c03fd | ||
|
0b5f513520 | ||
|
e285fa4801 | ||
|
db4a58c3e6 | ||
|
a9ef92721f | ||
|
8a1b9bf603 | ||
|
1916ad332b | ||
|
1c4663d753 | ||
|
f5f3f29595 | ||
|
36d2bcaf01 | ||
|
26e3d3db20 | ||
|
e49e5e086b | ||
|
3e5fdf4615 | ||
|
6262462323 | ||
|
52c7b25393 | ||
|
103ac813f9 | ||
|
f1bffbe56a | ||
|
b71a25165c |
50
.github/workflows/cd-backend.yml
vendored
Normal file
50
.github/workflows/cd-backend.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Deploy backend
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker-azure:
|
||||
runs-on: ubuntu-latest
|
||||
environment: battleship
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
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 }}
|
||||
|
54
.github/workflows/cd-frontend.yml
vendored
Normal file
54
.github/workflows/cd-frontend.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Deploy frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
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: build
|
||||
working-directory: app
|
||||
env:
|
||||
BASE_PATH: '/${{ github.event.repository.name }}'
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# this should match the `pages` option in your adapter-static options
|
||||
path: 'app/build/'
|
||||
|
||||
deploy-frontend:
|
||||
needs: build_frontend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
30
.github/workflows/cd.yml
vendored
30
.github/workflows/cd.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
environment: battleship
|
||||
steps:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
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
|
24
.github/workflows/ci.yml
vendored
24
.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
|
||||
- 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
|
||||
|
@@ -12,7 +12,8 @@
|
||||
"Enum": [
|
||||
"waiting",
|
||||
"p1turn",
|
||||
"p2turn"
|
||||
"p2turn",
|
||||
"gameover"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,8 @@
|
||||
"Enum": [
|
||||
"waiting",
|
||||
"p1turn",
|
||||
"p2turn"
|
||||
"p2turn",
|
||||
"gameover"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@
|
||||
"Enum": [
|
||||
"waiting",
|
||||
"p1turn",
|
||||
"p2turn"
|
||||
"p2turn",
|
||||
"gameover"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "battleship"
|
||||
version = "0.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dotenv",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "battleship"
|
||||
version = "0.1.0"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
28
Dockerfile
28
Dockerfile
@@ -45,25 +45,21 @@ 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 scratch AS final
|
||||
|
||||
FROM alpine AS final
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
# ARG UID=10001
|
||||
# RUN adduser \
|
||||
# --disabled-password \
|
||||
# --gecos "" \
|
||||
# --home "/nonexistent" \
|
||||
# --shell "/sbin/nologin" \
|
||||
# --no-create-home \
|
||||
# --uid "${UID}" \
|
||||
# appuser
|
||||
# USER appuser
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/sbin/nologin" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
USER appuser
|
||||
|
||||
# Copy the executable from the "build" stage.
|
||||
COPY --from=build /bin/server /bin/
|
||||
|
@@ -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`.
|
160
app/package-lock.json
generated
160
app/package-lock.json
generated
@@ -31,7 +31,7 @@
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^5.4.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -755,224 +755,208 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
|
||||
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
|
||||
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3507,11 +3491,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
|
||||
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
@@ -3523,22 +3506,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.21.3",
|
||||
"@rollup/rollup-android-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-x64": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.21.3",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.21.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.21.3",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||
"@rollup/rollup-android-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -4255,11 +4238,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.4.tgz",
|
||||
"integrity": "sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==",
|
||||
"version": "5.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
|
||||
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
@@ -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": {
|
||||
@@ -31,7 +31,7 @@
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^5.4.7"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
@@ -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,6 +44,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
<div class="text-center text-lg text-primary-content">OR</div>
|
||||
{#if !roomCode}
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -56,5 +60,15 @@
|
||||
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>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
export type Phase = 'placement' | 'waiting' | 'selfturn' | 'otherturn';
|
||||
export type Phase = 'placement' | 'waiting' | 'selfturn' | 'otherturn' | 'gameover';
|
||||
export type CellType = 'e' | 's' | 'h' | 'm'; // empty, ship, hit, miss
|
||||
|
||||
export class State {
|
||||
@@ -12,12 +12,11 @@ export class State {
|
||||
turn = $state(-1); // -1 not my turn, 0 might be, 1 is
|
||||
socket: Socket;
|
||||
|
||||
constructor(hostname: string, newSession: boolean = true) {
|
||||
// let session = sessionStorage.getItem('session');
|
||||
|
||||
this.socket = io(`ws://${hostname}:3000/`, {
|
||||
constructor() {
|
||||
const url = import.meta.env.DEV ? 'ws://localhost:3000' : 'wss://battleship.icyground-d91964e0.centralindia.azurecontainerapps.io';
|
||||
this.socket = io(url, {
|
||||
transports: ['websocket'],
|
||||
auth: { session: !newSession ? sessionStorage.getItem('session') : null }
|
||||
auth: { session: sessionStorage.getItem('session') }
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
@@ -30,17 +29,21 @@ export class State {
|
||||
this.room = room;
|
||||
this.users = users;
|
||||
});
|
||||
|
||||
this.socket.on('upload', (_, callback) => {
|
||||
if (this.phase == 'gameover') {
|
||||
this.playerBoard.randomize();
|
||||
this.opponentBoard = new Board(true);
|
||||
this.phase = 'waiting';
|
||||
}
|
||||
callback(this.playerBoard.board);
|
||||
});
|
||||
this.socket.on('turnover', (id) => {
|
||||
this.turn = (id == this.socket.id) ? 1 : -1;
|
||||
this.phase = this.turn ? 'selfturn' : 'otherturn';
|
||||
});
|
||||
this.socket.on('attacked', ({ by, at, hit, sunk }) => {
|
||||
this.socket.on('attacked', ({ by, at, hit, sunk, game_over }) => {
|
||||
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 {
|
||||
@@ -48,7 +51,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')
|
||||
@@ -71,13 +74,19 @@ export class State {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (game_over) {
|
||||
this.phase = 'gameover';
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('restore', ({ turn, player, opponent }: { turn: boolean, player: string[], opponent: string[] }) => {
|
||||
this.socket.on('restore', ({ turn, player, opponent, gameover }: { turn: boolean, player: string[], opponent: string[], gameover: boolean }) => {
|
||||
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));
|
||||
if (gameover) {
|
||||
this.phase = 'gameover';
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,13 +104,17 @@ export class State {
|
||||
|
||||
joinRoom(code: string) {
|
||||
code = code.toUpperCase();
|
||||
if (code.length != 4 || code == this.room) return;
|
||||
if (code.length != 4 || code == this.room && this.phase !== 'gameover') return;
|
||||
this.socket.emit('join', code);
|
||||
}
|
||||
|
||||
hasNotStarted() {
|
||||
return this.phase == 'placement' || this.phase == 'waiting';
|
||||
}
|
||||
|
||||
playAgain() {
|
||||
this.joinRoom(this.room);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +147,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;
|
||||
}
|
||||
|
@@ -5,8 +5,12 @@
|
||||
import { State } from '$lib/state.svelte';
|
||||
import { Users } from 'lucide-svelte';
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
let gameState = new State(hostname);
|
||||
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">
|
||||
@@ -39,13 +43,7 @@
|
||||
<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, true);
|
||||
}}>Leave</button
|
||||
>
|
||||
<button class="btn btn-error text-xl" onclick={leaveRoom}>Leave</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -68,12 +66,33 @@
|
||||
board={gameState.opponentBoard}
|
||||
callback={(i, j) => gameState.attack(i, j)}
|
||||
/>
|
||||
|
||||
{#if gameState.phase === 'gameover'}
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 pointer-events-none"
|
||||
>
|
||||
<div class="p-6 bg-base-300 rounded-xl text-center">
|
||||
<h3 class="text-2xl font-semibold">Game Over</h3>
|
||||
<p class="text-lg">
|
||||
{gameState.turn >= 0 ? 'You win!' : 'You lose!'}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary mt-4 pointer-events-auto"
|
||||
onclick={() => gameState.playAgain()}
|
||||
>
|
||||
Play Again
|
||||
</button>
|
||||
<button class="btn btn-secondary mt-4 ml-4 pointer-events-auto" onclick={leaveRoom}>Leave</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if gameState.hasNotStarted()}
|
||||
<Join
|
||||
class="absolute top-[24px] left-[15px] w-[calc(100%-15px)] h-[calc(100%-24px)]"
|
||||
roomCode={gameState.room}
|
||||
createRoom={() => gameState.createRoom()}
|
||||
joinRoom={(code) => gameState.joinRoom(code)}
|
||||
{leaveRoom}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@@ -5,12 +5,12 @@ export default {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui'),
|
||||
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
|
||||
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
|
||||
|
33
compose.yaml
33
compose.yaml
@@ -1,35 +1,14 @@
|
||||
# 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
|
||||
image: docker.registry.computerliebe.org/battleship
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:${DATABASE_PASSWORD}@db:5432/${DATABASE_NAME}
|
||||
ports:
|
||||
- 3000:3000
|
||||
# ports:
|
||||
# - 3000:3000
|
||||
depends_on:
|
||||
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,8 +18,10 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
expose:
|
||||
- 5432
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# expose:
|
||||
# - 5432
|
||||
healthcheck:
|
||||
test: [ "CMD", "pg_isready" ]
|
||||
interval: 10s
|
||||
|
34
compose.yaml.template
Normal file
34
compose.yaml.template
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
target: final
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:${DATABASE_PASSWORD}@db:5432/${DATABASE_NAME}
|
||||
ports:
|
||||
- 3000:3000
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
user: postgres
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
ports:
|
||||
- 5432:5432
|
||||
expose:
|
||||
- 5432
|
||||
healthcheck:
|
||||
test: [ "CMD", "pg_isready" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
db-data:
|
||||
|
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,5 @@
|
||||
-- DROP OWNED BY CURRENT_USER CASCADE;
|
||||
CREATE TYPE STAT AS ENUM ('waiting', 'p1turn', 'p2turn');
|
||||
CREATE TYPE STAT AS ENUM ('waiting', 'p1turn', 'p2turn', 'gameover');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id CHAR(16) PRIMARY KEY,
|
||||
|
@@ -134,6 +134,10 @@ impl Board {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_game_over(&self) -> bool {
|
||||
!self.iter().any(|row| row.iter().any(|&cell| cell == 's'))
|
||||
}
|
||||
|
||||
// fn validate_syntax(&self) -> bool {
|
||||
// self
|
||||
// .iter()
|
||||
|
71
src/game.rs
71
src/game.rs
@@ -15,6 +15,8 @@ pub enum Error {
|
||||
RoomFull(Option<String>),
|
||||
#[error("Room not full")]
|
||||
RoomNotFull,
|
||||
#[error("GameOver room joined")]
|
||||
GameOverRoom,
|
||||
#[error("Already in room")]
|
||||
AlreadyInRoom,
|
||||
#[error("Not in room")]
|
||||
@@ -33,6 +35,7 @@ pub enum Status {
|
||||
Waiting,
|
||||
P1Turn,
|
||||
P2Turn,
|
||||
GameOver,
|
||||
}
|
||||
|
||||
pub async fn room_if_player_exists(sid: &str, pool: &sqlx::PgPool) -> Result<Option<String>> {
|
||||
@@ -79,7 +82,7 @@ pub async fn add_room(sid: Sid, pool: &sqlx::PgPool) -> Result<String> {
|
||||
pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
|
||||
let code = code.to_uppercase();
|
||||
let room = sqlx::query!(
|
||||
r#"SELECT player1_id, player2_id FROM rooms WHERE code = $1"#,
|
||||
r#"SELECT player1_id, player2_id, stat AS "stat: Status" FROM rooms WHERE code = $1"#,
|
||||
code
|
||||
)
|
||||
.fetch_one(pool)
|
||||
@@ -87,26 +90,40 @@ pub async fn join_room(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()
|
||||
|
||||
let sid = sid.as_str();
|
||||
|
||||
// if player is already in room
|
||||
if [room.player1_id.as_ref(), room.player2_id.as_ref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|x| x == sid)
|
||||
{
|
||||
// if game was over, set status to waiting and return
|
||||
if room.stat == Status::GameOver {
|
||||
sqlx::query!(
|
||||
r"UPDATE rooms SET stat = $1 WHERE code = $2",
|
||||
Status::Waiting as Status,
|
||||
code
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
return Err(Error::AlreadyInRoom);
|
||||
}
|
||||
|
||||
if room.stat == Status::GameOver {
|
||||
return Err(Error::GameOverRoom);
|
||||
}
|
||||
|
||||
if let (Some(p1), Some(p2)) = (room.player1_id.as_ref(), room.player2_id.as_ref()) {
|
||||
if in_delete_sid(p1, pool).await? {
|
||||
update_sid(p1, sid, 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?;
|
||||
// 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 id == sid {
|
||||
return Err(Error::AlreadyInRoom);
|
||||
}
|
||||
}
|
||||
if let Some(id) = room.player2_id.as_ref() {
|
||||
if id == sid {
|
||||
return Err(Error::AlreadyInRoom);
|
||||
}
|
||||
}
|
||||
delete_sid(sid, pool).await?;
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
@@ -158,7 +175,7 @@ pub async fn get_game_state(
|
||||
sid: &str,
|
||||
room: &str,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<(bool, Vec<String>, Vec<String>)> {
|
||||
) -> Result<(bool, Vec<String>, Vec<String>, bool)> {
|
||||
let room_details = sqlx::query!(
|
||||
r#"SELECT player1_id, player2_id, stat AS "stat: Status" FROM rooms WHERE code = $1"#,
|
||||
room
|
||||
@@ -188,7 +205,6 @@ pub async fn get_game_state(
|
||||
.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"#,
|
||||
@@ -200,6 +216,11 @@ pub async fn get_game_state(
|
||||
.board
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let game_over = player_board.is_game_over() || opponent_board.is_game_over();
|
||||
|
||||
let player_board: Vec<String> = player_board.mark_redundant().into();
|
||||
|
||||
let opponent_board: Vec<String> = opponent_board.mark_redundant().into();
|
||||
let opponent_board: Vec<String> = opponent_board
|
||||
.into_iter()
|
||||
@@ -210,7 +231,7 @@ pub async fn get_game_state(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((turn, player_board, opponent_board))
|
||||
Ok((turn, player_board, opponent_board, game_over))
|
||||
}
|
||||
|
||||
pub async fn start(sid: Sid, code: String, pool: &sqlx::PgPool) -> Result<()> {
|
||||
@@ -247,7 +268,7 @@ pub async fn attack(
|
||||
sid: Sid,
|
||||
(i, j): (usize, usize),
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<(bool, Option<[(usize, usize); 2]>)> {
|
||||
) -> Result<(bool, Option<[(usize, usize); 2]>, bool)> {
|
||||
let player = sqlx::query!(r"SELECT room_code FROM players WHERE id = $1", sid.as_str())
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
@@ -302,9 +323,23 @@ pub async fn attack(
|
||||
.execute(&mut *txn)
|
||||
.await?;
|
||||
}
|
||||
let game_over = board.is_game_over();
|
||||
if game_over {
|
||||
sqlx::query!(
|
||||
r#"UPDATE rooms SET stat = $1 WHERE code = $2"#,
|
||||
Status::GameOver as Status,
|
||||
player.room_code
|
||||
)
|
||||
.execute(&mut *txn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok((hit, if hit { board.has_sunk((i, j)) } else { None }))
|
||||
Ok((
|
||||
hit,
|
||||
if hit { board.has_sunk((i, j)) } else { None },
|
||||
game_over,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_sid(oldsid: &str, newsid: &str, pool: &sqlx::PgPool) -> Result<()> {
|
||||
|
20
src/main.rs
20
src/main.rs
@@ -1,5 +1,6 @@
|
||||
mod board;
|
||||
mod game;
|
||||
|
||||
use axum::Router;
|
||||
use board::Board;
|
||||
use dotenv::dotenv;
|
||||
@@ -16,28 +17,25 @@ use socketioxide::{
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::subscriber::set_global_default(
|
||||
FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::ERROR)
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.finish(),
|
||||
)?;
|
||||
let _ = dotenv();
|
||||
let url = std::env::var("DATABASE_URL")?;
|
||||
let url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let pool = sqlx::postgres::PgPool::connect(&url).await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
sqlx::query("DELETE FROM players").execute(&pool).await?;
|
||||
let (layer, io) = SocketIo::builder().with_state(pool).build_layer();
|
||||
|
||||
io.ns("/", on_connect);
|
||||
let app = Router::new()
|
||||
// .route("/", post(game::create_board_route))
|
||||
.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()?);
|
||||
@@ -62,7 +60,7 @@ async fn on_connect(socket: SocketRef, Data(auth): Data<AuthPayload>, pool: Stat
|
||||
socket
|
||||
.emit(
|
||||
"restore",
|
||||
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2}),
|
||||
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2, "gameover": data.3}),
|
||||
)
|
||||
.unwrap();
|
||||
socket.join(room.clone()).unwrap();
|
||||
@@ -120,7 +118,7 @@ async fn on_connect(socket: SocketRef, Data(auth): Data<AuthPayload>, pool: Stat
|
||||
socket
|
||||
.emit(
|
||||
"restore",
|
||||
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2}),
|
||||
serde_json::json!({"turn": data.0, "player": data.1, "opponent": data.2, "gameover": data.3}),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
@@ -172,7 +170,7 @@ async fn on_connect(socket: SocketRef, Data(auth): Data<AuthPayload>, pool: Stat
|
||||
socket.on(
|
||||
"attack",
|
||||
|socket: SocketRef, Data::<[usize; 2]>([i, j]), pool: State<PgPool>| async move {
|
||||
let (hit, sunk) = match attack(socket.id, (i, j), &pool).await {
|
||||
let (hit, sunk, game_over) = match attack(socket.id, (i, j), &pool).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?}", e);
|
||||
@@ -184,7 +182,7 @@ async fn on_connect(socket: SocketRef, Data(auth): Data<AuthPayload>, pool: Stat
|
||||
.within(socket.rooms().unwrap().first().unwrap().clone())
|
||||
.emit(
|
||||
"attacked",
|
||||
serde_json::json!({"by": socket.id.as_str(), "at": [i, j], "hit": hit, "sunk": sunk}),
|
||||
serde_json::json!({"by": socket.id.as_str(), "at": [i, j], "hit": hit, "sunk": sunk, "game_over": game_over}),
|
||||
)
|
||||
.unwrap();
|
||||
},
|
||||
|
Reference in New Issue
Block a user