whose-turn-is-it/js/remoteState.js

248 lines
6.6 KiB
JavaScript
Raw Normal View History

2022-12-30 13:28:28 +01:00
document.addEventListener("alpine:init", () => {
Alpine.store("remoteState", {
players: [],
2023-01-05 12:42:04 +01:00
interval: null,
2022-12-30 13:28:28 +01:00
currentPlayer: null,
lastPlayerSwitch: null,
connected: false,
isMyTurn: false,
2023-01-05 18:50:55 +01:00
skipMe: false,
2023-01-05 10:22:17 +01:00
2022-12-30 13:28:28 +01:00
_client: null,
2023-01-05 10:22:17 +01:00
_currentPlayerTopic: null,
_gameStateTopic: null,
2023-01-05 12:58:08 +01:00
_lastGameState: null,
2023-01-05 18:40:06 +01:00
_key: null,
_iv: null,
_noSleep: null,
2022-12-30 13:28:28 +01:00
init() {
this._noSleep = new NoSleep();
2022-12-30 13:28:28 +01:00
Alpine.effect(() => {
if (
this.connected &&
Object.keys(this.players).indexOf(Alpine.store("localState").id) ===
-1
2022-12-30 13:28:28 +01:00
) {
2023-01-05 12:42:04 +01:00
this._updatePlayers({
[Alpine.store("localState").id]: Alpine.store("localState").name,
2023-01-05 13:05:28 +01:00
...this.players,
2023-01-05 12:42:04 +01:00
});
2022-12-30 13:28:28 +01:00
}
});
Alpine.effect(() => {
2023-01-05 19:46:46 +01:00
const myTurn = this.currentPlayer == Alpine.store("localState").id;
2023-01-05 18:50:55 +01:00
if (myTurn && !this.skipMe) {
2022-12-30 13:28:28 +01:00
this.isMyTurn = true;
2023-01-05 10:22:17 +01:00
Alpine.store("audio").playDing();
2023-01-05 19:46:46 +01:00
} else if (myTurn && this.skipMe) {
this.giveTurnToNextPlayer();
2022-12-30 13:28:28 +01:00
} else {
this.isMyTurn = false;
}
});
Alpine.effect(() => {
if (
Alpine.store("localState").nextPlayer == null ||
Alpine.store("localState").nextPlayer ==
Alpine.store("localState").id ||
2022-12-30 13:28:28 +01:00
!Object.keys(this.players).includes(
Alpine.store("localState").nextPlayer
)
) {
const players = Object.keys(this.players).sort();
const nextPlayer =
players[
(players.indexOf(Alpine.store("localState").id) + 1) %
players.length
2022-12-30 13:28:28 +01:00
];
Alpine.store("localState").nextPlayer = nextPlayer;
}
});
2023-01-05 12:58:08 +01:00
Alpine.effect(() => {
if (this.connected) {
this.updateInterval(this.interval);
}
});
2022-12-30 13:28:28 +01:00
},
connect() {
2023-01-05 10:22:17 +01:00
const that = this;
2022-12-30 13:28:28 +01:00
const url = "wss://broker.emqx.io:8084/mqtt";
2023-01-05 18:40:06 +01:00
const keySize = 512;
const ivSize = 128;
// derive key from room name
this._key = CryptoJS.PBKDF2(Alpine.store("localState").room, url, {
keySize: keySize / 32,
iterations: 1000,
});
// random iv
this._iv = CryptoJS.PBKDF2(Alpine.store("localState").room, url, {
keySize: ivSize / 32,
iterations: 5000,
});
2023-01-05 19:38:09 +01:00
const topicPrefix = `im.dorian.whose-turn-is-it.${btoa(
2023-01-05 13:05:28 +01:00
Alpine.store("localState").room
)}`;
2023-01-05 18:40:06 +01:00
this._gameStateTopic = CryptoJS.SHA256(
this._encrypt(`${topicPrefix}.gameState`)
).toString();
this._currentPlayerTopic = CryptoJS.SHA256(
this._encrypt(`${topicPrefix}.currentPlayer`)
).toString();
2023-01-05 10:22:17 +01:00
2023-01-05 18:40:06 +01:00
console.log("Connecting to MQTT broker...");
console.log("Game state topic:", this._gameStateTopic);
console.log("Current player topic:", this._currentPlayerTopic);
2022-12-30 13:28:28 +01:00
const options = {
// Clean session
clean: true,
connectTimeout: 4000,
// Authentication
clientId: Alpine.store("localState").id,
};
this._client = mqtt.connect(url, options);
2023-01-05 10:22:17 +01:00
this._client.on("connect", () => {
2023-01-05 12:42:04 +01:00
setTimeout(() => {
2023-01-05 13:05:28 +01:00
if (!that.connected) {
2023-01-05 12:42:04 +01:00
// reset game if not connected after 5 seconds
that.clear();
}
2023-01-05 19:38:09 +01:00
}, 1000 * 4);
2023-01-05 12:42:04 +01:00
that._client.subscribe(that._gameStateTopic);
2023-01-05 10:22:17 +01:00
that._client.subscribe(that._currentPlayerTopic);
2022-12-30 13:28:28 +01:00
});
2023-01-05 10:22:17 +01:00
this._client.on("message", (topic, message) => {
2022-12-30 13:28:28 +01:00
// message is Buffer
2023-01-05 18:40:06 +01:00
message = that._decrypt(message.toString());
const data = JSON.parse(JSON.parse(message));
2022-12-30 13:28:28 +01:00
2023-01-05 12:42:04 +01:00
if (topic === that._gameStateTopic) {
2023-01-05 18:40:06 +01:00
if (data.version !== 1 || !data.players || !data.interval) {
console.log("Invalid game state, resetting...");
that.clear();
return;
}
2023-01-05 10:24:28 +01:00
if (!that.connected) {
that.connected = true;
2022-12-30 13:28:28 +01:00
}
console.log("Received game state:", data);
2023-01-05 12:58:08 +01:00
that._lastGameState = data;
2023-01-05 12:42:04 +01:00
that.players = data.players;
that.interval = data.interval;
2023-01-05 10:22:17 +01:00
} else if (topic === that._currentPlayerTopic) {
2023-01-05 18:40:06 +01:00
if (!data.id || !data.since) {
console.log("Invalid current player, resetting...");
that.clear();
return;
}
if (data.since < that.lastPlayerSwitch) {
return;
}
2023-01-05 10:24:28 +01:00
that.currentPlayer = data.id;
that.lastPlayerSwitch = data.since;
2022-12-29 17:19:36 +01:00
}
2022-12-30 13:28:28 +01:00
});
},
giveTurnToNextPlayer() {
this._noSleep.enable();
2023-01-05 12:42:04 +01:00
this._updateCurrentPlayer(Alpine.store("localState").nextPlayer);
2022-12-30 13:28:28 +01:00
},
disconnect() {
this._client.end(true);
2023-01-05 10:22:17 +01:00
this._client = null;
this.players = [];
this.currentPlayer = null;
this.lastPlayerSwitch = null;
this.connected = false;
this.isMyTurn = false;
this._gameStateTopic = null;
this._currentPlayerTopic = null;
Alpine.store("localState").nextPlayer = null;
2022-12-30 13:28:28 +01:00
},
clear() {
if (!this.interval) {
this.interval = 30;
}
2023-01-05 12:42:04 +01:00
this._updatePlayers({
[Alpine.store("localState").id]: Alpine.store("localState").name,
});
this._updateCurrentPlayer(Alpine.store("localState").id);
},
2023-01-05 12:58:08 +01:00
updateInterval(interval) {
this.interval = interval;
2023-01-05 12:58:08 +01:00
this._updateGameState(this.players, interval);
},
2023-01-05 12:42:04 +01:00
_updatePlayers(players) {
this.players = players;
2023-01-05 12:42:04 +01:00
this._updateGameState(players, this.interval);
},
_updateGameState(players, interval) {
2023-01-05 12:58:08 +01:00
const newGameState = {
version: 1,
players: players,
interval: interval,
2023-01-05 13:05:28 +01:00
};
2023-01-05 12:58:08 +01:00
2023-01-05 13:05:28 +01:00
if (
this._lastGameState &&
JSON.stringify(this._lastGameState) === JSON.stringify(newGameState)
)
return;
2023-01-05 18:40:06 +01:00
this._lastGameState = newGameState;
2023-01-05 18:40:06 +01:00
console.log("Updating game state:", newGameState);
this._client.publish(
this._gameStateTopic,
this._encrypt(JSON.stringify(newGameState)),
{
qos: 1,
retain: true,
}
);
2023-01-05 12:42:04 +01:00
},
_updateCurrentPlayer(id) {
2022-12-30 13:28:28 +01:00
this._client.publish(
2023-01-05 10:22:17 +01:00
this._currentPlayerTopic,
2023-01-05 18:40:06 +01:00
this._encrypt(
JSON.stringify({
id: id,
since: new Date().getTime(),
})
),
2022-12-30 13:28:28 +01:00
{ qos: 1, retain: true }
);
2023-01-05 13:05:28 +01:00
},
2023-01-05 18:40:06 +01:00
_encrypt(data) {
return CryptoJS.AES.encrypt(JSON.stringify(data), this._key, {
iv: this._iv,
}).toString();
},
_decrypt(data) {
const decrypted = CryptoJS.AES.decrypt(data, this._key, { iv: this._iv });
return decrypted.toString(CryptoJS.enc.Utf8);
},
2022-12-30 13:28:28 +01:00
});
2023-01-05 13:05:28 +01:00
});