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 {
352 const word_sets = props.words.map(word => {
353 const set = new Set();
359 word_sets: word_sets,
365 async handle_submit() {
366 const response = await fetch_post_json(
367 `judging/${this.props.prompt.id}`,{
368 word_groups: this.state.word_sets.map(set => Array.from(set))
372 if (response.status === 200) {
373 const result = await response.json();
374 if (! result.valid) {
375 add_message("danger", result.message);
379 add_message("danger", "An error occurred submitting your answers");
389 if (this.state.selected == word) {
390 /* Second click on same word removes the word from the group. */
391 const idx = this.state.word_sets.findIndex(s => s.has(word));
392 const set = this.state.word_sets[idx];
395 const new_set = new Set([...set].filter(w => w !== word));
398 word_sets: [...this.state.word_sets.slice(0, idx),
401 ...this.state.word_sets.slice(idx+1)]
403 } else if (this.state.selected) {
404 /* Click of a second word groups the two together. */
405 const idx1 = this.state.word_sets.findIndex(s => s.has(this.state.selected));
406 const idx2 = this.state.word_sets.findIndex(s => s.has(word));
407 const set1 = this.state.word_sets[idx1];
408 const set2 = this.state.word_sets[idx2];
409 const new_set = new Set([...set2, ...set1]);
413 word_sets: [...this.state.word_sets.slice(0, idx1),
414 ...this.state.word_sets.slice(idx1 + 1, idx2),
416 ...this.state.word_sets.slice(idx2 + 1)]
421 word_sets: [...this.state.word_sets.slice(0, idx2),
423 ...this.state.word_sets.slice(idx2 + 1, idx1),
424 ...this.state.word_sets.slice(idx1 + 1)]
428 /* First click of a word selects it. */
436 if (this.state.submitted)
438 <div className="please-wait">
439 <h2>{this.props.players_judged}/
440 {this.props.players_total} players have responded</h2>
442 Please wait for the rest of the players to complete judging.
447 const btn_class = "ambiguity-button";
448 const btn_selected_class = btn_class + " selected";
451 <div className="ambiguities">
452 <h2>Judging Answers</h2>
454 Click on each pair of answers that should be scored as equivalent,
455 (and click any word twice to split it out from a group). Remember,
456 what goes around comes around, so it's best to be generous when
459 {this.state.word_sets.map(set => {
462 className="ambiguity-group"
463 key={Array.from(set)[0]}
465 {Array.from(set).map(word => {
468 className={this.state.selected === word ?
469 btn_selected_class : btn_class }
471 onClick={() => this.handle_click(word)}
481 Click here when done judging:<br/>
483 onClick={() => this.handle_submit()}
493 class ActivePrompt extends React.PureComponent {
497 const items = props.prompt.items;
503 this.answers = [...Array(items)].map(() => React.createRef());
504 this.handle_submit = this.handle_submit.bind(this);
507 async handle_submit(event) {
508 const form = event.currentTarget;
510 /* Prevent the default page-changing form-submission behavior. */
511 event.preventDefault();
513 const response = await fetch_post_json(`answer/${this.props.prompt.id}`, {
514 answers: this.answers.map(r => r.current.value)
516 if (response.status === 200) {
517 const result = await response.json();
518 if (! result.valid) {
519 add_message("danger", result.message);
523 add_message("danger", "An error occurred submitting your answers");
527 /* Everything worked. Server is happy with our answers. */
535 if (this.state.submitted)
537 <div className="please-wait">
538 <h2>{this.props.players_answered}/
539 {this.props.players_total} players have responded</h2>
541 Please wait for the rest of the players to submit their answers.
547 <div className="active-prompt">
548 <h2>The Game of Empathy</h2>
550 Remember, you're trying to match your answers with
551 what the other players submit.
552 Give {this.props.prompt.items} answers for the following prompt:
554 <h2>{this.props.prompt.prompt}</h2>
555 <form onSubmit={this.handle_submit}>
556 {[...Array(this.props.prompt.items)].map((whocares,i) => {
560 className="form-field large">
566 ref={this.answers[i]}
574 className="form-field large">
575 <button type="submit">
586 class Game extends React.PureComponent {
601 set_game_info(info) {
607 set_player_info(info) {
613 set_other_player_info(info) {
614 const other_players_copy = [...this.state.other_players];
615 const idx = other_players_copy.findIndex(o => o.id === info.id);
617 other_players_copy[idx] = info;
619 other_players_copy.push(info);
622 other_players: other_players_copy
626 set_prompts(prompts) {
632 add_or_update_prompt(prompt) {
633 const prompts_copy = [...this.state.prompts];
634 const idx = prompts_copy.findIndex(p => p.id === prompt.id);
636 prompts_copy[idx] = prompt;
638 prompts_copy.push(prompt);
641 prompts: prompts_copy
645 set_active_prompt(prompt) {
647 active_prompt: prompt
651 set_players_answered(players_answered) {
653 players_answered: players_answered
657 set_ambiguities(ambiguities) {
659 ambiguities: ambiguities
663 set_players_judged(players_judged) {
665 players_judged: players_judged
676 const state = this.state;
677 const players_total = 1 + state.other_players.length;
681 <div className="scores">
684 {state.scores.scores.map(score => {
686 <li key={score.player}>
687 {score.player}: {score.score}
692 <h2>Words submitted</h2>
694 {state.scores.words.map(word => {
697 {`${word.word}: ${word.players.join(', ')}`}
704 onClick={() => fetch_post_json('reset') }
712 if (state.ambiguities){
714 prompt={state.active_prompt}
715 words={state.ambiguities}
716 players_judged={state.players_judged}
717 players_total={players_total}
721 if (state.active_prompt) {
723 prompt={state.active_prompt}
724 players_answered={state.players_answered}
725 players_total={players_total}
732 id={state.game_info.id}
733 url={state.game_info.url}
738 player={state.player_info}
739 other_players={state.other_players}
741 <p key="spacer"></p>,
743 key="category-request"
747 prompts={state.prompts}
751 num_players={1+state.other_players.length}
752 prompts={state.prompts}
758 ReactDOM.render(<Game
759 ref={(me) => window.game = me}
760 />, document.getElementById("empathy"));