]> git.cworth.org Git - empires-server/blobdiff - empathy.js
Add some autofocus attributes to several forms
[empires-server] / empathy.js
index ba25cc4c49d7e996ba2c93505efc0ab2ba75b9d8..d6b007b4bc3ef75d32230cde000917cdacdb7fa4 100644 (file)
@@ -19,7 +19,7 @@ const PHASE_MINIMUM_TIME = 30;
  *
  * Specified in seconds
  */
-const PHASE_IDLE_TIMEOUT = 30;
+const PHASE_IDLE_TIMEOUT = 15;
 
 class Empathy extends Game {
   constructor(id) {
@@ -46,6 +46,7 @@ class Empathy extends Game {
     this.judging_start_time_ms = 0;
     this.next_prompt_id = 1;
     this.equivalencies = {};
+    this.kudos = {};
   }
 
   reset() {
@@ -76,12 +77,13 @@ class Empathy extends Game {
 
     /* Now that we're done with the active prompt, we remove it from
      * the list of prompts and also remove any prompts that received
-     * no votes. This keeps the list of prompts clean.
+     * more negative votes than positive. This keeps the list of
+     * prompts clean.
      */
     const active_id = this.state.active_prompt.id;
     this.state.prompts =
       this.state.prompts.filter(
-        p => p.id !== active_id && p.votes.length > 0
+        p => p.id !== active_id && p.votes.length >= p.votes_against.length
       );
 
     this.state.active_prompt = null;
@@ -109,16 +111,25 @@ class Empathy extends Game {
     this.judging_idle_timer = 0;
     this.judging_start_time_ms = 0;
     this.equivalencies = {};
+    this.kudos = {};
 
     this.broadcast_event('game-state', this.game_state_json());
   }
 
   add_prompt(items, prompt_string) {
-    if (items > MAX_PROMPT_ITEMS)
+    if (items > MAX_PROMPT_ITEMS) {
       return {
         valid: false,
         message: `Maximum number of items is ${MAX_PROMPT_ITEMS}`
       };
+    }
+
+    if (items < 1) {
+      return {
+        valid: false,
+        message: "Category must require at least one item"
+      };
+    }
 
     const prompt = new Prompt(this.next_prompt_id, items, prompt_string);
     this.next_prompt_id++;
@@ -148,6 +159,20 @@ class Empathy extends Game {
     return true;
   }
 
+  toggle_vote_against(prompt_id, session_id) {
+    const player = this.players_by_session[session_id];
+
+    const prompt = this.state.prompts.find(p => p.id === prompt_id);
+    if (! prompt || ! player)
+      return false;
+
+    prompt.toggle_vote_against(player.name);
+
+    this.broadcast_event_object('prompt', prompt);
+
+    return true;
+  }
+
   /* Returns true on success, false for prompt not found. */
   start(prompt_id) {
     const prompt = this.state.prompts.find(p => p.id === prompt_id);
@@ -322,15 +347,23 @@ class Empathy extends Game {
      */
     for (let group of word_groups) {
 
-      for (let i = 0; i < group.length - 1; i++) {
-        for (let j = i + 1; j < group.length; j++) {
-          let eq = [group[i], group[j]];
+      const words = group.words;
+      if (group.kudos) {
+        this.kudos[player.name] = {
+          player: player,
+          words: [...words]
+        };
+      }
+
+      for (let i = 0; i < words.length - 1; i++) {
+        for (let j = i + 1; j < words.length; j++) {
+          let eq = [words[i], words[j]];
 
           /* Put the two words into a reliable order so that we don't
            * miss a pair of equivalent equivalencies just because they
            * happen to be in the opposite order. */
           if (eq[0].localeCompare(eq[1]) > 0) {
-            eq = [group[j], group[i]];
+            eq = [words[j], words[i]];
           }
 
           const key=`${this.canonize(eq[0])}:${this.canonize(eq[1])}`;
@@ -457,7 +490,11 @@ class Empathy extends Game {
       if (! group)
         group = word_maps[word1_canon];
       if (! group)
-        group = { words: [], players: new Set()};
+        group = {
+          words: [],
+          players: new Set(),
+          kudos: new Set()
+        };
 
       if (! word_maps[word0_canon]) {
         word_maps[word0_canon] = group;
@@ -479,13 +516,33 @@ class Empathy extends Game {
         if (word_maps[word_canon]) {
           word_maps[word_canon].players.add(a.player);
         } else {
-          const group = { words: [word], players: new Set() };
+          const group = {
+            words: [word],
+            players: new Set(),
+            kudos: new Set()
+          };
           group.players.add(a.player);
           word_maps[word_canon] = group;
         }
       }
     }
 
+    /* Apply kudos from each player to the word maps, (using a set so
+     * that no word_map can get multiple kudos from a single
+     * player). */
+    for (let kudos of Object.values(this.kudos)) {
+      for (let word of kudos.words) {
+        const word_canon = this.canonize(word);
+        if (! word_maps[word_canon])
+          continue;
+        /* Don't let any player give kudos to a group where they
+         * submitted a word themself. That just wouldn't be right. */
+        if (! word_maps[word_canon].players.has(kudos.player)) {
+          word_maps[word_canon].kudos.add(kudos.player);
+        }
+      }
+    }
+
     /* Now that we've assigned the players to these word maps, we now
      * want to collapse the groups down to a single array of
      * word_groups.
@@ -504,33 +561,52 @@ class Empathy extends Game {
      *
      * Note: We do this by going through the word groups, (as opposed
      * to the list of words from the players again), specifically to
-     * avoid giving a player points for a wrod group twice (in the
+     * avoid giving a player points for a word group twice (in the
      * case where a player submits two different words that the group
      * ends up judging as equivalent).
      */
-    this.players.forEach(p => p.round_score = 0);
+    this.players.forEach(p => {
+      p.round_score = 0;
+      p.round_kudos = 0;
+    });
     for (let group of word_groups) {
-      group.players.forEach(p => p.round_score += group.players.size);
+      group.players.forEach(p => {
+        p.round_score += group.players.size;
+        p.round_kudos += group.kudos.size;
+      });
     }
 
     const scores = this.players.filter(p => p.active).map(p => {
       return {
         player: p.name,
-        score: p.round_score
+        score: p.round_score,
+        kudos: p.round_kudos
       };
     });
 
     scores.sort((a,b) => {
-      return b.score - a.score;
+      const delta = b.score - a.score;
+      if (delta)
+        return delta;
+      return b.kudos - a.kudos;
     });
 
     /* After sorting individual players by score, group players
      * together who have the same score. */
     const reducer = (list, next) => {
-      if (list.length && list[list.length-1].score == next.score)
+      if (list.length
+          && list[list.length-1].score == next.score
+          && list[list.length-1].kudos == next.kudos
+         )
+      {
         list[list.length-1].players.push(next.player);
-      else
-        list.push({players: [next.player], score: next.score});
+      } else {
+        list.push({
+          players: [next.player],
+          score: next.score,
+          kudos: next.kudos,
+        });
+      }
       return list;
     };
 
@@ -542,13 +618,17 @@ class Empathy extends Game {
       group => {
         return {
           word: group.words.join('/'),
-          players: Array.from(group.players).map(p => p.name)
+          players: Array.from(group.players).map(p => p.name),
+          kudos: Array.from(group.kudos).map(p => p.name)
         };
       }
     );
 
     words_submitted.sort((a,b) => {
-      return b.players.length - a.players.length;
+      const delta = b.players.length - a.players.length;
+      if (delta !== 0)
+        return delta;
+      return b.kudos.length - a.kudos.length;
     });
 
     /* Put this round's scores into the game state object so it will
@@ -572,6 +652,7 @@ class Prompt {
     this.items = items;
     this.prompt = prompt;
     this.votes = [];
+    this.votes_against = [];
   }
 
   toggle_vote(player_name) {
@@ -580,6 +661,17 @@ class Prompt {
     else
       this.votes.push(player_name);
   }
+
+  toggle_vote_against(player_name) {
+    if (this.votes_against.find(v => v === player_name)) {
+      this.votes_against = this.votes_against.filter(v => v !== player_name);
+    } else {
+      this.votes_against.push(player_name);
+      /* When voting against, we also remove any vote _for_ the same
+       * prompt. */
+      this.votes = this.votes.filter(v => v !== player_name);
+    }
+  }
 }
 
 router.post('/prompts', (request, response) => {
@@ -600,6 +692,16 @@ router.post('/vote/:prompt_id([0-9]+)', (request, response) => {
     response.sendStatus(404);
 });
 
+router.post('/vote_against/:prompt_id([0-9]+)', (request, response) => {
+  const game = request.game;
+  const prompt_id = parseInt(request.params.prompt_id, 10);
+
+  if (game.toggle_vote_against(prompt_id, request.session.id))
+    response.send('');
+  else
+    response.sendStatus(404);
+});
+
 router.post('/start/:prompt_id([0-9]+)', (request, response) => {
   const game = request.game;
   const prompt_id = parseInt(request.params.prompt_id, 10);