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