]> git.cworth.org Git - empires-server/blob - empathy.js
Add scoring of kudos along with word groups
[empires-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] = [...words];
353
354       for (let i = 0; i < words.length - 1; i++) {
355         for (let j = i + 1; j < words.length; j++) {
356           let eq = [words[i], words[j]];
357
358           /* Put the two words into a reliable order so that we don't
359            * miss a pair of equivalent equivalencies just because they
360            * happen to be in the opposite order. */
361           if (eq[0].localeCompare(eq[1]) > 0) {
362             eq = [words[j], words[i]];
363           }
364
365           const key=`${this.canonize(eq[0])}:${this.canonize(eq[1])}`;
366
367           const exist = this.equivalencies[key];
368           if (exist) {
369             exist.count++;
370           } else {
371             this.equivalencies[key] = {
372               count: 1,
373               words: eq
374             };
375           }
376
377         }
378       }
379     }
380
381     /* Update state (to be sent out to any future clients) */
382     this.state.players_judging.delete(player.name);
383     this.state.players_judged.push(player.name);
384
385     /* And notify all players that this player has judged. */
386     this.broadcast_event_object('player-judged', player.name);
387
388     /* If no players are left in the judging list then we don't need
389      * to wait for the judging_idle_timer to fire, because a person
390      * who isn't there obviously can't be judging. So we can broadcast
391      * the idle event right away. Note that we do this only if the
392      * judging phase has been going for a while to avoid the case of
393      * the first player being super fast and emptying the
394      * players_judging list before anyone else even got in.
395      */
396     if (this.state.players_judging.size === 0 &&
397         ((Date.now() - this.judging_start_time_ms) / 1000) > PHASE_MINIMUM_TIME)
398     {
399       this.broadcast_event_object('judging-idle', true);
400     }
401
402     return { valid: true };
403   }
404
405   receive_judging(prompt_id, session_id) {
406
407     const player = this.players_by_session[session_id];
408     if (! player)
409       return { valid: false, message: "Player not found" };
410
411     const prompt = this.state.prompts.find(p => p.id === prompt_id);
412     if (! prompt)
413       return { valid: false, message: "Prompt not found" };
414
415     if (prompt !== this.state.active_prompt)
416       return { valid: false, message: "Prompt no longer active" };
417
418     this.reset_judging_timeout();
419
420     /* Notify all players that this player is actively judging. */
421     this.state.players_judging.add(player.name);
422     this.broadcast_event_object('player-judging', player.name);
423
424     return { valid: true };
425   }
426
427   /* Returns true if vote toggled, false for player or prompt not found */
428   toggle_end_judging(prompt_id, session_id) {
429     const player = this.players_by_session[session_id];
430
431     const prompt = this.state.prompts.find(p => p.id === prompt_id);
432     if (! prompt || ! player)
433       return false;
434
435     if (this.state.end_judging.has(player.name)) {
436       this.state.end_judging.delete(player.name);
437       this.broadcast_event_object('unvote-end-judging', player.name);
438     } else {
439       this.state.end_judging.add(player.name);
440       this.broadcast_event_object('vote-end-judging', player.name);
441     }
442
443     return true;
444   }
445
446   /* Returns true if vote toggled, false for player or prompt not found */
447   toggle_new_game(prompt_id, session_id) {
448     const player = this.players_by_session[session_id];
449
450     const prompt = this.state.prompts.find(p => p.id === prompt_id);
451     if (! prompt || ! player)
452       return false;
453
454     if (this.state.new_game_votes.has(player.name)) {
455       this.state.new_game_votes.delete(player.name);
456       this.broadcast_event_object('unvote-new-game', player.name);
457     } else {
458       this.state.new_game_votes.add(player.name);
459       this.broadcast_event_object('vote-new-game', player.name);
460     }
461
462     return true;
463   }
464
465   canonize(word) {
466     return word.trim().toLowerCase();
467   }
468
469   compute_scores() {
470     const word_submitters = {};
471
472     /* Perform a (non-strict) majority ruling on equivalencies,
473      * dropping all that didn't get enough votes. */
474     const quorum = Math.floor((this.state.players_judged.length + 1)/2);
475     const agreed_equivalencies = Object.values(this.equivalencies).filter(
476       eq => eq.count >= quorum);
477
478     /* And with that agreed set of equivalencies, construct the full
479      * groups. */
480     const word_maps = {};
481
482     for (let e of agreed_equivalencies) {
483       const word0_canon = this.canonize(e.words[0]);
484       const word1_canon = this.canonize(e.words[1]);
485       let group = word_maps[word0_canon];
486       if (! group)
487         group = word_maps[word1_canon];
488       if (! group)
489         group = {
490           words: [],
491           players: new Set(),
492           kudos: new Set()
493         };
494
495       if (! word_maps[word0_canon]) {
496         word_maps[word0_canon] = group;
497         group.words.push(e.words[0]);
498       }
499
500       if (! word_maps[word1_canon]) {
501         word_maps[word1_canon] = group;
502         group.words.push(e.words[1]);
503       }
504     }
505
506     /* Now go through answers from each player and add the player
507      * to the set corresponding to each word group. */
508     for (let a of this.answers) {
509       for (let word of a.answers) {
510         const word_canon = this.canonize(word);
511         /* If there's no group yet, this is a singleton word. */
512         if (word_maps[word_canon]) {
513           word_maps[word_canon].players.add(a.player);
514         } else {
515           const group = {
516             words: [word],
517             players: new Set(),
518             kudos: new Set()
519           };
520           group.players.add(a.player);
521           word_maps[word_canon] = group;
522         }
523       }
524     }
525
526     /* Apply kudos from each player to the word maps, (using a set so
527      * that no word_map can get multiple kudos from a single
528      * player). */
529     for (let player of Object.keys(this.kudos)) {
530       for (let word of this.kudos[player]) {
531         const word_canon = this.canonize(word);
532         if (! word_maps[word_canon])
533           continue;
534         word_maps[word_canon].kudos.add(player);
535       }
536     }
537
538     /* Now that we've assigned the players to these word maps, we now
539      * want to collapse the groups down to a single array of
540      * word_groups.
541      *
542      * The difference between "word_maps" and "word_groups" is that
543      * the former has a property for every word that maps to a group,
544      * (so if you iterate over the keys you will see the same group
545      * multiple times). In contrast, iterating over"word_groups" will
546      * have you visit each group only once. */
547     const word_groups = Object.entries(word_maps).filter(
548       entry => entry[0] === this.canonize(entry[1].words[0]))
549           .map(entry => entry[1]);
550
551     /* Now, go through each word group and assign the scores out to
552      * the corresponding players.
553      *
554      * Note: We do this by going through the word groups, (as opposed
555      * to the list of words from the players again), specifically to
556      * avoid giving a player points for a word group twice (in the
557      * case where a player submits two different words that the group
558      * ends up judging as equivalent).
559      */
560     this.players.forEach(p => {
561       p.round_score = 0;
562       p.round_kudos = 0;
563     });
564     for (let group of word_groups) {
565       group.players.forEach(p => {
566         p.round_score += group.players.size;
567         p.round_kudos += group.kudos.size;
568       });
569     }
570
571     const scores = this.players.filter(p => p.active).map(p => {
572       return {
573         player: p.name,
574         score: p.round_score,
575         kudos: p.round_kudos
576       };
577     });
578
579     scores.sort((a,b) => {
580       const delta = b.score - a.score;
581       if (delta)
582         return delta;
583       return b.kudos - a.kudos;
584     });
585
586     /* After sorting individual players by score, group players
587      * together who have the same score. */
588     const reducer = (list, next) => {
589       if (list.length
590           && list[list.length-1].score == next.score
591           && list[list.length-1].kudos == next.kudos
592          )
593       {
594         list[list.length-1].players.push(next.player);
595       } else {
596         list.push({
597           players: [next.player],
598           score: next.score,
599           kudos: next.kudos,
600         });
601       }
602       return list;
603     };
604
605     const grouped_scores = scores.reduce(reducer, []);
606
607     /* Put the word groups into a form the client can consume.
608      */
609     const words_submitted = word_groups.map(
610       group => {
611         return {
612           word: group.words.join('/'),
613           players: Array.from(group.players).map(p => p.name),
614           kudos: Array.from(group.kudos)
615         };
616       }
617     );
618
619     words_submitted.sort((a,b) => {
620       const delta = b.players.length - a.players.length;
621       if (delta !== 0)
622         return delta;
623       return b.kudos.length - a.kudos.length;
624     });
625
626     /* Put this round's scores into the game state object so it will
627      * be sent to any new clients that join. */
628     this.state.scores = {
629       scores: grouped_scores,
630       words: words_submitted
631     };
632
633     /* And broadcast the scores to all connected clients. */
634     this.broadcast_event_object('scores', this.state.scores);
635   }
636 }
637
638 Empathy.router = express.Router();
639 const router = Empathy.router;
640
641 class Prompt {
642   constructor(id, items, prompt) {
643     this.id = id;
644     this.items = items;
645     this.prompt = prompt;
646     this.votes = [];
647     this.votes_against = [];
648   }
649
650   toggle_vote(player_name) {
651     if (this.votes.find(v => v === player_name))
652       this.votes = this.votes.filter(v => v !== player_name);
653     else
654       this.votes.push(player_name);
655   }
656
657   toggle_vote_against(player_name) {
658     if (this.votes_against.find(v => v === player_name)) {
659       this.votes_against = this.votes_against.filter(v => v !== player_name);
660     } else {
661       this.votes_against.push(player_name);
662       /* When voting against, we also remove any vote _for_ the same
663        * prompt. */
664       this.votes = this.votes.filter(v => v !== player_name);
665     }
666   }
667 }
668
669 router.post('/prompts', (request, response) => {
670   const game = request.game;
671
672   const result = game.add_prompt(request.body.items, request.body.prompt);
673
674   response.json(result);
675 });
676
677 router.post('/vote/:prompt_id([0-9]+)', (request, response) => {
678   const game = request.game;
679   const prompt_id = parseInt(request.params.prompt_id, 10);
680
681   if (game.toggle_vote(prompt_id, request.session.id))
682     response.send('');
683   else
684     response.sendStatus(404);
685 });
686
687 router.post('/vote_against/:prompt_id([0-9]+)', (request, response) => {
688   const game = request.game;
689   const prompt_id = parseInt(request.params.prompt_id, 10);
690
691   if (game.toggle_vote_against(prompt_id, request.session.id))
692     response.send('');
693   else
694     response.sendStatus(404);
695 });
696
697 router.post('/start/:prompt_id([0-9]+)', (request, response) => {
698   const game = request.game;
699   const prompt_id = parseInt(request.params.prompt_id, 10);
700
701   if (game.start(prompt_id))
702     response.send('');
703   else
704     response.sendStatus(404);
705 });
706
707 router.post('/answer/:prompt_id([0-9]+)', (request, response) => {
708   const game = request.game;
709   const prompt_id = parseInt(request.params.prompt_id, 10);
710
711   const result = game.receive_answer(prompt_id,
712                                      request.session.id,
713                                      request.body.answers);
714   response.json(result);
715
716   /* If every registered player has answered, then there's no need to
717    * wait for anything else. */
718   if (game.state.players_answered.length >= game.active_players)
719     game.perform_judging();
720 });
721
722 router.post('/answering/:prompt_id([0-9]+)', (request, response) => {
723   const game = request.game;
724   const prompt_id = parseInt(request.params.prompt_id, 10);
725
726   const result = game.receive_answering(prompt_id,
727                                         request.session.id);
728   response.json(result);
729 });
730
731 router.post('/end-answers/:prompt_id([0-9]+)', (request, response) => {
732   const game = request.game;
733   const prompt_id = parseInt(request.params.prompt_id, 10);
734
735   if (game.toggle_end_answers(prompt_id, request.session.id))
736     response.send('');
737   else
738     response.sendStatus(404);
739
740   /* The majority rule here includes all players that have answered as
741    * well as all that have started typing. */
742   const players_involved = (game.state.players_answered.length +
743                             game.state.players_answering.size);
744
745   if (game.state.end_answers.size > players_involved / 2)
746     game.perform_judging();
747 });
748
749 router.post('/judged/:prompt_id([0-9]+)', (request, response) => {
750   const game = request.game;
751   const prompt_id = parseInt(request.params.prompt_id, 10);
752
753   const result = game.receive_judged(prompt_id,
754                                      request.session.id,
755                                      request.body.word_groups);
756   response.json(result);
757
758   /* If every player who answered has also judged, then there's no
759    * need to wait for anything else. */
760   const judged_set = new Set(game.state.players_judged);
761   if ([...game.state.players_answered].filter(x => !judged_set.has(x)).length === 0)
762     game.compute_scores();
763 });
764
765 router.post('/judging/:prompt_id([0-9]+)', (request, response) => {
766   const game = request.game;
767   const prompt_id = parseInt(request.params.prompt_id, 10);
768
769   const result = game.receive_judging(prompt_id,
770                                       request.session.id);
771   response.json(result);
772 });
773
774 router.post('/end-judging/:prompt_id([0-9]+)', (request, response) => {
775   const game = request.game;
776   const prompt_id = parseInt(request.params.prompt_id, 10);
777
778   if (game.toggle_end_judging(prompt_id, request.session.id))
779     response.send('');
780   else
781     response.sendStatus(404);
782
783   if (game.state.end_judging.size > (game.state.players_answered.length / 2))
784     game.compute_scores();
785 });
786
787 router.post('/new-game/:prompt_id([0-9]+)', (request, response) => {
788   const game = request.game;
789   const prompt_id = parseInt(request.params.prompt_id, 10);
790
791   if (game.toggle_new_game(prompt_id, request.session.id))
792     response.send('');
793   else
794     response.sendStatus(404);
795
796   if (game.state.new_game_votes.size > (game.state.players_answered.length / 2))
797     game.reset();
798 });
799
800 router.post('/reset', (request, response) => {
801   const game = request.game;
802   game.reset();
803
804   response.send('');
805 });
806
807 Empathy.meta = {
808   name: "Empathy",
809   identifier: "empathy",
810 };
811
812 exports.Game = Empathy;