Compare commits

..

3 commits

Author SHA1 Message Date
428ed81e5b
Chore: format 2023-01-05 19:46:46 +01:00
c517ff3c20
Docs: update readme 2023-01-05 19:46:36 +01:00
d0857233f4
Chore: make create room page prettier 2023-01-05 19:38:09 +01:00
4 changed files with 63 additions and 19 deletions

View file

@ -1,3 +1,27 @@
# whose-turn-is-it <h1 align="center">
Whose turn is it?
</h1>
Simple webapp for playing games. <p align="center">
<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)

View file

@ -38,12 +38,24 @@
border-top: var(--border-width) solid var(--accordion-border-color); border-top: var(--border-width) solid var(--accordion-border-color);
} }
:is(button, input[type="submit"], input[type="button"], [role="button"]).outline.invalid, input[type="reset"].outline { :is(
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(button, input[type="submit"], input[type="button"], [role="button"]).invalid, input[type="reset"] { :is(
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;

View file

@ -37,8 +37,7 @@
</button> </button>
<label for="skip_switch" class="mb"> <label for="skip_switch" class="mb">
<input x-model="$store.remoteState.skipMe" <input x-model="$store.remoteState.skipMe" type="checkbox" id="skip_switch" role="switch" />
type="checkbox" id="skip_switch" role="switch" />
Skip me Skip me
</label> </label>
@ -97,9 +96,19 @@
</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">
<input type="text" x-model="formData.name" placeholder="Name" /> <label for="joinForm_name">Name:</label>
<input type="text" x-model="formData.room" placeholder="Room" /> <input id="joinForm_name" type="text" x-model="formData.name" placeholder="Name" />
<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>

View file

@ -30,13 +30,12 @@ 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) {
else if(myTurn && this.skipMe) { this.giveTurnToNextPlayer();
this.giveTurnToNextPlayer()
} else { } else {
this.isMyTurn = false; this.isMyTurn = false;
} }
@ -84,7 +83,7 @@ document.addEventListener("alpine:init", () => {
iterations: 5000, iterations: 5000,
}); });
const topicPrefix = `im.dorian.whos-turn-is-it.${btoa( const topicPrefix = `im.dorian.whose-turn-is-it.${btoa(
Alpine.store("localState").room Alpine.store("localState").room
)}`; )}`;
this._gameStateTopic = CryptoJS.SHA256( this._gameStateTopic = CryptoJS.SHA256(
@ -114,7 +113,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 * 5); }, 1000 * 4);
that._client.subscribe(that._gameStateTopic); that._client.subscribe(that._gameStateTopic);
that._client.subscribe(that._currentPlayerTopic); that._client.subscribe(that._currentPlayerTopic);