--- /dev/null
+const express = require("express");
+const Game = require("./game.js");
+const TWL_WORDS = new Set(require("./twl-words.js"));
+const { TILE_DISTRIBUTION_NO_BLANKS, make_bag } = require("./tiles.js");
+
+const CLAIM_TIMEOUT_MS = 60000; /* 60 seconds to form a word */
+const CLAIM_WARNING_MS = 15000; /* show timer for last 15 seconds */
+const VOTE_TIMEOUT_MS = 15000; /* 15 seconds to vote */
+const REVEAL_COUNTDOWN_MS = 3000; /* 3 seconds to reveal a tile */
+const MIN_WORD_LENGTH = 4;
+
+class Anagrams extends Game {
+ constructor(id) {
+ super(id);
+ this.state = {
+ bag: make_bag(TILE_DISTRIBUTION_NO_BLANKS),
+ /* Center letters: array of { id, letter }.
+ * id is a unique incrementing integer for tracking. */
+ center: [],
+ next_letter_id: 0,
+ /* Per-player claimed words: { session_id: [ { id, word, letters } ] }
+ * letters is an array of { id, letter } for display. */
+ player_words: {},
+ /* Claim queue: array of session_ids. First entry is active claimer. */
+ claim_queue: [],
+ /* Letters/words currently held by the active claimer. */
+ claimed_letters: [], /* letter objects from center */
+ claimed_words: [], /* { owner_session, word_id, word_obj } */
+ /* Voting state. */
+ vote_pending: null,
+ /* Letter request state. */
+ letter_requests: new Set(),
+ /* Game state. */
+ finished: false,
+ done_players: new Set()
+ };
+ this._claim_timer = null;
+ this._claim_warning_timer = null;
+ this._vote_timer = null;
+ this._letter_request_timer = null;
+ this._reveal_timer = null;
+ }
+
+ /*****************************************************
+ * Helpers *
+ *****************************************************/
+
+ active_claimer_session() {
+ return this.state.claim_queue.length > 0
+ ? this.state.claim_queue[0] : null;
+ }
+
+ active_claimer_player() {
+ const sid = this.active_claimer_session();
+ return sid ? this.players_by_session[sid] : null;
+ }
+
+ /* Count active players who have joined (have a words array). */
+ joined_player_count() {
+ let count = 0;
+ for (const sid of Object.keys(this.state.player_words)) {
+ const p = this.players_by_session[sid];
+ if (p && p.active) count++;
+ }
+ return count;
+ }
+
+ /*****************************************************
+ * Letter management *
+ *****************************************************/
+
+ /* Deal a letter from bag to center with a countdown reveal. */
+ deal_letter() {
+ if (this.state.bag.length === 0) return false;
+
+ const letter = this.state.bag.pop();
+ const id = this.state.next_letter_id++;
+ const tile = { id, letter };
+
+ this.state.center.push(tile);
+ this.state.letter_requests = new Set();
+
+ this.broadcast_event_object("letter-reveal", {
+ tile,
+ remaining: this.state.bag.length,
+ countdown_ms: REVEAL_COUNTDOWN_MS
+ });
+
+ /* Also broadcast updated bag count. */
+ this.broadcast_event_object("bag-count", {
+ remaining: this.state.bag.length
+ });
+
+ return true;
+ }
+
+ /* Handle a player requesting a new letter. */
+ handle_request_letter(request, response) {
+ const session_id = request.session.id;
+ if (!this.state.player_words[session_id]) {
+ response.sendStatus(400);
+ return;
+ }
+
+ if (this.state.bag.length === 0) {
+ response.json({ remaining: 0 });
+ return;
+ }
+
+ /* Don't allow requests during a reveal countdown. */
+ if (this._reveal_timer) {
+ response.json({ queued: false });
+ return;
+ }
+
+ this.state.letter_requests.add(session_id);
+
+ const joined = this.joined_player_count();
+ const votes = this.state.letter_requests.size;
+
+ this.broadcast_event_object("letter-request", {
+ votes,
+ needed: joined
+ });
+
+ /* Scale timer: all players voting = immediate.
+ * One player = full 10-second wait.
+ * Linear interpolation between. */
+ if (this._letter_request_timer) {
+ clearTimeout(this._letter_request_timer);
+ this._letter_request_timer = null;
+ }
+
+ if (votes >= joined) {
+ this.deal_letter();
+ } else {
+ const ratio = votes / joined;
+ const delay_ms = Math.round(10000 * (1 - ratio));
+ this._letter_request_timer = setTimeout(() => {
+ this._letter_request_timer = null;
+ this.deal_letter();
+ }, delay_ms);
+ }
+
+ response.json({ queued: true, remaining: this.state.bag.length });
+ }
+
+ /*****************************************************
+ * Claim mode *
+ *****************************************************/
+
+ handle_claim(request, response) {
+ const session_id = request.session.id;
+ if (!this.state.player_words[session_id]) {
+ response.sendStatus(400);
+ return;
+ }
+
+ /* Don't allow claiming if game is finished. */
+ if (this.state.finished) {
+ response.json({ queued: false });
+ return;
+ }
+
+ /* Don't allow if already in queue. */
+ if (this.state.claim_queue.includes(session_id)) {
+ response.json({ queued: true, active: session_id === this.active_claimer_session() });
+ return;
+ }
+
+ this.state.claim_queue.push(session_id);
+
+ /* If this is the only person in queue, activate them. */
+ if (this.state.claim_queue.length === 1) {
+ this._activate_claimer();
+ } else {
+ /* Let them know they're queued. */
+ const player = this.players_by_session[session_id];
+ if (player) {
+ player.send(`event: claim-queued\ndata: ${JSON.stringify({
+ position: this.state.claim_queue.indexOf(session_id)
+ })}\n\n`);
+ }
+ }
+
+ response.json({ queued: true, active: session_id === this.active_claimer_session() });
+ }
+
+ _activate_claimer() {
+ const session_id = this.active_claimer_session();
+ if (!session_id) return;
+
+ this.state.claimed_letters = [];
+ this.state.claimed_words = [];
+
+ const player = this.players_by_session[session_id];
+ this.broadcast_event_object("claim-start", {
+ player_name: player ? player.name : "Unknown",
+ timeout_ms: CLAIM_TIMEOUT_MS,
+ warning_ms: CLAIM_WARNING_MS
+ });
+
+ /* Start claim timeout. */
+ this._claim_warning_timer = setTimeout(() => {
+ this.broadcast_event_object("claim-warning", {
+ remaining_ms: CLAIM_WARNING_MS
+ });
+ }, CLAIM_TIMEOUT_MS - CLAIM_WARNING_MS);
+
+ this._claim_timer = setTimeout(() => {
+ this._cancel_claim("timeout");
+ }, CLAIM_TIMEOUT_MS);
+ }
+
+ /* Take a letter from the center. */
+ handle_take_letter(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ const letter_id = request.body.letter_id;
+ const idx = this.state.center.findIndex(t => t.id === letter_id);
+ if (idx < 0) {
+ response.sendStatus(404);
+ return;
+ }
+
+ const tile = this.state.center.splice(idx, 1)[0];
+ this.state.claimed_letters.push(tile);
+
+ const player = this.players_by_session[session_id];
+ this.broadcast_event_object("letter-claimed", {
+ player_name: player ? player.name : "Unknown",
+ tile
+ });
+
+ response.json({ ok: true });
+ }
+
+ /* Return a letter to the center. */
+ handle_return_letter(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ const letter_id = request.body.letter_id;
+ const idx = this.state.claimed_letters.findIndex(t => t.id === letter_id);
+ if (idx < 0) {
+ response.sendStatus(404);
+ return;
+ }
+
+ const tile = this.state.claimed_letters.splice(idx, 1)[0];
+ this.state.center.push(tile);
+
+ const player = this.players_by_session[session_id];
+ this.broadcast_event_object("letter-returned", {
+ player_name: player ? player.name : "Unknown",
+ tile
+ });
+
+ response.json({ ok: true });
+ }
+
+ /* Steal a word from a player. */
+ handle_steal_word(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ const { owner_session, word_id } = request.body;
+ const owner_words = this.state.player_words[owner_session];
+ if (!owner_words) {
+ response.sendStatus(404);
+ return;
+ }
+
+ const word_idx = owner_words.findIndex(w => w.id === word_id);
+ if (word_idx < 0) {
+ response.sendStatus(404);
+ return;
+ }
+
+ /* Check if already claimed this word. */
+ if (this.state.claimed_words.find(cw => cw.word_id === word_id)) {
+ response.sendStatus(409);
+ return;
+ }
+
+ const word_obj = owner_words[word_idx];
+
+ /* Remove from owner and add to claimed. */
+ owner_words.splice(word_idx, 1);
+ this.state.claimed_words.push({ owner_session, word_id, word_obj });
+
+ const player = this.players_by_session[session_id];
+ const owner_player = this.players_by_session[owner_session];
+ this.broadcast_event_object("word-stolen", {
+ player_name: player ? player.name : "Unknown",
+ from_player: owner_player ? owner_player.name : "Unknown",
+ from_session: owner_session,
+ word: word_obj.word,
+ word_id
+ });
+
+ response.json({ ok: true });
+ }
+
+ /* Return a stolen word to its owner. */
+ handle_return_word(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ const word_id = request.body.word_id;
+ const idx = this.state.claimed_words.findIndex(cw => cw.word_id === word_id);
+ if (idx < 0) {
+ response.sendStatus(404);
+ return;
+ }
+
+ const { owner_session, word_obj } = this.state.claimed_words.splice(idx, 1)[0];
+ this.state.player_words[owner_session].push(word_obj);
+
+ const player = this.players_by_session[session_id];
+ const owner_player = this.players_by_session[owner_session];
+ this.broadcast_event_object("word-returned", {
+ player_name: player ? player.name : "Unknown",
+ to_player: owner_player ? owner_player.name : "Unknown",
+ to_session: owner_session,
+ word: word_obj.word,
+ word_id
+ });
+
+ response.json({ ok: true });
+ }
+
+ /* Cancel claim and return all letters/words. */
+ handle_cancel_claim(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ this._cancel_claim("cancelled");
+ response.json({ ok: true });
+ }
+
+ _cancel_claim(reason) {
+ /* Return all claimed letters to center. */
+ for (const tile of this.state.claimed_letters) {
+ this.state.center.push(tile);
+ }
+ this.state.claimed_letters = [];
+
+ /* Return all claimed words to their owners. */
+ for (const { owner_session, word_obj } of this.state.claimed_words) {
+ this.state.player_words[owner_session].push(word_obj);
+ }
+ this.state.claimed_words = [];
+
+ this._clear_claim_timers();
+
+ const session_id = this.state.claim_queue.shift();
+ const player = this.players_by_session[session_id];
+ this.broadcast_event_object("claim-end", {
+ player_name: player ? player.name : "Unknown",
+ reason
+ });
+
+ /* Activate next claimer if any. */
+ if (this.state.claim_queue.length > 0) {
+ this._activate_claimer();
+ }
+ }
+
+ _clear_claim_timers() {
+ if (this._claim_timer) {
+ clearTimeout(this._claim_timer);
+ this._claim_timer = null;
+ }
+ if (this._claim_warning_timer) {
+ clearTimeout(this._claim_warning_timer);
+ this._claim_warning_timer = null;
+ }
+ }
+
+ /*****************************************************
+ * Word submission and validation *
+ *****************************************************/
+
+ handle_submit(request, response) {
+ const session_id = request.session.id;
+ if (session_id !== this.active_claimer_session()) {
+ response.sendStatus(403);
+ return;
+ }
+
+ const word = (request.body.word || "").toUpperCase();
+ if (word.length < MIN_WORD_LENGTH) {
+ response.json({ ok: false, error: "Word must be at least 4 letters" });
+ return;
+ }
+
+ /* Verify the submitted word uses exactly the claimed letters. */
+ const available = [];
+ for (const tile of this.state.claimed_letters) {
+ available.push(tile.letter);
+ }
+ for (const cw of this.state.claimed_words) {
+ for (const ch of cw.word_obj.word) {
+ available.push(ch);
+ }
+ }
+
+ if (word.length !== available.length) {
+ response.json({ ok: false, error: "Word must use all claimed letters" });
+ return;
+ }
+
+ /* Check letter-by-letter match. */
+ const avail_sorted = available.slice().sort().join("");
+ const word_sorted = word.split("").sort().join("");
+ if (avail_sorted !== word_sorted) {
+ response.json({ ok: false, error: "Word must use exactly the claimed letters" });
+ return;
+ }
+
+ /* Validate steal rules. */
+ const steal_error = this._validate_steal(word);
+ if (steal_error) {
+ response.json({ ok: false, error: steal_error });
+ return;
+ }
+
+ /* Check 4-letter word limit. */
+ if (word.length === 4) {
+ const existing_four = (this.state.player_words[session_id] || [])
+ .find(w => w.word.length === 4);
+ if (existing_four) {
+ response.json({ ok: false, error: "You already have a 4-letter word" });
+ return;
+ }
+ }
+
+ /* Check dictionary. */
+ if (TWL_WORDS.has(word)) {
+ this._accept_word(session_id, word);
+ response.json({ ok: true, accepted: true });
+ } else {
+ /* Start voting. */
+ this._start_vote(session_id, word);
+ response.json({ ok: true, voting: true });
+ }
+ }
+
+ /* Validate that steal rules are followed.
+ * Returns an error string, or null if valid. */
+ _validate_steal(word) {
+ const stolen = this.state.claimed_words;
+ const center_count = this.state.claimed_letters.length;
+
+ if (stolen.length === 0) {
+ /* Pure center claim — no steal rules to check. */
+ return null;
+ }
+
+ /* Can't just rearrange a single word to the same length. */
+ if (stolen.length === 1 && center_count === 0) {
+ return "Must add at least one letter when stealing";
+ }
+
+ /* For single-word steal: the original word must not appear as a
+ * contiguous prefix or suffix (but can appear in the middle). */
+ if (stolen.length === 1) {
+ const original = stolen[0].word_obj.word;
+ if (word.startsWith(original) && !word.endsWith(original)) {
+ /* Only appended to end. */
+ return "Cannot steal by only adding letters to the end";
+ }
+ if (word.endsWith(original) && !word.startsWith(original)) {
+ /* Only prepended to front. */
+ return "Cannot steal by only adding letters to the front";
+ }
+ if (word.startsWith(original) && word.endsWith(original)
+ && word.length === original.length) {
+ /* Same word (shouldn't happen due to length check above). */
+ return "Must add at least one letter when stealing";
+ }
+ /* startsWith AND endsWith with added letters = both sides = OK.
+ * Neither starts nor ends = rearranged = OK. */
+ }
+
+ return null;
+ }
+
+ _accept_word(session_id, word) {
+ const word_id = this.state.next_letter_id++;
+
+ /* Collect the letter objects for display. */
+ const letters = [];
+ for (const tile of this.state.claimed_letters) {
+ letters.push(tile);
+ }
+ for (const cw of this.state.claimed_words) {
+ for (const ch of cw.word_obj.word) {
+ letters.push({ id: this.state.next_letter_id++, letter: ch });
+ }
+ }
+
+ const word_obj = { id: word_id, word, letters };
+ this.state.player_words[session_id].push(word_obj);
+
+ /* Clear claimed state. */
+ this.state.claimed_letters = [];
+ this.state.claimed_words = [];
+ this._clear_claim_timers();
+
+ const session_id_done = this.state.claim_queue.shift();
+ const player = this.players_by_session[session_id_done];
+
+ const score = this._compute_score(session_id);
+ this.broadcast_event_object("word-accepted", {
+ player_name: player ? player.name : "Unknown",
+ session: session_id,
+ word,
+ word_id,
+ score
+ });
+
+ /* Broadcast full player words for state sync. */
+ this._broadcast_player_state();
+
+ /* Activate next claimer if any. */
+ if (this.state.claim_queue.length > 0) {
+ this._activate_claimer();
+ }
+ }
+
+ _compute_score(session_id) {
+ const words = this.state.player_words[session_id] || [];
+ let score = 0;
+ for (const w of words) {
+ score += w.word.length - 1;
+ }
+ return score;
+ }
+
+ _compute_all_scores() {
+ const scores = {};
+ for (const sid of Object.keys(this.state.player_words)) {
+ const player = this.players_by_session[sid];
+ scores[sid] = {
+ name: player ? player.name : "Unknown",
+ score: this._compute_score(sid),
+ words: this.state.player_words[sid].map(w => w.word)
+ };
+ }
+ return scores;
+ }
+
+ /*****************************************************
+ * Voting *
+ *****************************************************/
+
+ _start_vote(session_id, word) {
+ this.state.vote_pending = {
+ session_id,
+ word,
+ votes: {}, /* session_id -> true/false */
+ /* Submitter's vote is implicitly "accept". */
+ };
+
+ const player = this.players_by_session[session_id];
+ this.broadcast_event_object("vote-start", {
+ player_name: player ? player.name : "Unknown",
+ word,
+ timeout_ms: VOTE_TIMEOUT_MS
+ });
+
+ this._vote_timer = setTimeout(() => {
+ this._resolve_vote();
+ }, VOTE_TIMEOUT_MS);
+ }
+
+ handle_vote(request, response) {
+ const session_id = request.session.id;
+ const vote = this.state.vote_pending;
+
+ if (!vote) {
+ response.sendStatus(400);
+ return;
+ }
+
+ /* Submitter cannot vote (their vote is implicit). */
+ if (session_id === vote.session_id) {
+ response.sendStatus(400);
+ return;
+ }
+
+ vote.votes[session_id] = !!request.body.accept;
+
+ this.broadcast_event_object("vote-update", {
+ votes_cast: Object.keys(vote.votes).length,
+ voters_total: this.joined_player_count() - 1
+ });
+
+ /* If all other players have voted, resolve immediately. */
+ if (Object.keys(vote.votes).length >= this.joined_player_count() - 1) {
+ this._resolve_vote();
+ }
+
+ response.json({ ok: true });
+ }
+
+ _resolve_vote() {
+ if (this._vote_timer) {
+ clearTimeout(this._vote_timer);
+ this._vote_timer = null;
+ }
+
+ const vote = this.state.vote_pending;
+ if (!vote) return;
+
+ /* Count votes. Non-voters are treated as abstentions. */
+ let accept = 0, reject = 0;
+ for (const v of Object.values(vote.votes)) {
+ if (v) accept++;
+ else reject++;
+ }
+
+ /* Majority rules, ties accept (submitter is the tiebreaker). */
+ const accepted = accept >= reject;
+
+ this.state.vote_pending = null;
+
+ if (accepted) {
+ this._accept_word(vote.session_id, vote.word);
+ } else {
+ /* Word rejected — cancel the claim. */
+ this._cancel_claim("word rejected");
+ }
+
+ this.broadcast_event_object("vote-result", {
+ word: vote.word,
+ accepted,
+ accept_count: accept,
+ reject_count: reject
+ });
+ }
+
+ /*****************************************************
+ * Join / Events / Game lifecycle *
+ *****************************************************/
+
+ handle_join(request, response) {
+ const session_id = request.session.id;
+ const player = this.players_by_session[session_id];
+
+ if (!player) {
+ response.sendStatus(404);
+ return;
+ }
+
+ if (!this.state.player_words[session_id]) {
+ this.state.player_words[session_id] = [];
+ }
+
+ response.json({
+ center: this.state.center,
+ player_words: this._all_player_words(),
+ scores: this._compute_all_scores(),
+ remaining: this.state.bag.length
+ });
+ }
+
+ handle_done(request, response) {
+ const session_id = request.session.id;
+ if (!this.state.player_words[session_id]) {
+ response.sendStatus(400);
+ return;
+ }
+
+ this.state.done_players.add(session_id);
+
+ const joined = this.joined_player_count();
+ const done = this.state.done_players.size;
+
+ this.broadcast_event_object("done-update", { done, needed: joined });
+
+ if (done >= joined) {
+ this._finish_game();
+ }
+
+ response.json({ ok: true });
+ }
+
+ _finish_game() {
+ this.state.finished = true;
+
+ /* Attempt a final "game steal" for fun. */
+ const game_steal = this._find_game_steal();
+
+ this.broadcast_event_object("game-over", {
+ scores: this._compute_all_scores(),
+ game_steal
+ });
+ }
+
+ /* Find a possible steal the "game" could make using words from
+ * different players. Returns null or a description of the steal. */
+ _find_game_steal() {
+ const all_words = [];
+ for (const sid of Object.keys(this.state.player_words)) {
+ for (const w of this.state.player_words[sid]) {
+ const player = this.players_by_session[sid];
+ all_words.push({
+ word: w.word,
+ owner: player ? player.name : "Unknown",
+ session: sid,
+ word_id: w.id
+ });
+ }
+ }
+
+ /* Try combining pairs of words to see if they form a valid word. */
+ for (let i = 0; i < all_words.length; i++) {
+ for (let j = i + 1; j < all_words.length; j++) {
+ const combined = (all_words[i].word + all_words[j].word).split("");
+ combined.sort();
+ const sorted = combined.join("");
+
+ /* Check all TWL words of this length. */
+ for (const candidate of TWL_WORDS) {
+ if (candidate.length !== combined.length) continue;
+ const candidate_sorted = candidate.split("").sort().join("");
+ if (candidate_sorted === sorted) {
+ return {
+ words: [all_words[i], all_words[j]],
+ result: candidate
+ };
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ _all_player_words() {
+ const result = {};
+ for (const sid of Object.keys(this.state.player_words)) {
+ const player = this.players_by_session[sid];
+ result[sid] = {
+ name: player ? player.name : "Unknown",
+ words: this.state.player_words[sid]
+ };
+ }
+ return result;
+ }
+
+ _broadcast_player_state() {
+ this.broadcast_event_object("player-words", this._all_player_words());
+ this.broadcast_event_object("scores", this._compute_all_scores());
+ }
+
+ handle_events(request, response) {
+ super.handle_events(request, response);
+
+ const session_id = request.session.id;
+
+ /* Send center state. */
+ response.write(`event: center\ndata: ${JSON.stringify(
+ this.state.center
+ )}\n\n`);
+
+ /* Send all player words. */
+ response.write(`event: player-words\ndata: ${JSON.stringify(
+ this._all_player_words()
+ )}\n\n`);
+
+ /* Send scores. */
+ response.write(`event: scores\ndata: ${JSON.stringify(
+ this._compute_all_scores()
+ )}\n\n`);
+
+ /* Send bag count. */
+ response.write(`event: bag-count\ndata: ${JSON.stringify({
+ remaining: this.state.bag.length
+ })}\n\n`);
+
+ /* Send active claim state if any. */
+ if (this.state.claim_queue.length > 0) {
+ const claimer = this.active_claimer_player();
+ response.write(`event: claim-start\ndata: ${JSON.stringify({
+ player_name: claimer ? claimer.name : "Unknown",
+ timeout_ms: 0,
+ warning_ms: 0
+ })}\n\n`);
+ }
+
+ /* Send vote state if any. */
+ if (this.state.vote_pending) {
+ const v = this.state.vote_pending;
+ const submitter = this.players_by_session[v.session_id];
+ response.write(`event: vote-start\ndata: ${JSON.stringify({
+ player_name: submitter ? submitter.name : "Unknown",
+ word: v.word,
+ timeout_ms: 0
+ })}\n\n`);
+ }
+
+ /* Send game-over if finished. */
+ if (this.state.finished) {
+ response.write(`event: game-over\ndata: ${JSON.stringify({
+ scores: this._compute_all_scores(),
+ game_steal: null
+ })}\n\n`);
+ }
+ }
+}
+
+Anagrams.router = express.Router();
+const router = Anagrams.router;
+
+router.post('/join', (request, response) => {
+ request.game.handle_join(request, response);
+});
+
+router.post('/claim', (request, response) => {
+ request.game.handle_claim(request, response);
+});
+
+router.post('/take-letter', (request, response) => {
+ request.game.handle_take_letter(request, response);
+});
+
+router.post('/return-letter', (request, response) => {
+ request.game.handle_return_letter(request, response);
+});
+
+router.post('/steal-word', (request, response) => {
+ request.game.handle_steal_word(request, response);
+});
+
+router.post('/return-word', (request, response) => {
+ request.game.handle_return_word(request, response);
+});
+
+router.post('/submit', (request, response) => {
+ request.game.handle_submit(request, response);
+});
+
+router.post('/cancel-claim', (request, response) => {
+ request.game.handle_cancel_claim(request, response);
+});
+
+router.post('/request-letter', (request, response) => {
+ request.game.handle_request_letter(request, response);
+});
+
+router.post('/vote', (request, response) => {
+ request.game.handle_vote(request, response);
+});
+
+router.post('/done', (request, response) => {
+ request.game.handle_done(request, response);
+});
+
+Anagrams.meta = {
+ name: "Anagrams",
+ identifier: "anagrams",
+ options: {
+ allow_guest: true
+ }
+};
+
+exports.Game = Anagrams;