From 3dce1ecbdc99e7afbcb0338e585b4e587e0c6cd2 Mon Sep 17 00:00:00 2001
From: Carl Worth <cworth@cworth.org>
Date: Sat, 13 Jun 2020 15:02:14 -0700
Subject: [PATCH] Add a "Move On" button to the end of both the answering and
 judging phases

These use the same voting style as the prompt voting, and allow the
game to proceed once a majority of players have voted to move on. This
should help avoid the game being locked out just because a single
player has decided not to play anymore, or somehow became disconnected
(which has already happened in practice more than once).

With this change, we're also now displaying the actual names of the
players that have already answered/judged (as opposed to just the
count as we were displaying previously). And we've written code to
also display the list of player names who are still in the
answering/judging process but that list is not yet being populated
(we'll need just a little more plumbing to have the client send an
activity ping to the server when typing/tapping/clicking before
submitting).
---
 empathy/empathy.jsx | 222 +++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 201 insertions(+), 21 deletions(-)

diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx
index dee5a67..1a9ad80 100644
--- a/empathy/empathy.jsx
+++ b/empathy/empathy.jsx
@@ -57,6 +57,8 @@ events.addEventListener("player-update", event => {
 events.addEventListener("game-state", event => {
   const state = JSON.parse(event.data);
 
+  window.game.reset_game_state();
+
   window.game.set_prompts(state.prompts);
 
   window.game.set_active_prompt(state.active_prompt);
@@ -78,10 +80,28 @@ events.addEventListener("start", event => {
   window.game.set_active_prompt(prompt);
 });
 
-events.addEventListener("answered", event => {
-  const players_answered = JSON.parse(event.data);
+events.addEventListener("player-answered", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_answered(player);
+});
+
+events.addEventListener("player-answering", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_answering(player);
+});
+
+events.addEventListener("vote-end-answers", event => {
+  const player = JSON.parse(event.data);
 
-  window.game.set_players_answered(players_answered);
+  window.game.set_player_vote_end_answers(player);
+});
+
+events.addEventListener("unvote-end-answers", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_unvote_end_answers(player);
 });
 
 events.addEventListener("ambiguities", event => {
@@ -90,10 +110,28 @@ events.addEventListener("ambiguities", event => {
   window.game.set_ambiguities(ambiguities);
 });
 
-events.addEventListener("judged", event => {
-  const players_judged = JSON.parse(event.data);
+events.addEventListener("player-judged", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_judged(player);
+});
+
+events.addEventListener("player-judging", event => {
+  const player = JSON.parse(event.data);
 
-  window.game.set_players_judged(players_judged);
+  window.game.set_player_judging(player);
+});
+
+events.addEventListener("vote-end-judging", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_vote_end_judging(player);
+});
+
+events.addEventListener("unvote-end-judging", event => {
+  const player = JSON.parse(event.data);
+
+  window.game.set_player_unvote_end_judging(player);
 });
 
 events.addEventListener("scores", event => {
@@ -225,7 +263,6 @@ class CategoryRequest extends React.PureComponent {
 
     if (response.status === 200) {
       const result = await response.json();
-      console.log(result);
       if (! result.valid) {
         add_message("danger", result.message);
         return;
@@ -436,11 +473,46 @@ class Ambiguities extends React.PureComponent {
     if (this.state.submitted)
       return (
         <div className="please-wait">
-          <h2>{this.props.players_judged}/
-            {this.props.players_total} players have responded</h2>
+          <h2>Submission received</h2>
           <p>
-            Please wait for the rest of the players to complete judging.
+            The following players have completed judging:
+            {[...this.props.players_judged].join(', ')}
           </p>
+          <p>
+            Still waiting for the following players:
+          </p>
+          <ul>
+            {Object.entries(this.props.players_judging).map(player => {
+              return (
+                <li
+                  key={player}
+                >
+                  {player}
+                  {this.props.players_judging[player] ?
+                   <span className="typing"/> : null }
+                </li>
+              );
+            })}
+          </ul>
+          <button
+            className="vote-button"
+            onClick={() => fetch_post_json(`end-judging/${this.props.prompt.id}`) }
+          >
+            Move On
+            <div className="vote-choices">
+              {[...this.props.votes].map(v => {
+                return (
+                  <div
+                    key={v}
+                    className="vote-choice"
+                  >
+                    {v}
+                  </div>
+                );
+              })}
+            </div>
+          </button>
+
         </div>
       );
 
@@ -535,11 +607,46 @@ class ActivePrompt extends React.PureComponent {
     if (this.state.submitted)
       return (
         <div className="please-wait">
-          <h2>{this.props.players_answered}/
-            {this.props.players_total} players have responded</h2>
+          <h2>Submission received</h2>
           <p>
-            Please wait for the rest of the players to submit their answers.
+            The following players have submitted their answers:
+            {[...this.props.players_answered].join(', ')}
           </p>
+          <p>
+          Still waiting for the following players:
+          </p>
+          <ul>
+            {Object.entries(this.props.players_answering).map(player => {
+              return (
+                <li
+                  key={player}
+                >
+                  {player}
+                  {this.props.players_answering[player] ?
+                   <span className="typing"/> : null }
+                </li>
+              );
+            })}
+          </ul>
+          <button
+            className="vote-button"
+            onClick={() => fetch_post_json(`end-answers/${this.props.prompt.id}`) }
+          >
+            Move On
+            <div className="vote-choices">
+              {[...this.props.votes].map(v => {
+                return (
+                  <div
+                    key={v}
+                    className="vote-choice"
+                  >
+                    {v}
+                  </div>
+                );
+              })}
+            </div>
+          </button>
+
         </div>
       );
 
@@ -592,9 +699,14 @@ class Game extends React.PureComponent {
       other_players: [],
       prompts: [],
       active_prompt: null,
-      players_answered: 0,
+      players_answered: new Set(),
+      players_answering: {},
+      end_answers_votes: new Set(),
       ambiguities: null,
-      players_judged: 0
+      players_judged: new Set(),
+      players_judging: {},
+      end_judging_votes: new Set(),
+      scores: null
     };
   }
 
@@ -623,6 +735,21 @@ class Game extends React.PureComponent {
     });
   }
 
+  reset_game_state() {
+    this.setState({
+      prompts: [],
+      active_prompt: null,
+      players_answered: new Set(),
+      players_answering: {},
+      end_answers_votes: new Set(),
+      ambiguities: null,
+      players_judged: new Set(),
+      players_judging: {},
+      end_judging_votes: new Set(),
+      scores: null
+    });
+  }
+
   set_prompts(prompts) {
     this.setState({
       prompts: prompts
@@ -648,9 +775,34 @@ class Game extends React.PureComponent {
     });
   }
 
-  set_players_answered(players_answered) {
+  set_player_answered(player) {
+    const new_players_answering = {...this.state.players_answering};
+    delete new_players_answering[player];
+
     this.setState({
-      players_answered: players_answered
+      players_answered: new Set([...this.state.players_answered, player]),
+      players_answering: new_players_answering
+    });
+  }
+
+  set_player_answering(player) {
+    this.setState({
+      players_answering: {
+        ...this.state.players_answering,
+        [player]: {active: true}
+      }
+    });
+  }
+
+  set_player_vote_end_answers(player) {
+    this.setState({
+      end_answers_votes: new Set([...this.state.end_answers_votes, player])
+    });
+  }
+
+  set_player_unvote_end_answers(player) {
+    this.setState({
+      end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
     });
   }
 
@@ -660,9 +812,35 @@ class Game extends React.PureComponent {
     });
   }
 
-  set_players_judged(players_judged) {
+  set_player_judged(player) {
+    const new_players_judging = {...this.state.players_judging};
+    delete new_players_judging[player];
+
+    this.setState({
+      players_judged: new Set([...this.state.players_judged, player]),
+      players_judging: new_players_judging
+    });
+  }
+
+  set_player_judging(player) {
+    this.setState({
+      players_judging: {
+        ...this.state.players_judging,
+        [player]: {active: true}
+      }
+    });
+  }
+
+
+  set_player_vote_end_judging(player) {
+    this.setState({
+      end_judging_votes: new Set([...this.state.end_judging_votes, player])
+    });
+  }
+
+  set_player_unvote_end_judging(player) {
     this.setState({
-      players_judged: players_judged
+      end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
     });
   }
 
@@ -714,7 +892,8 @@ class Game extends React.PureComponent {
                prompt={state.active_prompt}
                words={state.ambiguities}
                players_judged={state.players_judged}
-               players_total={players_total}
+               players_judging={state.players_judging}
+               votes={state.end_judging_votes}
              />;
     }
 
@@ -722,7 +901,8 @@ class Game extends React.PureComponent {
       return <ActivePrompt
                prompt={state.active_prompt}
                players_answered={state.players_answered}
-               players_total={players_total}
+               players_answering={state.players_answering}
+               votes={state.end_answers_votes}
              />;
     }
 
-- 
2.45.2