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