From 3dae3833cb5247943e27649a9b16639c2187c8c5 Mon Sep 17 00:00:00 2001
From: Carl Worth <cworth@cworth.org>
Date: Sat, 27 Jun 2020 17:21:38 -0700
Subject: [PATCH] Two alterations to player scoring: per-round grouping, and
 round normalization

Previously, we were returning an array of player scores where some
succesive players in the array may have had identical scores. Now,
instead, we have only one entry per score, and instead of just a
single player name, an array of player names in case there is a tie at
any score. This should help clients display per-round scores in a way
that make all the ties obvious.

Second, we were previously accumulating the per-round points directly
into a player's total. This had the defect of weighting some rounds
more than others, (rounds with more items were worth a higher maximum
number of points than rounds with fewer items). Now, instead, we only
accumulate into a player's total the number of players that they beat
out in each round. This gives an equal weight to every round.

The test suite is updated in this commit for the first fix above. But
the test suite does not yet cover the player's overall scores so that
change is not tested here.
---
 empathy.js | 47 +++++++++++++++++++++++++++++++++++------------
 test       |  6 +++---
 2 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/empathy.js b/empathy.js
index fdf62a1..7b27ee4 100644
--- a/empathy.js
+++ b/empathy.js
@@ -50,17 +50,28 @@ class Empathy extends Game {
 
   reset() {
 
-    /* Before closing out the current round, we accumulate the score
-     * for each player into their runnning total. */
-    for (let score of this.state.scores.scores) {
-      const player = this.players.find(p => p.name === score.player);
-      if (player.score)
-        player.score += score.score;
-      else
-        player.score = score.score;
-
-      /* And broadcast that new score out. */
-      this.broadcast_event('player-update', player.info_json());
+    /* Before closing out the current round, we accumulate into each
+     * player's overall score the results from the current round.
+     *
+     * Note: Rather than applying the actual points from each round
+     * into the player's score, we instead accumulate up the number of
+     * players that they bested in each round. This ensures that each
+     * round receives an equal weight in the overall scoring. */
+    let bested = this.state.scores.scores.reduce(
+      (total, score) => total + score.players.length, 0);
+    for (let i = 0; i < this.state.scores.scores.length; i++) {
+      const score = this.state.scores.scores[i];
+      bested -= score.players.length;
+      for (let player_name of score.players) {
+        const player = this.players.find(p => p.name === player_name);
+        if (player.score)
+          player.score += bested;
+        else
+          player.score = bested;
+
+        /* And broadcast that new score out. */
+        this.broadcast_event('player-update', player.info_json());
+      }
     }
 
     /* Now that we're done with the active prompt, we remove it from
@@ -513,6 +524,18 @@ class Empathy extends Game {
       return b.score - a.score;
     });
 
+    /* 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)
+        list[list.length-1].players.push(next.player);
+      else
+        list.push({players: [next.player], score: next.score});
+      return list;
+    };
+
+    const grouped_scores = scores.reduce(reducer, []);
+
     /* Put the word groups into a form the client can consume.
      */
     const words_submitted = word_groups.map(
@@ -531,7 +554,7 @@ class Empathy extends Game {
     /* Put this round's scores into the game state object so it will
      * be sent to any new clients that join. */
     this.state.scores = {
-      scores: scores,
+      scores: grouped_scores,
       words: words_submitted
     };
 
diff --git a/test b/test
index f62d682..fcb556c 100755
--- a/test
+++ b/test
@@ -690,7 +690,7 @@ TEST_END
 # Usage: empathy_scores_names_numbers <player_name>
 empathy_scores_names_numbers()
 {
-    empathy_get_event $1 game-state | jq '.scores.scores[]|.player,.score'
+    empathy_get_event $1 game-state | jq '.scores.scores[]|.players[],.score'
 }
 
 TEST_SUBSECTION "Scoring"
@@ -890,7 +890,7 @@ TEST_END
 TEST "Verify the match passed the vote"
 # echo here is to strip newlines
 result=$(echo $(empathy_scores_names_numbers alice))
-test "$result" = '"alice" 2 "bob" 2 "charlie" 0 "dale" 0 "eric" 0 "fred" 0'
+test "$result" = '"alice" "bob" 2 "charlie" "dale" "eric" "fred" 0'
 TEST_END
 
 echo ""
@@ -929,7 +929,7 @@ TEST_END
 TEST "Verify scores don't include inactive players"
 # echo here is to strip newlines
 result=$(echo $(empathy_scores_names_numbers alice))
-test "$result" = '"alice" 1 "bob" 1 "charlie" 0'
+test "$result" = '"alice" "bob" 1 "charlie" 0'
 TEST_END
 
 TEST_SUBSECTION "Deactivated players don't block future game phase advances"
-- 
2.45.2