Skip to content
Snippets Groups Projects
SkribblServer.js 4.51 KiB
Newer Older
import DataChannel from "./networking/DataChannel.js";
import Signaler from "./networking/Signaler.js";

 * Format of the messages send to the server by the clients.
 * @typedef {{action:"join",name:string}|{action:"startGame"}|{action:"changeSettings",rounds:number,drawTime:number}} MessageToServer
 * 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 {{hasGameStarted:false,host:boolean,players:{name:string}[]}|{hasGameStarted:true,round:number,rounds:number,host:boolean,players:{name:string,points:number}[]}} GameState
/**
 * 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}[]} */
			this._hasGameStarted = false;
			this._rounds = 3;
			this._drawTime = 180;
			this._id = await Signaler.host(dataChannel=>{
				this.connect(DataChannel.from(dataChannel,JSON.stringify,JSON.parse));
			/** @type {[DataChannel<MessageToServer,MessageToClient>,DataChannel<MessageToClient,MessageToServer>]} */
			let [endpointA,endpointB] = DataChannel.createPair();
			this._dataChannel = endpointA;
			this.connect(endpointB);
		})();
	}

	/**
	 * Waits until the server is ready.
	 */
	async waitUntilReady(){
		return this._readyPromise;
	}

	/**
	 * 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.
	 * @param {DataChannel<MessageToClient,MessageToServer>} dataChannel
	 */
	connect(dataChannel){
		(async()=>{
			let message = await dataChannel.next();
			console.log("message: ",message);
			if (message.action==="join"){
				let name = message.name;
				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){
						}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});
								});
							}
				}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++){
					this._sendStateUpdate();
					await new Promise(resolve=>setTimeout(resolve,2000));
				}
				this._hasGameStarted = false;
				this._sendStateUpdate();
			})();
		}
	}

	/**
	 * Sends a state update to all connected clients (that is, clients that have already joined the game).
	 */
	_sendStateUpdate(){
		if (this._hasGameStarted){
			let players = this._clients.map(({name,points})=>({name,points}));
			this._clients.forEach(({dataChannel},index)=>{
				dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:true,rounds:this._rounds,round:this._round}});
			});
		}else{
			let players = this._clients.map(({name})=>({name}));
			this._clients.forEach(({dataChannel},index)=>{
				dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:false}});
			});
		}
	}