From 7fff3e70a7fb134212c1e77c14c37fcbc6aaf5f0 Mon Sep 17 00:00:00 2001
From: deepdigger <32137458+deepdigger@users.noreply.github.com>
Date: Fri, 2 Apr 2021 15:44:09 +0200
Subject: [PATCH] SkribblClient: added BackgroundOrder to change
 CanvasBackground SkribblServer: added BackgroundOrder SkribblCanvas: added
 logic to draw on the board, added Radius,Color,Backgroundcolor,Erase

---
 client/logic/SkribblClient.js |   9 +-
 client/logic/SkribblServer.js | 664 +++++++++++++++++-----------------
 client/ui/SkribblCanvas.js    | 160 +++++++-
 3 files changed, 487 insertions(+), 346 deletions(-)

diff --git a/client/logic/SkribblClient.js b/client/logic/SkribblClient.js
index 7faad7f..20ea0e8 100644
--- a/client/logic/SkribblClient.js
+++ b/client/logic/SkribblClient.js
@@ -10,6 +10,7 @@ import Util, {CallbackHandler,Value,ReadOnlyValue} from "../util/Util.js";
  * @typedef {import("./SkribblServer.js").ClearOrder} ClearOrder
  * @typedef {import("./SkribblServer.js").DrawOrder} DrawOrder
  * @typedef {import("./SkribblServer.js").WordReveal} WordReveal
+ * @typedef {import("./SkribblServer.js").BackgroundOrder} BackgroundOrder
  */
 /**
  * @typedef {import("../util/Util.js").DeepReadOnly<T>} DeepReadOnly
@@ -32,7 +33,7 @@ export default class SkribblClient {
 		this._state = new Value(null);
 		/** @type {CallbackHandler<[guessData:GuessResponse]>} */
 		this._onGuessCallbacks = new CallbackHandler();
-		/** @type {CallbackHandler<[order:ClearOrder|DrawOrder]>} */
+		/** @type {CallbackHandler<[order:ClearOrder|BackgroundOrder|DrawOrder]>} */
 		this._onDrawCallbacks = new CallbackHandler();
 		/** @type {CallbackHandler<[message:WordReveal]>} */
 		this._onWordRevealCallbacks = new CallbackHandler();
@@ -55,7 +56,7 @@ export default class SkribblClient {
 					}else if(message.action=="guessedWord"){
 						let data = (({action,...data})=>data)(message);
 						this._onGuessCallbacks.callAll(data);
-					}else if(message.action=="clearCanvas"||message.action=="draw"||message.action=="erase"){
+					}else if(message.action=="clearCanvas"||message.action=="changeBackgroundColor"||message.action=="draw"||message.action=="erase"){
 						this._onDrawCallbacks.callAll(message);
 					}else if(message.action=="revealWord"){
 						this._onWordRevealCallbacks.callAll(message);
@@ -197,7 +198,7 @@ export default class SkribblClient {
 
 	/**
 	 * Sends the given drawing order to the server and the immediately returns, without waiting for a response.
-	 * @param {ClearOrder|DrawOrder} order
+	 * @param {ClearOrder|BackgroundOrder|DrawOrder} order
 	 */
 	draw(order){
 		this._dataChannel.send(order);
@@ -205,7 +206,7 @@ export default class SkribblClient {
 
 	/**
 	 * Registers a callback to be called whenever someone draws something on the canvas.
-	 * @param {(order:ClearOrder|DrawOrder)=>void} callback
+	 * @param {(order:ClearOrder|BackgroundOrder|DrawOrder)=>void} callback
 	 * @param {object} [options]
 	 * @param {Value<boolean>|ReadOnlyValue<boolean>} [options.onlyWhen] when specified, calls the callback only when this is value is true.
 	 */
diff --git a/client/logic/SkribblServer.js b/client/logic/SkribblServer.js
index dac9882..63392c7 100644
--- a/client/logic/SkribblServer.js
+++ b/client/logic/SkribblServer.js
@@ -1,16 +1,16 @@
 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 Signaler from "./networking/Signaler.js";
 import SkribblWords from "./SkribblWords.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}|{action:"chooseWord",word:number}|{action:"guess",word:string}|ClearOrder|DrawOrder} MessageToServer
+ * @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
  */
 /**
  * Format of the messages send to the clients by the server.
- * @typedef {"yup"|"nope"|{action:"stateUpdate",newState:GameState}|{action:"settingsUpdate",rounds:number,drawTime:number}|({action:"guessedWord"}&GuessResponse)|ClearOrder|DrawOrder|WordReveal} MessageToClient
+ * @typedef {"yup"|"nope"|{action:"stateUpdate",newState:GameState}|{action:"settingsUpdate",rounds:number,drawTime:number}|({action:"guessedWord"}&GuessResponse)|ClearOrder|DrawOrder|BackgroundOrder|WordReveal} MessageToClient
  */
 /**
  * Format in which the game state is send to the clients by the server.
@@ -52,6 +52,12 @@ import SkribblWords from "./SkribblWords.js";
  * @property {number} radius radius in pixels
  * @property {{x:number,y:number}[]} points array of points between which lines should be drawn
  */
+/**
+ * Format to change the Background-Color of the Canvas
+ * @typedef {object} BackgroundOrder
+ * @property {"changeBackgroundColor"} action
+ * @property {string} color
+ */
 /**
  * @typedef {object} WordReveal
  * @property {"revealWord"} action
@@ -64,341 +70,343 @@ import SkribblWords from "./SkribblWords.js";
  * 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,guessedWord:boolean,guessedIndex:number}[]} */
-			this._clients = [];
-			this._hasGameStarted = false;
-			/** 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=>{
-				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);
-		})();
-	}
+    /**
+     * Starts a new SkribblServer.
+     */
+    constructor() {
+        this._readyPromise = (async() => {
+            /** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number,guessedWord:boolean,guessedIndex:number}[]} */
+            this._clients = [];
+            this._hasGameStarted = false;
+            /** 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 => {
+                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, 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();
+            }
+        })();
+    }
 
-	/**
-	 * Waits until the server is ready.
-	 */
-	async waitUntilReady(){
-		return this._readyPromise;
-	}
+    /**
+     * Sends a message to all currently connected clients.
+     * @param {MessageToClient} message 
+     */
+    _sendToAll(message) {
+        this._clients.forEach(({ dataChannel }) => {
+            dataChannel.send(message);
+        })
+    }
 
-	/**
-	 * A dataChannel talking to this server like any other client.
-	 * @readonly
-	 */
-	get dataChannel(){
-		return this._dataChannel;
-	}
+    /**
+     * 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;
+            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();
+            })();
+        }
+    }
 
-	/**
-	 * The ID others can use to connect to this server.
-	 * @readonly
-	 */
-	get id(){
-		return this._id;
-	}
+    /**
+     * 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, 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 } });
+            });
+        }
+    }
 
-	/**
-	 * Returns the full url others can use to connect to this server.
-	 * @readonly
-	 */
-	get url(){
-		return document.location.host+document.location.pathname+"#"+this._id;
-	}
+    /**
+     * Handles a guess send from a given dataChannel.
+     * @param {number} playerIndex
+     * @param {string} word
+     */
+    _handleGuess(playerIndex, word) {
+        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 });
+                    }
+                });
+            }
+        }
+    }
 
-	/**
-	 * 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=="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();
-			}
-		})();
-	}
+    /**
+     * 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.
+     * @todo find better formula for the number of points each player should get
+     */
+    _awardPoints() {
+        let guessingPlayers = this._clients.length - 1;
+        let successfullPlayers = Math.max(...this._clients.map(client => client.guessedIndex));
+        let points = this._clients.map((client, index) => {
+            let points;
+            if (this._drawingPlayer == index) {
+                points = successfullPlayers * 50;
+            } 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;
+    }
 
-	/**
-	 * Sends a message to all currently connected clients.
-	 * @param {MessageToClient} message 
-	 */
-	_sendToAll(message){
-		this._clients.forEach(({dataChannel})=>{
-			dataChannel.send(message);
-		})
-	}
+    /**
+     * 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);
 
-	/**
-	 * 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;
-			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();
-			})();
-		}
-	}
+        //if equal
+        if (guess == word) {
+            return true;
+        }
 
-	/**
-	 * 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,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}});
-			});
-		}
-	}
+        //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;
+        }
 
-	/**
-	 * Handles a guess send from a given dataChannel.
-	 * @param {number} playerIndex
-	 * @param {string} word
-	 */
-	_handleGuess(playerIndex,word){
-		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});
-					}
-				});
-			}
-		}
-	}
+        //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;
+        }
 
-	/**
-	 * 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.
-	 * @todo find better formula for the number of points each player should get
-	 */
-	_awardPoints(){
-		let guessingPlayers = this._clients.length-1;
-		let successfullPlayers = Math.max(...this._clients.map(client=>client.guessedIndex));
-		let points = this._clients.map((client,index)=>{
-			let points;
-			if (this._drawingPlayer==index){
-				points = successfullPlayers*50;
-			}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;
-	}
+        //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;
+        }
 
-	/**
-	 * 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;
-	}
+        return false;
+    }
 
-	/**
-	 * Checks if the guess is correct. It is not case-sensitive.
-	 * @param {string} guess
-	 * @param {string} word
-	 */
-	static _isCorrect(guess, word) {
-		guess = guess.toLowerCase().replace(/-/g," ");
-		word = word.toLowerCase().replace(/-/g," ");
-		return guess==word;
-	}
+    /**
+     * Checks if the guess is correct. It is not case-sensitive.
+     * @param {string} guess
+     * @param {string} word
+     */
+    static _isCorrect(guess, word) {
+        guess = guess.toLowerCase().replace(/-/g, " ");
+        word = word.toLowerCase().replace(/-/g, " ");
+        return guess == word;
+    }
 }
\ No newline at end of file
diff --git a/client/ui/SkribblCanvas.js b/client/ui/SkribblCanvas.js
index 4b7645e..0fd71f8 100644
--- a/client/ui/SkribblCanvas.js
+++ b/client/ui/SkribblCanvas.js
@@ -1,5 +1,10 @@
 import SkribblClient from "../logic/SkribblClient.js";
 import { CustomElement } from "../util/Util.js";
+/**
+ * @typedef {import("../logic/SkribblServer.js").DrawOrder} DrawOrder
+ */
+
+
 
 /**
  * Custom element `<skribbl-canvas>` for the canvas that can be drawn on by whoever is currently drawing.
@@ -27,27 +32,122 @@ export default class SkribblCanvas extends CustomElement {
 			<canvas width="960" height="720"></canvas>
 		`;
         this._canvas = this.shadowRoot.querySelector("canvas");
+        this._radius = 10;
+        this._color = "red";
+        this._client = client;
 
+        /**
+         * looks what kind of order is given, then either draws, erases(=drawing with background color), changes the backgroundcolor or clears the canvas
+         * Todo: test functionality
+         */
         client.onDraw(order => {
+            // runs whenever the server tells all clients to draw something on the canvas
+
+            this._canvas = this.shadowRoot.querySelector("canvas");
+            var ctx = this._canvas.getContext("2d");
+            ctx.lineCap = "round";
 
+            if (order.action == "draw") {
+                let x = 960 * order.points[0].x / 1000;
+                let y = 720 * order.points[0].y / 1000;
+                ctx.beginPath();
+                ctx.moveTo(x, y);
+                let xx = 960 * order.points[1].x / 1000;
+                let yy = 720 * order.points[1].y / 1000;
+                ctx.lineTo(xx, yy);
+                ctx.lineWidth = order.radius;
+                ctx.strokeStyle = order.color;
+                ctx.stroke();
+            } else if (order.action == "erase") {
+                let x = 960 * order.points[0].x / 1000;
+                let y = 720 * order.points[0].y / 1000;
+                ctx.beginPath();
+                ctx.moveTo(x, y);
+                let xx = 960 * order.points[1].x / 1000;
+                let yy = 720 * order.points[1].y / 1000;
+                ctx.lineTo(xx, yy);
+                ctx.lineWidth = order.radius;
+                ctx.strokeStyle = this._backgroundColor;
+                ctx.stroke();
+            } else if (order.action == "changeBackgroundColor") {
+                this._backgroundColor = order.color;
+                ctx.fillStyle = order.color;
+                ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
+            } else if (order.action == "clearCanvas") {
+                ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+            }
 
-            // runs whenever the server tells all clients to draw something on the canvas
-            // TODO draw stuff to the canvas here
             console.log("incoming draw order:", order); // just for testing, can be removed once this has been implemented
         }, { onlyWhen: this.connected });
-        // TODO whenever the player draws stuff, send it to the server using `client.draw(order)` here
+
+        /**
+         * is allowed to Draw and last known mouse position
+         */
+        this._isDrawing = false;
+        this._relativeMousePositionX = 0;
+        this._relativeMousePositionY = 0;
+
+
+        /**
+         * Sends on MouseMove event if you are allowed to draw a draw event to the server with color, radius and erase/draw
+         */
+        this._canvas.addEventListener("mousemove", e => {
+            if (this._drawable && this._isDrawing) {
+                let oldMousePositionX = this._relativeMousePositionX;
+                let oldMousePositionY = this._relativeMousePositionY;
+                this.updateRelativeMousePosition(e); // Size not in Percent but in perMille (per Thousand)
+
+                /** @type {"draw"|"erase"} */
+                let actionToDo = "draw";
+                if (this._erasorActive) {
+                    actionToDo = "erase"
+                }
+                /** @type {DrawOrder} */
+                let coolOrder = { action: actionToDo, color: this._color, radius: this._radius, points: [{ x: oldMousePositionX, y: oldMousePositionY }, { x: this._relativeMousePositionX, y: this._relativeMousePositionY }] };
+                client.draw(coolOrder);
+            }
+        });
+
+        /**
+         * look if you can draw on the canvas
+         */
         this._canvas.addEventListener("mousedown", e => {
-            let cAction="draw";
-            let coolOrder={action:cAction,color:"#abcdef",radius:10,points:[{x:1,y:1},{x:100,y:100}]};
-            client.draw(coolOrder);
-            let uncoolOrder={action:"clearCanvas"};
-            this._canvas = this.shadowRoot.querySelector("canvas");
-            var ctx = this._canvas.getContext("2d");
-            ctx.moveTo(0, 0);
-            ctx.lineTo(960, 720);
-            ctx.stroke();
-            // for example, this code runs whenever the user clicks on the canvas. "mousemove", "mouseup", "mouseenter" and "mouseleave" events can be monitored similarly.
+            this._isDrawing = true;
+            this.updateRelativeMousePosition(e);
+        });
+
+        this._canvas.addEventListener("mouseup", e => {
+            this._isDrawing = false;
+        });
+
+        this._canvas.addEventListener("mouseleave", e => {
+            this._isDrawing = false;
         });
+
+    }
+
+    /**
+     * Sets the backgroundColor of the canvas
+     */
+    /** @param {string} backgroundColor */
+    set backgroundColor(backgroundColor) {
+        /** @type {import("../logic/SkribblServer.js").BackgroundOrder} */
+        let coolOrder = { action: "changeBackgroundColor", color: backgroundColor };
+        this._client.draw(coolOrder);
+    }
+
+    /**
+     * Tells the relative mouse position on the canvas in per Mille (min0, max1000)
+     */
+    /** @param {MouseEvent} e */
+    updateRelativeMousePosition(e) {
+        this._canvas = this.shadowRoot.querySelector("canvas");
+        let canvasSizeWidth = this._canvas.getBoundingClientRect().width;
+        let canvasSizeHeight = this._canvas.getBoundingClientRect().height;
+
+
+        this._relativeMousePositionX = Math.round(1000 * e.offsetX / canvasSizeWidth); // Size not in Percent but in perMille (per Thousand)
+        this._relativeMousePositionY = Math.round(1000 * e.offsetY / canvasSizeHeight); // Size not in Percent but in perMille (per Thousand)
     }
 
     /**
@@ -64,6 +164,38 @@ export default class SkribblCanvas extends CustomElement {
         return this._drawable;
     }
 
-    // TODO add getters and setters for properties like `color` and `radius` here, so they can then be set in `SkribblCanvasContainer.js` whenever the user clicks on the corresponding controls.
+    /**
+     * which radius the pen should have 
+     */
+    /** @param {number} radius */
+    set radius(radius) {
+        this._radius = radius;
+    }
+
+    /**
+     * Which color the pen should have 
+     */
+    /** @param {string} color */
+    set color(color) {
+        this._color = color;
+    }
+
+    /**
+     * Whether the erasor is active
+     */
+    /** @param {boolean} erasorActive */
+    set erasorActive(erasorActive) {
+        this._penActive = false;
+        this._erasorActive = erasorActive;
+    }
+
+    /**
+     * Whether the pen is active
+     */
+    /** @param {boolean} penActive */
+    set penActive(penActive) {
+        this._penActive = false;
+        this._penActive = penActive;
+    }
 }
 customElements.define("skribbl-canvas", SkribblCanvas);
\ No newline at end of file
-- 
GitLab