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