Newer
Older
import DataChannel from "./networking/DataChannel.js";
import Signaler from "./networking/Signaler.js";
import SkribblWords from "./SkribblWords.js";

Ben Eltschig
committed
/**
* Format of the messages send to the server by the clients.
* @typedef {{action:"join",name:string}|{action:"startGame"}|{action:"changeSettings",rounds:number,drawTime:number}|{action:"chooseWord",word:number}} MessageToServer

Ben Eltschig
committed
*/
/**
* Format of the messages send to the clients by the server.
* @typedef {"yup"|"nope"|{action:"stateUpdate",newState:GameState}|{action:"settingsUpdate",rounds:number,drawTime:number}} MessageToClient
*/
/**
* Format in which the game state is send to the clients by the server.
* @typedef {LobbyGameState|ActiveGameState} GameState
*/
/**
* Format in which the game state is send to the clients by the server when the game hasn't started yet.
* @typedef {object} LobbyGameState
* @property {false} hasGameStarted whether or not the game has started already
* @property {boolean} host whether this specific client has host permissions
* @property {number} playerIndex the index of this specific player in the player list
* @property {{name:string}[]} players list of all currently connected players
*/
/**
* Format in which the game state is send to the clients by the server when the game has already started.
* @typedef {object} ActiveGameState
* @property {true} hasGameStarted whether or not the game has started already
* @property {boolean} host whether this specific client has host permissions
* @property {number} playerIndex the index of this specific player in the player list
* @property {{name:string,points:number}[]} players list of all currently connected players
* @property {number} round current round, ranging from `1` to `n` where `n` is the value of the `.rounds`-property
* @property {number} rounds max number of rounds
* @property {number} drawingPlayer index of the player that is currently drawing or choosing a word in the player list
* @property {null|string|string[]} word word that is currently being drawn (with all letters replaced with underscores if it's someone elses turn drawing), or a list of words to draw next to choose from, or `null` if someone else is currently choosing a word

Ben Eltschig
committed
*/
/**
* A local server. Handles all the important game logic, and communicates with clients via DataChannels.
*/
export default class SkribblServer {
/**
* Starts a new SkribblServer.
*/
constructor(){
this._readyPromise = (async()=>{
/** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number}[]} */

Ben Eltschig
committed
this._clients = [];
/** current round, starts counting at 1, only 0 when the game hasn't started yet */
this._round = 0;
this._rounds = 3;
this._drawTime = 180;
/** index of the currently drawing player in the player list */
this._drawingPlayer = 0;
/**
* word that is currently being drawn, or list of words a word is currently getting choosen from
* @type {string|string[]}
*/
this._word = null;
this._id = await Signaler.host(dataChannel=>{

Ben Eltschig
committed
this.connect(DataChannel.from(dataChannel,JSON.stringify,JSON.parse));

Ben Eltschig
committed
/** @type {[DataChannel<MessageToServer,MessageToClient>,DataChannel<MessageToClient,MessageToServer>]} */

Ben Eltschig
committed
let [endpointA,endpointB] = DataChannel.createPair();
this._dataChannel = endpointA;
this.connect(endpointB);
})();
}
/**
* Waits until the server is ready.
*/
async waitUntilReady(){
return this._readyPromise;
}

Ben Eltschig
committed
/**
* A dataChannel talking to this server like any other client.
* @readonly
*/
get dataChannel(){
return this._dataChannel;
}
/**
* The ID others can use to connect to this server.
* @readonly
*/
get id(){
return this._id;
}
/**
* Returns the full url others can use to connect to this server.
* @readonly
*/
get url(){
return document.location.host+document.location.pathname+"#"+this._id;
}
/**
* Adds an incoming connection as a client.

Ben Eltschig
committed
* @param {DataChannel<MessageToClient,MessageToServer>} dataChannel
*/
connect(dataChannel){

Ben Eltschig
committed
(async()=>{
let message = await dataChannel.next();
console.log("message: ",message);

Ben Eltschig
committed
if (message.action==="join"){
let name = message.name;

Ben Eltschig
committed
if (name.length<30&&name.length>=1){
dataChannel.send("yup");
this._clients.push({name,dataChannel,points:0});
this._sendStateUpdate();
if (!this._hasGameStarted){
dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime});
}
dataChannel.onMessage(message=>{
let isHost = this._clients[0].dataChannel==dataChannel;
if (message.action=="startGame"){
if (isHost&&!this._hasGameStarted){
this._startGame();
}else if(message.action=="changeSettings"){
if (isHost&&!this._hasGameStarted){
this._rounds = message.rounds;
this._drawTime = message.drawTime;
this._clients.forEach(({dataChannel})=>{
dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime});
});
}

Ben Eltschig
committed
}else{
dataChannel.send("nope");
dataChannel.close();
}
}else{
dataChannel.send("nope");
dataChannel.close();
}
})();
/**
* Starts the game if it hasn't already started.
*/
_startGame(){
if (this._hasGameStarted){
console.warn("Tried to start a game that has already started.");
}else{
this._hasGameStarted = true;
(async()=>{
for (this._round=1;this._round<=this._rounds;this._round++){
for (this._drawingPlayer=0;this._drawingPlayer<this._clients.length;this._drawingPlayer++){
let words = await Promise.all([SkribblWords.get(),SkribblWords.get(),SkribblWords.get()]);
this._word = words.map(({word})=>word);
this._sendStateUpdate();
let wordIndex = await new Promise(async resolve=>{
let cancelled = false;
setTimeout(()=>{
cancelled = true;
resolve(Math.floor(Math.random()*3));
},10000);
let dataChannel = this._clients[this._drawingPlayer].dataChannel;
while (!cancelled){
let message = await dataChannel.next();
if (message.action=="chooseWord"){
resolve(message.word);
}
}
});
this._word = words[wordIndex].word;
this._sendStateUpdate();
await new Promise(resolve=>setTimeout(resolve,3000));
}
}
this._hasGameStarted = false;
this._sendStateUpdate();
})();
}
}
/**
* Sends a state update to all connected clients (that is, clients that have already joined the game).
*/
_sendStateUpdate(){
console.log("sending state update! players:",this._clients);
if (this._hasGameStarted){
let players = this._clients.map(({name,points})=>({name,points}));
this._clients.forEach(({dataChannel},index)=>{
let host = index==0;
let word = this._drawingPlayer==index?this._word:(typeof this._word=="string"?this._word.replace(/[^ -]/g,"_"):null);
/** @type {ActiveGameState} */
let state = {players,playerIndex:index,host,hasGameStarted:true,rounds:this._rounds,round:this._round,drawingPlayer:this._drawingPlayer,word};
dataChannel.send({action:"stateUpdate",newState:state});
});
}else{
let players = this._clients.map(({name})=>({name}));
this._clients.forEach(({dataChannel},index)=>{
dataChannel.send({action:"stateUpdate",newState:{players,playerIndex:index,host:index==0,hasGameStarted:false}});
});
}
}