1 const MAX_PROMPT_ITEMS = 20;
3 function undisplay(element) {
4 element.style.display="none";
7 function add_message(severity, message) {
8 message = `<div class="message ${severity}" onclick="undisplay(this)">
9 <span class="hide-button" onclick="undisplay(this.parentElement)">×</span>
12 const message_area = document.getElementById('message-area');
13 message_area.insertAdjacentHTML('beforeend', message);
16 /*********************************************************
17 * Handling server-sent event stream *
18 *********************************************************/
20 const events = new EventSource("events");
22 events.onerror = function(event) {
23 if (event.target.readyState === EventSource.CLOSED) {
25 add_message("danger", "Connection to server lost.");
30 events.addEventListener("game-info", event => {
31 const info = JSON.parse(event.data);
33 window.game.set_game_info(info);
36 events.addEventListener("player-info", event => {
37 const info = JSON.parse(event.data);
39 window.game.set_player_info(info);
42 events.addEventListener("player-enter", event => {
43 const info = JSON.parse(event.data);
45 window.game.set_other_player_info(info);
48 events.addEventListener("player-exit", event => {
49 const info = JSON.parse(event.data);
51 window.game.disable_player(info);
54 events.addEventListener("player-update", event => {
55 const info = JSON.parse(event.data);
57 if (info.id === window.game.state.player_info.id)
58 window.game.set_player_info(info);
60 window.game.set_other_player_info(info);
63 events.addEventListener("game-state", event => {
64 const state = JSON.parse(event.data);
66 window.game.reset_game_state();
68 window.game.set_prompts(state.prompts);
70 window.game.set_active_prompt(state.active_prompt);
72 window.game.set_players_answered(state.players_answered);
74 window.game.set_players_answering(state.players_answering);
76 window.game.set_answering_idle(state.answering_idle);
78 window.game.set_end_answers(state.end_answers);
80 window.game.set_ambiguities(state.ambiguities);
82 window.game.set_players_judged(state.players_judged);
84 window.game.set_players_judging(state.players_judging);
86 window.game.set_judging_idle(state.judging_idle);
88 window.game.set_end_judging(state.end_judging);
90 window.game.set_scores(state.scores);
92 window.game.set_new_game_votes(state.new_game_votes);
94 window.game.state_ready();
97 events.addEventListener("prompt", event => {
98 const prompt = JSON.parse(event.data);
100 window.game.add_or_update_prompt(prompt);
103 events.addEventListener("start", event => {
104 const prompt = JSON.parse(event.data);
106 window.game.set_active_prompt(prompt);
109 events.addEventListener("player-answered", event => {
110 const player = JSON.parse(event.data);
112 window.game.set_player_answered(player);
115 events.addEventListener("player-answering", event => {
116 const player = JSON.parse(event.data);
118 window.game.set_player_answering(player);
121 events.addEventListener("answering-idle", event => {
122 const value = JSON.parse(event.data);
124 window.game.set_answering_idle(value);
127 events.addEventListener("vote-end-answers", event => {
128 const player = JSON.parse(event.data);
130 window.game.set_player_vote_end_answers(player);
133 events.addEventListener("unvote-end-answers", event => {
134 const player = JSON.parse(event.data);
136 window.game.set_player_unvote_end_answers(player);
139 events.addEventListener("ambiguities", event => {
140 const ambiguities = JSON.parse(event.data);
142 window.game.set_ambiguities(ambiguities);
145 events.addEventListener("player-judged", event => {
146 const player = JSON.parse(event.data);
148 window.game.set_player_judged(player);
151 events.addEventListener("player-judging", event => {
152 const player = JSON.parse(event.data);
154 window.game.set_player_judging(player);
157 events.addEventListener("judging-idle", event => {
158 const value = JSON.parse(event.data);
160 window.game.set_judging_idle(value);
163 events.addEventListener("vote-end-judging", event => {
164 const player = JSON.parse(event.data);
166 window.game.set_player_vote_end_judging(player);
169 events.addEventListener("unvote-end-judging", event => {
170 const player = JSON.parse(event.data);
172 window.game.set_player_unvote_end_judging(player);
175 events.addEventListener("scores", event => {
176 const scores = JSON.parse(event.data);
178 window.game.set_scores(scores);
181 events.addEventListener("vote-new-game", event => {
182 const player = JSON.parse(event.data);
184 window.game.set_player_vote_new_game(player);
187 events.addEventListener("unvote-new-game", event => {
188 const player = JSON.parse(event.data);
190 window.game.set_player_unvote_new_game(player);
193 /*********************************************************
194 * Game and supporting classes *
195 *********************************************************/
197 function copy_to_clipboard(id)
199 const tmp = document.createElement("input");
200 tmp.setAttribute("value", document.getElementById(id).innerHTML);
201 document.body.appendChild(tmp);
203 document.execCommand("copy");
204 document.body.removeChild(tmp);
207 const GameInfo = React.memo(props => {
212 <div className="game-info">
213 <span className="game-id">{props.id}</span>
215 Share this link to invite friends:{" "}
216 <span id="game-share-url">{props.url}</span>
220 onClick={() => copy_to_clipboard('game-share-url')}
226 const PlayerInfo = React.memo(props => {
227 if (! props.player.id)
230 const all_players = [{...props.player, active:true}, ...props.other_players];
232 const sorted_players = all_players.sort((a,b) => {
233 return b.score - a.score;
236 /* Return a new array with the separator interspersed between
237 * each element of the array passed in as the argument.
239 function intersperse(arr, sep) {
240 return arr.reduce((acc, val) => [...acc, sep, val], []).slice(1);
243 let names_and_scores = sorted_players.map(player => {
248 className={player.active ? "player-active" : "player-idle"}
250 {player.name} ({player.score})
259 }).filter(component => component != null);
261 names_and_scores = intersperse(names_and_scores, ", ");
264 <div className="player-info">
265 <span className="players-header">Players: </span>
271 function fetch_method_json(method, api = '', data = {}) {
272 const response = fetch(api, {
275 'Content-Type': 'application/json'
277 body: JSON.stringify(data)
282 function fetch_post_json(api = '', data = {}) {
283 return fetch_method_json('POST', api, data);
286 async function fetch_put_json(api = '', data = {}) {
287 return fetch_method_json('PUT', api, data);
290 class CategoryRequest extends React.PureComponent {
293 this.category = React.createRef();
295 this.handle_change = this.handle_change.bind(this);
296 this.handle_submit = this.handle_submit.bind(this);
299 handle_change(event) {
300 const category_input = this.category.current;
301 const category = category_input.value;
303 const match = category.match(/[0-9]+/);
305 const num_items = parseInt(match[0], 10);
306 if (num_items > 0 && num_items <= MAX_PROMPT_ITEMS)
307 category_input.setCustomValidity("");
311 async handle_submit(event) {
312 const form = event.currentTarget;
313 const category_input = this.category.current;
314 const category = category_input.value;
316 /* Prevent the default page-changing form-submission behavior. */
317 event.preventDefault();
319 const match = category.match(/[0-9]+/);
320 if (match === null) {
321 category_input.setCustomValidity("Category must include a number");
322 form.reportValidity();
326 const num_items = parseInt(match[0], 10);
328 if (num_items > MAX_PROMPT_ITEMS) {
329 category_input.setCustomValidity(`Maximum number of items is ${MAX_PROMPT_ITEMS}`);
330 form.reportValidity();
335 category_input.setCustomValidity("Category must require at least one item.");
336 form.reportValidity();
340 const response = await fetch_post_json("prompts", {
345 if (response.status === 200) {
346 const result = await response.json();
347 if (! result.valid) {
348 add_message("danger", result.message);
352 add_message("danger", "An error occurred submitting your category");
360 <div className="category-request">
361 <h2>Submit a Category</h2>
363 Suggest a category to play. Don't forget to include the
364 number of items for each person to submit.
367 <form onSubmit={this.handle_submit} >
368 <div className="form-field large">
372 placeholder="6 things at the beach"
375 onChange={this.handle_change}
380 <div className="form-field large">
381 <button type="submit">
392 const PromptOption = React.memo(props => {
394 const prompt = props.prompt;
396 if (prompt.votes_against.find(v => v === props.player.name))
401 className="vote-button"
403 onClick={() => fetch_post_json(`vote/${prompt.id}`) }
406 className="hide-button"
407 onClick={(event) => {
408 event.stopPropagation();
409 fetch_post_json(`vote_against/${prompt.id}`);
415 <div className="vote-choices">
416 {prompt.votes.map(v => {
420 className="vote-choice"
431 const PromptOptions = React.memo(props => {
433 if (props.prompts.length === 0)
437 <div className="prompt-options">
438 <h2>Vote on Categories</h2>
440 Select any categories below that you'd like to play.
441 You can choose as many as you'd like.
444 prompt => <PromptOption
447 player={props.player}
454 const LetsPlay = React.memo(props => {
456 const quorum = Math.max(0, props.num_players - props.prompts.length);
457 const max_votes = props.prompts.reduce(
458 (max_so_far, v) => Math.max(max_so_far, v.votes.length), 0);
460 if (max_votes < quorum) {
461 let text = `Before we play, we should collect a bit
462 more information about what category would
463 be interesting for this group. So, either
464 type a new category option above, or else`;
465 if (props.prompts.length) {
466 if (props.prompts.length > 1)
467 text += " vote on some of the categories below.";
469 text += " vote on the category below.";
471 text += " wait for others to submit, and then vote on them below.";
475 <div className="before-we-play">
483 const candidates = props.prompts.filter(p => p.votes.length >= max_votes);
484 const index = Math.floor(Math.random() * candidates.length);
485 const winner = candidates[index];
488 <div className="lets-play">
491 That should be enough voting. If you're not waiting for any
492 other players to join, then let's start.
495 className="lets-play"
496 onClick={() => fetch_post_json(`start/${winner.id}`) }
504 class Ambiguities extends React.PureComponent {
509 function canonize(word) {
510 return word.replace(/((a|an|the) )?(.*?)s?$/i, '$3');
513 const word_sets = [];
515 for (let word of props.words) {
516 const word_canon = canonize(word);
517 let found_match = false;
518 for (let set of word_sets) {
519 const set_canon = canonize(set.values().next().value);
520 if (word_canon === set_canon) {
527 const set = new Set();
534 word_sets: word_sets,
539 this.submitted = false;
540 this.judging_sent_recently = false;
543 async handle_submit() {
545 /* Don't submit a second time. */
549 const response = await fetch_post_json(
550 `judged/${this.props.prompt.id}`,{
551 word_groups: this.state.word_sets.map(
553 words: Array.from(set),
554 kudos: this.state.starred === set ? true : false
559 if (response.status === 200) {
560 const result = await response.json();
561 if (! result.valid) {
562 add_message("danger", result.message);
566 add_message("danger", "An error occurred submitting the results of your judging");
570 this.submitted = true;
575 /* Let the server know we are doing some judging, (but rate limit
576 * this so we don't send a "judging" notification more frquently
579 if (! this.judging_sent_recently) {
580 fetch_post_json(`judging/${this.props.prompt.id}`);
581 this.judging_sent_recently = true;
582 setTimeout(() => { this.judging_sent_recently = false; }, 1000);
585 if (this.state.selected == word) {
586 /* Second click on same word removes the word from the group. */
587 const idx = this.state.word_sets.findIndex(s => s.has(word));
588 const set = this.state.word_sets[idx];
589 if (set.size === 1) {
590 /* When the word is already alone, there's nothing to do but
591 * to un-select it. */
598 const new_set = new Set([...set].filter(w => w !== word));
601 word_sets: [...this.state.word_sets.slice(0, idx),
604 ...this.state.word_sets.slice(idx+1)]
606 } else if (this.state.selected) {
607 /* Click of a second word groups the two together. */
608 const idx1 = this.state.word_sets.findIndex(s => s.has(this.state.selected));
609 const idx2 = this.state.word_sets.findIndex(s => s.has(word));
610 const set1 = this.state.word_sets[idx1];
611 const set2 = this.state.word_sets[idx2];
612 const new_set = new Set([...set2, ...set1]);
616 word_sets: [...this.state.word_sets.slice(0, idx1),
617 ...this.state.word_sets.slice(idx1 + 1, idx2),
619 ...this.state.word_sets.slice(idx2 + 1)]
624 word_sets: [...this.state.word_sets.slice(0, idx2),
626 ...this.state.word_sets.slice(idx2 + 1, idx1),
627 ...this.state.word_sets.slice(idx1 + 1)]
631 /* First click of a word selects it. */
639 let move_on_button = null;
641 if (this.props.idle) {
644 className="vote-button"
645 onClick={() => fetch_post_json(`end-judging/${this.props.prompt.id}`) }
647 Move On Without Their Input
648 <div className="vote-choices">
649 {[...this.props.votes].map(v => {
653 className="vote-choice"
664 let still_waiting = null;
665 const judging_players = Object.keys(this.props.players_judging);
666 if (judging_players.length) {
670 Still waiting for the following player
671 {judging_players.length > 1 ? 's' : '' }
675 {judging_players.map(player => {
682 {this.props.players_judging[player].active ?
686 <span>{'.'}</span><span>{'.'}</span><span>{'.'}</span>
696 if (this.props.players_judged.has(this.props.player.name)) {
698 <div className="please-wait">
699 <h2>Submission received</h2>
701 The following players have completed judging:{' '}
702 {[...this.props.players_judged].join(', ')}
711 const btn_class = "ambiguity-button";
712 const btn_selected_class = btn_class + " selected";
715 <div className="ambiguities">
716 <h2>Judging Answers</h2>
718 Click/tap on each pair of answers that should be scored as equivalent,
719 (or click a word twice to split it out from a group). Remember,
720 what goes around comes around, so it's best to be generous when
724 Also, for an especially fun or witty answer, you can give kudos
725 by clicking the star on the right. You may only do this for one
728 <h2>{this.props.prompt.prompt}</h2>
729 {this.state.word_sets.map(set => {
732 className="ambiguity-group"
733 key={Array.from(set)[0]}
735 {Array.from(set).map(word => {
738 className={this.state.selected === word ?
739 btn_selected_class : btn_class }
741 onClick={() => this.handle_click(word)}
748 className={this.state.starred === set ?
749 "star-button selected" : "star-button"
751 onClick={(event) => {
752 event.stopPropagation();
753 if (this.state.starred === set) {
764 {this.state.starred === set ?
772 Click here when done judging:<br/>
774 onClick={() => this.handle_submit()}
784 class ActivePrompt extends React.PureComponent {
788 const items = props.prompt.items;
790 this.submitted = false;
792 this.answers = [...Array(items)].map(() => React.createRef());
793 this.answering_sent_recently = false;
795 this.handle_submit = this.handle_submit.bind(this);
796 this.handle_change = this.handle_change.bind(this);
799 handle_change(event) {
800 /* We don't care (or even look) at what the player is typing at
801 * this point. We simply want to be informed that the player _is_
802 * typing so that we can tell the server (which will tell other
803 * players) that there is activity here.
806 /* Rate limit so that we don't send an "answering" notification
807 * more frequently than necessary.
809 if (! this.answering_sent_recently) {
810 fetch_post_json(`answering/${this.props.prompt.id}`);
811 this.answering_sent_recently = true;
812 setTimeout(() => { this.answering_sent_recently = false; }, 1000);
816 async handle_submit(event) {
817 const form = event.currentTarget;
819 /* Prevent the default page-changing form-submission behavior. */
820 event.preventDefault();
822 /* And don't submit a second time. */
826 const response = await fetch_post_json(`answer/${this.props.prompt.id}`, {
827 answers: this.answers.map(r => r.current.value)
829 if (response.status === 200) {
830 const result = await response.json();
831 if (! result.valid) {
832 add_message("danger", result.message);
836 add_message("danger", "An error occurred submitting your answers");
840 /* Everything worked. Server is happy with our answers. */
842 this.submitted = true;
847 let still_waiting = null;
848 const answering_players = Object.keys(this.props.players_answering);;
849 if (answering_players.length) {
853 Still waiting for the following player
854 {answering_players.length > 1 ? 's' : ''}
858 {answering_players.map(player => {
865 {this.props.players_answering[player].active ?
869 <span>{'.'}</span><span>{'.'}</span><span>{'.'}</span>
879 let move_on_button = null;
880 if (this.props.idle) {
883 className="vote-button"
884 onClick={() => fetch_post_json(`end-answers/${this.props.prompt.id}`) }
886 {answering_players.length ?
887 "Move On Without Their Answers" :
888 "Move On Without Anyone Else"}
889 <div className="vote-choices">
890 {[...this.props.votes].map(v => {
894 className="vote-choice"
905 if (this.props.players_answered.has(this.props.player.name)) {
907 <div className="please-wait">
908 <h2>Submission received</h2>
910 The following players have submitted their answers:{' '}
911 {[...this.props.players_answered].join(', ')}
921 <div className="active-prompt">
922 <h2>The Game of Empathy</h2>
924 Remember, you're trying to match your answers with
925 what the other players submit.
926 Give {this.props.prompt.items} answer
927 {this.props.prompt.items > 1 ? 's' : ''} for the following prompt:
929 <h2>{this.props.prompt.prompt}</h2>
930 <form onSubmit={this.handle_submit}>
931 {[...Array(this.props.prompt.items)].map((whocares,i) => {
935 className="form-field large">
941 onChange={this.handle_change}
942 ref={this.answers[i]}
950 className="form-field large">
951 <button type="submit">
962 class Game extends React.PureComponent {
971 players_answered: new Set(),
972 players_answering: {},
973 answering_idle: false,
974 end_answers_votes: new Set(),
976 players_judged: new Set(),
979 end_judging_votes: new Set(),
981 new_game_votes: new Set(),
986 set_game_info(info) {
992 set_player_info(info) {
998 set_other_player_info(info) {
999 const player_object = {...info, active: true};
1000 const other_players_copy = [...this.state.other_players];
1001 const idx = other_players_copy.findIndex(o => o.id === info.id);
1003 other_players_copy[idx] = player_object;
1005 other_players_copy.push(player_object);
1008 other_players: other_players_copy
1012 disable_player(info) {
1013 const idx = this.state.other_players.findIndex(o => o.id === info.id);
1017 const other_players_copy = [...this.state.other_players];
1018 other_players_copy[idx].active = false;
1021 other_players: other_players_copy
1025 reset_game_state() {
1028 active_prompt: null,
1029 players_answered: new Set(),
1030 players_answering: {},
1031 answering_idle: false,
1032 end_answers_votes: new Set(),
1034 players_judged: new Set(),
1035 players_judging: {},
1036 judging_idle: false,
1037 end_judging_votes: new Set(),
1039 new_game_votes: new Set(),
1044 set_prompts(prompts) {
1050 add_or_update_prompt(prompt) {
1051 const prompts_copy = [...this.state.prompts];
1052 const idx = prompts_copy.findIndex(p => p.id === prompt.id);
1054 prompts_copy[idx] = prompt;
1056 prompts_copy.push(prompt);
1059 prompts: prompts_copy
1063 set_active_prompt(prompt) {
1065 active_prompt: prompt
1069 set_players_answered(players) {
1071 players_answered: new Set(players)
1075 set_player_answered(player) {
1076 const new_players_answering = {...this.state.players_answering};
1077 delete new_players_answering[player];
1080 players_answered: new Set([...this.state.players_answered, player]),
1081 players_answering: new_players_answering
1085 set_players_answering(players) {
1086 const players_answering = {};
1087 for (let player of players) {
1088 players_answering[player] = {active: false};
1091 players_answering: players_answering
1095 set_player_answering(player) {
1096 /* Set the player as actively answering now. */
1098 players_answering: {
1099 ...this.state.players_answering,
1100 [player]: {active: true}
1103 /* And arrange to have them marked idle very shortly.
1105 * Note: This timeout is intentionally very, very short. We only
1106 * need it long enough that the browser has latched onto the state
1107 * change to "active" above. We actually use a CSS transition
1108 * delay to control the user-perceptible length of time after
1109 * which an active player appears inactive.
1113 players_answering: {
1114 ...this.state.players_answering,
1115 [player]: {active: false}
1121 set_answering_idle(value) {
1123 answering_idle: value
1127 set_end_answers(players) {
1129 end_answers_votes: new Set(players)
1133 set_player_vote_end_answers(player) {
1135 end_answers_votes: new Set([...this.state.end_answers_votes, player])
1139 set_player_unvote_end_answers(player) {
1141 end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
1145 set_ambiguities(ambiguities) {
1147 ambiguities: ambiguities
1151 set_players_judged(players) {
1153 players_judged: new Set(players)
1157 set_player_judged(player) {
1158 const new_players_judging = {...this.state.players_judging};
1159 delete new_players_judging[player];
1162 players_judged: new Set([...this.state.players_judged, player]),
1163 players_judging: new_players_judging
1167 set_players_judging(players) {
1168 const players_judging = {};
1169 for (let player of players) {
1170 players_judging[player] = {active: false};
1173 players_judging: players_judging
1177 set_player_judging(player) {
1178 /* Set the player as actively judging now. */
1181 ...this.state.players_judging,
1182 [player]: {active: true}
1185 /* And arrange to have them marked idle very shortly.
1187 * Note: This timeout is intentionally very, very short. We only
1188 * need it long enough that the browser has latched onto the state
1189 * change to "active" above. We actually use a CSS transition
1190 * delay to control the user-perceptible length of time after
1191 * which an active player appears inactive.
1196 ...this.state.players_judging,
1197 [player]: {active: false}
1204 set_judging_idle(value) {
1210 set_end_judging(players) {
1212 end_judging_votes: new Set(players)
1216 set_player_vote_end_judging(player) {
1218 end_judging_votes: new Set([...this.state.end_judging_votes, player])
1222 set_player_unvote_end_judging(player) {
1224 end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
1228 set_scores(scores) {
1234 set_new_game_votes(players) {
1236 new_game_votes: new Set(players)
1240 set_player_vote_new_game(player) {
1242 new_game_votes: new Set([...this.state.new_game_votes, player])
1246 set_player_unvote_new_game(player) {
1248 new_game_votes: new Set([...this.state.new_game_votes].filter(p => p !== player))
1259 const state = this.state;
1263 const players_total = state.players_answered.size;
1265 let perfect_score = 0;
1267 i < state.active_prompt.items &&
1268 i < state.scores.words.length;
1271 perfect_score += state.scores.words[i].players.length;
1275 <div className="scores">
1276 <h2>{state.active_prompt.prompt}</h2>
1279 {state.scores.scores.map(score => {
1281 if (score.score === perfect_score) {
1282 perfect = <span className="achievement">Perfect!</span>;
1284 let quirkster = null;
1285 if (score.score === state.active_prompt.items) {
1286 quirkster = <span className="achievement">Quirkster!</span>;
1288 let kudos_slam = null;
1289 if (score.kudos > 0 && score.kudos >= players_total - 1) {
1290 kudos_slam = <span className="achievement">Kudos Slam!</span>;
1293 <li key={score.players[0]}>
1294 {score.players.join("/")}: {score.score}
1295 {score.kudos ? `, ${'★'.repeat(score.kudos)}` : ""}
1296 {' '}{perfect} {quirkster} {kudos_slam}
1301 <h2>Words submitted</h2>
1303 {state.scores.words.map(word => {
1304 let great_minds = null;
1305 if (word.kudos.length && word.players.length > 1) {
1306 great_minds = <span className="achievement">Great Minds!</span>;
1308 let kudos_slam = null;
1309 if (word.kudos.length > 0 && word.kudos.length >= players_total - 1) {
1310 kudos_slam = <span className="achievement">Kudos Slam!</span>;
1313 <li key={word.word}>
1314 {word.word} ({word.players.length}
1315 {word.kudos.length ? `, ${'★'.repeat(word.kudos.length)}` : ""}
1316 ): {word.players.join(', ')}
1317 {' '}{great_minds}{kudos_slam}
1323 className="vote-button"
1324 onClick={() => fetch_post_json(`new-game/${state.active_prompt.id}`) }
1327 <div className="vote-choices">
1328 {[...state.new_game_votes].map(v => {
1332 className="vote-choice"
1344 if (state.ambiguities){
1346 prompt={state.active_prompt}
1347 words={state.ambiguities}
1348 player={state.player_info}
1349 players_judged={state.players_judged}
1350 players_judging={state.players_judging}
1351 idle={state.judging_idle}
1352 votes={state.end_judging_votes}
1356 if (state.active_prompt) {
1357 return <ActivePrompt
1358 prompt={state.active_prompt}
1359 player={state.player_info}
1360 players_answered={state.players_answered}
1361 players_answering={state.players_answering}
1362 idle={state.answering_idle}
1363 votes={state.end_answers_votes}
1373 id={state.game_info.id}
1374 url={state.game_info.url}
1379 player={state.player_info}
1380 other_players={state.other_players}
1382 <p key="spacer"></p>,
1384 key="category-request"
1388 num_players={1+state.other_players.filter(p => p.active).length}
1389 prompts={state.prompts}
1393 prompts={state.prompts}
1394 player={state.player_info}
1400 ReactDOM.render(<Game
1401 ref={(me) => window.game = me}
1402 />, document.getElementById("empathy"));