Compare commits
No commits in common. "428ed81e5be74a35d522f4a6112a67efd6d3594c" and "6d5f4047e28a0e7c936354612e13b62d9068ca83" have entirely different histories.
428ed81e5b
...
6d5f4047e2
4 changed files with 19 additions and 63 deletions
28
README.md
28
README.md
|
@ -1,27 +1,3 @@
|
||||||
<h1 align="center">
|
# whose-turn-is-it
|
||||||
Whose turn is it?
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
Simple webapp for playing games.
|
||||||
<a href="https://www.gnu.org/licenses/agpl-3.0">
|
|
||||||
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Simple webapp to keep track of who is currently playing and how much time they have left. Originally designed for [Rummikub](https://de.wikipedia.org/wiki/Rummikub).
|
|
||||||
Please note: this is by no means perfect and just meant to be a simple solution to a simple problem :)
|
|
||||||
|
|
||||||
# Featues
|
|
||||||
- Create an independent room
|
|
||||||
- Play with multiple players across multiple devices
|
|
||||||
- Decentralized
|
|
||||||
- End-to-end encrypted
|
|
||||||
|
|
||||||
# How it works
|
|
||||||
The browsers of all players connect to a common MQTT server and communicate through that server. An AES Key is derived from the room name, which is then used to encrypt all communication.
|
|
||||||
|
|
||||||
# Technologies
|
|
||||||
- [MQTT.js](https://github.com/mqttjs/MQTT.js)
|
|
||||||
- [Alpine.js](https://alpinejs.dev/)
|
|
||||||
- [Pico.css](https://picocss.com/)
|
|
||||||
- [crypto-js](https://github.com/brix/crypto-js)
|
|
16
index.css
16
index.css
|
@ -38,24 +38,12 @@
|
||||||
border-top: var(--border-width) solid var(--accordion-border-color);
|
border-top: var(--border-width) solid var(--accordion-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(
|
:is(button, input[type="submit"], input[type="button"], [role="button"]).outline.invalid, input[type="reset"].outline {
|
||||||
button,
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="button"],
|
|
||||||
[role="button"]
|
|
||||||
).outline.invalid,
|
|
||||||
input[type="reset"].outline {
|
|
||||||
--color: var(--form-element-invalid-border-color);
|
--color: var(--form-element-invalid-border-color);
|
||||||
--background-color: transparent;
|
--background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(
|
:is(button, input[type="submit"], input[type="button"], [role="button"]).invalid, input[type="reset"] {
|
||||||
button,
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="button"],
|
|
||||||
[role="button"]
|
|
||||||
).invalid,
|
|
||||||
input[type="reset"] {
|
|
||||||
--background-color: var(--form-element-invalid-border-color);
|
--background-color: var(--form-element-invalid-border-color);
|
||||||
--border-color: var(--form-element-invalid-border-color);
|
--border-color: var(--form-element-invalid-border-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
17
index.html
17
index.html
|
@ -37,7 +37,8 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label for="skip_switch" class="mb">
|
<label for="skip_switch" class="mb">
|
||||||
<input x-model="$store.remoteState.skipMe" type="checkbox" id="skip_switch" role="switch" />
|
<input x-model="$store.remoteState.skipMe"
|
||||||
|
type="checkbox" id="skip_switch" role="switch" />
|
||||||
Skip me
|
Skip me
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -96,19 +97,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="!$store.localState.room">
|
<div x-show="!$store.localState.room">
|
||||||
<hgroup>
|
|
||||||
<h1>
|
|
||||||
Whose turn is it?
|
|
||||||
</h1>
|
|
||||||
<h2>Please create or join a room</h2>
|
|
||||||
</hgroup>
|
|
||||||
<form x-data="JoinForm()" @submit.prevent="submitForm">
|
<form x-data="JoinForm()" @submit.prevent="submitForm">
|
||||||
<label for="joinForm_name">Name:</label>
|
<input type="text" x-model="formData.name" placeholder="Name" />
|
||||||
<input id="joinForm_name" type="text" x-model="formData.name" placeholder="Name" />
|
<input type="text" x-model="formData.room" placeholder="Room" />
|
||||||
<small>How others will see you</small>
|
|
||||||
<label for="joinForm_room">Room:</label>
|
|
||||||
<input id="joinForm_room" type="text" x-model="formData.room" placeholder="Room" />
|
|
||||||
<small>Make sure, this is exactly the same for all players</small>
|
|
||||||
<button type="submit">Join</button>
|
<button type="submit">Join</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,12 +30,13 @@ document.addEventListener("alpine:init", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Alpine.effect(() => {
|
Alpine.effect(() => {
|
||||||
const myTurn = this.currentPlayer == Alpine.store("localState").id;
|
const myTurn = this.currentPlayer == Alpine.store("localState").id
|
||||||
if (myTurn && !this.skipMe) {
|
if (myTurn && !this.skipMe) {
|
||||||
this.isMyTurn = true;
|
this.isMyTurn = true;
|
||||||
Alpine.store("audio").playDing();
|
Alpine.store("audio").playDing();
|
||||||
} else if (myTurn && this.skipMe) {
|
}
|
||||||
this.giveTurnToNextPlayer();
|
else if(myTurn && this.skipMe) {
|
||||||
|
this.giveTurnToNextPlayer()
|
||||||
} else {
|
} else {
|
||||||
this.isMyTurn = false;
|
this.isMyTurn = false;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +84,7 @@ document.addEventListener("alpine:init", () => {
|
||||||
iterations: 5000,
|
iterations: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const topicPrefix = `im.dorian.whose-turn-is-it.${btoa(
|
const topicPrefix = `im.dorian.whos-turn-is-it.${btoa(
|
||||||
Alpine.store("localState").room
|
Alpine.store("localState").room
|
||||||
)}`;
|
)}`;
|
||||||
this._gameStateTopic = CryptoJS.SHA256(
|
this._gameStateTopic = CryptoJS.SHA256(
|
||||||
|
@ -113,7 +114,7 @@ document.addEventListener("alpine:init", () => {
|
||||||
// reset game if not connected after 5 seconds
|
// reset game if not connected after 5 seconds
|
||||||
that.clear();
|
that.clear();
|
||||||
}
|
}
|
||||||
}, 1000 * 4);
|
}, 1000 * 5);
|
||||||
|
|
||||||
that._client.subscribe(that._gameStateTopic);
|
that._client.subscribe(that._gameStateTopic);
|
||||||
that._client.subscribe(that._currentPlayerTopic);
|
that._client.subscribe(that._currentPlayerTopic);
|
||||||
|
|
Loading…
Reference in a new issue