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 other_players_copy = [...this.state.other_players];
1000 const idx = other_players_copy.findIndex(o => o.id === info.id);
1002 other_players_copy[idx] = info;
1004 other_players_copy.push(info);
1007 other_players: other_players_copy
1011 disable_player(info) {
1012 const idx = this.state.other_players.findIndex(o => o.id === info.id);
1016 const other_players_copy = [...this.state.other_players];
1017 other_players_copy[idx].active = false;
1020 other_players: other_players_copy
1024 reset_game_state() {
1027 active_prompt: null,
1028 players_answered: new Set(),
1029 players_answering: {},
1030 answering_idle: false,
1031 end_answers_votes: new Set(),
1033 players_judged: new Set(),
1034 players_judging: {},
1035 judging_idle: false,
1036 end_judging_votes: new Set(),
1038 new_game_votes: new Set(),
1043 set_prompts(prompts) {
1049 add_or_update_prompt(prompt) {
1050 const prompts_copy = [...this.state.prompts];
1051 const idx = prompts_copy.findIndex(p => p.id === prompt.id);
1053 prompts_copy[idx] = prompt;
1055 prompts_copy.push(prompt);
1058 prompts: prompts_copy
1062 set_active_prompt(prompt) {
1064 active_prompt: prompt
1068 set_players_answered(players) {
1070 players_answered: new Set(players)
1074 set_player_answered(player) {
1075 const new_players_answering = {...this.state.players_answering};
1076 delete new_players_answering[player];
1079 players_answered: new Set([...this.state.players_answered, player]),
1080 players_answering: new_players_answering
1084 set_players_answering(players) {
1085 const players_answering = {};
1086 for (let player of players) {
1087 players_answering[player] = {active: false};
1090 players_answering: players_answering
1094 set_player_answering(player) {
1095 /* Set the player as actively answering now. */
1097 players_answering: {
1098 ...this.state.players_answering,
1099 [player]: {active: true}
1102 /* And arrange to have them marked idle very shortly.
1104 * Note: This timeout is intentionally very, very short. We only
1105 * need it long enough that the browser has latched onto the state
1106 * change to "active" above. We actually use a CSS transition
1107 * delay to control the user-perceptible length of time after
1108 * which an active player appears inactive.
1112 players_answering: {
1113 ...this.state.players_answering,
1114 [player]: {active: false}
1120 set_answering_idle(value) {
1122 answering_idle: value
1126 set_end_answers(players) {
1128 end_answers_votes: new Set(players)
1132 set_player_vote_end_answers(player) {
1134 end_answers_votes: new Set([...this.state.end_answers_votes, player])
1138 set_player_unvote_end_answers(player) {
1140 end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
1144 set_ambiguities(ambiguities) {
1146 ambiguities: ambiguities
1150 set_players_judged(players) {
1152 players_judged: new Set(players)
1156 set_player_judged(player) {
1157 const new_players_judging = {...this.state.players_judging};
1158 delete new_players_judging[player];
1161 players_judged: new Set([...this.state.players_judged, player]),
1162 players_judging: new_players_judging
1166 set_players_judging(players) {
1167 const players_judging = {};
1168 for (let player of players) {
1169 players_judging[player] = {active: false};
1172 players_judging: players_judging
1176 set_player_judging(player) {
1177 /* Set the player as actively judging now. */
1180 ...this.state.players_judging,
1181 [player]: {active: true}
1184 /* And arrange to have them marked idle very shortly.
1186 * Note: This timeout is intentionally very, very short. We only
1187 * need it long enough that the browser has latched onto the state
1188 * change to "active" above. We actually use a CSS transition
1189 * delay to control the user-perceptible length of time after
1190 * which an active player appears inactive.
1195 ...this.state.players_judging,
1196 [player]: {active: false}
1203 set_judging_idle(value) {
1209 set_end_judging(players) {
1211 end_judging_votes: new Set(players)
1215 set_player_vote_end_judging(player) {
1217 end_judging_votes: new Set([...this.state.end_judging_votes, player])
1221 set_player_unvote_end_judging(player) {
1223 end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
1227 set_scores(scores) {
1233 set_new_game_votes(players) {
1235 new_game_votes: new Set(players)
1239 set_player_vote_new_game(player) {
1241 new_game_votes: new Set([...this.state.new_game_votes, player])
1245 set_player_unvote_new_game(player) {
1247 new_game_votes: new Set([...this.state.new_game_votes].filter(p => p !== player))
1258 const state = this.state;
1262 const players_total = state.players_answered.size;
1264 let perfect_score = 0;
1266 i < state.active_prompt.items &&
1267 i < state.scores.words.length;
1270 perfect_score += state.scores.words[i].players.length;
1274 <div className="scores">
1275 <h2>{state.active_prompt.prompt}</h2>
1278 {state.scores.scores.map(score => {
1280 if (score.score === perfect_score) {
1281 perfect = <span className="achievement">Perfect!</span>;
1283 let quirkster = null;
1284 if (score.score === state.active_prompt.items) {
1285 quirkster = <span className="achievement">Quirkster!</span>;
1287 let kudos_slam = null;
1288 if (score.kudos > 0 && score.kudos >= players_total - 1) {
1289 kudos_slam = <span className="achievement">Kudos Slam!</span>;
1292 <li key={score.players[0]}>
1293 {score.players.join("/")}: {score.score}
1294 {score.kudos ? `, ${'★'.repeat(score.kudos)}` : ""}
1295 {' '}{perfect} {quirkster} {kudos_slam}
1300 <h2>Words submitted</h2>
1302 {state.scores.words.map(word => {
1303 let great_minds = null;
1304 if (word.kudos.length && word.players.length > 1) {
1305 great_minds = <span className="achievement">Great Minds!</span>;
1307 let kudos_slam = null;
1308 if (word.kudos.length > 0 && word.kudos.length >= players_total - 1) {
1309 kudos_slam = <span className="achievement">Kudos Slam!</span>;
1312 <li key={word.word}>
1313 {word.word} ({word.players.length}
1314 {word.kudos.length ? `, ${'★'.repeat(word.kudos.length)}` : ""}
1315 ): {word.players.join(', ')}
1316 {' '}{great_minds}{kudos_slam}
1322 className="vote-button"
1323 onClick={() => fetch_post_json(`new-game/${state.active_prompt.id}`) }
1326 <div className="vote-choices">
1327 {[...state.new_game_votes].map(v => {
1331 className="vote-choice"
1343 if (state.ambiguities){
1345 prompt={state.active_prompt}
1346 words={state.ambiguities}
1347 player={state.player_info}
1348 players_judged={state.players_judged}
1349 players_judging={state.players_judging}
1350 idle={state.judging_idle}
1351 votes={state.end_judging_votes}
1355 if (state.active_prompt) {
1356 return <ActivePrompt
1357 prompt={state.active_prompt}
1358 player={state.player_info}
1359 players_answered={state.players_answered}
1360 players_answering={state.players_answering}
1361 idle={state.answering_idle}
1362 votes={state.end_answers_votes}
1372 id={state.game_info.id}
1373 url={state.game_info.url}
1378 player={state.player_info}
1379 other_players={state.other_players}
1381 <p key="spacer"></p>,
1383 key="category-request"
1387 num_players={1+state.other_players.filter(p => p.active).length}
1388 prompts={state.prompts}
1392 prompts={state.prompts}
1393 player={state.player_info}
1399 ReactDOM.render(<Game
1400 ref={(me) => window.game = me}
1401 />, document.getElementById("empathy"));