]> git.cworth.org Git - lmno-server/blob - empathy.js
Add some autofocus attributes to several forms
[lmno-server] / empathy.js
1 const express = require("express");
2 const Game = require("./game.js");
3
4 const MAX_PROMPT_ITEMS = 20;
5
6 /* This time parameter specifies a time period in which a phase will
7  * not be considered idle in any circumstance. That is, this should be
8  * a reasonable time in which any "active" player should have at least
9  * started interacting with the current phase.
10  *
11  * Specified in seconds
12  */
13 const PHASE_MINIMUM_TIME = 30;
14
15 /* This parameter gives the amount of time that the game will wait to
16  * see activity from pending players. If this amount of time passes
17  * with no activity from any of them, the server will emit an "idle"
18  * event which will let clients issue a vote to end the current phase.
19  *
20  * Specified in seconds
21  */
22 const PHASE_IDLE_TIMEOUT = 15;
23
24 class Empathy extends Game {
25   constructor(id) {
26     super(id);
27     this.state = {
28       prompts: [],
29       active_prompt: null,
30       players_answered: [],
31       players_answering: new Set(),
32       answering_idle: false,
33       end_answers: new Set(),
34       ambiguities: null,
35       players_judged: [],
36       players_judging: new Set(),
37       judging_idle: false,
38       end_judging: new Set(),
39       scores: null,
40       new_game_votes: new Set()
41     };
42     this.answers = [];
43     this.answering_idle_timer = 0;
44     this.answering_start_time_ms = 0;
45     this.judging_idle_timer = 0;
46     this.judging_start_time_ms = 0;
47     this.next_prompt_id = 1;
48     this.equivalencies = {};
49     this.kudos = {};
50   }
51
52   reset() {
53
54     /* Before closing out the current round, we accumulate into each
55      * player's overall score the results from the current round.
56      *
57      * Note: Rather than applying the actual points from each round
58      * into the player's score, we instead accumulate up the number of
59      * players that they bested in each round. This ensures that each
60      * round receives an equal weight in the overall scoring. */
61     let bested = this.state.scores.scores.reduce(
62       (total, score) => total + score.players.length, 0);
63     for (let i = 0; i < this.state.scores.scores.length; i++) {
64       const score = this.state.scores.scores[i];
65       bested -= score.players.length;
66       for (let player_name of score.players) {
67         const player = this.players.find(p => p.name === player_name);
68         if (player.score)
69           player.score += bested;
70         else
71           player.score = bested;
72
73         /* And broadcast that new score out. */
74         this.broadcast_event('player-update', player.info_json());
75       }
76     }
77
78     /* Now that we're done with the active prompt, we remove it from
79      * the list of prompts and also remove any prompts that received
80      * more negative votes than positive. This keeps the list of
81      * prompts clean.
82      */
83     const active_id = this.state.active_prompt.id;
84     this.state.prompts =
85       this.state.prompts.filter(
86         p => p.id !== active_id && p.votes.length >= p.votes_against.length
87       );
88
89     this.state.active_prompt = null;
90     this.state.players_answered = [];
91     this.state.players_answering = new Set();
92     this.state.answering_idle = false;
93     this.state.end_answers = new Set();
94     this.state.ambiguities = null;
95     this.state.players_judged = [];
96     this.state.players_judging = new Set();
97     this.state.judging_idle = false;
98     this.state.end_judging = new Set();
99     this.state.scores = null;
100     this.state.new_game_votes = new Set();
101
102     this.answers = [];
103     if (this.answering_idle_timer) {
104       clearTimeout(this.answering_idle_timer);
105     }
106     this.answering_idle_timer = 0;
107     this.answering_start_time_ms = 0;
108     if (this.judging_idle_timer) {
109       clearTimeout(this.judging_idle_timer);
110     }
111     this.judging_idle_timer = 0;
112     this.judging_start_time_ms = 0;
113     this.equivalencies = {};
114     this.kudos = {};
115
116     this.broadcast_event('game-state', this.game_state_json());
117   }
118
119   add_prompt(items, prompt_string) {
120     if (items > MAX_PROMPT_ITEMS) {
121       return {
122         valid: false,
123         message: `Maximum number of items is ${MAX_PROMPT_ITEMS}`
124       };
125     }
126
127     if (items < 1) {
128       return {
129         valid: false,
130         message: "Category must require at least one item"
131       };
132     }
133
134     const prompt = new Prompt(this.next_prompt_id, items, prompt_string);
135     this.next_prompt_id++;
136
137     this.state.prompts.push(prompt);
138
139     this.broadcast_event_object('prompt', prompt);
140
141     return {
142       valid: true,
143       id: prompt.id
144     };
145   }
146
147   /* Returns true if vote toggled, false for player or prompt not found */
148   toggle_vote(prompt_id, session_id) {
149     const player = this.players_by_session[session_id];
150
151     const prompt = this.state.prompts.find(p => p.id === prompt_id);
152     if (! prompt || ! player)
153       return false;
154
155     prompt.toggle_vote(player.name);
156
157     this.broadcast_event_object('prompt', prompt);
158
159     return true;
160   }
161
162   toggle_vote_against(prompt_id, session_id) {
163     const player = this.players_by_session[session_id];
164
165     const prompt = this.state.prompts.find(p => p.id === prompt_id);
166     if (! prompt || ! player)
167       return false;
168
169     prompt.toggle_vote_against(player.name);
170
171     this.broadcast_event_object('prompt', prompt);
172
173     return true;
174   }
175
176   /* Returns true on success, false for prompt not found. */
177   start(prompt_id) {
178     const prompt = this.state.prompts.find(p => p.id === prompt_id);
179     if (! prompt)
180       return false;
181
182     /* Ignore any start request that comes in while a prompt is
183      * already being played. */
184     if (this.state.active_prompt)
185       return false;
186
187     this.state.active_prompt = prompt;
188
189     this.broadcast_event_object('start', prompt);
190
191     return true;
192   }
193
194   receive_answer(prompt_id, session_id, answers) {
195     const player = this.players_by_session[session_id];
196     if (! player)
197       return { valid: false, message: "Player not found" };
198
199     const prompt = this.state.prompts.find(p => p.id === prompt_id);
200     if (! prompt)
201       return { valid: false, message: "Prompt not found" };
202
203     if (prompt !== this.state.active_prompt)
204       return { valid: false, message: "Prompt no longer active" };
205
206     /* Save the complete answers for our own use later. */
207     this.answers.push({
208       player: player,
209       answers: answers
210     });
211
212     /* Update state (to be sent out to any future clients) */
213     this.state.players_answering.delete(player.name);
214     this.state.players_answered.push(player.name);
215
216     /* And notify all players that this player has answered. */
217     this.broadcast_event_object('player-answered', player.name);
218
219     /* If no players are left in the answering list then we don't need
220      * to wait for the answering_idle_timer to fire, because a person
221      * who isn't there obviously can't be typing. So we can broadcast
222      * the idle event right away. Note that we do this only if the
223      * answering phase has been going for a while to avoid the case of
224      * the first player being super fast and emptying the
225      * players_answering list before anyone else even got in.
226      */
227     if (this.state.players_answering.size === 0 &&
228         ((Date.now() - this.answering_start_time_ms) / 1000) > PHASE_MINIMUM_TIME)
229     {
230       this.broadcast_event_object('answering-idle', true);
231     }
232
233     return { valid: true };
234   }
235
236   receive_answering(prompt_id, session_id) {
237     const player = this.players_by_session[session_id];
238     if (! player)
239       return { valid: false, message: "Player not found" };
240
241     const prompt = this.state.prompts.find(p => p.id === prompt_id);
242     if (! prompt)
243       return { valid: false, message: "Prompt not found" };
244
245     if (prompt !== this.state.active_prompt)
246       return { valid: false, message: "Prompt no longer active" };
247
248     if (this.answering_idle_timer) {
249       clearTimeout(this.answering_idle_timer);
250       this.ansering_idle_timer = 0;
251     }
252     if (! this.state.answering_idle) {
253       this.answering_idle_timer = setTimeout(() => {
254         this.state.answering_idle = true;
255         this.broadcast_event_object('answering-idle', true);
256       }, PHASE_IDLE_TIMEOUT * 1000);
257     }
258
259     if (this.answering_start_time_ms === 0)
260       this.answering_start_time_ms = Date.now();
261
262     /* Notify all players that this player is actively answering. */
263     this.state.players_answering.add(player.name);
264     this.broadcast_event_object('player-answering', player.name);
265
266     return { valid: true };
267   }
268
269   /* Returns true if vote toggled, false for player or prompt not found */
270   toggle_end_answers(prompt_id, session_id) {
271     const player = this.players_by_session[session_id];
272
273     const prompt = this.state.prompts.find(p => p.id === prompt_id);
274     if (! prompt || ! player)
275       return false;
276
277     if (this.state.end_answers.has(player.name)) {
278       this.state.end_answers.delete(player.name);
279       this.broadcast_event_object('unvote-end-answers', player.name);
280     } else {
281       this.state.end_answers.add(player.name);
282       this.broadcast_event_object('vote-end-answers', player.name);
283     }
284
285     return true;
286   }
287
288   perform_judging() {
289     const word_map = {};
290
291     for (let a of this.answers) {
292       for (let word of a.answers) {
293         const key = this.canonize(word);
294         word_map[key] = word;
295       }
296     }
297
298     this.state.ambiguities = Object.values(word_map).sort((a,b) => {
299       return a.toLowerCase().localeCompare(b.toLowerCase());
300     });
301
302     if (this.judging_start_time_ms === 0) {
303       this.judging_start_time_ms = Date.now();
304     }
305
306     this.broadcast_event_object('ambiguities', this.state.ambiguities);
307
308     /* Notify all players of every player that is judging. */
309     for (let player_name of this.state.players_answered) {
310       this.state.players_judging.add(player_name);
311       this.broadcast_event_object('player-judging', player_name);
312     }
313   }
314
315   reset_judging_timeout() {
316     if (this.judging_idle_timer) {
317       clearTimeout(this.judging_idle_timer);
318       this.judging_idle_timer = 0;
319     }
320     if (! this.state.judging_idle) {
321       this.judging_idle_timer = setTimeout(() => {
322         this.state.judging_idle = true;
323         this.broadcast_event_object('judging-idle', true);
324       }, PHASE_IDLE_TIMEOUT * 1000);
325     }
326   }
327
328   receive_judged(prompt_id, session_id, word_groups) {
329     const player = this.players_by_session[session_id];
330     if (! player)
331       return { valid: false, message: "Player not found" };
332
333     const prompt = this.state.prompts.find(p => p.id === prompt_id);
334     if (! prompt)
335       return { valid: false, message: "Prompt not found" };
336
337     if (prompt !== this.state.active_prompt)
338       return { valid: false, message: "Prompt no longer active" };
339
340     this.reset_judging_timeout();
341
342     /* Each player submits some number of groups of answers that
343      * should be considered equivalent. The server expands that into
344      * the set of pair-wise equivalencies that are expressed. The
345      * reason for that is so that the server can determine which
346      * pair-wise equivalencies have majority support.
347      */
348     for (let group of word_groups) {
349
350       const words = group.words;
351       if (group.kudos) {
352         this.kudos[player.name] = {
353           player: player,
354           words: [...words]
355         };
356       }
357
358       for (let i = 0; i < words.length - 1; i++) {
359         for (let j = i + 1; j < words.length; j++) {
360           let eq = [words[i], words[j]];
361
362           /* Put the two words into a reliable order so that we don't
363            * miss a pair of equivalent equivalencies just because they
364            * happen to be in the opposite order. */
365           if (eq[0].localeCompare(eq[1]) > 0) {
366             eq = [words[j], words[i]];
367           }
368
369           const key=`${this.canonize(eq[0])}:${this.canonize(eq[1])}`;
370
371           const exist = this.equivalencies[key];
372           if (exist) {
373             exist.count++;
374           } else {
375             this.equivalencies[key] = {
376               count: 1,
377               words: eq
378             };
379           }
380
381         }
382       }
383     }
384
385     /* Update state (to be sent out to any future clients) */
386     this.state.players_judging.delete(player.name);
387     this.state.players_judged.push(player.name);
388
389     /* And notify all players that this player has judged. */
390     this.broadcast_event_object('player-judged', player.name);
391
392     /* If no players are left in the judging list then we don't need
393      * to wait for the judging_idle_timer to fire, because a person
394      * who isn't there obviously can't be judging. So we can broadcast
395      * the idle event right away. Note that we do this only if the
396      * judging phase has been going for a while to avoid the case of
397      * the first player being super fast and emptying the
398      * players_judging list before anyone else even got in.
399      */
400     if (this.state.players_judging.size === 0 &&
401         ((Date.now() - this.judging_start_time_ms) / 1000) > PHASE_MINIMUM_TIME)
402     {
403       this.broadcast_event_object('judging-idle', true);
404     }
405
406     return { valid: true };
407   }
408
409   receive_judging(prompt_id, session_id) {
410
411     const player = this.players_by_session[session_id];
412     if (! player)
413       return { valid: false, message: "Player not found" };
414
415     const prompt = this.state.prompts.find(p => p.id === prompt_id);
416     if (! prompt)
417       return { valid: false, message: "Prompt not found" };
418
419     if (prompt !== this.state.active_prompt)
420       return { valid: false, message: "Prompt no longer active" };
421
422     this.reset_judging_timeout();
423
424     /* Notify all players that this player is actively judging. */
425     this.state.players_judging.add(player.name);
426     this.broadcast_event_object('player-judging', player.name);
427
428     return { valid: true };
429   }
430
431   /* Returns true if vote toggled, false for player or prompt not found */
432   toggle_end_judging(prompt_id, session_id) {
433     const player = this.players_by_session[session_id];
434
435     const prompt = this.state.prompts.find(p => p.id === prompt_id);
436     if (! prompt || ! player)
437       return false;
438
439     if (this.state.end_judging.has(player.name)) {
440       this.state.end_judging.delete(player.name);
441       this.broadcast_event_object('unvote-end-judging', player.name);
442     } else {
443       this.state.end_judging.add(player.name);
444       this.broadcast_event_object('vote-end-judging', player.name);
445     }
446
447     return true;
448   }
449
450   /* Returns true if vote toggled, false for player or prompt not found */
451   toggle_new_game(prompt_id, session_id) {
452     const player = this.players_by_session[session_id];
453
454     const prompt = this.state.prompts.find(p => p.id === prompt_id);
455     if (! prompt || ! player)
456       return false;
457
458     if (this.state.new_game_votes.has(player.name)) {
459       this.state.new_game_votes.delete(player.name);
460       this.broadcast_event_object('unvote-new-game', player.name);
461     } else {
462       this.state.new_game_votes.add(player.name);
463       this.broadcast_event_object('vote-new-game', player.name);
464     }
465
466     return true;
467   }
468
469   canonize(word) {
470     return word.trim().toLowerCase();
471   }
472
473   compute_scores() {
474     const word_submitters = {};
475
476     /* Perform a (non-strict) majority ruling on equivalencies,
477      * dropping all that didn't get enough votes. */
478     const quorum = Math.floor((this.state.players_judged.length + 1)/2);
479     const agreed_equivalencies = Object.values(this.equivalencies).filter(
480       eq => eq.count >= quorum);
481
482     /* And with that agreed set of equivalencies, construct the full
483      * groups. */
484     const word_maps = {};
485
486     for (let e of agreed_equivalencies) {
487       const word0_canon = this.canonize(e.words[0]);
488       const word1_canon = this.canonize(e.words[1]);
489       let group = word_maps[word0_canon];
490       if (! group)
491         group = word_maps[word1_canon];
492       if (! group)
493         group = {
494           words: [],
495           players: new Set(),
496           kudos: new Set()
497         };
498
499       if (! word_maps[word0_canon]) {
500         word_maps[word0_canon] = group;
501         group.words.push(e.words[0]);
502       }
503
504       if (! word_maps[word1_canon]) {
505         word_maps[word1_canon] = group;
506         group.words.push(e.words[1]);
507       }
508     }
509
510     /* Now go through answers from each player and add the player
511      * to the set corresponding to each word group. */
512     for (let a of this.answers) {
513       for (let word of a.answers) {
514         const word_canon = this.canonize(word);
515         /* If there's no group yet, this is a singleton word. */
516         if (word_maps[word_canon]) {
517           word_maps[word_canon].players.add(a.player);
518         } else {
519           const group = {
520             words: [word],
521             players: new Set(),
522             kudos: new Set()
523           };
524           group.players.add(a.player);
525           word_maps[word_canon] = group;
526         }
527       }
528     }
529
530     /* Apply kudos from each player to the word maps, (using a set so
531      * that no word_map can get multiple kudos from a single
532      * player). */
533     for (let kudos of Object.values(this.kudos)) {
534       for (let word of kudos.words) {
535         const word_canon = this.canonize(word);
536         if (! word_maps[word_canon])
537           continue;
538         /* Don't let any player give kudos to a group where they
539          * submitted a word themself. That just wouldn't be right. */
540         if (! word_maps[word_canon].players.has(kudos.player)) {
541           word_maps[word_canon].kudos.add(kudos.player);
542         }
543       }
544     }
545
546     /* Now that we've assigned the players to these word maps, we now
547      * want to collapse the groups down to a single array of
548      * word_groups.
549      *
550      * The difference between "word_maps" and "word_groups" is that
551      * the former has a property for every word that maps to a group,
552      * (so if you iterate over the keys you will see the same group
553      * multiple times). In contrast, iterating over"word_groups" will
554      * have you visit each group only once. */
555     const word_groups = Object.entries(word_maps).filter(
556       entry => entry[0] === this.canonize(entry[1].words[0]))
557           .map(entry => entry[1]);
558
559     /* Now, go through each word group and assign the scores out to
560      * the corresponding players.
561      *
562      * Note: We do this by going through the word groups, (as opposed
563      * to the list of words from the players again), specifically to
564      * avoid giving a player points for a word group twice (in the
565      * case where a player submits two different words that the group
566      * ends up judging as equivalent).
567      */
568     this.players.forEach(p => {
569       p.round_score = 0;
570       p.round_kudos = 0;
571     });
572     for (let group of word_groups) {
573       group.players.forEach(p => {
574         p.round_score += group.players.size;
575         p.round_kudos += group.kudos.size;
576       });
577     }
578
579     const scores = this.players.filter(p => p.active).map(p => {
580       return {
581         player: p.name,
582         score: p.round_score,
583         kudos: p.round_kudos
584       };
585     });
586
587     scores.sort((a,b) => {
588       const delta = b.score - a.score;
589       if (delta)
590         return delta;
591       return b.kudos - a.kudos;
592     });
593
594     /* After sorting individual players by score, group players
595      * together who have the same score. */
596     const reducer = (list, next) => {
597       if (list.length
598           && list[list.length-1].score == next.score
599           && list[list.length-1].kudos == next.kudos
600          )
601       {
602         list[list.length-1].players.push(next.player);
603       } else {
604         list.push({
605           players: [next.player],
606           score: next.score,
607           kudos: next.kudos,
608         });
609       }
610       return list;
611     };
612
613     const grouped_scores = scores.reduce(reducer, []);
614
615     /* Put the word groups into a form the client can consume.
616      */
617     const words_submitted = word_groups.map(
618       group => {
619         return {
620           word: group.words.join('/'),
621           players: Array.from(group.players).map(p => p.name),
622           kudos: Array.from(group.kudos).map(p => p.name)
623         };
624       }
625     );
626
627     words_submitted.sort((a,b) => {
628       const delta = b.players.length - a.players.length;
629       if (delta !== 0)
630         return delta;
631       return b.kudos.length - a.kudos.length;
632     });
633
634     /* Put this round's scores into the game state object so it will
635      * be sent to any new clients that join. */
636     this.state.scores = {
637       scores: grouped_scores,
638       words: words_submitted
639     };
640
641     /* And broadcast the scores to all connected clients. */
642     this.broadcast_event_object('scores', this.state.scores);
643   }
644 }
645
646 Empathy.router = express.Router();
647 const router = Empathy.router;
648
649 class Prompt {
650   constructor(id, items, prompt) {
651     this.id = id;
652     this.items = items;
653     this.prompt = prompt;
654     this.votes = [];
655     this.votes_against = [];
656   }
657
658   toggle_vote(player_name) {
659     if (this.votes.find(v => v === player_name))
660       this.votes = this.votes.filter(v => v !== player_name);
661     else
662       this.votes.push(player_name);
663   }
664
665   toggle_vote_against(player_name) {
666     if (this.votes_against.find(v => v === player_name)) {
667       this.votes_against = this.votes_against.filter(v => v !== player_name);
668     } else {
669       this.votes_against.push(player_name);
670       /* When voting against, we also remove any vote _for_ the same
671        * prompt. */
672       this.votes = this.votes.filter(v => v !== player_name);
673     }
674   }
675 }
676
677 router.post('/prompts', (request, response) => {
678   const game = request.game;
679
680   const result = game.add_prompt(request.body.items, request.body.prompt);
681
682   response.json(result);
683 });
684
685 router.post('/vote/:prompt_id([0-9]+)', (request, response) => {
686   const game = request.game;
687   const prompt_id = parseInt(request.params.prompt_id, 10);
688
689   if (game.toggle_vote(prompt_id, request.session.id))
690     response.send('');
691   else
692     response.sendStatus(404);
693 });
694
695 router.post('/vote_against/:prompt_id([0-9]+)', (request, response) => {
696   const game = request.game;
697   const prompt_id = parseInt(request.params.prompt_id, 10);
698
699   if (game.toggle_vote_against(prompt_id, request.session.id))
700     response.send('');
701   else
702     response.sendStatus(404);
703 });
704
705 router.post('/start/:prompt_id([0-9]+)', (request, response) => {
706   const game = request.game;
707   const prompt_id = parseInt(request.params.prompt_id, 10);
708
709   if (game.start(prompt_id))
710     response.send('');
711   else
712     response.sendStatus(404);
713 });
714
715 router.post('/answer/:prompt_id([0-9]+)', (request, response) => {
716   const game = request.game;
717   const prompt_id = parseInt(request.params.prompt_id, 10);
718
719   const result = game.receive_answer(prompt_id,
720                                      request.session.id,
721                                      request.body.answers);
722   response.json(result);
723
724   /* If every registered player has answered, then there's no need to
725    * wait for anything else. */
726   if (game.state.players_answered.length >= game.active_players)
727     game.perform_judging();
728 });
729
730 router.post('/answering/:prompt_id([0-9]+)', (request, response) => {
731   const game = request.game;
732   const prompt_id = parseInt(request.params.prompt_id, 10);
733
734   const result = game.receive_answering(prompt_id,
735                                         request.session.id);
736   response.json(result);
737 });
738
739 router.post('/end-answers/:prompt_id([0-9]+)', (request, response) => {
740   const game = request.game;
741   const prompt_id = parseInt(request.params.prompt_id, 10);
742
743   if (game.toggle_end_answers(prompt_id, request.session.id))
744     response.send('');
745   else
746     response.sendStatus(404);
747
748   /* The majority rule here includes all players that have answered as
749    * well as all that have started typing. */
750   const players_involved = (game.state.players_answered.length +
751                             game.state.players_answering.size);
752
753   if (game.state.end_answers.size > players_involved / 2)
754     game.perform_judging();
755 });
756
757 router.post('/judged/:prompt_id([0-9]+)', (request, response) => {
758   const game = request.game;
759   const prompt_id = parseInt(request.params.prompt_id, 10);
760
761   const result = game.receive_judged(prompt_id,
762                                      request.session.id,
763                                      request.body.word_groups);
764   response.json(result);
765
766   /* If every player who answered has also judged, then there's no
767    * need to wait for anything else. */
768   const judged_set = new Set(game.state.players_judged);
769   if ([...game.state.players_answered].filter(x => !judged_set.has(x)).length === 0)
770     game.compute_scores();
771 });
772
773 router.post('/judging/:prompt_id([0-9]+)', (request, response) => {
774   const game = request.game;
775   const prompt_id = parseInt(request.params.prompt_id, 10);
776
777   const result = game.receive_judging(prompt_id,
778                                       request.session.id);
779   response.json(result);
780 });
781
782 router.post('/end-judging/:prompt_id([0-9]+)', (request, response) => {
783   const game = request.game;
784   const prompt_id = parseInt(request.params.prompt_id, 10);
785
786   if (game.toggle_end_judging(prompt_id, request.session.id))
787     response.send('');
788   else
789     response.sendStatus(404);
790
791   if (game.state.end_judging.size > (game.state.players_answered.length / 2))
792     game.compute_scores();
793 });
794
795 router.post('/new-game/:prompt_id([0-9]+)', (request, response) => {
796   const game = request.game;
797   const prompt_id = parseInt(request.params.prompt_id, 10);
798
799   if (game.toggle_new_game(prompt_id, request.session.id))
800     response.send('');
801   else
802     response.sendStatus(404);
803
804   if (game.state.new_game_votes.size > (game.state.players_answered.length / 2))
805     game.reset();
806 });
807
808 router.post('/reset', (request, response) => {
809   const game = request.game;
810   game.reset();
811
812   response.send('');
813 });
814
815 Empathy.meta = {
816   name: "Empathy",
817   identifier: "empathy",
818 };
819
820 exports.Game = Empathy;