Skip to content
Snippets Groups Projects
Verified Commit 0fca5447 authored by Ben Eltschig's avatar Ben Eltschig
Browse files

Formatierungen in SkribblServer.js wiederhergestellt

parent 60658c44
Branches
1 merge request!8Merge Changes into custom-server, mainly paint-functionality
import { SkribblWord } from "../docs/CustomElements.js"; import { SkribblWord } from "../docs/CustomElements.js";
import Util, { ConditionFulfilledPromise } from "../util/Util.js"; import Util, {ConditionFulfilledPromise} from "../util/Util.js";
import DataChannel from "./networking/DataChannel.js"; import DataChannel from "./networking/DataChannel.js";
import Signaler from "./networking/Signaler.js"; import Signaler from "./networking/Signaler.js";
import SkribblWords from "./SkribblWords.js"; import SkribblWords from "./SkribblWords.js";
/** /**
* Format of the messages send to the server by the clients. * 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}|{action:"guess",word:string}|ClearOrder|BackgroundOrder|DrawOrder} MessageToServer * @typedef {{action:"join",name:string}|{action:"startGame"}|{action:"changeSettings",rounds:number,drawTime:number}|{action:"chooseWord",word:number}|{action:"guess",word:string}|ClearOrder|DrawOrder|BackgroundOrder} MessageToServer
*/ */
/** /**
* Format of the messages send to the clients by the server. * Format of the messages send to the clients by the server.
...@@ -58,6 +58,7 @@ import SkribblWords from "./SkribblWords.js"; ...@@ -58,6 +58,7 @@ import SkribblWords from "./SkribblWords.js";
* @property {"changeBackgroundColor"} action * @property {"changeBackgroundColor"} action
* @property {string} color * @property {string} color
*/ */
/** /**
* @typedef {object} WordReveal * @typedef {object} WordReveal
* @property {"revealWord"} action * @property {"revealWord"} action
...@@ -70,343 +71,341 @@ import SkribblWords from "./SkribblWords.js"; ...@@ -70,343 +71,341 @@ import SkribblWords from "./SkribblWords.js";
* A local server. Handles all the important game logic, and communicates with clients via DataChannels. * A local server. Handles all the important game logic, and communicates with clients via DataChannels.
*/ */
export default class SkribblServer { export default class SkribblServer {
/** /**
* Starts a new SkribblServer. * Starts a new SkribblServer.
*/ */
constructor() { constructor(){
this._readyPromise = (async() => { this._readyPromise = (async()=>{
/** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number,guessedWord:boolean,guessedIndex:number}[]} */ /** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number,guessedWord:boolean,guessedIndex:number}[]} */
this._clients = []; this._clients = [];
this._hasGameStarted = false; this._hasGameStarted = false;
/** current round, starts counting at 1, only 0 when the game hasn't started yet */ /** current round, starts counting at 1, only 0 when the game hasn't started yet */
this._round = 0; this._round = 0;
this._rounds = 3; this._rounds = 3;
this._drawTime = 180; this._drawTime = 180;
/** index of the currently drawing player in the player list */ /** index of the currently drawing player in the player list */
this._drawingPlayer = 0; this._drawingPlayer = 0;
/** /**
* word that is currently being drawn, or list of words a word is currently getting choosen from * word that is currently being drawn, or list of words a word is currently getting choosen from
* @type {string|string[]} * @type {string|string[]}
*/ */
this._word = null; this._word = null;
this._id = await Signaler.host(dataChannel => { this._id = await Signaler.host(dataChannel=>{
this.connect(DataChannel.from(dataChannel, JSON.stringify, JSON.parse)); this.connect(DataChannel.from(dataChannel,JSON.stringify,JSON.parse));
}); });
/** @type {[DataChannel<MessageToServer,MessageToClient>,DataChannel<MessageToClient,MessageToServer>]} */ /** @type {[DataChannel<MessageToServer,MessageToClient>,DataChannel<MessageToClient,MessageToServer>]} */
let [endpointA, endpointB] = DataChannel.createPair(); let [endpointA,endpointB] = DataChannel.createPair();
this._dataChannel = endpointA; this._dataChannel = endpointA;
this.connect(endpointB); 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, guessedWord: false, guessedIndex: 0 });
this._sendStateUpdate();
if (!this._hasGameStarted) {
dataChannel.send({ action: "settingsUpdate", rounds: this._rounds, drawTime: this._drawTime });
}
dataChannel.onMessage(message => {
let playerIndex = this._clients.map(({ dataChannel }) => dataChannel).indexOf(dataChannel);
let isHost = (playerIndex == 0);
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._sendToAll({ action: "settingsUpdate", rounds: this._rounds, drawTime: this._drawTime });
}
} else if (message.action == "guess") {
this._handleGuess(playerIndex, message.word);
} else if (message.action == "clearCanvas" || message.action == "changeBackgroundColor" || message.action == "draw" || message.action == "erase") {
// sends the drawing order to all connected clients iff the player is currently drawing and has already chosen a word
if (this._hasGameStarted && this._drawingPlayer == playerIndex && typeof this._word == "string") {
this._sendToAll(message);
}
}
});
} else {
dataChannel.send("nope");
dataChannel.close();
}
} else {
dataChannel.send("nope");
dataChannel.close();
}
})();
}
/** /**
* Sends a message to all currently connected clients. * Waits until the server is ready.
* @param {MessageToClient} message */
*/ async waitUntilReady(){
_sendToAll(message) { return this._readyPromise;
this._clients.forEach(({ dataChannel }) => { }
dataChannel.send(message);
})
}
/** /**
* Starts the game if it hasn't already started. * A dataChannel talking to this server like any other client.
*/ * @readonly
_startGame() { */
if (this._hasGameStarted) { get dataChannel(){
console.warn("Tried to start a game that has already started."); return this._dataChannel;
} else { }
this._hasGameStarted = true;
this._clients.forEach(client => {
client.points = 0;
client.guessedWord = false;
client.guessedIndex = 0;
});
(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();
/** @type {number} */
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._sendToAll({ action: "clearCanvas" });
this._sendStateUpdate();
this._allGuessedPromise = new ConditionFulfilledPromise(() => {
return this._clients.every((client, index) => client.guessedWord || index == this._drawingPlayer);
});
let timeOverPromise = Util.wait(this._drawTime);
await Promise.race([this._allGuessedPromise, timeOverPromise]);
let points = this._awardPoints();
this._sendStateUpdate();
this._sendToAll({ action: "revealWord", word: this._word, description: words[wordIndex].description, macros: await SkribblWords.macros, points });
await Util.wait(8);
}
}
this._hasGameStarted = false;
this._sendStateUpdate();
})();
}
}
/** /**
* Sends a state update to all connected clients (that is, clients that have already joined the game). * The ID others can use to connect to this server.
*/ * @readonly
_sendStateUpdate() { */
console.log("sending state update! players:", this._clients); get id(){
if (this._hasGameStarted) { return this._id;
let players = this._clients.map(({ name, points, guessedWord }) => ({ name, points, guessedWord })); }
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 } });
});
}
}
/** /**
* Handles a guess send from a given dataChannel. * Returns the full url others can use to connect to this server.
* @param {number} playerIndex * @readonly
* @param {string} word */
*/ get url(){
_handleGuess(playerIndex, word) { return document.location.host+document.location.pathname+"#"+this._id;
let player = this._clients[playerIndex]; }
if (this._hasGameStarted) {
if (player.guessedWord || playerIndex == this._drawingPlayer) {
// TODO let people who already know the word send ghost messages to others who already know it too
} else if (typeof this._word == "string" && SkribblServer._isCorrect(word, this._word)) {
player.guessedWord = true;
player.guessedIndex = Math.max(...this._clients.map(client => client.guessedIndex)) + 1;
this._clients.forEach((client, index) => {
if (index == playerIndex) {
client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: true, word });
} else {
client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: true });
}
});
this._allGuessedPromise.checkCondition();
} else {
this._clients.forEach((client, index) => {
let close = typeof this._word == "string" ? SkribblServer._isClose(word, this._word) : false;
if (index == playerIndex) {
client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: false, word, close });
} else {
client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: false, word });
}
});
}
}
}
/** /**
* Awards points to each player based on if and when they guessed the word, resets the corresponding properties and then returns the list of points each player got. * Adds an incoming connection as a client.
* @todo find better formula for the number of points each player should get * @param {DataChannel<MessageToClient,MessageToServer>} dataChannel
*/ */
_awardPoints() { connect(dataChannel){
let guessingPlayers = this._clients.length - 1; (async()=>{
let successfullPlayers = Math.max(...this._clients.map(client => client.guessedIndex)); let message = await dataChannel.next();
let points = this._clients.map((client, index) => { console.log("message: ",message);
let points; if (message.action==="join"){
if (this._drawingPlayer == index) { let name = message.name;
points = successfullPlayers * 50; if (name.length<30&&name.length>=1){
} else if (client.guessedIndex != 0) { dataChannel.send("yup");
points = ((guessingPlayers - client.guessedIndex + 1) ** 2) / guessingPlayers * 100; this._clients.push({name,dataChannel,points:0,guessedWord:false,guessedIndex:0});
} else { this._sendStateUpdate();
points = 0; if (!this._hasGameStarted){
} dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime});
points = Math.round(points / 5) * 5; }
client.points += points; dataChannel.onMessage(message=>{
client.guessedWord = false; let playerIndex = this._clients.map(({dataChannel})=>dataChannel).indexOf(dataChannel);
client.guessedIndex = 0; let isHost = (playerIndex==0);
return points; if (message.action=="startGame"){
}); if (isHost&&!this._hasGameStarted){
return points; this._startGame();
} }
}else if(message.action=="changeSettings"){
if (isHost&&!this._hasGameStarted){
this._rounds = message.rounds;
this._drawTime = message.drawTime;
this._sendToAll({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime});
}
}else if (message.action=="guess"){
this._handleGuess(playerIndex,message.word);
}else if (message.action=="clearCanvas"||message.action=="draw"||message.action=="erase"){
// sends the drawing order to all connected clients iff the player is currently drawing and has already chosen a word
if (this._hasGameStarted&&this._drawingPlayer==playerIndex&&typeof this._word=="string"){
this._sendToAll(message);
}
}
});
}else{
dataChannel.send("nope");
dataChannel.close();
}
}else{
dataChannel.send("nope");
dataChannel.close();
}
})();
}
/** /**
* Checks whether a given guess is close to the given word. A guess counts as close, if one letter is wrong, * Sends a message to all currently connected clients.
* two letters are swapped or the guess has one letter too much or too little. * @param {MessageToClient} message
* @param {string} guess */
* @param {string} word _sendToAll(message){
*/ this._clients.forEach(({dataChannel})=>{
static _isClose(guess, word) { dataChannel.send(message);
guess = guess.toLowerCase().replace(/-/g, " "); })
word = word.toLowerCase().replace(/-/g, " "); }
const wordArray = Array.from(word);
const guessArray = Array.from(guess);
//if equal /**
if (guess == word) { * Starts the game if it hasn't already started.
return true; */
} _startGame(){
if (this._hasGameStarted){
console.warn("Tried to start a game that has already started.");
}else{
this._hasGameStarted = true;
this._clients.forEach(client=>{
client.points = 0;
client.guessedWord = false;
client.guessedIndex = 0;
});
(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();
/** @type {number} */
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._sendToAll({action:"clearCanvas"});
this._sendStateUpdate();
this._allGuessedPromise = new ConditionFulfilledPromise(()=>{
return this._clients.every((client,index)=>client.guessedWord||index==this._drawingPlayer);
});
let timeOverPromise = Util.wait(this._drawTime);
await Promise.race([this._allGuessedPromise,timeOverPromise]);
let points = this._awardPoints();
this._sendStateUpdate();
this._sendToAll({action:"revealWord",word:this._word,description:words[wordIndex].description,macros:await SkribblWords.macros,points});
await Util.wait(8);
}
}
this._hasGameStarted = false;
this._sendStateUpdate();
})();
}
}
//either one letter wrong or two letters swapped /**
if (guess.length == word.length) { * Sends a state update to all connected clients (that is, clients that have already joined the game).
//Counts the mistakes and their position */
let errorCounter = 0; _sendStateUpdate(){
let errorPos = 0; console.log("sending state update! players:",this._clients);
for (var i = 0; i < wordArray.length; i++) { if (this._hasGameStarted){
if (wordArray[i] != guessArray[i]) { let players = this._clients.map(({name,points,guessedWord})=>({name,points,guessedWord}));
if (errorCounter == 0) { this._clients.forEach(({dataChannel},index)=>{
errorPos = i; let host = index==0;
} let word = this._drawingPlayer==index?this._word:(typeof this._word=="string"?this._word.replace(/[^ -]/g,"_"):null);
if (errorCounter == 1) { //if a second mistake occurs, either the letters are swapped or it is not correct /** @type {ActiveGameState} */
//but there could be a third mistake, so in the case of a swap, true is not directly returned. let state = {players,playerIndex:index,host,hasGameStarted:true,rounds:this._rounds,round:this._round,drawingPlayer:this._drawingPlayer,word};
if ((wordArray[i] != guessArray[errorPos]) || (wordArray[errorPos] != guessArray[i])) { dataChannel.send({action:"stateUpdate",newState:state});
return false; });
} }else{
} let players = this._clients.map(({name})=>({name}));
if (errorCounter >= 2) { //with two or more mistakes, the word is not close this._clients.forEach(({dataChannel},index)=>{
return false; dataChannel.send({action:"stateUpdate",newState:{players,playerIndex:index,host:index==0,hasGameStarted:false}});
} });
errorCounter++; }
} }
}
//if it hasnt returned false by now, the guess is close
return true;
}
//if one letter too much /**
if (guess.length - 1 == word.length) { * Handles a guess send from a given dataChannel.
let errorCounter = 0; //also the offset * @param {number} playerIndex
for (var i = 0; i < wordArray.length; i++) { * @param {string} word
if (wordArray[i] != guessArray[i + errorCounter]) { */
errorCounter++; _handleGuess(playerIndex,word){
if (errorCounter >= 2) { let player = this._clients[playerIndex];
return false; if (this._hasGameStarted){
} if (player.guessedWord||playerIndex==this._drawingPlayer){
} // TODO let people who already know the word send ghost messages to others who already know it too
} }else if (typeof this._word=="string"&&SkribblServer._isCorrect(word,this._word)){
return true; player.guessedWord = true;
} player.guessedIndex = Math.max(...this._clients.map(client=>client.guessedIndex))+1;
this._clients.forEach((client,index)=>{
if (index==playerIndex){
client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:true,word});
}else{
client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:true});
}
});
this._allGuessedPromise.checkCondition();
}else{
this._clients.forEach((client,index)=>{
let close = typeof this._word=="string"?SkribblServer._isClose(word,this._word):false;
if (index==playerIndex){
client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:false,word,close});
}else{
client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:false,word});
}
});
}
}
}
//if one letter too little /**
if (guess.length + 1 == word.length) { * Awards points to each player based on if and when they guessed the word, resets the corresponding properties and then returns the list of points each player got.
let errorCounter = 0; //also the offset * @todo find better formula for the number of points each player should get
for (var i = 0; i < guessArray.length; i++) { */
if (wordArray[i + errorCounter] != guessArray[i]) { _awardPoints(){
errorCounter++; let guessingPlayers = this._clients.length-1;
if (errorCounter >= 2) { let successfullPlayers = Math.max(...this._clients.map(client=>client.guessedIndex));
return false; let points = this._clients.map((client,index)=>{
} let points;
} if (this._drawingPlayer==index){
} points = successfullPlayers*50;
return true; }else if(client.guessedIndex!=0){
} points = ((guessingPlayers-client.guessedIndex+1)**2)/guessingPlayers*100;
}else{
points = 0;
}
points = Math.round(points/5)*5;
client.points += points;
client.guessedWord = false;
client.guessedIndex = 0;
return points;
});
return points;
}
return false; /**
} * Checks whether a given guess is close to the given word. A guess counts as close, if one letter is wrong,
* two letters are swapped or the guess has one letter too much or too little.
* @param {string} guess
* @param {string} word
*/
static _isClose(guess,word) {
guess = guess.toLowerCase().replace(/-/g," ");
word = word.toLowerCase().replace(/-/g," ");
const wordArray = Array.from(word);
const guessArray = Array.from(guess);
//if equal
if (guess == word) {
return true;
}
//either one letter wrong or two letters swapped
if (guess.length == word.length) {
//Counts the mistakes and their position
let errorCounter = 0;
let errorPos = 0;
for (var i = 0; i < wordArray.length; i++) {
if (wordArray[i] != guessArray[i]) {
if (errorCounter == 0) {
errorPos = i;
} if (errorCounter == 1) { //if a second mistake occurs, either the letters are swapped or it is not correct
//but there could be a third mistake, so in the case of a swap, true is not directly returned.
if ((wordArray[i] != guessArray[errorPos]) || (wordArray[errorPos] != guessArray[i])) {
return false;
}
} if (errorCounter >= 2) { //with two or more mistakes, the word is not close
return false;
}
errorCounter++;
}
}
//if it hasnt returned false by now, the guess is close
return true;
}
//if one letter too much
if (guess.length - 1 == word.length) {
let errorCounter = 0; //also the offset
for (var i = 0; i < wordArray.length; i++) {
if (wordArray[i] != guessArray[i + errorCounter]) {
errorCounter++;
if (errorCounter >= 2) {
return false;
}
}
}
return true;
}
//if one letter too little
if (guess.length + 1 == word.length) {
let errorCounter = 0; //also the offset
for (var i = 0; i < guessArray.length; i++) {
if (wordArray[i + errorCounter] != guessArray[i]) {
errorCounter++;
if (errorCounter >= 2) {
return false;
}
}
}
return true;
}
return false;
}
/** /**
* Checks if the guess is correct. It is not case-sensitive. * Checks if the guess is correct. It is not case-sensitive.
* @param {string} guess * @param {string} guess
* @param {string} word * @param {string} word
*/ */
static _isCorrect(guess, word) { static _isCorrect(guess, word) {
guess = guess.toLowerCase().replace(/-/g, " "); guess = guess.toLowerCase().replace(/-/g," ");
word = word.toLowerCase().replace(/-/g, " "); word = word.toLowerCase().replace(/-/g," ");
return guess == word; return guess==word;
} }
} }
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment