]> git.cworth.org Git - empires-server/blob - empathy.js
Add a game phase to perform judging
[empires-server] / empathy.js
1 const express = require("express");
2 const Game = require("./game.js");
3
4 class Empathy extends Game {
5   constructor(id) {
6     super(id);
7     this.state = {
8       prompts: [],
9       active_prompt: null,
10       players_answered: 0,
11       ambiguities: null,
12       players_judged: 0,
13       scores: null
14     };
15     this.answers = [];
16     this.next_prompt_id = 1;
17   }
18
19   reset() {
20
21     /* Before closing out the current round, we accumulate that score
22      * for each player into their runnning total. */
23     for (let score of this.state.scores.scores) {
24       const player = this.players.find(p => p.name === score.player);
25       if (player.score)
26         player.score += score.score;
27       else
28         player.score = score.score;
29
30       /* And broadcast that new score out. */
31       this.broadcast_event('player-update', player.info_json());
32     }
33
34     /* Now that we're done with the active prompt, we remove it from
35      * the list of prompts and also remove any prompts that received
36      * no votes. This keeps the list of prompts clean.
37      */
38     const active_id = this.state.active_prompt.id;
39     this.state.prompts =
40       this.state.prompts.filter(
41         p => p.id !== active_id && p.votes.length > 0
42       );
43
44     this.state.active_prompt = null;
45     this.state.players_answered = 0;
46     this.state.ambiguities = 0;
47     this.state.players_judged = 0;
48     this.state.scores = null;
49
50     this.answers = [];
51
52     this.broadcast_event_object('game-state', this.state);
53   }
54
55   add_prompt(items, prompt_string) {
56     const prompt = new Prompt(this.next_prompt_id, items, prompt_string);
57     this.next_prompt_id++;
58
59     this.state.prompts.push(prompt);
60
61     this.broadcast_event_object('prompt', prompt);
62
63     return prompt;
64   }
65
66   /* Returns true if vote toggled, false for player or prompt not found */
67   toggle_vote(prompt_id, session_id) {
68     const player = this.players_by_session[session_id];
69
70     const prompt = this.state.prompts.find(p => p.id === prompt_id);
71     if (! prompt || ! player)
72       return false;
73
74     prompt.toggle_vote(player.name);
75
76     this.broadcast_event_object('prompt', prompt);
77
78     return true;
79   }
80
81   /* Returns true on success, false for prompt not found. */
82   start(prompt_id) {
83     const prompt = this.state.prompts.find(p => p.id === prompt_id);
84     if (! prompt)
85       return false;
86
87     /* Ignore any start request that comes in while a prompt is
88      * already being played. */
89     if (this.state.active_prompt)
90       return false;
91
92     this.state.active_prompt = prompt;
93
94     this.broadcast_event_object('start', prompt);
95
96     return true;
97   }
98
99   receive_answer(prompt_id, session_id, answers) {
100     const player = this.players_by_session[session_id];
101     if (! player)
102       return { valid: false, message: "Player not found" };
103
104     const prompt = this.state.prompts.find(p => p.id === prompt_id);
105     if (! prompt)
106       return { valid: false, message: "Prompt not found" };
107
108     if (prompt !== this.state.active_prompt)
109       return { valid: false, message: "Prompt no longer active" };
110
111     /* Save the complete answers for our own use later. */
112     this.answers.push({
113       player: player,
114       answers: answers
115     });
116
117     /* And notify players how many players have answered. */
118     this.state.players_answered++;
119     this.broadcast_event_object('answered', this.state.players_answered);
120
121     return { valid: true };
122   }
123
124   perform_judging() {
125     const word_map = {};
126
127     for (let a of this.answers) {
128       for (let word of a.answers) {
129         const key = this.canonize(word);
130         word_map[key] = word;
131       }
132     }
133
134     this.state.ambiguities = Object.values(word_map);
135
136     this.broadcast_event_object('ambiguities', this.state.ambiguities);
137   }
138
139   receive_judging() {
140     /* And notify players how many players have completed judging. */
141     this.state.players_judged++;
142     this.broadcast_event_object('judged', this.state.players_judged);
143
144     return { valid: true };
145   }
146
147   canonize(word) {
148     return word.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
149   }
150
151   compute_scores() {
152     const word_submitters = {};
153     const scores = [];
154
155     for (let a of this.answers) {
156       for (let word of a.answers) {
157         if (word_submitters[word])
158           word_submitters[word].push(a.player.name);
159         else
160           word_submitters[word] = [a.player.name];
161       }
162     }
163
164     for (let a of this.answers) {
165       let score = 0;
166       for (let word of a.answers) {
167         score += word_submitters[word].length;
168       }
169       scores.push({
170         player: a.player.name,
171         score: score
172       });
173     }
174
175     scores.sort((a,b) => {
176       return b.score - a.score;
177     });
178
179     const word_submitters_arr = [];
180     for (let word in word_submitters)
181       word_submitters_arr.push({word: word, players: word_submitters[word]});
182
183     word_submitters_arr.sort((a,b) => {
184       return b.players.length - a.players.length;
185     });
186
187     /* Put this round's scores into the game state object so it will
188      * be sent to any new clients that join. */
189     this.state.scores = {
190       scores: scores,
191       words: word_submitters_arr
192     };
193
194     /* And broadcast the scores to all connected clients. */
195     this.broadcast_event_object('scores', this.state.scores);
196   }
197 }
198
199 Empathy.router = express.Router();
200 const router = Empathy.router;
201
202 class Prompt {
203   constructor(id, items, prompt) {
204     this.id = id;
205     this.items = items;
206     this.prompt = prompt;
207     this.votes = [];
208   }
209
210   toggle_vote(player_name) {
211     if (this.votes.find(v => v === player_name))
212       this.votes = this.votes.filter(v => v !== player_name);
213     else
214       this.votes.push(player_name);
215   }
216 }
217
218 router.post('/prompts', (request, response) => {
219   const game = request.game;
220
221   game.add_prompt(request.body.items, request.body.prompt);
222 });
223
224 router.post('/vote/:prompt_id([0-9]+)', (request, response) => {
225   const game = request.game;
226   const prompt_id = parseInt(request.params.prompt_id, 10);
227
228   if (game.toggle_vote(prompt_id, request.session.id))
229     response.sendStatus(200);
230   else
231     response.sendStatus(404);
232 });
233
234 router.post('/start/:prompt_id([0-9]+)', (request, response) => {
235   const game = request.game;
236   const prompt_id = parseInt(request.params.prompt_id, 10);
237
238   if (game.start(prompt_id))
239     response.sendStatus(200);
240   else
241     response.sendStatus(404);
242 });
243
244 router.post('/answer/:prompt_id([0-9]+)', (request, response) => {
245   const game = request.game;
246   const prompt_id = parseInt(request.params.prompt_id, 10);
247
248   const result = game.receive_answer(prompt_id,
249                                      request.session.id,
250                                      request.body.answers);
251   response.json(result);
252
253   if (game.state.players_answered >= game.players.length)
254     game.perform_judging();
255 });
256
257 router.post('/judging/:prompt_id([0-9]+)', (request, response) => {
258   const game = request.game;
259   const prompt_id = parseInt(request.params.prompt_id, 10);
260
261   const result = game.receive_judging(prompt_id,
262                                       request.session.id,
263                                       request.body);
264   response.json(result);
265
266   if (game.state.players_judged >= game.players.length)
267     game.compute_scores();
268 });
269
270 router.post('/reset', (request, response) => {
271   const game = request.game;
272   game.reset();
273 });
274
275 Empathy.meta = {
276   name: "Empathy",
277   identifier: "empathy",
278 };
279
280 exports.Game = Empathy;