Browse Source

looking better

Nicolas Winkler 4 years ago
parent
commit
65cc914802
9 changed files with 699 additions and 154 deletions
  1. 66 1
      src/datasource.rs
  2. 21 10
      src/game.rs
  3. 39 20
      src/server/gamelobby.rs
  4. 20 16
      src/server/messages.rs
  5. 55 19
      src/server/server.rs
  6. 2 0
      src/websocket.rs
  7. 147 54
      static/comm.js
  8. 142 34
      static/index.html
  9. 207 0
      static/style.css

+ 66 - 1
src/datasource.rs

@@ -7,7 +7,7 @@ use std::fs::File;
 use std::io::{self, BufRead};
 use rand::thread_rng;
 use rand::seq::SliceRandom;
-
+use rand::distributions::{Distribution, WeightedIndex};
 
 pub fn create_array_source(filename: &str) -> Arc<dyn DataSource<String>> {
     let file = File::open(filename).unwrap();
@@ -123,3 +123,68 @@ impl<T> Shuffler<T> {
         self.source.get_ith(self.permutation[old_index]).unwrap()
     }
 }
+
+
+pub struct LetterDistribution {
+    consonants: Vec<char>,
+    consonant_weights: WeightedIndex<f32>,
+    vowels: Vec<char>,
+    vowel_weights: WeightedIndex<f32>,
+}
+
+
+impl LetterDistribution {
+    pub fn create() -> Self {
+        Self {
+            consonants: vec![
+                'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' 
+            ],
+            consonant_weights: WeightedIndex::new(vec![
+                4.5f32,
+                5.5f32,
+                6.5f32,
+                3.5f32,
+                5.0f32,
+                7.5f32,
+                1.0f32,
+                3.0f32,
+                5.0f32,
+                5.0f32,
+                1.0f32,
+                2.5f32,
+                1.0f32,
+                9.0f32,
+                8.5f32,
+                8.5f32,
+                2.5f32,
+                3.0f32,
+                1.0f32,
+                1.0f32,
+                3.5f32,
+            ]).unwrap(),
+            vowels: vec!['A', 'E', 'I', 'O', 'U', 'Ä', 'Ö', 'Ü'],
+            vowel_weights: WeightedIndex::new(vec![
+                17.0f32,
+                32.0f32,
+                22.0f32,
+                10.0f32,
+                13.0f32,
+                 2.0f32,
+                 2.0f32,
+                 2.0f32,
+            ]).unwrap()
+        }
+    }
+    pub fn get(&self, vowels: usize, consonants: usize) -> Vec<char> {
+        let mut result: Vec<char> = Vec::new();
+        let rng = &mut thread_rng();
+        for _i in 0..vowels {
+            result.push(self.vowels[self.vowel_weights.sample(rng)]);
+        }
+        for _i in 0..consonants {
+            result.push(self.consonants[self.consonant_weights.sample(rng)]);
+        }
+
+        return result;
+    }
+}

+ 21 - 10
src/game.rs

@@ -1,7 +1,5 @@
 use serde::{Serialize, Deserialize};
 use std::collections::BTreeMap;
-use crate::websocket;
-use crate::server;
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
 pub enum GameState {
@@ -49,17 +47,25 @@ impl Game {
     }
 
     pub fn check_letters(word: &str, letters: &Vec<char>) -> bool {
-        let mut countmap: BTreeMap<char, isize> = BTreeMap::new();
+
+        if word.len() == 0 {
+            return false;
+        }
+
+        let mut countmap: BTreeMap<String, isize> = BTreeMap::new();
         for c in letters {
-            countmap.insert(*c, countmap.get(c).unwrap_or(&0) + 1);
+            let upper: String = c.to_uppercase().collect();
+            let val = countmap.get(&upper).unwrap_or(&0) + 1;
+            countmap.insert(upper, val);
         }
-        for c in word.chars() {
-            let count = countmap.get(&c);
+        for c in word.to_uppercase().chars() {
+            let upper = String::from(c);
+            let count = countmap.get(&upper);
             if let Some(&v) = count {
                 if v <= 0 {
                     return false;
                 }
-                countmap.insert(c, v - 1);
+                countmap.insert(upper, v - 1);
             }
             else {
                 return false;
@@ -121,8 +127,9 @@ impl Game {
         }
     }
 
-    pub fn start_round<F>(&mut self, mut word_generator: F)
-            where F: FnMut() -> String {
+    pub fn start_round<F, G>(&mut self, mut word_generator: F, mut char_generator: G)
+                where F: FnMut() -> String,
+                      G: FnMut() -> Vec<char> {
         self.clear_submissions();
 
         /*let mut exes = vec![
@@ -135,7 +142,7 @@ impl Game {
             p.creating_exercise = Some(
                 CreatingEx{
                     question: word_generator(),
-                    letters: vec!['a', 'b']
+                    letters: char_generator()
                 }
             );
         }
@@ -164,6 +171,10 @@ impl Game {
         });
     }
 
+    pub fn remove_player(&mut self, nick: &str) {
+        self.players.retain(|p| p.nick != nick);
+    }
+
     pub fn create_results(&self) -> Vec<Vec<Vec<String>>> {
         let mut result: Vec<Vec<Vec<String>>> = Vec::new();
         let mut questions = self.players.iter()

+ 39 - 20
src/server/gamelobby.rs

@@ -24,6 +24,7 @@ enum LobbyState {
 }
 
 pub struct GameLobby {
+    server: Addr<Server>,
     connected_players: BTreeMap<String, Addr<GameConnection>>,
     game_id: String,
     game: game::Game,
@@ -32,7 +33,8 @@ pub struct GameLobby {
     lobby_state: LobbyState,
     
     //data_source: Arc<dyn datasource::DataSource<String>>,
-    shuffler: datasource::Shuffler<String>
+    shuffler: datasource::Shuffler<String>,
+    letter_distribution: datasource::LetterDistribution
 }
 
 impl Actor for GameLobby {
@@ -43,25 +45,45 @@ impl Actor for GameLobby {
 }
 
 impl Handler<JoinRequest> for GameLobby {
-    type Result = Answer;
+    type Result = ();
     fn handle(&mut self, jr: JoinRequest, ctx: &mut Self::Context) -> Self::Result {
         if self.lobby_state == LobbyState::Starting {
-            jr.p.do_send(LobbyJoined(ctx.address()));
-            self.connected_players.insert(jr.nick.clone(), jr.p);
-            self.game.player_join(jr.nick);
+
+            self.connected_players.insert(jr.nick.clone(), jr.p.clone());
+            self.game.player_join(jr.nick.clone());
+
+            jr.p.do_send(
+                LobbyJoined {
+                    lobby: ctx.address(),
+                    game_id: self.game_id.clone(),
+                    nick: jr.nick
+                }
+            );
+
             self.send_game_to_all();
-            Answer::LobbyJoined(ctx.address())
         }
         else {
             self.waiting_players.insert(jr.nick.clone(), jr.p);
-            Answer::NoSuchLobby
+        }
+    }
+}
+
+impl Handler<LeaveMsg> for GameLobby {
+    type Result = ();
+    fn handle(&mut self, lm: LeaveMsg, _ctx: &mut Self::Context) -> Self::Result {
+        self.waiting_players.remove(&lm.0);
+        self.connected_players.remove(&lm.0);
+        self.game.remove_player(&lm.0);
+
+        if self.connected_players.is_empty() {
+            self.server.do_send(LobbyFinished(self.game_id.clone()));
         }
     }
 }
 
 impl Handler<ReadyMsg> for GameLobby {
     type Result = ();
-    fn handle(&mut self, rm: ReadyMsg, ctx: &mut Self::Context) -> Self::Result {
+    fn handle(&mut self, rm: ReadyMsg, _ctx: &mut Self::Context) -> Self::Result {
         if !self.ready_players.contains(&rm.0) {
             self.ready_players.push(rm.0);
         }
@@ -74,8 +96,8 @@ impl Handler<ReadyMsg> for GameLobby {
 
 impl Handler<SubmitWordMsg> for GameLobby {
     type Result = ();
-    fn handle(&mut self, swm: SubmitWordMsg, ctx: &mut Self::Context) -> Self::Result {
-        let correct = self.game.submit_creation(&swm.nick, swm.word);
+    fn handle(&mut self, swm: SubmitWordMsg, _ctx: &mut Self::Context) -> Self::Result {
+        let _correct = self.game.submit_creation(&swm.nick, swm.word);
         if self.game.all_words_submitted() {
             self.set_state(LobbyState::Guessing);
             self.game.next_state();
@@ -86,7 +108,7 @@ impl Handler<SubmitWordMsg> for GameLobby {
 
 impl Handler<SubmitGuessMsg> for GameLobby {
     type Result = ();
-    fn handle(&mut self, sgm: SubmitGuessMsg, ctx: &mut Self::Context) -> Self::Result {
+    fn handle(&mut self, sgm: SubmitGuessMsg, _ctx: &mut Self::Context) -> Self::Result {
         self.game.submit_guess(&sgm.nick, sgm.guesses);
         if self.game.all_guesses_submitted() {
             self.set_state(LobbyState::Revealing);
@@ -100,21 +122,17 @@ impl Handler<SubmitGuessMsg> for GameLobby {
 }
 
 impl GameLobby {
-    pub fn new(gi: String, data_source: Arc<dyn datasource::DataSource<String>>) -> Self {
+    pub fn new(gi: String, data_source: Arc<dyn datasource::DataSource<String>>, server: Addr<Server>) -> Self {
         GameLobby {
+            server: server,
             connected_players: BTreeMap::new(),
             game_id: gi,
             game: game::Game::new(),
             waiting_players: BTreeMap::new(),
             ready_players: Vec::new(),
             lobby_state: LobbyState::Starting,
-            shuffler: datasource::Shuffler::create(data_source)
-            /*Box::new(datasource::ArraySource::create(vec![
-                "Delikatessfutter für Hunde".to_owned(),
-                "Ein Hotel für die ganze Familie".to_owned(),
-                "Brasilianischer Superstar".to_owned(),
-                "Buchstabe des griechischen Alphabets".to_owned(),
-            ]))*/
+            shuffler: datasource::Shuffler::create(data_source),
+            letter_distribution: datasource::LetterDistribution::create()
         }
     }
 
@@ -130,8 +148,9 @@ impl GameLobby {
             },
             LobbyState::Creating => {
                 let s = &mut self.shuffler;
+                let ld = &self.letter_distribution;
                 let mut index = 0;
-                self.game.start_round(|| s.get());
+                self.game.start_round(|| s.get(), || ld.get(4, 6));
             },
             _ => {}
         }

+ 20 - 16
src/server/messages.rs

@@ -6,7 +6,7 @@ use crate::websocket::*;
 use crate::datasource::DataSource;
 
 #[derive(Message)]
-#[rtype(result = "Answer")]
+#[rtype(result = "()")]
 pub struct JoinRequest {
     pub lobby_id: String,
     pub nick: String,
@@ -14,9 +14,10 @@ pub struct JoinRequest {
 }
 
 #[derive(Message)]
-#[rtype(result = "Answer")]
+#[rtype(result = "()")]
 pub struct CreateLobbyRequest {
     pub lobby_id: String,
+    pub nick: String,
     pub p: Addr<GameConnection>
 }
 
@@ -26,6 +27,18 @@ pub struct ReadyMsg(pub String);
 
 #[derive(Message)]
 #[rtype(result = "()")]
+pub struct LeaveMsg(pub String);
+
+#[derive(Message)]
+#[rtype(result = "()")]
+pub struct LobbyFinished(pub String);
+
+#[derive(Message)]
+#[rtype(result = "()")]
+pub struct StopMsg;
+
+#[derive(Message)]
+#[rtype(result = "()")]
 pub struct SubmitWordMsg {
     pub word: String,
     pub nick: String
@@ -50,7 +63,11 @@ pub struct NoSuchLobby(pub String);
 
 #[derive(Message)]
 #[rtype(result = "()")]
-pub struct LobbyJoined(pub Addr<GameLobby>);
+pub struct LobbyJoined {
+    pub lobby: Addr<GameLobby>,
+    pub game_id: String,
+    pub nick: String
+}
 
 #[derive(Message)]
 #[rtype(result = "()")]
@@ -64,16 +81,3 @@ pub enum Answer {
     LobbyAlreadyExists,
     NoSuchLobby,
 }
-
-
-impl<A, M> MessageResponse<A, M> for Answer
-where
-    A: Actor,
-    M: Message<Result = Answer>,
-{
-    fn handle<R: ResponseChannel<M>>(self, _: &mut A::Context, tx: Option<R>) {
-        if let Some(tx) = tx {
-            tx.send(self);
-        }
-    }
-}

+ 55 - 19
src/server/server.rs

@@ -37,38 +37,47 @@ impl Actor for Server {
 }
 
 impl Handler<JoinRequest> for Server {
-    type Result = Answer;
+    type Result = ();
     fn handle(&mut self, jr: JoinRequest, ctx: &mut Self::Context) -> Self::Result {
         let mb_lobby = self.lobbies.get(&jr.lobby_id);
         match mb_lobby {
             Some(lobby) => {
                 let _sent = lobby.do_send(jr);
-                Answer::LobbyJoined(lobby.clone())
             },
             None => {
                 jr.p.do_send(NoSuchLobby(jr.lobby_id));
-                Answer::NoSuchLobby
             }
         }
     }
 }
 
 impl Handler<CreateLobbyRequest> for Server {
-    type Result = Answer;
+    type Result = ();
     fn handle(&mut self, clr: CreateLobbyRequest, ctx: &mut Self::Context) -> Self::Result {
         let existing_lobby = self.lobbies.get(&clr.lobby_id);
         match existing_lobby {
-            Some(_) => Answer::LobbyAlreadyExists,
+            Some(_) => {},
             None => {
-                let lobby = GameLobby::new(clr.lobby_id.clone(), self.default_data.clone());
+                let lobby = GameLobby::new(clr.lobby_id.clone(), self.default_data.clone(), ctx.address());
                 let lobby_addr = lobby.start();
                 self.lobbies.insert(clr.lobby_id.clone(), lobby_addr.clone());
-                Answer::LobbyCreated(lobby_addr)
+                self.handle(JoinRequest {
+                    lobby_id: clr.lobby_id,
+                    nick: clr.nick,
+                    p: clr.p
+                }, ctx);
             }
         }
     }
 }
 
+impl Handler<LobbyFinished> for Server {
+    type Result = ();
+    fn handle(&mut self, lf: LobbyFinished, ctx: &mut Self::Context) -> Self::Result {
+        self.lobbies.remove(&lf.0);
+    }
+}
+
 ///
 /// connection to one single client
 /// 
@@ -91,7 +100,9 @@ impl Actor for GameConnection {
 impl Handler<LobbyJoined> for GameConnection {
     type Result = ();
     fn handle(&mut self, gu: LobbyJoined, ctx: &mut Self::Context) -> Self::Result {
-        self.game_lobby = Some(gu.0);
+        self.game_lobby = Some(gu.lobby);
+        self.game_id = Some(gu.game_id);
+        self.nick = Some(gu.nick);
     }
 }
 
@@ -116,6 +127,14 @@ impl Handler<ResultMsg> for GameConnection {
     }
 }
 
+impl Handler<StopMsg> for GameConnection {
+    type Result = ();
+    fn handle(&mut self, _sm: StopMsg, ctx: &mut Self::Context) -> Self::Result {
+        self.leave_lobby(ctx);
+        ctx.stop();
+    }
+}
+
 impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for GameConnection {
     fn handle(
         &mut self,
@@ -136,6 +155,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for GameConnection {
             },
             Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
             Ok(ws::Message::Close(reason)) => {
+                self.leave_lobby(ctx);
                 ctx.close(reason);
                 ctx.stop();
             }
@@ -158,8 +178,7 @@ impl GameConnection {
     pub fn initiate_heartbeat(&self, ctx: &mut <Self as Actor>::Context) {
         ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
             if Instant::now().duration_since(act.heartbeat) > CLIENT_TIMEOUT {
-                //println!("Websocket Client heartbeat failed, disconnecting!");
-                ctx.stop();
+                ctx.address().do_send(StopMsg);
                 return;
             }
             ctx.ping(b"");
@@ -177,17 +196,25 @@ impl GameConnection {
         if let Ok(msg) = parsed {
             match msg {
                 ClientMessage::CreateGame{game_id, nick} => {
-                    self.game_id = Some(game_id.clone());
-                    self.nick = Some(nick);
-                    self.server.do_send(CreateLobbyRequest{
-                        lobby_id: game_id.clone(),
-                        p: ctx.address()
-                    });
+                    if nick != "" && game_id != "" {
+                        self.game_id = Some(game_id.clone());
+                        self.nick = Some(nick.clone());
+                        self.server.do_send(CreateLobbyRequest {
+                            lobby_id: game_id.clone(),
+                            nick: nick.clone(),
+                            p: ctx.address()
+                        });
+                    }
                 },
                 ClientMessage::Join{game_id, nick} => {
-                    self.server.do_send(JoinRequest{ lobby_id: game_id.clone(), nick: nick.clone(), p: ctx.address() });
-                    self.game_id = Some(game_id.clone());
-                    self.nick = Some(nick);
+                    if nick != "" {
+                        self.server.do_send(JoinRequest{ lobby_id: game_id.clone(), nick: nick.clone(), p: ctx.address() });
+                        self.game_id = Some(game_id.clone());
+                        self.nick = Some(nick);
+                    }
+                },
+                ClientMessage::LeaveLobby => {
+                    self.leave_lobby(ctx);
                 },
                 ClientMessage::Ready => {
                     if let Some(lobby) = &self.game_lobby {
@@ -216,4 +243,13 @@ impl GameConnection {
             println!("error parsing json");
         }
     }
+
+    pub fn leave_lobby(&mut self, ctx: &mut <Self as Actor>::Context) {
+        if let Some(lobby) = &self.game_lobby {
+            if let Some(nick) = &self.nick {
+                lobby.do_send(LeaveMsg(nick.clone()));
+                self.send_message(&UpdateMessage::LeftLobby{ nick: nick.clone() }, ctx);
+            }
+        }
+    }
 }

+ 2 - 0
src/websocket.rs

@@ -13,6 +13,7 @@ use crate::server::server::GameConnection;
 pub enum ClientMessage {
     CreateGame{ game_id: String, nick: String },
     Join{ game_id: String, nick: String },
+    LeaveLobby,
     Ready,
     SubmitWord{ word: String },
     SubmitGuess{ guesses: Vec<(String, String)> },
@@ -55,6 +56,7 @@ pub struct RoundResultData {
 pub enum UpdateMessage {
     GameNotFound{ game_id: String },
     GameAlreadyExists{ game_id: String },
+    LeftLobby{ nick: String },
     GameState(GameData),
     RoundResult(RoundResultData),
 }

+ 147 - 54
static/comm.js

@@ -1,9 +1,44 @@
 $(function() {
     var connection = null;
 
+    
+    // when creating a word
+    var question = null;
+    var char_list = null;
+
+
+    // when guessing results
     var wordlist = null;
     var questionlist = null;
 
+
+    function validate_word(word) {
+        var countmap = {};
+        for (var i = 0; i < char_list.length; i++) {
+            var upper = char_list[i].toUpperCase();
+            if (countmap[upper] == null) {
+                countmap[upper] = 0;
+            }
+            countmap[upper] += 1;
+        }
+
+        var uppercase_word = word.toUpperCase();
+        for (var i = 0; i < uppercase_word.length; i++) {
+            var upper = uppercase_word.charAt(i);
+            var count = countmap[upper];
+            if (count != null) {
+                if (count <= 0) {
+                    return false;
+                }
+                countmap[upper] = count - 1;
+            }
+            else {
+                return false;
+            }
+        }
+        return word.length > 0;
+    }
+
     $('#join').click(function() {
         var game_id = $('#gameId').val();
         var nick = $('#nick').val();
@@ -13,7 +48,8 @@ $(function() {
                 nick: nick,
             }
         };
-        connect(msg);
+
+        send(msg);
     });
 
     $('#create').click(function() {
@@ -25,7 +61,13 @@ $(function() {
                 nick: nick,
             }
         };
-        connect(msg);
+
+        send(msg);
+    });
+
+    $('#leave-lobby').click(function() {
+        var msg = "leave_lobby";
+        send(msg);
     });
 
     $('#ready').click(function() {
@@ -37,12 +79,17 @@ $(function() {
 
     $('#submit').click(function() {
         var word = $('#word').val();
-        var msg = {
-            submit_word: {
-                word: word
-            }
-        };
-        send(msg);
+        if (validate_word(word)) {
+            var msg = {
+                submit_word: {
+                    word: word
+                }
+            };
+            send(msg);
+        }
+        else {
+            statusMessage("Please use only given characters");
+        }
     });
 
     $('#submitGuess').click(function() {
@@ -55,7 +102,7 @@ $(function() {
                 list.push([wordlist[i], questionlist[number - 1]]);
             }
             else {
-                $("#status").html("Please enter a valid number");
+                statusMessage("Please enter a valid number");
                 return;
             }
         }
@@ -88,6 +135,9 @@ $(function() {
         if (connection != null) {
             connection.send(JSON.stringify(message));
         }
+        else {
+            connect(message);
+        }
     }
 
     function disconnect() {
@@ -96,71 +146,71 @@ $(function() {
     }
 
     function onReceive(msg) {
-        var obj = jQuery.parseJSON(msg.data);
+        var obj = JSON.parse(msg.data);
         if (obj.game_not_found != null) {
-            $('#status').html("Game \"" + obj.game_not_found.game_id + "\" not found");
+            statusMessage("Game \"" + obj.game_not_found.game_id + "\" not found");
         }
         else if (obj.game_already_exists != null) {
-            $('#status').html("Game \"" + obj.game_already_exists.game_id + "\" already exists");
+            statusMessage("Game \"" + obj.game_already_exists.game_id + "\" already exists");
+        }
+        else if (obj.left_lobby != null) {
+            setView('login');
+            $('#lobby-control').hide();
         }
         else if (obj.game_state != null) {
+
+            updatePlayerList(obj.game_state.players);
+
             var gs = obj.game_state;
             if (gs.state_data === "starting") {
-                $('#startingform').show();
+                setView('starting');
             }
             else if (gs.state_data.creating != null) {
-                var players = gs.players;
-                playerlist = "";
-                for (var i = 0; i < players.length; i++) {
-                    playerlist += players[i].nick + "(" + players[i].points + ")";
-                    if (i + 1 < players.length)
-                        playerlist += ", ";
-                }
-                $('#status').html("Players: " + playerlist);
 
                 var creating = gs.state_data.creating;
                 var chars = creating.available_chars;
-                $('#question').val(creating.question);
-                $('#letters').val(chars.join());
 
+                question = creating.question;
+                char_list = chars;
+                $('#question-box').html(creating.question);
+                $('#letter-box').html(chars.join(" "));
 
-                $('#guessing').hide();
-                $('#startingform').hide();
-                $('#results').hide();
-                $('#createform').show();
+                setView('creating');
             }
             else if (gs.state_data.guessing != null) {
                 var guesses = gs.state_data.guessing;
-                var sub_words = guesses.submitted_words;
-                var questions = guesses.questions;
-                var questionsHtml = "";
-                for (var i = 0; i < questions.length; i++) {
-                    questionsHtml += (i + 1) + ": " + questions[i] + "<br>";
-                }
-                $('#guessingDynTable').html(questionsHtml);
-                $('#guessingDyn').html("");
-                for (var i = 0; i < sub_words.length; i++) {
-                    var $label = $("<label to='g" + i + "'>" + sub_words[i][0] + "</label>");
-                    var $field = $("<input id='g" + i + "' type='text' /><br>");
-                    $('#guessingDyn').append($label);
-                    $('#guessingDyn').append($field);
-                }
-
-                questionlist = questions;
-                wordlist = sub_words.map(pair => pair[0]);
-
-                $('#startingform').hide();
-                $('#results').hide();
-                $('#createform').hide();
-                $('#guessing').show();
+                displayGuessing(guesses);
             }
         }
         else if (obj.round_result != null) {
             displayResult(obj.round_result);
         }
         else {
-            $('#status').html("Unknown Message recieved");
+            statusMessage("Unknown message retrieved");
+        }
+    }
+
+    function displayGuessing(guesses) {
+        var sub_words = guesses.submitted_words;
+        var questions = guesses.questions;
+        var questionsHtml = "";
+        for (var i = 0; i < questions.length; i++) {
+            questionsHtml += (i + 1) + ": " + questions[i] + "<br>";
         }
+        $('#guessingDynTable').html(questionsHtml);
+        $('#guessingDyn').html("");
+        for (var i = 0; i < sub_words.length; i++) {
+            var $label = $("<label to='g" + i + "'>" + sub_words[i][0] + "</label>");
+            var $field = $("<input id='g" + i + "' type='text' /><br>");
+            $('#guessingDyn').append($label);
+            $('#guessingDyn').append($field);
+        }
+
+        questionlist = questions;
+        wordlist = sub_words.map(pair => pair[0]);
+
+        
+        setView('guessing');
     }
 
     function displayResult(result) {
@@ -170,6 +220,7 @@ $(function() {
             solution_dict[sol[i][0]] = sol[i][1];
         }
         var $table = $('<table/>');
+        //$table.addClass('result-table');
 
         var wordline = "<tr><th></th>";
         for(var i = 0; i < result.words.length; i++) {
@@ -182,10 +233,10 @@ $(function() {
             var wordline = "<tr><th>" + result.questions[i] + "</th>";
             for(var j = 0; j < result.words.length; j++) {
                 if (solution_dict[result.words[j]] == result.questions[i]) {
-                    wordline += "<td bgcolor='lightgreen'>";
+                    wordline += "<td class='result-correct'>";
                 }
                 else {
-                    wordline += "<td bgcolor='pink'>";
+                    wordline += "<td class='result-wrong'>";
                 }
                 wordline += result.guesses[j][i] + "</td>";
             }
@@ -195,9 +246,51 @@ $(function() {
 
         $('#results').html($table);
 
+        setView('results');
+    }
+
+    function updatePlayerList(players) {
+        playerlist = "";
+        for (var i = 0; i < players.length; i++) {
+            playerlist += players[i].nick + "(" + players[i].points + ")";
+            if (i + 1 < players.length)
+                playerlist += "<br>";
+        }
+        $('#player-list').html(playerlist);
+        $('#lobby-control').show();
+    }
+
+    function setView(view) {
+        $('#loginform').hide();
         $('#startingform').hide();
-        $('#createform').hide();
+        $('#creating').hide();
         $('#guessing').hide();
-        $('#results').show();
+        $('#results').hide();
+
+        switch (view) {
+            case 'login':
+                $('#loginform').show();
+                break;
+            case 'starting':
+                $('#startingform').show();
+                $('#results').show();
+                break;
+            case 'creating':
+                $('#creating').show();
+                break;
+            case 'guessing':
+                $('#guessing').show();
+                break;
+            case 'results':
+                $('#results').show();
+                break;
+        }
+    }
+
+    function statusMessage(message) {
+        $('#status').html(message);
+        setTimeout(function () {
+            $('#status').html("");
+        }, 4000);
     }
 });

+ 142 - 34
static/index.html

@@ -4,44 +4,152 @@
 <head>
     <script src="jquery-3.5.1.js"></script>
     <script src="comm.js"></script>
+    <link rel="stylesheet" href="style.css">
+    <title>Eichelhäutgerät</title>
 </head>
 <body>
-<form id="loginform">
-  <label for="gameId">Game ID:</label>
-  <input id="gameId" type="text"  /><br>
-  <label for="nick">Nickname:</label>
-  <input id="nick" type="text" /><br>
-  <input id="join" type="button" value="Join" />
-  <input id="create" type="button" value="Create Game" />
-</form>
-
-<form id="startingform" style="display:none">
-    <input id="ready" type="button" value="Ready" />
-</form>
-
-<form id="createform" style="display:none">
-    <input id="question" type="text" readonly="readonly" /><br>
-    <label for="letters">Create a word with the following letters</label>
-    <input id="letters" type="text" readonly="readonly" /><br>
-    <label for="word">Word:</label>
-    <input id="word" type="text" /><br>
-    <input id="submit" type="button" value="Submit Word" />
-</form>
-<div id="guessing" style="display:none">
-    You now have to guess!
-    <div id="guessingDynTable">
+    <div class="header">
+        <h1>Welcome to Eichelhäutgerät</h1>
     </div>
+    <aside class="side-control">
+        <div id="side-info">
+            <div id="lobby-control" style="display:none">
+                <div id="lobby-info">
 
-    <form id="guessingform">
-        <div id="guessingDyn">
+                </div>
+                
+                <h3>Connected Players</h3>
+                <div id="player-list" class="player-list">
+                </div>
+                <button type="button" class="button" id="leave-lobby">Leave Lobby</button>
+            </div>
         </div>
-        <input id="submitGuess" type="button" value="Submit Guess" />
-    </form>
-</div>
-<div id="results" style="display:none">
-
-</div>
-<div id="status">
-</div>
+    </aside>
+    <div class="main_container">
+        <form id="loginform" class="loginform">
+            <div class="row">
+                <div class="label-col">
+                    <label for="gameId">Game ID:</label>
+                </div>
+                <div class="text-col">
+                    <input id="gameId" type="text" />
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="label-col">
+                    <label for="nick">Nickname:</label>
+                </div>
+                <div class="text-col">
+                    <input id="nick" type="text" />
+                </div>
+            </div>
+
+            <input id="join" type="button" value="Join" />
+            <input id="create" type="button" value="Create Game" />
+        </form>
+
+        <div id="startingform" style="display:none">
+            A new round is ready to be played. Click &quot;Ready&quot; to indicate that you are ready.
+            <form>
+                <input id="ready" type="button" value="Ready" />
+            </form>
+        </div>
+
+
+        <div id="creating" style="margin: 0px; display:none">
+            <table class="qtable">
+                <tr><td style="border:none; vertical-align:top">
+                    <div class="questiontext">
+                        Create a word sounding like...
+                        <div class="question-box" id="question-box">
+                        </div>
+                    </div>
+                </td><td style="border:none; vertical-align:top">
+                    ... using the following letters:
+                    <div class="letter-box" id="letter-box">
+                    </div>
+                </td></tr>
+            </table>
+            <form id="createform">
+                <label for="word">Word:</label>
+                <input id="word" type="text" /><br>
+                <input id="submit" type="button" value="Submit Word" />
+            </form>
+        </div>
+
+        <div id="guessing" style="display:none">
+            You now have to guess!<br><br>
+            <div id="guessingDynTable">
+            </div>
+        
+            <form id="guessingform">
+                <div id="guessingDyn">
+                </div>
+                <input id="submitGuess" type="button" value="Submit Guess" />
+            </form>
+
+            <div id="guessing-draggame" style="display: flex;flex-direction: row; min-height: 110px;">
+                <script lang="javascript">
+                    function allowDrag(e) {
+                        e.preventDefault();
+                    }
+                    function drag(e) {
+                        e.dataTransfer.setData("id", e.target.id);
+                    }
+                    function drop(e) {
+                        e.preventDefault();
+                        var id = e.dataTransfer.getData("id");
+                        var target = e.target;
+                        while (!target.classList.contains("question-dropzone") && !target.classList.contains("question-dropsource"))
+                            target = target.parentNode;
+                        
+                        var occupyingDrop = target.querySelector(".question-dropelement");
+                        var element = document.getElementById(id);
+
+                        if (occupyingDrop != null && !target.classList.contains("question-dropsource")) {
+                            element.parentNode.appendChild(occupyingDrop);
+                        }
+                        target.appendChild(element);
+                    }
+                </script>
+                <div id="dropzones" style="flex: 1;">
+                    <div id="qdz1" class="question-dropzone" ondragover="allowDrag(event)" ondrop="drop(event)">
+                        q1
+                    </div>
+                    <div id="qdz2" class="question-dropzone" ondragover="allowDrag(event)" ondrop="drop(event)">
+                        q2
+                    </div>
+                </div>
+                <div id="qdz-source" class="question-dropsource" ondragover="allowDrag(event)" ondrop="drop(event)" style="flex: 1">
+                    <p id="qde1" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        a1
+                    </p>
+                    <p id="qde2" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        a2
+                    </p>
+                    <p id="qde3" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        ae
+                    </p>
+                    <p id="qde4" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        wea2
+                    </p>
+                    <p id="qde5" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        aqqqwq2
+                    </p>
+                    <p id="qde6" class="question-dropelement" draggable="true" ondragstart="drag(event)">
+                        aganz langa text wo muas azeigt werda
+                    </p>
+                </div>
+
+            </div>
+        </div>
+        <div id="results" style="margin-top: 20px; display:none">
+        
+        </div>
+        <div id="status">
+        </div>
+    </div>
+
 </body>
 </html>

+ 207 - 0
static/style.css

@@ -0,0 +1,207 @@
+:root {
+    --color-brighter: #646464;
+    --color-bright: #4d4d4d;
+    --color-dark: #353535;
+    --color-darker: #202020;
+
+    --color2: #921010;
+    --color2-darker: #5a0e11;
+
+    --color3-bright: #4a4a66;
+    --color3: #41415c;
+    --color3-dark: #242438;
+    --color3-darker: #161622;
+
+    --font-color: #d0d0d0;
+}
+
+body {
+    background-color: var(--color-dark);
+    color: var(--font-color);
+    font-family: Arial, Helvetica, sans-serif;
+    font-size: large;
+}
+
+.header {
+    left: 0;
+    right: 0;
+    margin: 0 auto;
+    min-height: 140px;
+    max-width: 960px;
+}
+
+.loginform {
+    width: 460px;
+    align-content: stretch;
+    display: table;
+}
+
+.main_container {
+    left: 0;
+    right: 0;
+    margin: 0 auto;
+    max-width: 960px;
+    padding: 20px;
+    align-content: stretch;
+    border-radius: 12px;
+    background-color: var(--color-darker);
+}
+
+.side-control {
+    float: left;
+    align-content: right;
+}
+
+input[type="button"], input[type="submit"], button {
+    border: none;
+    border-radius: 4px;
+    color: white;
+    padding: 8px 15px;
+    text-align: center;
+    text-decoration: none;
+    display: inline-block;
+    margin: 6px 0px;
+    font-size: 16px;
+    box-shadow: 0 4px 10px 0 rgba(0,0,0,0.3);
+
+    background-color: var(--color2-darker);
+    -webkit-transition-duration: 0.3s; /* Safari */
+    transition-duration: 0.3s;
+}
+
+input[type="button"]:hover, input[type="submit"]:hover, button:hover {
+    background-color: var(--color2);
+}
+
+input[type="text"], textarea {
+    border: 1px solid var(--color3);
+    border-radius: 4px;
+    color: white;
+    padding: 5px 10px;
+    text-decoration: none;
+    margin: 4px 2px;
+    font-size: 16px;
+
+    background-color: var(--color3-darker);
+    -webkit-transition-duration: 0.3s; /* Safari */
+    transition-duration: 0.3s;
+}
+
+input[type="text"]:focus, textarea:focus {
+    background-color: var(--color3-dark);
+}
+
+label {
+    padding: 12px 12px 12px 0;
+    display: inline-block;
+}
+
+.label-col {
+    float: left;
+    width: 25%;
+    margin-top: 4px;
+}
+
+.text-col {
+    float: left;
+    width: 75%;
+    margin-top: 4px;
+}
+
+.row {
+    width: 100%;
+}
+.row:after {
+    content: "";
+    display: table;
+    clear: both;
+}
+
+.question-box {
+    width: intrinsic;
+    width: -moz-max-content;
+    width: -webkit-max-content;
+    border: 2px solid var(--color2-darker);
+    border-radius: 8px;
+    background-color: var(--color3-darker);
+    padding: 30px 20px;
+    margin: 20px;
+    font-size: 16px;
+}
+
+.letter-box {
+    width: intrinsic;
+    width: -moz-max-content;
+    width: -webkit-max-content;
+    border: 2px solid var(--color2-darker);
+    border-radius: 8px;
+    background-color: var(--color3-darker);
+    padding: 30px 20px;
+    margin: 20px;
+    font-size: 24px;
+}
+
+.question-dropzone {
+    height: 20px;
+    border: 2px solid var(--color2-darker);
+    border-radius: 8px;
+    background-color: var(--color3-darker);
+    padding: 8px 8px;
+    margin: 12px;
+    font-size: 16px;
+    position: relative;
+}
+
+
+.question-dropsource {
+    min-height: max-content;
+    border: 2px solid var(--color2-darker);
+    border-radius: 8px;
+    background-color: var(--color3-darker);
+    padding: 8px 8px;
+    margin: 12px;
+    font-size: 16px;
+    position: relative;
+}
+
+.question-dropelement {
+    width: intrinsic;
+    width: -moz-max-content;
+    width: -webkit-max-content;
+    border: 0px;
+    border-radius: 4px;
+    background-color: var(--color3-bright);
+    padding: 6px 6px;
+    margin: 6px;
+    font-size: 16px;
+    height: 18px;
+    cursor:move;
+    position: relative;
+    float: right;
+    -ms-transform: translate(0%, -11px);
+    transform: translate(0%, -11px);
+}
+
+.qtable {
+    width: 100%;
+    border: none;
+}
+
+.result-table {
+    font-family: Arial, Helvetica, sans-serif;
+    border-collapse: collapse;
+    margin: 12px 0px;
+    width: 70%;
+}
+
+td, th {
+    border: 1px solid #2b2b4d;
+    padding: 8px;
+}
+
+.result-correct {
+    background-color: #005218;
+}
+.result-wrong {
+    background-color: #520000;
+}