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-update", event => {
49 const info = JSON.parse(event.data);
51 if (info.id === window.game.state.player_info.id)
52 window.game.set_player_info(info);
54 window.game.set_other_player_info(info);
57 events.addEventListener("game-state", event => {
58 const state = JSON.parse(event.data);
60 window.game.set_prompts(state.prompts);
62 window.game.set_active_prompt(state.active_prompt);
64 window.game.set_scores(state.scores);
66 window.game.set_ambiguities(state.ambiguities);
69 events.addEventListener("prompt", event => {
70 const prompt = JSON.parse(event.data);
72 window.game.add_or_update_prompt(prompt);
75 events.addEventListener("start", event => {
76 const prompt = JSON.parse(event.data);
78 window.game.set_active_prompt(prompt);
81 events.addEventListener("answered", event => {
82 const players_answered = JSON.parse(event.data);
84 window.game.set_players_answered(players_answered);
87 events.addEventListener("ambiguities", event => {
88 const ambiguities = JSON.parse(event.data);
90 window.game.set_ambiguities(ambiguities);
93 events.addEventListener("judged", event => {
94 const players_judged = JSON.parse(event.data);
96 window.game.set_players_judged(players_judged);
99 events.addEventListener("scores", event => {
100 const scores = JSON.parse(event.data);
102 window.game.set_scores(scores);
105 /*********************************************************
106 * Game and supporting classes *
107 *********************************************************/
109 function copy_to_clipboard(id)
111 const tmp = document.createElement("input");
112 tmp.setAttribute("value", document.getElementById(id).innerHTML);
113 document.body.appendChild(tmp);
115 document.execCommand("copy");
116 document.body.removeChild(tmp);
119 const GameInfo = React.memo(props => {
124 <div className="game-info">
125 <span className="game-id">{props.id}</span>
127 Share this link to invite friends:{" "}
128 <span id="game-share-url">{props.url}</span>
132 onClick={() => copy_to_clipboard('game-share-url')}
138 const PlayerInfo = React.memo(props => {
139 if (! props.player.id)
143 <div className="player-info">
144 <span className="players-header">Players: </span>
146 {props.player.score > 0 ? ` (${props.player.score})` : ""}
147 {props.other_players.map(other => (
148 <span key={other.id}>
151 {other.score > 0 ? ` (${other.score})` : ""}
158 function fetch_method_json(method, api = '', data = {}) {
159 const response = fetch(api, {
162 'Content-Type': 'application/json'
164 body: JSON.stringify(data)
169 function fetch_post_json(api = '', data = {}) {
170 return fetch_method_json('POST', api, data);
173 async function fetch_put_json(api = '', data = {}) {
174 return fetch_method_json('PUT', api, data);
177 class CategoryRequest extends React.PureComponent {
180 this.category = React.createRef();
182 this.handle_change = this.handle_change.bind(this);
183 this.handle_submit = this.handle_submit.bind(this);
186 handle_change(event) {
187 const category_input = this.category.current;
188 const category = category_input.value;
190 const match = category.match(/[0-9]+/);
192 const num_items = parseInt(match[0], 10);
193 if (num_items <= MAX_PROMPT_ITEMS)
194 category_input.setCustomValidity("");
198 async handle_submit(event) {
199 const form = event.currentTarget;
200 const category_input = this.category.current;
201 const category = category_input.value;
203 /* Prevent the default page-changing form-submission behavior. */
204 event.preventDefault();
206 const match = category.match(/[0-9]+/);
207 if (match === null) {
208 category_input.setCustomValidity("Category must include a number");
209 form.reportValidity();
213 const num_items = parseInt(match[0], 10);
215 if (num_items > MAX_PROMPT_ITEMS) {
216 category_input.setCustomValidity(`Maximum number of items is ${MAX_PROMPT_ITEMS}`);
217 form.reportValidity();
221 const response = await fetch_post_json("prompts", {
226 if (response.status === 200) {
227 const result = await response.json();
229 if (! result.valid) {
230 add_message("danger", result.message);
234 add_message("danger", "An error occurred submitting your category");
242 <div className="category-request">
243 <h2>Submit a Category</h2>
245 Suggest a category to play. Don't forget to include the
246 number of items for each person to submit.
249 <form onSubmit={this.handle_submit} >
250 <div className="form-field large">
254 placeholder="6 things at the beach"
257 onChange={this.handle_change}
262 <div className="form-field large">
263 <button type="submit">
274 const PromptOptions = React.memo(props => {
276 if (props.prompts.length === 0)
280 <div className="prompt-options">
281 <h2>Vote on Categories</h2>
283 Select any categories below that you'd like to play.
284 You can choose as many as you'd like.
286 {props.prompts.map(p => {
289 className="vote-button"
291 onClick={() => fetch_post_json(`vote/${p.id}`) }
294 <div className="vote-choices">
299 className="vote-choice"
313 const LetsPlay = React.memo(props => {
315 function handle_click(prompt_id) {
319 const quorum = Math.round((props.num_players + 1) / 2);
320 const max_votes = props.prompts.reduce(
321 (max_so_far, v) => Math.max(max_so_far, v.votes.length), 0);
323 if (max_votes < quorum)
326 const candidates = props.prompts.filter(p => p.votes.length >= quorum);
327 const index = Math.floor(Math.random() * candidates.length);
328 const winner = candidates[index];
331 <div className="lets-play">
334 That should be enough voting. If you're not waiting for any
335 other players to join, then let's start.
338 className="lets-play"
339 onClick={() => fetch_post_json(`start/${winner.id}`) }
347 class Ambiguities extends React.PureComponent {
353 word_groups: props.words.map(word => [word]),
359 async handle_submit() {
360 const response = await fetch_post_json(
361 `judging/${this.props.prompt.id}`,{
362 word_groups: this.state.word_groups
366 if (response.status === 200) {
367 const result = await response.json();
368 if (! result.valid) {
369 add_message("danger", result.message);
373 add_message("danger", "An error occurred submitting your answers");
383 if (this.state.selected == word) {
384 /* Second click on same word removes the word from the group. */
385 const new_groups = this.state.word_groups.filter(
386 group => (! group.includes(this.state.selected)) || (group.length > 1)).map(
388 return group.filter(w => w !== this.state.selected);
393 word_groups: [...new_groups, [word]]
395 } else if (this.state.selected) {
396 /* Click of a second word groups the two together. */
397 const new_groups = this.state.word_groups.filter(
398 group => (! group.includes(word)) || (group.length > 1)).map(
400 if (group.includes(this.state.selected)) {
401 if (! group.includes(word))
402 return [...group, word];
406 return group.filter(w => w !== word);
412 word_groups: new_groups
415 /* First click of a word selects it. */
423 if (this.state.submitted)
425 <div className="please-wait">
426 <h2>{this.props.players_judged}/
427 {this.props.players_total} players have responded</h2>
429 Please wait for the rest of the players to complete judging.
434 const btn_class = "ambiguity-button";
435 const btn_selected_class = btn_class + " selected";
438 <div className="ambiguities">
439 <h2>Judging Answers</h2>
441 Click on each pair of answers that should be scored as equivalent,
442 (and click any word twice to split it out from a group). Remember,
443 what goes around comes around, so it's best to be generous when
446 {this.state.word_groups.map(word_group => {
449 className="ambiguity-group"
452 {word_group.map(word => {
455 className={this.state.selected === word ?
456 btn_selected_class : btn_class }
458 onClick={() => this.handle_click(word)}
468 Click here when done judging:<br/>
470 onClick={() => this.handle_submit()}
480 class ActivePrompt extends React.PureComponent {
484 const items = props.prompt.items;
490 this.answers = [...Array(items)].map(() => React.createRef());
491 this.handle_submit = this.handle_submit.bind(this);
494 async handle_submit(event) {
495 const form = event.currentTarget;
497 /* Prevent the default page-changing form-submission behavior. */
498 event.preventDefault();
500 const response = await fetch_post_json(`answer/${this.props.prompt.id}`, {
501 answers: this.answers.map(r => r.current.value)
503 if (response.status === 200) {
504 const result = await response.json();
505 if (! result.valid) {
506 add_message("danger", result.message);
510 add_message("danger", "An error occurred submitting your answers");
514 /* Everything worked. Server is happy with our answers. */
522 if (this.state.submitted)
524 <div className="please-wait">
525 <h2>{this.props.players_answered}/
526 {this.props.players_total} players have responded</h2>
528 Please wait for the rest of the players to submit their answers.
534 <div className="active-prompt">
535 <h2>The Game of Empathy</h2>
537 Remember, you're trying to match your answers with
538 what the other players submit.
539 Give {this.props.prompt.items} answers for the following prompt:
541 <h2>{this.props.prompt.prompt}</h2>
542 <form onSubmit={this.handle_submit}>
543 {[...Array(this.props.prompt.items)].map((whocares,i) => {
547 className="form-field large">
553 ref={this.answers[i]}
561 className="form-field large">
562 <button type="submit">
573 class Game extends React.PureComponent {
588 set_game_info(info) {
594 set_player_info(info) {
600 set_other_player_info(info) {
601 const other_players_copy = [...this.state.other_players];
602 const idx = other_players_copy.findIndex(o => o.id === info.id);
604 other_players_copy[idx] = info;
606 other_players_copy.push(info);
609 other_players: other_players_copy
613 set_prompts(prompts) {
619 add_or_update_prompt(prompt) {
620 const prompts_copy = [...this.state.prompts];
621 const idx = prompts_copy.findIndex(p => p.id === prompt.id);
623 prompts_copy[idx] = prompt;
625 prompts_copy.push(prompt);
628 prompts: prompts_copy
632 set_active_prompt(prompt) {
634 active_prompt: prompt
638 set_players_answered(players_answered) {
640 players_answered: players_answered
644 set_ambiguities(ambiguities) {
646 ambiguities: ambiguities
650 set_players_judged(players_judged) {
652 players_judged: players_judged
663 const state = this.state;
664 const players_total = 1 + state.other_players.length;
668 <div className="scores">
671 {state.scores.scores.map(score => {
673 <li key={score.player}>
674 {score.player}: {score.score}
679 <h2>Words submitted</h2>
681 {state.scores.words.map(word => {
684 {`${word.word}: ${word.players.join(', ')}`}
691 onClick={() => fetch_post_json('reset') }
699 if (state.ambiguities){
701 prompt={state.active_prompt}
702 words={state.ambiguities}
703 players_judged={state.players_judged}
704 players_total={players_total}
708 if (state.active_prompt) {
710 prompt={state.active_prompt}
711 players_answered={state.players_answered}
712 players_total={players_total}
719 id={state.game_info.id}
720 url={state.game_info.url}
725 player={state.player_info}
726 other_players={state.other_players}
728 <p key="spacer"></p>,
730 key="category-request"
734 prompts={state.prompts}
738 num_players={1+state.other_players.length}
739 prompts={state.prompts}
745 ReactDOM.render(<Game
746 ref={(me) => window.game = me}
747 />, document.getElementById("empathy"));