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