]> git.cworth.org Git - lmno.games/blob - empathy/empathy.jsx
empathy: Add button to hide/(vote against) categories
[lmno.games] / empathy / empathy.jsx
1 const MAX_PROMPT_ITEMS = 20;
2
3 function undisplay(element) {
4   element.style.display="none";
5 }
6
7 function add_message(severity, message) {
8   message = `<div class="message ${severity}" onclick="undisplay(this)">
9 <span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
10 ${message}
11 </div>`;
12   const message_area = document.getElementById('message-area');
13   message_area.insertAdjacentHTML('beforeend', message);
14 }
15
16 /*********************************************************
17  * Handling server-sent event stream                     *
18  *********************************************************/
19
20 const events = new EventSource("events");
21
22 events.onerror = function(event) {
23   if (event.target.readyState === EventSource.CLOSED) {
24     setTimeout(() => {
25       add_message("danger", "Connection to server lost.");
26     }, 1000);
27   }
28 };
29
30 events.addEventListener("game-info", event => {
31   const info = JSON.parse(event.data);
32
33   window.game.set_game_info(info);
34 });
35
36 events.addEventListener("player-info", event => {
37   const info = JSON.parse(event.data);
38
39   window.game.set_player_info(info);
40 });
41
42 events.addEventListener("player-enter", event => {
43   const info = JSON.parse(event.data);
44
45   window.game.set_other_player_info(info);
46 });
47
48 events.addEventListener("player-exit", event => {
49   const info = JSON.parse(event.data);
50
51   window.game.remove_player(info);
52 });
53
54 events.addEventListener("player-update", event => {
55   const info = JSON.parse(event.data);
56
57   if (info.id === window.game.state.player_info.id)
58     window.game.set_player_info(info);
59   else
60     window.game.set_other_player_info(info);
61 });
62
63 events.addEventListener("game-state", event => {
64   const state = JSON.parse(event.data);
65
66   window.game.reset_game_state();
67
68   window.game.set_prompts(state.prompts);
69
70   window.game.set_active_prompt(state.active_prompt);
71
72   window.game.set_players_answered(state.players_answered);
73
74   window.game.set_players_answering(state.players_answering);
75
76   window.game.set_answering_idle(state.answering_idle);
77
78   window.game.set_end_answers(state.end_answers);
79
80   window.game.set_ambiguities(state.ambiguities);
81
82   window.game.set_players_judged(state.players_judged);
83
84   window.game.set_players_judging(state.players_judging);
85
86   window.game.set_judging_idle(state.judging_idle);
87
88   window.game.set_end_judging(state.end_judging);
89
90   window.game.set_scores(state.scores);
91
92   window.game.set_new_game_votes(state.new_game_votes);
93
94   window.game.state_ready();
95 });
96
97 events.addEventListener("prompt", event => {
98   const prompt = JSON.parse(event.data);
99
100   window.game.add_or_update_prompt(prompt);
101 });
102
103 events.addEventListener("start", event => {
104   const prompt = JSON.parse(event.data);
105
106   window.game.set_active_prompt(prompt);
107 });
108
109 events.addEventListener("player-answered", event => {
110   const player = JSON.parse(event.data);
111
112   window.game.set_player_answered(player);
113 });
114
115 events.addEventListener("player-answering", event => {
116   const player = JSON.parse(event.data);
117
118   window.game.set_player_answering(player);
119 });
120
121 events.addEventListener("answering-idle", event => {
122   const value = JSON.parse(event.data);
123
124   window.game.set_answering_idle(value);
125 });
126
127 events.addEventListener("vote-end-answers", event => {
128   const player = JSON.parse(event.data);
129
130   window.game.set_player_vote_end_answers(player);
131 });
132
133 events.addEventListener("unvote-end-answers", event => {
134   const player = JSON.parse(event.data);
135
136   window.game.set_player_unvote_end_answers(player);
137 });
138
139 events.addEventListener("ambiguities", event => {
140   const ambiguities = JSON.parse(event.data);
141
142   window.game.set_ambiguities(ambiguities);
143 });
144
145 events.addEventListener("player-judged", event => {
146   const player = JSON.parse(event.data);
147
148   window.game.set_player_judged(player);
149 });
150
151 events.addEventListener("player-judging", event => {
152   const player = JSON.parse(event.data);
153
154   window.game.set_player_judging(player);
155 });
156
157 events.addEventListener("judging-idle", event => {
158   const value = JSON.parse(event.data);
159
160   window.game.set_judging_idle(value);
161 });
162
163 events.addEventListener("vote-end-judging", event => {
164   const player = JSON.parse(event.data);
165
166   window.game.set_player_vote_end_judging(player);
167 });
168
169 events.addEventListener("unvote-end-judging", event => {
170   const player = JSON.parse(event.data);
171
172   window.game.set_player_unvote_end_judging(player);
173 });
174
175 events.addEventListener("scores", event => {
176   const scores = JSON.parse(event.data);
177
178   window.game.set_scores(scores);
179 });
180
181 events.addEventListener("vote-new-game", event => {
182   const player = JSON.parse(event.data);
183
184   window.game.set_player_vote_new_game(player);
185 });
186
187 events.addEventListener("unvote-new-game", event => {
188   const player = JSON.parse(event.data);
189
190   window.game.set_player_unvote_new_game(player);
191 });
192
193 /*********************************************************
194  * Game and supporting classes                           *
195  *********************************************************/
196
197 function copy_to_clipboard(id)
198 {
199   const tmp = document.createElement("input");
200   tmp.setAttribute("value", document.getElementById(id).innerHTML);
201   document.body.appendChild(tmp);
202   tmp.select();
203   document.execCommand("copy");
204   document.body.removeChild(tmp);
205 }
206
207 const GameInfo = React.memo(props => {
208   if (! props.id)
209     return null;
210
211   return (
212     <div className="game-info">
213       <span className="game-id">{props.id}</span>
214       {" "}
215       Share this link to invite friends:{" "}
216       <span id="game-share-url">{props.url}</span>
217       {" "}
218       <button
219         className="inline"
220         onClick={() => copy_to_clipboard('game-share-url')}
221       >Copy Link</button>
222     </div>
223   );
224 });
225
226 const PlayerInfo = React.memo(props => {
227   if (! props.player.id)
228     return null;
229
230   const all_players = [props.player, ...props.other_players];
231
232   const sorted_players = all_players.sort((a,b) => {
233     return b.score - a.score;
234   });
235
236   const names_and_scores = sorted_players.map(player => {
237     if (player.score)
238       return `${player.name} (${player.score})`;
239     else
240       return player.name;
241   }).join(', ');
242
243   return (
244     <div className="player-info">
245       <span className="players-header">Players: </span>
246       <span>{names_and_scores}</span>
247     </div>
248   );
249 });
250
251 function fetch_method_json(method, api = '', data = {}) {
252   const response = fetch(api, {
253     method: method,
254     headers: {
255       'Content-Type': 'application/json'
256     },
257     body: JSON.stringify(data)
258   });
259   return response;
260 }
261
262 function fetch_post_json(api = '', data = {}) {
263   return fetch_method_json('POST', api, data);
264 }
265
266 async function fetch_put_json(api = '', data = {}) {
267   return fetch_method_json('PUT', api, data);
268 }
269
270 class CategoryRequest extends React.PureComponent {
271   constructor(props) {
272     super(props);
273     this.category = React.createRef();
274
275     this.handle_change = this.handle_change.bind(this);
276     this.handle_submit = this.handle_submit.bind(this);
277   }
278
279   handle_change(event) {
280     const category_input = this.category.current;
281     const category = category_input.value;
282
283     const match = category.match(/[0-9]+/);
284     if (match) {
285       const num_items = parseInt(match[0], 10);
286       if (num_items <= MAX_PROMPT_ITEMS)
287         category_input.setCustomValidity("");
288     }
289   }
290
291   async handle_submit(event) {
292     const form = event.currentTarget;
293     const category_input = this.category.current;
294     const category = category_input.value;
295
296     /* Prevent the default page-changing form-submission behavior. */
297     event.preventDefault();
298
299     const match = category.match(/[0-9]+/);
300     if (match === null) {
301       category_input.setCustomValidity("Category must include a number");
302       form.reportValidity();
303       return;
304     }
305
306     const num_items = parseInt(match[0], 10);
307
308     if (num_items > MAX_PROMPT_ITEMS) {
309       category_input.setCustomValidity(`Maximum number of items is ${MAX_PROMPT_ITEMS}`);
310       form.reportValidity();
311       return;
312     }
313
314     const response = await fetch_post_json("prompts", {
315       items: num_items,
316       prompt: category
317     });
318
319     if (response.status === 200) {
320       const result = await response.json();
321       if (! result.valid) {
322         add_message("danger", result.message);
323         return;
324       }
325     } else {
326       add_message("danger", "An error occurred submitting your category");
327     }
328
329     form.reset();
330   }
331
332   render() {
333     return (
334       <div className="category-request">
335         <h2>Submit a Category</h2>
336         <p>
337           Suggest a category to play. Don't forget to include the
338           number of items for each person to submit.
339         </p>
340
341         <form onSubmit={this.handle_submit} >
342           <div className="form-field large">
343             <input
344               type="text"
345               id="category"
346               placeholder="6 things at the beach"
347               required
348               autoComplete="off"
349               onChange={this.handle_change}
350               ref={this.category}
351             />
352           </div>
353
354           <div className="form-field large">
355             <button type="submit">
356               Send
357             </button>
358           </div>
359
360         </form>
361       </div>
362     );
363   }
364 }
365
366 const PromptOption = React.memo(props => {
367
368   const prompt = props.prompt;
369   const [show_prompt, set_show_prompt] = React.useState(true);
370
371   if (! show_prompt)
372     return false;
373
374   if (prompt.votes_against.find(v => v === props.player.name))
375     return false;
376
377   return (
378     <button
379       className="vote-button"
380       key={prompt.id}
381       onClick={() => fetch_post_json(`vote/${prompt.id}`) }
382     >
383       <span
384         className="hide-button"
385         onClick={(event) => {
386           event.stopPropagation();
387           fetch_post_json(`vote_against/${prompt.id}`);
388           set_show_prompt(false);
389         }}
390       >
391         &times;
392       </span>
393       {prompt.prompt}
394       <div className="vote-choices">
395         {prompt.votes.map(v => {
396           return (
397             <div
398               key={v}
399               className="vote-choice"
400             >
401               {v}
402             </div>
403           );
404         })}
405       </div>
406     </button>
407   );
408 });
409
410 const PromptOptions = React.memo(props => {
411
412   if (props.prompts.length === 0)
413     return null;
414
415   return (
416     <div className="prompt-options">
417       <h2>Vote on Categories</h2>
418       <p>
419         Select any categories below that you'd like to play.
420         You can choose as many as you'd like.
421       </p>
422     {props.prompts.map(p => <PromptOption prompt={p} player={props.player} />)}
423     </div>
424   );
425 });
426
427 const LetsPlay = React.memo(props => {
428
429   const quorum = Math.round((props.num_players + 1) / 2);
430   const max_votes = props.prompts.reduce(
431     (max_so_far, v) => Math.max(max_so_far, v.votes.length), 0);
432
433   if (max_votes < quorum)
434     return null;
435
436   const candidates = props.prompts.filter(p => p.votes.length >= quorum);
437   const index = Math.floor(Math.random() * candidates.length);
438   const winner = candidates[index];
439
440   return (
441     <div className="lets-play">
442       <h2>Let's Play</h2>
443       <p>
444         That should be enough voting. If you're not waiting for any
445         other players to join, then let's start.
446       </p>
447       <button
448         className="lets-play"
449         onClick={() => fetch_post_json(`start/${winner.id}`) }
450       >
451         Start Game
452       </button>
453     </div>
454   );
455 });
456
457 class Ambiguities extends React.PureComponent {
458
459   constructor(props) {
460     super(props);
461
462     const word_sets = props.words.map(word => {
463       const set = new Set();
464       set.add(word);
465       return set;
466     });
467
468     this.state = {
469       word_sets: word_sets,
470       selected: null
471     };
472
473     this.submitted = false;
474     this.judging_sent_recently = false;
475   }
476
477   async handle_submit() {
478
479     /* Don't submit a second time. */
480     if (this.submitted)
481       return;
482
483     const response = await fetch_post_json(
484       `judged/${this.props.prompt.id}`,{
485         word_groups: this.state.word_sets.map(set => Array.from(set))
486       }
487     );
488
489     if (response.status === 200) {
490       const result = await response.json();
491       if (! result.valid) {
492         add_message("danger", result.message);
493         return;
494       }
495     } else {
496       add_message("danger", "An error occurred submitting the results of your judging");
497       return;
498     }
499
500     this.submitted = true;
501   }
502
503   handle_click(word) {
504
505     /* Let the server know we are doing some judging, (but rate limit
506      * this so we don't send a "judging" notification more frquently
507      * than necessary.
508      */
509     if (! this.judging_sent_recently) {
510       fetch_post_json(`judging/${this.props.prompt.id}`);
511       this.judging_sent_recently = true;
512       setTimeout(() => { this.judging_sent_recently = false; }, 1000);
513     }
514
515     if (this.state.selected == word) {
516       /* Second click on same word removes the word from the group. */
517       const idx = this.state.word_sets.findIndex(s => s.has(word));
518       const set = this.state.word_sets[idx];
519       if (set.size === 1) {
520         /* When the word is already alone, there's nothing to do but
521          * to un-select it. */
522         this.setState({
523           selected: null
524         });
525         return;
526       }
527
528       const new_set = new Set([...set].filter(w => w !== word));
529       this.setState({
530         selected: null,
531         word_sets: [...this.state.word_sets.slice(0, idx),
532                     new_set,
533                     new Set().add(word),
534                     ...this.state.word_sets.slice(idx+1)]
535       });
536     } else if (this.state.selected) {
537       /* Click of a second word groups the two together. */
538       const idx1 = this.state.word_sets.findIndex(s => s.has(this.state.selected));
539       const idx2 = this.state.word_sets.findIndex(s => s.has(word));
540       const set1 = this.state.word_sets[idx1];
541       const set2 = this.state.word_sets[idx2];
542       const new_set = new Set([...set2, ...set1]);
543       if (idx1 < idx2) {
544         this.setState({
545           selected: null,
546           word_sets: [...this.state.word_sets.slice(0, idx1),
547                       ...this.state.word_sets.slice(idx1 + 1, idx2),
548                       new_set,
549                       ...this.state.word_sets.slice(idx2 + 1)]
550         });
551       } else {
552         this.setState({
553           selected: null,
554           word_sets: [...this.state.word_sets.slice(0, idx2),
555                       new_set,
556                       ...this.state.word_sets.slice(idx2 + 1, idx1),
557                       ...this.state.word_sets.slice(idx1 + 1)]
558         });
559       }
560     } else {
561       /* First click of a word selects it. */
562       this.setState({
563         selected: word
564       });
565     }
566   }
567
568   render() {
569     let move_on_button = null;
570
571     if (this.props.idle) {
572       move_on_button = (
573         <button
574           className="vote-button"
575           onClick={() => fetch_post_json(`end-judging/${this.props.prompt.id}`) }
576         >
577           Move On
578           <div className="vote-choices">
579             {[...this.props.votes].map(v => {
580               return (
581                 <div
582                   key={v}
583                   className="vote-choice"
584                 >
585                   {v}
586                   </div>
587               );
588             })}
589           </div>
590         </button>
591       );
592     }
593
594     let still_waiting = null;
595     const judging_players = Object.keys(this.props.players_judging);
596     if (judging_players.length) {
597       still_waiting = (
598         <div>
599           <p>
600             Still waiting for the following player
601             {judging_players.length > 1 ? 's' : '' }
602             :
603           </p>
604           <ul>
605             {judging_players.map(player => {
606               return (
607                 <li
608                   key={player}
609                 >
610                   {player}{' '}
611                   <span className=
612                   {this.props.players_judging[player].active ?
613                    "typing active"
614                    :
615                    "typing idle"}>
616                     <span>{'.'}</span><span>{'.'}</span><span>{'.'}</span>
617                   </span>
618                 </li>
619               );
620             })}
621           </ul>
622         </div>
623       );
624     }
625
626     if (this.props.players_judged.has(this.props.player.name)) {
627       return (
628         <div className="please-wait">
629           <h2>Submission received</h2>
630           <p>
631             The following players have completed judging:{' '}
632             {[...this.props.players_judged].join(', ')}
633           </p>
634           {still_waiting}
635           {move_on_button}
636
637         </div>
638       );
639     }
640
641     const btn_class = "ambiguity-button";
642     const btn_selected_class = btn_class + " selected";
643
644     return (
645       <div className="ambiguities">
646         <h2>Judging Answers</h2>
647         <p>
648           Click on each pair of answers that should be scored as equivalent,
649           (and click any word twice to split it out from a group). Remember,
650           what goes around comes around, so it's best to be generous when
651           judging.
652         </p>
653         <h2>{this.props.prompt.prompt}</h2>
654         {this.state.word_sets.map(set => {
655           return (
656             <div
657               className="ambiguity-group"
658               key={Array.from(set)[0]}
659             >
660               {Array.from(set).map(word => {
661                 return (
662                   <button
663                     className={this.state.selected === word ?
664                                btn_selected_class : btn_class }
665                     key={word}
666                     onClick={() => this.handle_click(word)}
667                   >
668                     {word}
669                   </button>
670                 );
671               })}
672             </div>
673           );
674         })}
675         <p>
676           Click here when done judging:<br/>
677           <button
678             onClick={() => this.handle_submit()}
679           >
680             Send
681           </button>
682         </p>
683       </div>
684     );
685   }
686 }
687
688 class ActivePrompt extends React.PureComponent {
689
690   constructor(props) {
691     super(props);
692     const items = props.prompt.items;
693
694     this.submitted = false;
695
696     this.answers = [...Array(items)].map(() => React.createRef());
697     this.answering_sent_recently = false;
698
699     this.handle_submit = this.handle_submit.bind(this);
700     this.handle_change = this.handle_change.bind(this);
701   }
702
703   handle_change(event) {
704     /* We don't care (or even look) at what the player is typing at
705      * this point. We simply want to be informed that the player _is_
706      * typing so that we can tell the server (which will tell other
707      * players) that there is activity here.
708      */
709
710     /* Rate limit so that we don't send an "answering" notification
711      * more frequently than necessary.
712      */
713     if (! this.answering_sent_recently) {
714       fetch_post_json(`answering/${this.props.prompt.id}`);
715       this.answering_sent_recently = true;
716       setTimeout(() => { this.answering_sent_recently = false; }, 1000);
717     }
718   }
719
720   async handle_submit(event) {
721     const form = event.currentTarget;
722
723     /* Prevent the default page-changing form-submission behavior. */
724     event.preventDefault();
725
726     /* And don't submit a second time. */
727     if (this.submitted)
728       return;
729
730     const response = await fetch_post_json(`answer/${this.props.prompt.id}`, {
731       answers: this.answers.map(r => r.current.value)
732     });
733     if (response.status === 200) {
734       const result = await response.json();
735       if (! result.valid) {
736         add_message("danger", result.message);
737         return;
738       }
739     } else {
740       add_message("danger", "An error occurred submitting your answers");
741       return;
742     }
743
744     /* Everything worked. Server is happy with our answers. */
745     form.reset();
746     this.submitted = true;
747   }
748
749   render() {
750     let move_on_button = null;
751     if (this.props.idle) {
752       move_on_button =(
753         <button
754           className="vote-button"
755           onClick={() => fetch_post_json(`end-answers/${this.props.prompt.id}`) }
756         >
757           Move On
758           <div className="vote-choices">
759             {[...this.props.votes].map(v => {
760               return (
761                 <div
762                   key={v}
763                   className="vote-choice"
764                 >
765                   {v}
766                 </div>
767               );
768             })}
769           </div>
770         </button>
771       );
772     }
773
774     let still_waiting = null;
775     const answering_players = Object.keys(this.props.players_answering);;
776     if (answering_players.length) {
777       still_waiting = (
778         <div>
779           <p>
780             Still waiting for the following player
781             {answering_players.length > 1 ? 's' : ''}
782             :
783           </p>
784           <ul>
785             {answering_players.map(player => {
786               return (
787                 <li
788                   key={player}
789                 >
790                   {player}{' '}
791                   <span className=
792                   {this.props.players_answering[player].active ?
793                    "typing active"
794                    :
795                    "typing idle"}>
796                     <span>{'.'}</span><span>{'.'}</span><span>{'.'}</span>
797                   </span>
798                 </li>
799               );
800             })}
801           </ul>
802         </div>
803       );
804     }
805
806     if (this.props.players_answered.has(this.props.player.name)) {
807       return (
808         <div className="please-wait">
809           <h2>Submission received</h2>
810           <p>
811             The following players have submitted their answers:{' '}
812             {[...this.props.players_answered].join(', ')}
813           </p>
814           {still_waiting}
815           {move_on_button}
816
817         </div>
818       );
819     }
820
821     return (
822       <div className="active-prompt">
823         <h2>The Game of Empathy</h2>
824         <p>
825           Remember, you're trying to match your answers with
826           what the other players submit.
827           Give {this.props.prompt.items} answer
828           {this.props.prompt.items > 1 ? 's' : ''} for the following prompt:
829         </p>
830         <h2>{this.props.prompt.prompt}</h2>
831         <form onSubmit={this.handle_submit}>
832           {[...Array(this.props.prompt.items)].map((whocares,i) => {
833             return (
834               <div
835                 key={i}
836                 className="form-field large">
837                 <input
838                   type="text"
839                   name={`answer_${i}`}
840                   required
841                   autoComplete="off"
842                   onChange={this.handle_change}
843                   ref={this.answers[i]}
844                 />
845               </div>
846             );
847           })}
848
849           <div
850             key="submit-button"
851             className="form-field large">
852             <button type="submit">
853               Send
854             </button>
855           </div>
856
857         </form>
858       </div>
859     );
860   }
861 }
862
863 class Game extends React.PureComponent {
864   constructor(props) {
865     super(props);
866     this.state = {
867       game_info: {},
868       player_info: {},
869       other_players: [],
870       prompts: [],
871       active_prompt: null,
872       players_answered: new Set(),
873       players_answering: {},
874       answering_idle: false,
875       end_answers_votes: new Set(),
876       ambiguities: null,
877       players_judged: new Set(),
878       players_judging: {},
879       judging_idle: false,
880       end_judging_votes: new Set(),
881       scores: null,
882       new_game_votes: new Set(),
883       ready: false
884     };
885   }
886
887   set_game_info(info) {
888     this.setState({
889       game_info: info
890     });
891   }
892
893   set_player_info(info) {
894     this.setState({
895       player_info: info
896     });
897   }
898
899   set_other_player_info(info) {
900     const other_players_copy = [...this.state.other_players];
901     const idx = other_players_copy.findIndex(o => o.id === info.id);
902     if (idx >= 0) {
903       other_players_copy[idx] = info;
904     } else {
905       other_players_copy.push(info);
906     }
907     this.setState({
908       other_players: other_players_copy
909     });
910   }
911
912   remove_player(info) {
913     this.setState({
914       other_players: this.state.other_players.filter(o => o.id !== info.id)
915     });
916   }
917
918   reset_game_state() {
919     this.setState({
920       prompts: [],
921       active_prompt: null,
922       players_answered: new Set(),
923       players_answering: {},
924       answering_idle: false,
925       end_answers_votes: new Set(),
926       ambiguities: null,
927       players_judged: new Set(),
928       players_judging: {},
929       judging_idle: false,
930       end_judging_votes: new Set(),
931       scores: null,
932       new_game_votes: new Set(),
933       ready: false
934     });
935   }
936
937   set_prompts(prompts) {
938     this.setState({
939       prompts: prompts
940     });
941   }
942
943   add_or_update_prompt(prompt) {
944     const prompts_copy = [...this.state.prompts];
945     const idx = prompts_copy.findIndex(p => p.id === prompt.id);
946     if (idx >= 0) {
947       prompts_copy[idx] = prompt;
948     } else {
949       prompts_copy.push(prompt);
950     }
951     this.setState({
952       prompts: prompts_copy
953     });
954   }
955
956   set_active_prompt(prompt) {
957     this.setState({
958       active_prompt: prompt
959     });
960   }
961
962   set_players_answered(players) {
963     this.setState({
964       players_answered: new Set(players)
965     });
966   }
967
968   set_player_answered(player) {
969     const new_players_answering = {...this.state.players_answering};
970     delete new_players_answering[player];
971
972     this.setState({
973       players_answered: new Set([...this.state.players_answered, player]),
974       players_answering: new_players_answering
975     });
976   }
977
978   set_players_answering(players) {
979     const players_answering = {};
980     for (let player of players) {
981       players_answering[player] = {active: false};
982     }
983     this.setState({
984       players_answering: players_answering
985     });
986   }
987
988   set_player_answering(player) {
989     /* Set the player as actively answering now. */
990     this.setState({
991       players_answering: {
992         ...this.state.players_answering,
993         [player]: {active: true}
994       }
995     });
996     /* And arrange to have them marked idle very shortly.
997      *
998      * Note: This timeout is intentionally very, very short. We only
999      * need it long enough that the browser has latched onto the state
1000      * change to "active" above. We actually use a CSS transition
1001      * delay to control the user-perceptible length of time after
1002      * which an active player appears inactive.
1003      */
1004     setTimeout(() => {
1005       this.setState({
1006         players_answering: {
1007           ...this.state.players_answering,
1008           [player]: {active: false}
1009         }
1010       });
1011     }, 100);
1012   }
1013
1014   set_answering_idle(value) {
1015     this.setState({
1016       answering_idle: value
1017     });
1018   }
1019
1020   set_end_answers(players) {
1021     this.setState({
1022       end_answers_votes: new Set(players)
1023     });
1024   }
1025
1026   set_player_vote_end_answers(player) {
1027     this.setState({
1028       end_answers_votes: new Set([...this.state.end_answers_votes, player])
1029     });
1030   }
1031
1032   set_player_unvote_end_answers(player) {
1033     this.setState({
1034       end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
1035     });
1036   }
1037
1038   set_ambiguities(ambiguities) {
1039     this.setState({
1040       ambiguities: ambiguities
1041     });
1042   }
1043
1044   set_players_judged(players) {
1045     this.setState({
1046       players_judged: new Set(players)
1047     });
1048   }
1049
1050   set_player_judged(player) {
1051     const new_players_judging = {...this.state.players_judging};
1052     delete new_players_judging[player];
1053
1054     this.setState({
1055       players_judged: new Set([...this.state.players_judged, player]),
1056       players_judging: new_players_judging
1057     });
1058   }
1059
1060   set_players_judging(players) {
1061     const players_judging = {};
1062     for (let player of players) {
1063       players_judging[player] = {active: false};
1064     }
1065     this.setState({
1066       players_judging: players_judging
1067     });
1068   }
1069
1070   set_player_judging(player) {
1071     /* Set the player as actively judging now. */
1072     this.setState({
1073       players_judging: {
1074         ...this.state.players_judging,
1075         [player]: {active: true}
1076       }
1077     });
1078     /* And arrange to have them marked idle very shortly.
1079      *
1080      * Note: This timeout is intentionally very, very short. We only
1081      * need it long enough that the browser has latched onto the state
1082      * change to "active" above. We actually use a CSS transition
1083      * delay to control the user-perceptible length of time after
1084      * which an active player appears inactive.
1085      */
1086     setTimeout(() => {
1087       this.setState({
1088         players_judging: {
1089           ...this.state.players_judging,
1090           [player]: {active: false}
1091         }
1092       });
1093     }, 100);
1094
1095   }
1096
1097   set_judging_idle(value) {
1098     this.setState({
1099       judging_idle: value
1100     });
1101   }
1102
1103   set_end_judging(players) {
1104     this.setState({
1105       end_judging_votes: new Set(players)
1106     });
1107   }
1108
1109   set_player_vote_end_judging(player) {
1110     this.setState({
1111       end_judging_votes: new Set([...this.state.end_judging_votes, player])
1112     });
1113   }
1114
1115   set_player_unvote_end_judging(player) {
1116     this.setState({
1117       end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
1118     });
1119   }
1120
1121   set_scores(scores) {
1122     this.setState({
1123       scores: scores
1124     });
1125   }
1126
1127   set_new_game_votes(players) {
1128     this.setState({
1129       new_game_votes: new Set(players)
1130     });
1131   }
1132
1133   set_player_vote_new_game(player) {
1134     this.setState({
1135       new_game_votes: new Set([...this.state.new_game_votes, player])
1136     });
1137   }
1138
1139   set_player_unvote_new_game(player) {
1140     this.setState({
1141       new_game_votes: new Set([...this.state.new_game_votes].filter(p => p !== player))
1142     });
1143   }
1144
1145   state_ready() {
1146     this.setState({
1147       ready: true
1148     });
1149   }
1150
1151   render() {
1152     const state = this.state;
1153     const players_total = 1 + state.other_players.length;
1154
1155     if (state.scores) {
1156       return (
1157         <div className="scores">
1158           <h2>{state.active_prompt.prompt}</h2>
1159           <h2>Scores</h2>
1160           <ul>
1161             {state.scores.scores.map(score => {
1162               return (
1163                 <li key={score.player}>
1164                   {score.players.join("/")}: {score.score}
1165                 </li>
1166               );
1167             })}
1168           </ul>
1169           <h2>Words submitted</h2>
1170           <ul>
1171             {state.scores.words.map(word => {
1172               return (
1173                 <li key={word.word}>
1174                   {word.word} ({word.players.length}): {word.players.join(', ')}
1175                 </li>
1176               );
1177             })}
1178           </ul>
1179           <button
1180             className="vote-button"
1181             onClick={() => fetch_post_json(`new-game/${state.active_prompt.id}`) }
1182           >
1183             New Game
1184             <div className="vote-choices">
1185               {[...state.new_game_votes].map(v => {
1186                 return (
1187                   <div
1188                     key={v}
1189                     className="vote-choice"
1190                   >
1191                     {v}
1192                   </div>
1193                 );
1194               })}
1195             </div>
1196           </button>
1197         </div>
1198       );
1199     }
1200
1201     if (state.ambiguities){
1202       return <Ambiguities
1203                prompt={state.active_prompt}
1204                words={state.ambiguities}
1205                player={state.player_info}
1206                players_judged={state.players_judged}
1207                players_judging={state.players_judging}
1208                idle={state.judging_idle}
1209                votes={state.end_judging_votes}
1210              />;
1211     }
1212
1213     if (state.active_prompt) {
1214       return <ActivePrompt
1215                prompt={state.active_prompt}
1216                player={state.player_info}
1217                players_answered={state.players_answered}
1218                players_answering={state.players_answering}
1219                idle={state.answering_idle}
1220                votes={state.end_answers_votes}
1221              />;
1222     }
1223
1224     if (! state.ready)
1225       return null;
1226
1227     return [
1228       <GameInfo
1229         key="game-info"
1230         id={state.game_info.id}
1231         url={state.game_info.url}
1232       />,
1233       <PlayerInfo
1234         key="player-info"
1235         game={this}
1236         player={state.player_info}
1237         other_players={state.other_players}
1238       />,
1239       <p key="spacer"></p>,
1240       <CategoryRequest
1241         key="category-request"
1242       />,
1243       <PromptOptions
1244         key="prompts"
1245         prompts={state.prompts}
1246         player={state.player_info}
1247       />,
1248       <LetsPlay
1249         key="lets-play"
1250         num_players={1+state.other_players.length}
1251         prompts={state.prompts}
1252       />
1253     ];
1254   }
1255 }
1256
1257 ReactDOM.render(<Game
1258                   ref={(me) => window.game = me}
1259                 />, document.getElementById("empathy"));