├── .nvmrc ├── .gitignore ├── captain-definition ├── client ├── js │ ├── templates.js │ ├── request.js │ ├── home.js │ ├── auth.js │ ├── timer.js │ ├── ws.js │ ├── error.js │ ├── results.js │ ├── app.js │ ├── game.js │ ├── hangmanCanvas.js │ └── lobby.js ├── index.html ├── results.html ├── game.html └── css │ └── app.css ├── .vscode └── launch.json ├── game ├── words.js ├── wordlists │ ├── getWords.js │ ├── 20_chars.json │ ├── 19_chars.json │ ├── 4_chars.json │ ├── 18_chars.json │ ├── 5_chars.json │ ├── 6_chars.json │ ├── 7_chars.json │ ├── 8_chars.json │ ├── 9_chars.json │ ├── 10_chars.json │ ├── 11_chars.json │ ├── 12_chars.json │ ├── 13_chars.json │ ├── 14_chars.json │ ├── 15_chars.json │ ├── 16_chars.json │ └── 17_chars.json ├── rules.js ├── rules.json ├── results.js ├── players.js ├── lobby.js └── game.js ├── package.json ├── storage ├── db.js └── schema.sql ├── api ├── websocket.js └── rest.js ├── index.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 17.3.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.db 3 | ./memory 4 | memory 5 | .env 6 | *.db -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion" :2 , 3 | "templateId" :"node/19" 4 | } -------------------------------------------------------------------------------- /client/js/templates.js: -------------------------------------------------------------------------------- 1 | export default (id, data) => { 2 | const template = document.getElementById(id); 3 | return template.innerHTML.replace(/\{{(.*?)\}}/g, (match) => { 4 | return data[match.slice(2, -2).trim()]; 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /client/js/request.js: -------------------------------------------------------------------------------- 1 | import error from './error.js'; 2 | 3 | async function request(address, opts = {}) { 4 | 5 | const res = await fetch(address, opts); 6 | const text = await res.text(); 7 | try { 8 | if ('error' in JSON.parse(text)) { 9 | error.create(JSON.parse(text).error); 10 | } 11 | } catch { 12 | // No error 13 | } 14 | return text; 15 | } 16 | 17 | export default { 18 | POST: (addr, data) => { 19 | return request(addr, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); 20 | }, 21 | GET: request, 22 | }; 23 | -------------------------------------------------------------------------------- /client/js/home.js: -------------------------------------------------------------------------------- 1 | import render from './templates.js'; 2 | import './error.js'; 3 | 4 | function displayRecentResults() { 5 | const results = (JSON.parse(localStorage.getItem('results') || '[]')).slice(-5).reverse(); 6 | const resultList = document.getElementById('home_results'); 7 | console.log(results); 8 | if (results.length === 0) { 9 | resultList.innerHTML += 'No results could be found'; 10 | } else { 11 | for (const { id, date } of results) { 12 | const dateString = new Date(date).toLocaleString('en-gb', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true }); 13 | resultList.innerHTML += render('result', { id, date: dateString }); 14 | } 15 | } 16 | } 17 | 18 | displayRecentResults(); 19 | -------------------------------------------------------------------------------- /game/words.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | 5 | const words = new Proxy({}, { 6 | get: function (target, prop) { 7 | if (target[prop] !== undefined) { 8 | return target[prop]; 9 | } 10 | const file = fs.readFileSync(`./game/wordlists/${prop}_chars.json`); 11 | const data = JSON.parse(file); 12 | return data; 13 | }, 14 | }); 15 | 16 | 17 | function getDaily() { 18 | const today = new Date(Date.now()).setHours(0, 0, 0, 0); 19 | const sublist = words[Math.floor(Math.abs(Math.sin(today)) * 6) + 4]; 20 | const tomorrow = new Date(today); 21 | tomorrow.setDate(tomorrow.getDate() + 1); 22 | return sublist[Math.floor(Math.abs(Math.sin(tomorrow)) * sublist.length)]; 23 | } 24 | 25 | export { words, getDaily }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hangman", 3 | "version": "1.0.0", 4 | "description": "NodeJS web application created for Application Programming M30221.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint *.js", 8 | "start": "node index" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "bugs": {}, 13 | "type": "module", 14 | "dependencies": { 15 | "cookie-parser": "^1.4.6", 16 | "cookie-session": "^2.0.0", 17 | "dotenv": "^16.0.0", 18 | "express": "^4.17.3", 19 | "sqlite3": "^5.0.2", 20 | "ws": "^8.5.0" 21 | }, 22 | "eslintConfig": { 23 | "extends": "portsoc", 24 | "root": true, 25 | "env": { 26 | "browser": true 27 | } 28 | }, 29 | "devDependencies": { 30 | "eslint": "^8.10.0", 31 | "eslint-config-portsoc": "^1.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/js/auth.js: -------------------------------------------------------------------------------- 1 | import render from './templates.js'; 2 | import request from './request.js'; 3 | export default () => { 4 | document.getElementById('page_content').innerHTML = render('identity', { createOrJoin: (window.gameData.players.length === 0) ? 'Create' : 'Join' }); 5 | document.querySelector('#identity_form input').addEventListener('input', (e) => { 6 | if (e.target.value.length > 0) { 7 | document.querySelector('#identity_form button').disabled = false; 8 | } else { 9 | document.querySelector('#identity_form button').disabled = true; 10 | } 11 | }); 12 | document.getElementById('identity_form').addEventListener('submit', async (e) => { 13 | e.preventDefault(); 14 | const data = new FormData(e.target); 15 | await request.POST(`/api/${window.gameId}/join`, { 16 | name: data.get('name'), 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /client/js/timer.js: -------------------------------------------------------------------------------- 1 | const intervalRate = 0.2; 2 | 3 | function startTimer(time, id, cb) { 4 | const timeout = setTimeout(() => { 5 | cb(); 6 | end(); 7 | }, (time + intervalRate) * 1000 ); 8 | let displayTime = time + intervalRate; 9 | const interval = setInterval(() => { 10 | updateTimers(displayTime, id); 11 | displayTime -= intervalRate; 12 | }, intervalRate * 1000); 13 | 14 | 15 | function end() { 16 | window.clearTimeout(timeout); 17 | window.clearInterval(interval); 18 | updateTimers(time, id); 19 | } 20 | return end; 21 | } 22 | 23 | function updateTimers(value, id) { 24 | const displayElements = document.querySelectorAll(`*[data-timer="${id}"]`); 25 | for (const element of displayElements) { 26 | element.innerHTML = `${Math.floor(value / 60)}:${(value % 60).toFixed(2)}`; 27 | } 28 | } 29 | 30 | export default startTimer; 31 | -------------------------------------------------------------------------------- /client/js/ws.js: -------------------------------------------------------------------------------- 1 | class WebsocketConnection { 2 | constructor() { 3 | this.connection = new WebSocket(`ws${(location.protocol == "https:") ? "s" : ""}://${window.location.host}/${window.gameId}`); 4 | this.createListeners(); 5 | this.handlers = {}; 6 | } 7 | 8 | createListeners() { 9 | this.connection.onmessage = (e) => { 10 | const { event, data } = JSON.parse(e.data); 11 | console.log('Received WebSocket Message:', event); 12 | if (this.handlers[event] !== undefined) { 13 | for (const handler of this.handlers[event]) { 14 | handler(data); 15 | } 16 | } 17 | }; 18 | 19 | this.connection.onclose = () => { 20 | window.location.href = '/'; 21 | }; 22 | } 23 | 24 | on(event, handler) { 25 | if (this.handlers[event] === undefined) { 26 | this.handlers[event] = []; 27 | } 28 | this.handlers[event].push(handler); 29 | } 30 | } 31 | 32 | export default WebsocketConnection; 33 | -------------------------------------------------------------------------------- /game/wordlists/getWords.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | 3 | const file = await fs.readFile('./google.csv'); 4 | const data = file.toString().split('\n').map(e => e.trim()).map(e => e.split(',').map(e => e.trim())); 5 | console.log(data); 6 | const bad_file = await fs.readFile('./bad-words.txt'); 7 | const bad_data = bad_file.toString().split('\n'); 8 | const split = {}; 9 | for(const wordLength of [...Array(17).keys()].map(a => a + 4)){ 10 | split[wordLength] = []; 11 | } 12 | 13 | const exp = new RegExp(bad_data.join("|")); 14 | for(const [pos, word] of data){ 15 | if( 16 | word.length > 3 && word.length < 21 && 17 | /^[a-zA-Z]+$/.test(word) 18 | && !(exp.test(word)) 19 | ){ 20 | if(split[word.length].length < 1000){ 21 | split[word.length].push(word.toLowerCase()); 22 | } 23 | } 24 | } 25 | 26 | console.log(Object.values(split).map(a=> a.length)) 27 | 28 | 29 | for(const [length, words] of Object.entries(split)){ 30 | const top = words.splice(0, 300); 31 | fs.writeFile(`./${length}_chars.json`, JSON.stringify(top, null, 2)); 32 | } -------------------------------------------------------------------------------- /game/wordlists/20_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "internationalization", 3 | "electrophysiological", 4 | "electrocardiographic", 5 | "immunohistochemistry", 6 | "counterrevolutionary", 7 | "internationalisation", 8 | "electroencephalogram", 9 | "uncharacteristically", 10 | "compartmentalization", 11 | "magnetohydrodynamics", 12 | "keratoconjunctivitis", 13 | "crystallographically", 14 | "hyperlipoproteinemia", 15 | "adrenocorticotrophic", 16 | "hyperadrenocorticism", 17 | "neuropharmacological", 18 | "microminiaturization", 19 | "compartmentalisation", 20 | "abetalipoproteinemia", 21 | "paraphenylenediamine", 22 | "adrenocorticotrophin", 23 | "paleoanthropological", 24 | "iodochlorhydroxyquin", 25 | "trockenbeerenauslese", 26 | "incomprehensibleness", 27 | "contradistinguishing", 28 | "disproportionateness", 29 | "pseudohermaphroditic", 30 | "counterargumentation", 31 | "palatopharyngoplasty", 32 | "modulatordemodulator", 33 | "hydrometallurgically", 34 | "counterrevolutionist", 35 | "characteristicalness", 36 | "counterinterrogation", 37 | "embourgeoisification", 38 | "calcareoargillaceous", 39 | "architecturalization", 40 | "antiestablishmentism", 41 | "interdifferentiation", 42 | "incontrovertibleness", 43 | "nineteenthcenturyism", 44 | "electrochronographic" 45 | ] -------------------------------------------------------------------------------- /storage/db.js: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import fs from 'fs/promises'; 3 | let db = null; 4 | 5 | // TODO: Support multiple database types such as postgres to improve performance 6 | function init() { 7 | db = new sqlite3.Database('./hangman.db', async (err) => { 8 | if (err) console.log(err); 9 | 10 | const file = await fs.readFile('./storage/schema.sql', 'utf8'); 11 | for (const queryString of (file).split(';').filter(a => a.length > 1)) { 12 | await query(queryString + ';'); 13 | } 14 | }); 15 | return db; 16 | } 17 | 18 | function query(string, values = {}) { 19 | if (Array.isArray(values)) { 20 | const newVals = {}; 21 | const paramValues = /VALUES *(?\(.*?\))/mg.exec(string).groups.values; 22 | const params = paramValues.substring(1, paramValues.length - 1).split(',').map(a => a.trim()); 23 | let paramString = ''; 24 | for (let i = 0; i < values.length; i++) { 25 | for (const [name, value] of Object.entries(values[i])) { 26 | newVals[name + i] = value; 27 | } 28 | paramString += `(${params.map(a => `${a}${i}`).join(', ')}),`; 29 | } 30 | string = string.replace(/VALUES *\(.*?\)/mg, `VALUES ${paramString.slice(0, -1)}`); 31 | values = newVals; 32 | } 33 | return new Promise((resolve, reject) => { 34 | if (db === null) { 35 | reject(new Error('DB not initialised')); 36 | } 37 | db[string.includes('SELECT') ? 'all' : 'run'](string, values, (err, data) => { 38 | if (err) { 39 | reject(err); 40 | } else { 41 | resolve(data); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | export default { 48 | init, 49 | query, 50 | }; 51 | -------------------------------------------------------------------------------- /api/websocket.js: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws'; 2 | const clients = {}; 3 | 4 | function startServer({ server, sessionParser, lobbies }) { 5 | const wss = new WebSocketServer({ server }); 6 | 7 | wss.on('connection', (ws, req) => { 8 | sessionParser(req, {}, () => { 9 | const sid = req.sessionID; 10 | // remove leading slash 11 | const lobbyId = req.url.substring(1); 12 | if (clients[lobbyId] === undefined) { 13 | clients[lobbyId] = {}; 14 | } 15 | if (clients[lobbyId][sid] !== undefined) { 16 | clearInterval(clients[lobbyId][sid].heartbeat); 17 | clients[lobbyId][sid].ws.terminate(); 18 | } 19 | 20 | let alive = true; 21 | 22 | ws.on('pong', () => { 23 | alive = true; 24 | }); 25 | 26 | const heartbeat = setInterval(async () => { 27 | if (alive === false) { 28 | clearInterval(heartbeat); 29 | ws.terminate(); 30 | delete clients[lobbyId][sid]; 31 | await lobbies.leave(lobbyId, sid); 32 | updateClient(lobbyId); 33 | } 34 | alive = false; 35 | ws.ping(); 36 | }, 5000); 37 | 38 | clients[lobbyId][sid] = { ws, heartbeat }; 39 | }); 40 | }); 41 | 42 | // tell all ws connections for this client to do an update 43 | // but send the lobby id so only one actually has to do it 44 | function updateClient(lobbyId, socketId = null) { 45 | if (socketId == null) { 46 | for (const sid of Object.keys(clients[lobbyId])) { 47 | updateClient(lobbyId, sid); 48 | } 49 | } else { 50 | if (clients[lobbyId] !== undefined && clients[lobbyId][socketId] !== undefined) { 51 | clients[lobbyId][socketId].ws.send(JSON.stringify({ event: 'do_update' })); 52 | } 53 | } 54 | } 55 | 56 | return { updateClient, clients }; 57 | } 58 | 59 | export default startServer; 60 | -------------------------------------------------------------------------------- /storage/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS results ( 2 | id CHAR(32) PRIMARY KEY NOT NULL, 3 | max_lives INTEGER DEFAULT null, 4 | max_time INTEGER DEFAULT null 5 | ); 6 | 7 | CREATE TABLE IF NOT EXISTS result_players ( 8 | result_id CHAR(32) REFERENCES results(id), 9 | name VARCHAR(100) NOT NULL, 10 | word VARCHAR(50), 11 | known_letters VARCHAR(50), 12 | score INTEGER NOT NULL DEFAULT 0, 13 | time_used INTEGER DEFAULT null, 14 | lives_used INTEGER DEFAULT null, 15 | finished BOOLEAN NOT NULL DEFAULT false 16 | ); 17 | 18 | CREATE TABLE IF NOT EXISTS lobbies ( 19 | id CHAR(8) UNIQUE PRIMARY KEY NOT NULL, 20 | status VARCHAR(16) NOT NULL CHECK (status IN ("lobby", "game", "results")), 21 | last_result CHAR(32) REFERENCES results(id), 22 | end_time INTEGER DEFAULT null 23 | ); 24 | 25 | CREATE TABLE IF NOT EXISTS rules ( 26 | lobby_id CHAR(8) REFERENCES lobbies(id), 27 | rule_id VARCHAR(16), 28 | -- Use 1 and 0 for true/ false, can't see any scenario where a standard rule would need to be a string or anything else (except null) 29 | -- Define constraints in code 30 | value INTEGER, 31 | PRIMARY KEY (lobby_id, rule_id) 32 | ); 33 | 34 | /** 35 | * Each player has: 36 | * id 37 | * type: ws/ rest 38 | * name 39 | * gameState 40 | */ 41 | 42 | CREATE TABLE IF NOT EXISTS active_players ( 43 | id CHAR(32) PRIMARY KEY NOT NULL, 44 | session_id VARCHAR(64) NOT NULL, 45 | name VARCHAR(32) NOT NULL, 46 | lobby_id CHAR(8) REFERENCES lobbies(id), 47 | is_host BOOLEAN NOT NULL, 48 | is_active BOOLEAN NOT NULL, 49 | CONSTRAINT unique_name UNIQUE (name, lobby_id) 50 | ); 51 | 52 | /** 53 | * Each gamestate has: 54 | * score 55 | * word 56 | * livesUsed 57 | * timeUsed 58 | */ 59 | 60 | CREATE TABLE IF NOT EXISTS player_gamestates ( 61 | player_id CHAR(32) PRIMARY KEY NOT NULL, 62 | word VARCHAR(24), 63 | known_letters VARCHAR(24), 64 | used_letters VARCHAR(26), 65 | lives_used INTEGER, 66 | time_used INTEGER, 67 | finished BOOLEAN DEFAULT false 68 | ); -------------------------------------------------------------------------------- /game/rules.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | const rules = JSON.parse(await fs.readFile("./game/rules.json")); 3 | const ruleQuery = { 4 | string: "INSERT INTO rules (lobby_id, rule_id, value) VALUES ($lobby_id, $rule_id, $value);", 5 | data: Object.entries(rules).map((a) => { 6 | return { 7 | $lobby_id: null, 8 | $rule_id: a[0], 9 | $value: a[1].defaultValue 10 | } 11 | }) 12 | }; 13 | 14 | export default ({ 15 | db 16 | }) => { 17 | async function createRules(lobbyId) { 18 | return await db.query(ruleQuery.string, ruleQuery.data.map(a => { 19 | a.$lobby_id = lobbyId; 20 | return a 21 | })); 22 | } 23 | 24 | async function deleteRules(lobbyId) { 25 | return await db.query(`DELETE FROM rules WHERE lobby_id=$lobby_id`, { 26 | $lobby_id: lobbyId 27 | }); 28 | } 29 | 30 | async function getLobbyRules(lobbyId) { 31 | const data = await db.query(`SELECT rule_id, value FROM rules WHERE lobby_id=$lobby_id`, { 32 | $lobby_id: lobbyId 33 | }); 34 | return Object.fromEntries(data.map(a => [a.rule_id, a.value])); 35 | } 36 | 37 | async function setRule(lobbyId, id, value) { 38 | if (value == "null") value = null; 39 | if (rules[id].type == 'number' && value != null && Number(value) != NaN) { 40 | value = Number(value); 41 | } 42 | if (!(rules[id] != undefined && (rules[id].type === typeof value || (value === null && rules[id].allowNull == true)))) { 43 | throw (new Error("rule_type")); 44 | } 45 | if (rules[id].minVal !== undefined && value < rules[id].minVal && value != null) { 46 | throw (new Error("rule_small")); 47 | } 48 | if (rules[id].maxVal !== undefined && value > rules[id].maxVal && value != null) { 49 | throw (new Error("rule_large")); 50 | } 51 | return await db.query("UPDATE rules SET value=$value WHERE lobby_id=$lobby_id AND rule_id=$rule_id;", { 52 | $lobby_id: lobbyId, 53 | $rule_id: id, 54 | $value: value 55 | }); 56 | } 57 | 58 | return { 59 | all: rules, 60 | setValue: setRule, 61 | getByLobby: getLobbyRules, 62 | delete: deleteRules, 63 | init: createRules 64 | } 65 | } -------------------------------------------------------------------------------- /game/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "fullGuesses": { 3 | "name": "Allow full guesses", 4 | "type": "boolean", 5 | "defaultValue": true, 6 | "allowNull": false 7 | }, 8 | "asyncTurns": { 9 | "name": "Simultaneous turns", 10 | "type": "boolean", 11 | "defaultValue": false, 12 | "allowNull": false 13 | }, 14 | "maxTime": { 15 | "name": "Max Game Time (Seconds)", 16 | "type": "number", 17 | "defaultValue": null, 18 | "allowNull": true, 19 | "minVal": 10, 20 | "requires": { 21 | "asyncTurns": true 22 | } 23 | }, 24 | "turnTime": { 25 | "name": "Turn Time (Seconds)", 26 | "type": "number", 27 | "defaultValue": null, 28 | "allowNull": true, 29 | "minVal": 5, 30 | "requires": { 31 | "asyncTurns": false 32 | } 33 | }, 34 | "multiplayer": { 35 | "name": "Multiplayer", 36 | "type": "boolean", 37 | "defaultValue": false, 38 | "allowNull": false 39 | }, 40 | "maxPlayers": { 41 | "name": "Max Players", 42 | "type": "number", 43 | "defaultValue": null, 44 | "allowNull": true, 45 | "minVal": 2, 46 | "requires": { 47 | "multiplayer": true 48 | } 49 | }, 50 | "maxLives": { 51 | "name": "Max Lives", 52 | "type": "number", 53 | "defaultValue": 8, 54 | "allowNull": true, 55 | "minVal": 1 56 | }, 57 | "sameWord": { 58 | "name": "Same Word", 59 | "type": "boolean", 60 | "defaultValue": false, 61 | "allowNull": false, 62 | "requires": { 63 | "dailyChallenge": false, 64 | "multiplayer": true 65 | } 66 | }, 67 | "wordLength": { 68 | "name": "Word Length", 69 | "type": "number", 70 | "defaultValue": 6, 71 | "allowNull": false, 72 | "minVal": 4, 73 | "maxVal": 20, 74 | "requires": { 75 | "dailyChallenge": false 76 | } 77 | }, 78 | "discovery": { 79 | "name": "Allow Strangers", 80 | "type": "boolean", 81 | "defaultValue": false, 82 | "allowNull": false 83 | }, 84 | "dailyChallenge": { 85 | "name": "Daily Challenge", 86 | "type": "boolean", 87 | "defaultValue": false, 88 | "allowNull": false 89 | } 90 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hangman 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 19 |
20 |
21 | 22 |
23 |
24 | Play 25 |
26 |
27 | 28 | 29 |
30 |
31 | or 32 |
33 |
34 |
35 | Create new game 36 |
37 |
38 | Join a random game 39 |
40 |
41 |
42 | 43 |
44 |
45 | Recent Results 46 |
47 |
48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 | 59 | 72 | 73 | 76 | 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import 'dotenv/config'; 3 | import express from 'express'; 4 | import session from 'cookie-session'; 5 | import cookieParser from 'cookie-parser'; 6 | import { resolve } from 'path'; 7 | import { randomBytes } from 'crypto'; 8 | import db from './storage/db.js'; 9 | import lobby from './game/lobby.js'; 10 | import restAPI from './api/rest.js'; 11 | import startWSS from './api/websocket.js'; 12 | 13 | const app = express(); 14 | const port = process.env.PORT || 8080; 15 | const secret = randomBytes(8).toString('hex'); 16 | 17 | const server = app.listen(port, async () => { 18 | console.log(`Listening on port: ${server.address().port}`); 19 | 20 | // const dbType = process.env.DB_TYPE || 'memory'; 21 | await db.init(); 22 | 23 | const lobbies = lobby(db); 24 | 25 | app.use(express.urlencoded()); 26 | app.use(express.json()); 27 | app.use(cookieParser(secret)); 28 | 29 | 30 | app.use((req, res, next) => { 31 | req.sessionID = req.cookies?.session; 32 | next(); 33 | }); 34 | 35 | app.use('/', express.static('./client')); 36 | app.get('/', (req, res) => { 37 | res.sendFile(resolve('./client/index.html')); 38 | }); 39 | 40 | app.get('/game', (req, res) => { 41 | res.sendFile(resolve('./client/game.html')); 42 | }); 43 | 44 | app.get('/results/:id', (req, res) => { 45 | res.sendFile(resolve('./client/results.html')); 46 | }); 47 | 48 | app.get('/game/new', async (req, res) => { 49 | const newLobby = await lobbies.create(); 50 | res.redirect(`/game?id=${newLobby}`); 51 | }); 52 | 53 | app.get('/game/random', async (req, res) => { 54 | try { 55 | const randomLobby = await lobbies.getPublic(); 56 | res.redirect(`/game?id=${randomLobby}`); 57 | } catch (err) { 58 | // Should mean that lobby does not exist 59 | res.redirect('/?error=no_lobbies'); 60 | } 61 | }); 62 | 63 | const sessionParser = session({ 64 | // cookies won't need to be saved between server sessions so this can be created dynamically 65 | name: 'session', 66 | keys: [secret], 67 | resave: false, 68 | saveUninitialized: true, 69 | }); 70 | 71 | app.use(sessionParser); 72 | 73 | 74 | const wss = startWSS({ server, lobbies, sessionParser }); 75 | app.use('/api', restAPI({ express, db, lobbies, wss }).router); 76 | }); 77 | -------------------------------------------------------------------------------- /client/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hangman 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | 56 | 57 | 58 |
37 | Awards 38 | 40 | Position 41 | 43 | Name 44 | 46 | Word 47 | 49 | Lives Remaining 50 | 52 | Score 53 |
59 |
60 |
61 |
62 |
63 | 64 | 65 | 87 | 88 | 91 | 92 | -------------------------------------------------------------------------------- /game/wordlists/19_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "hyperparathyroidism", 3 | "professionalization", 4 | "counterintelligence", 5 | "countertransference", 6 | "electrocardiography", 7 | "reticuloendothelial", 8 | "magnetohydrodynamic", 9 | "interdenominational", 10 | "hydrochlorothiazide", 11 | "extraterritoriality", 12 | "immunocytochemistry", 13 | "sternocleidomastoid", 14 | "straightforwardness", 15 | "hydrometeorological", 16 | "radiopharmaceutical", 17 | "adrenocorticotropic", 18 | "meningoencephalitis", 19 | "dihydrostreptomycin", 20 | "phenylpropanolamine", 21 | "contradistinguished", 22 | "tetrachloroethylene", 23 | "professionalisation", 24 | "chromatographically", 25 | "otorhinolaryngology", 26 | "tetrafluoroethylene", 27 | "schizosaccharomyces", 28 | "intellectualization", 29 | "xenotransplantation", 30 | "bacteriochlorophyll", 31 | "adrenocorticotropin", 32 | "nonrepresentational", 33 | "phacoemulsification", 34 | "paradichlorobenzene", 35 | "integrodifferential", 36 | "conventionalization", 37 | "auriculoventricular", 38 | "parthenogenetically", 39 | "penecontemporaneous", 40 | "reindustrialization", 41 | "impressionistically", 42 | "deoxyribonucleoside", 43 | "individualistically", 44 | "interesterification", 45 | "radioallergosorbent", 46 | "hypoadrenocorticism", 47 | "contemporaneousness", 48 | "deoxyribonucleotide", 49 | "chromoblastomycosis", 50 | "interstratification", 51 | "pneumoencephalogram", 52 | "sociolinguistically", 53 | "pseudohermaphrodite", 54 | "physicomathematical", 55 | "unselfconsciousness", 56 | "intellectualisation", 57 | "ballistocardiograph", 58 | "geisteswissenschaft", 59 | "conventionalisation", 60 | "parliamentarization", 61 | "glottochronological", 62 | "cinematographically", 63 | "uncommunicativeness", 64 | "transdenominational", 65 | "anthropocentrically", 66 | "selfdifferentiation", 67 | "interchangeableness", 68 | "unconscientiousness", 69 | "radiopasteurization", 70 | "hypolipoproteinemia", 71 | "encephalomeningitis", 72 | "incommunicativeness", 73 | "antiparliamentarian", 74 | "spectroheliographic", 75 | "hydroxytetracycline", 76 | "pseudohallucination", 77 | "spiritualmindedness", 78 | "disadvantageousness", 79 | "incomprehensiveness", 80 | "distinguishableness", 81 | "resorcinolphthalein", 82 | "multidisciplinarian", 83 | "unsophisticatedness", 84 | "makataimeshekiakiak", 85 | "incommensurableness", 86 | "unexceptionableness", 87 | "pretransformational", 88 | "dissatisfactoriness", 89 | "inconsequentialness", 90 | "interecclesiastical", 91 | "untransubstantiated", 92 | "historiographership", 93 | "unparliamentariness", 94 | "irreprehensibleness", 95 | "intermeddlesomeness", 96 | "electropuncturation", 97 | "schematologetically" 98 | ] -------------------------------------------------------------------------------- /client/js/error.js: -------------------------------------------------------------------------------- 1 | import render from './templates.js'; 2 | 3 | const errorData = { 4 | name_short: { 5 | name: 'Name is too short', 6 | desc: 'Please ensure name is longer than 1 character.', 7 | }, 8 | player_creation: { 9 | name: 'Could not create the player', 10 | desc: 'Try changing your name or joining a different game.', 11 | }, 12 | rule_type: { 13 | name: 'Submitted rule value is of wrong type', 14 | desc: 'The server was expecting a different type of data for this rule, please refresh the page and try again.', 15 | }, 16 | rule_small: { 17 | name: 'Submitted rule value is too small', 18 | desc: 'The server was expecting a larger value, please refresh the page and try again.', 19 | }, 20 | rule_large: { 21 | name: 'Submitted rule value is too large', 22 | desc: 'The server was expecting a larger value, please refresh the page and try again.', 23 | }, 24 | lobby_singleplayer: { 25 | name: 'Game is singleplayer', 26 | desc: 'The game you are trying to join is set to be singleplayer only, tell the host to enable the multiplayer rule or join another lobby.', 27 | }, 28 | lobby_max_players: { 29 | name: 'Max players reached', 30 | desc: 'The game you are trying to join has reached the max number of players, tell the host to increase the max players rule or join another lobby.', 31 | }, 32 | lobby_not_exist: { 33 | name: 'Game does not exist', 34 | desc: 'The game you are trying to join does not exist, please check the game ID in the URL bar.', 35 | }, 36 | no_lobbies: { 37 | name: 'No games available', 38 | desc: 'There are no games available for you to join at this moment, please wait and try again or create a new game.', 39 | }, 40 | letter_used: { 41 | name: 'Letter already used', 42 | desc: 'You have already used that letter, please try another.', 43 | }, 44 | guess_not_allowed: { 45 | name: 'Guesses not allowed', 46 | desc: 'Full guesses are not allowed in this game, please try entering a letter instead.', 47 | }, 48 | guess_length: { 49 | name: 'Guess is wrong length', 50 | desc: 'The guess you have entered is the incorrect length, please check you have spelt it correctly.', 51 | }, 52 | }; 53 | 54 | function createError(data) { 55 | const errorArea = document.getElementById('popup_area'); 56 | errorArea.insertAdjacentHTML('afterbegin', render('error', (typeof data === 'string') ? errorData[data] : data)); 57 | } 58 | 59 | const urlSearchParams = new URLSearchParams(window.location.search); 60 | const params = Object.fromEntries(urlSearchParams.entries()); 61 | if (params.error !== undefined) { 62 | createError(params.error); 63 | window.history.pushState({}, document.title, window.location.pathname); 64 | } 65 | 66 | export default { 67 | create: createError, 68 | data: errorData, 69 | }; 70 | -------------------------------------------------------------------------------- /game/results.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | 3 | export default ({ db }) => { 4 | async function createResults(lobbyId) { 5 | const ruleData = await db.query('SELECT * FROM rules WHERE lobby_id=$id', { 6 | $id: lobbyId, 7 | }); 8 | const rules = Object.fromEntries(ruleData.map(a => { 9 | return [a.rule_id, a.value]; 10 | })); 11 | const resultId = randomUUID(); 12 | await db.query('INSERT INTO results (id, max_lives, max_time) VALUES ($id, $max_lives, $max_time)', { 13 | $id: resultId, 14 | $max_lives: rules.maxLives, 15 | $max_time: rules.maxTime, 16 | }); 17 | const gameStates = await db.query('SELECT * FROM active_players LEFT JOIN player_gamestates on active_players.id=player_gamestates.player_id WHERE active_players.lobby_id=$lobby_id', { 18 | $lobby_id: lobbyId, 19 | }); 20 | await db.query('INSERT INTO result_players (result_id, name, word, known_letters, score, time_used, lives_used, finished) VALUES ($result_id, $name, $word, $known_letters, $score, $time_used, $lives_used, $finished)', 21 | gameStates.map((a) => { 22 | return { 23 | $result_id: resultId, 24 | $name: a.name, 25 | $word: a.word, 26 | $known_letters: a.known_letters, 27 | $score: calcScore(a, rules), 28 | $time_used: a.time_used, 29 | $lives_used: a.lives_used, 30 | $finished: a.finished, 31 | }; 32 | }), 33 | ); 34 | 35 | await db.query('UPDATE lobbies SET last_result=$last_result WHERE id=$id', { 36 | $id: lobbyId, 37 | $last_result: resultId, 38 | }); 39 | 40 | await db.query('DELETE FROM player_gamestates WHERE player_id IN (SELECT id FROM active_players WHERE lobby_id=$lobby_id)', { 41 | $lobby_id: lobbyId, 42 | }); 43 | return resultId; 44 | } 45 | 46 | function calcScore(gamestate, rules) { 47 | let score = 0; 48 | score += (gamestate.known_letters.replace(/\s/g, '')).length * 2; 49 | if (gamestate.known_letters === gamestate.word) { 50 | if (rules.maxTime !== null) { 51 | score += (rules.maxTime - gamestate.time_used); 52 | } 53 | if (rules.maxLives !== null) { 54 | score += (rules.maxLives - gamestate.lives_used) * 4; 55 | } 56 | } 57 | return score; 58 | } 59 | 60 | async function getResultById(resultId) { 61 | const data = (await db.query('SELECT * FROM results WHERE results.id=$id', { 62 | $id: resultId, 63 | }))[0]; 64 | data.players = await db.query('SELECT * FROM result_players WHERE result_id=$id ORDER BY score DESC ', { 65 | $id: resultId, 66 | }); 67 | return data; 68 | } 69 | 70 | async function leaveResults(lobbyId) { 71 | await db.query("UPDATE lobbies SET status='lobby' WHERE id=$id", { 72 | $id: lobbyId, 73 | }); 74 | } 75 | 76 | return { 77 | getResult: getResultById, 78 | create: createResults, 79 | leave: leaveResults, 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /game/players.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | 3 | export default ({ db }) => { 4 | async function createPlayer(lobbyId, { name, sid }) { 5 | if (name.trim().length === 0) { 6 | throw (new Error('name_short')); 7 | } 8 | const isHost = await db.query('SELECT is_host FROM active_players WHERE is_host=true AND lobby_id=$lobby_id', { 9 | $lobby_id: lobbyId, 10 | }); 11 | const id = randomUUID(); 12 | try { 13 | await db.query('INSERT INTO active_players (id, session_id, name, lobby_id, is_host, is_active) VALUES ($id, $session_id, $name, $lobby_id, $is_host, $is_active);', { 14 | $id: id, 15 | $session_id: sid, 16 | $name: name.trim(), 17 | $lobby_id: lobbyId, 18 | $is_host: (isHost.length === 0), 19 | $is_active: false, 20 | }); 21 | } catch (err) { 22 | throw (new Error('player_creation')); 23 | } 24 | return id; 25 | } 26 | 27 | async function getLobbyPlayers(lobbyId) { 28 | const players = await db.query('SELECT name, is_host, is_active, id FROM active_players WHERE lobby_id=$lobby_id', { 29 | $lobby_id: lobbyId, 30 | }); 31 | return players; 32 | } 33 | 34 | async function getPlayerBySession(lobbyId, sid) { 35 | const results = await db.query('SELECT id FROM active_players WHERE lobby_id=$lobby_id AND session_id=$session_id', { 36 | $lobby_id: lobbyId, 37 | $session_id: sid, 38 | }); 39 | return (results.length === 0) ? null : results[0].id; 40 | } 41 | 42 | async function getSessionByPlayer(lobbyId, playerId) { 43 | const results = await db.query('SELECT session_id FROM active_players WHERE lobby_id=$lobby_id AND id=$id', { 44 | $lobby_id: lobbyId, 45 | $id: playerId, 46 | }); 47 | return (results.length === 0) ? null : results[0].session_id; 48 | } 49 | 50 | async function checkHost(lobbyId, sid) { 51 | const results = await db.query('SELECT id, lobby_id FROM active_players WHERE lobby_id=$lobby_id AND session_id=$session_id AND is_host=true', { 52 | $lobby_id: lobbyId, 53 | $session_id: sid, 54 | }); 55 | return (results.length === 1); 56 | } 57 | 58 | async function deletePlayer(lobbyId, sid) { 59 | await db.query('DELETE FROM active_players WHERE session_id=$session_id AND lobby_id=$lobby_id', { 60 | $session_id: sid, 61 | $lobby_id: lobbyId, 62 | }); 63 | } 64 | 65 | async function checkActive(lobbyId, sid) { 66 | const results = await db.query('SELECT id, lobby_id FROM active_players WHERE lobby_id=$lobby_id AND session_id=$session_id AND is_active=true', { 67 | $lobby_id: lobbyId, 68 | $session_id: sid, 69 | }); 70 | return (results.length === 0) ? null : results[0].id; 71 | } 72 | 73 | return { 74 | create: createPlayer, 75 | getByLobby: getLobbyPlayers, 76 | delete: deletePlayer, 77 | getId: getPlayerBySession, 78 | getSid: getSessionByPlayer, 79 | isHost: checkHost, 80 | isActive: checkActive, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /client/js/results.js: -------------------------------------------------------------------------------- 1 | import request from './request.js'; 2 | import render from './templates.js'; 3 | 4 | const page = location.pathname.split('/').filter(a => a.length > 0)[0]; 5 | if (page === 'results') { 6 | main(); 7 | } 8 | 9 | async function main() { 10 | window.results = JSON.parse(await request.GET(`/api/results/${location.pathname.split('/').filter(a => a.length > 0)[1]}`)); 11 | constructPlayerTable(); 12 | } 13 | 14 | async function displayResults() { 15 | // Handle code a bit differently here so that it can be shared between ingame results and dedicated results 16 | 17 | if (window.currentPage !== 'results') { 18 | document.getElementById('page_content').innerHTML = render('results'); 19 | window.currentPage = 'results'; 20 | if (window.isHost) { 21 | // host stuff 22 | document.getElementById('lobby_return').addEventListener('click', () => { 23 | // send request 24 | request.POST(`/api/${window.gameId}/goto_lobby`); 25 | }); 26 | } 27 | const resultList = JSON.parse(localStorage.getItem('results') || '[]'); 28 | resultList.push({ id: window.gameData.lastResult, date: Date.now() }); 29 | localStorage.setItem('results', JSON.stringify(resultList)); 30 | } 31 | window.results = JSON.parse(await request.GET(`/api/results/${window.gameData.lastResult}`)); 32 | constructPlayerTable(); 33 | } 34 | 35 | function constructPlayerTable() { 36 | const playerTable = document.querySelector('#results_table>table>tbody'); 37 | playerTable.innerHTML = ''; 38 | for (const [position, player] of window.results.players.entries()) { 39 | let awardString = ''; 40 | if (position === 0) awardString += '🏆'; 41 | if (player.word === player.known_letters && player.lives_used === 0) awardString += '🔥'; 42 | if (player.known_letters.trim().length === 0) awardString += '💤'; 43 | if (player.word === player.known_letters) awardString += '🏁'; 44 | if (player.known_letters.includes(' ') && (player.known_letters.match(/ /g)).length === 1) awardString += '😭'; 45 | if (window.results.max_lives - player.lives_used === 1) awardString += '😅'; 46 | if (player.word !== player.known_letters) awardString += ''; 47 | 48 | let wordString = ''; 49 | for (const letter of player.word) { 50 | const known = player.known_letters.includes(letter.toLowerCase()) ? 'success' : 'failure'; 51 | wordString += render('word_letter', { 52 | letter: letter.toUpperCase().trim(), 53 | known, 54 | }); 55 | } 56 | 57 | playerTable.innerHTML += render('player_row', { 58 | misc: awardString, 59 | pos: position + 1, 60 | name: player.name, 61 | word: wordString, 62 | time: player.time_used, 63 | lives: window.results.max_lives - player.lives_used, 64 | score: player.score, 65 | }); 66 | } 67 | } 68 | 69 | export default { 70 | display: displayResults, 71 | }; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hangman (V2) 2 | NodeJS web application created for Application Programming M30221. 3 | 4 | ## Design 5 | ### Aims 6 | - Allow users to customize their games through a system of rules 7 | - Limit amount of trust given to clients as much as possible without affecting server performance too much 8 | - Minimise barriers to entry such as forced accounts etc 9 | 10 | ### Assumptions 11 | - Hosts will be able to kick players at their discretion thus some trust can be put in the client e.g. for turn timers 12 | 13 | ### Basic flow 14 | ```mermaid 15 | stateDiagram-v2 16 | [*] --> Homepage 17 | Homepage --> Lobby 18 | Lobby --> Homepage 19 | state Lobby { 20 | [*] --> Identification 21 | Identification --> Rules 22 | Rules --> Game 23 | Game --> Results 24 | Results --> Rules 25 | } 26 | ``` 27 | 28 | ### Practices 29 | #### Variable naming 30 | - HTML Classes: `dash-case` 31 | - HTML ID's: `snake_case` 32 | - JS: `lowerCamelCase` 33 | - JS (Instantiable): `UpperCamelCase` 34 | - SQL: `snake_case` 35 | 36 | #### File Structure 37 | Includes additional information for files/ folders of note. 38 | ``` 39 | hangman 40 | │ index.js - Main server script that connects the different components. 41 | │ package.json 42 | │ .nvmrc 43 | │ README.md 44 | │ .gitignore 45 | │ 46 | └──storage - Files related to the database 47 | │ │ db.js - Script to provide wrapper functions to interface with the database and add support for async/ await. 48 | │ │ schema.sql - Database initialisation code for the necessary tables 49 | │ 50 | └──game - Files related to the logical components of the game 51 | │ │ rules.js 52 | │ │ rules.json - Contains JSON data for all rules 53 | │ │ lobby.js 54 | │ │ game.js 55 | │ │ results.js 56 | │ │ players.js 57 | │ │ words.js - Wrapper for accessing lists contained in the wordlists directory through a dictionary. 58 | │ └──wordlists - JSON files containing all possible words, separated by word length. 59 | │ │ │ getWords.js - Script to filter + convert wordlist CSV into JSON files 60 | │ │ │ ... 61 | │ 62 | └──client - Static files for the client. 63 | │ │ index.html 64 | │ │ game.html 65 | │ └──css 66 | │ │ │ ... 67 | │ │ 68 | │ └──js 69 | │ │ │ ... 70 | │ 71 | └──api - API router that interacts with the logical components. 72 | │ rest.js - Express router to handle most client interactions. 73 | │ websocket.js - Websocket server that checks client connections and informs clients of updates. 74 | ``` 75 | *May not be fully exhaustive of all files included in the repository.* 76 | 77 | 78 | ## Running the server 79 | 1. Install NodeJS/ NPM 80 | 2. Open the code directory 81 | ```bash 82 | cd ./hangman2 83 | ``` 84 | 3. Install npm dependencies 85 | ```bash 86 | npm i 87 | ``` 88 | 4. Run the server 89 | ```bash 90 | npm start 91 | ``` 92 | 93 | Optionally you may set the following environment variables in a `.env` file in the root directory or by including them in the run command. 94 | ``` 95 | PORT: The port to be used by the server (defaults to 8080) 96 | ``` 97 | 98 | ### Sources 99 | I have used libraries or obtained data from the following sources: 100 | - [Express](https://www.npmjs.com/package/express) 101 | - [DotEnv](https://www.npmjs.com/package/dotenv) 102 | - Words: 103 | - [hackerb9/gwordlist](https://github.com/hackerb9/gwordlist) 104 | - Filtered with: [Bad Words List](https://www.cs.cmu.edu/~biglou/resources/bad-words.txt) -------------------------------------------------------------------------------- /client/js/app.js: -------------------------------------------------------------------------------- 1 | import request from './request.js'; 2 | import render from './templates.js'; 3 | import auth from './auth.js'; 4 | import lobby from './lobby.js'; 5 | import game from './game.js'; 6 | import results from './results.js'; 7 | import Ws from './ws.js'; 8 | import error from './error.js'; 9 | window.error = error; 10 | 11 | const query = new URLSearchParams(window.location.search); 12 | const gameId = window.gameId = query.get('id'); 13 | 14 | async function main() { 15 | window.ruleData = JSON.parse(await request.GET('/api/rules')); 16 | if (gameId != null) { 17 | const wsc = new Ws(); 18 | wsc.on('do_update', () => { 19 | getLobbyData(); 20 | }); 21 | getLobbyData(); 22 | } 23 | document.querySelector('nav .brand').addEventListener('click', () => { 24 | // Kind of hacky but tidies up the URL query 25 | window.history.replaceState(null, null, window.location.pathname); 26 | window.location.pathname = '/'; 27 | }); 28 | document.addEventListener('keydown', (event) => { 29 | if (window.currentPage === 'game' && window.alphabet.includes(event.key.toUpperCase()) && document.querySelector('#guess_form input') !== document.activeElement) { 30 | game.takeTurn('letter', event.key); 31 | } 32 | }); 33 | } 34 | 35 | async function getLobbyData() { 36 | const data = JSON.parse(await request.GET(`/api/${gameId}`)); 37 | if ('error' in data) { 38 | console.log('ERROR', data.error); 39 | window.error = data.error; 40 | switch (data.error) { 41 | case 'lobby_not_exist': 42 | window.location.href = `${window.location.protocol}//${window.location.host}`; 43 | break; 44 | } 45 | } else { 46 | for (const [id, value] of Object.entries(data.rules)) { 47 | window.ruleData[id].value = value; 48 | } 49 | data.rules = window.ruleData; 50 | window.gameData = data; 51 | if (data.playerId == null) { 52 | auth(); 53 | } else { 54 | if (Object.fromEntries(data.players.map(a => [a.id, a.is_host]))[data.playerId] === 1) { 55 | window.isHost = true; 56 | document.querySelector('body').dataset.host = 'true'; 57 | } 58 | if (Object.fromEntries(data.players.map(a => [a.id, a.is_active]))[data.playerId] === 1) { 59 | window.isActive = true; 60 | document.querySelector('body').dataset.active = 'true'; 61 | } else { 62 | document.querySelector('body').dataset.active = 'false'; 63 | } 64 | switch (data.status) { 65 | case 'lobby': 66 | lobby.display(); 67 | break; 68 | case 'game': 69 | game.display(); 70 | break; 71 | case 'results': 72 | results.display(); 73 | break; 74 | } 75 | constructPlayerList(); 76 | } 77 | } 78 | } 79 | 80 | function constructPlayerList() { 81 | document.querySelectorAll('.player-list').forEach((playerGrid) => { 82 | playerGrid.innerHTML = ''; 83 | for (const player of window.gameData.players) { 84 | playerGrid.innerHTML += render('player', { 85 | isClient: player.id === window.gameData.playerId, 86 | classes: ((player.is_host === 1) ? 'host' : '') + ((player.is_active === 1) ? ' active' : ''), 87 | name: ((player.is_host) ? "👑" : '') + player.name, 88 | id: player.id, 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | window.kickPlayer = async (playerId) => { 95 | await request.POST(`/api/${window.gameId}/kick_player`, { playerId }); 96 | }; 97 | 98 | main(); 99 | -------------------------------------------------------------------------------- /client/js/game.js: -------------------------------------------------------------------------------- 1 | import hangmanCanvas from './hangmanCanvas.js'; 2 | import render from './templates.js'; 3 | import request from './request.js'; 4 | import startTimer from './timer.js'; 5 | 6 | let wasActive = false; 7 | let gui = null; 8 | window.alphabet = Array.from({ length: 26 }, (v, i) => String.fromCharCode(65 + i)); 9 | let endTimeout = () => {}; 10 | 11 | function displayGame() { 12 | if (window.isActive === true && wasActive === false) { 13 | if (window.gameData.rules.turnTime.value !== null) { 14 | endTimeout = startTimer(window.gameData.rules.turnTime.value, 'turnTimer', () => { 15 | request.POST(`/api/${window.gameId}/turn`, { type: null, data: null }); 16 | wasActive = false; 17 | }); 18 | } 19 | wasActive = true; 20 | } 21 | if (window.currentPage !== 'game') { 22 | if (window.gameData.rules.maxTime.value !== null) { 23 | // do something 24 | endTimeout = startTimer(window.gameData.rules.maxTime.value, 'turnTimer', () => { 25 | request.POST(`/api/${window.gameId}/checkEnd`); 26 | }); 27 | } 28 | document.getElementById('page_content').innerHTML = render('game'); 29 | window.currentPage = 'game'; 30 | gui = hangmanCanvas.create(document.getElementById('hangman_canvas'), window.gameData.rules.maxLives.value); 31 | } 32 | gui.setLivesUsed(window.gameData.gameStatus.lives_used); 33 | createInputArea(); 34 | createStatusArea(); 35 | } 36 | 37 | function takeTurn(turnType, turnData) { 38 | request.POST(`/api/${window.gameId}/turn`, { type: turnType, data: turnData }); 39 | endTimeout(); 40 | } 41 | 42 | function createInputArea() { 43 | const letterInput = document.getElementById('letter_input'); 44 | letterInput.innerHTML = ''; 45 | if (window.gameData.gameStatus.known_letters.includes(' ')) { 46 | for (const letter of window.alphabet) { 47 | const letterEl = document.createElement('button'); 48 | letterEl.classList.add('letter'); 49 | letterEl.innerText = letter; 50 | if (window.gameData.gameStatus.used_letters.includes(letter.toLowerCase())) { 51 | if (window.gameData.gameStatus.known_letters.includes(letter.toLowerCase())) { 52 | letterEl.classList.add('success'); 53 | } else { 54 | letterEl.classList.add('failure'); 55 | } 56 | } else { 57 | letterEl.addEventListener('click', () => { 58 | takeTurn('letter', letter); 59 | }); 60 | } 61 | letterInput.appendChild(letterEl); 62 | } 63 | 64 | const guessInput = document.getElementById('guess_form'); 65 | if (window.gameData.rules.fullGuesses.value) { 66 | guessInput.addEventListener('submit', (e) => { 67 | e.preventDefault(); 68 | const guessText = document.querySelector('#guess_form input').value; 69 | // this.ws.emit("guess", guessText); 70 | takeTurn('full_guess', guessText); 71 | }); 72 | } else { 73 | guessInput.classList.add('hidden'); 74 | } 75 | } else { 76 | document.getElementById('guess_form').classList.add('hidden'); 77 | } 78 | } 79 | 80 | function createStatusArea() { 81 | const statusArea = document.getElementById('word_status'); 82 | statusArea.innerHTML = ''; 83 | for (let i = 0; i < window.gameData.gameStatus.known_letters.length; i++) { 84 | const letterContainer = document.createElement('div'); 85 | letterContainer.classList.add('letter'); 86 | const letterBox = document.createElement('input'); 87 | letterBox.type = 'text'; 88 | letterBox.maxLength = 1; 89 | letterBox.disabled = true; 90 | if (window.gameData.gameStatus.known_letters.split('')[i] !== ' ') letterBox.value = window.gameData.gameStatus.known_letters.split('')[i].toUpperCase(); 91 | letterContainer.appendChild(letterBox); 92 | statusArea.appendChild(letterContainer); 93 | } 94 | } 95 | 96 | export default { 97 | display: displayGame, 98 | takeTurn, 99 | }; 100 | -------------------------------------------------------------------------------- /client/js/hangmanCanvas.js: -------------------------------------------------------------------------------- 1 | function createHangmanCanvas(element, initMaxLives, size = null) { 2 | let livesUsed = 0; 3 | const container = element; 4 | container.innerHTML = ''; 5 | const display = container.appendChild(document.createElement('canvas')); 6 | display.width = (size != null) ? size : window.innerWidth; 7 | display.height = (size != null) ? size : window.innerWidth; 8 | const context = display.getContext('2d'); 9 | context.fillStyle = '#000'; 10 | context.lineWidth = display.width / 70; 11 | let maxLives = initMaxLives; 12 | 13 | function setMaxLives(newMax) { 14 | maxLives = newMax; 15 | setLivesUsed(livesUsed); 16 | } 17 | 18 | // Helper functions for canvas drawing 19 | // Whilst I could use a fixed size with coordinates, this seems to be the best way to allow to scale it to a device 20 | // If size is set to display width then it will always be able to scale to the right size as it seems to have issues going very large/ small using pure css 21 | function drawRect(x, y, width, height) { 22 | return context.fillRect(x * display.width, y * display.height, width * display.width, height * display.height); 23 | } 24 | 25 | function drawLine(startX, startY, endX, endY) { 26 | context.beginPath(); 27 | context.moveTo(startX * display.width, startY * display.height); 28 | context.lineTo(endX * display.width, endY * display.height); 29 | context.stroke(); 30 | } 31 | 32 | function drawCircle(x, y, radius) { 33 | context.beginPath(); 34 | context.arc(x * display.width, y * display.height, radius * display.width, 0, 2 * Math.PI); 35 | context.stroke(); 36 | } 37 | 38 | function setLivesUsed(val) { 39 | context.clearRect(0, 0, display.width, display.height); 40 | // Just have the option of -1 for an empty canvas 41 | if (val !== -1) { 42 | drawLine(0.1, 0.015, 0.1, 0.98); 43 | drawLine(0.02, 0.98, 0.2, 0.98); 44 | drawLine(0.1, 0.02, 0.5, 0.02); 45 | } 46 | 47 | 48 | const percent = val / maxLives; 49 | 50 | let stages = []; 51 | const getEnd = (start, end, currentStage) => { 52 | const diff = ((end > start) ? end - start : start - end); 53 | return (start + (((end > start) ? 1 : -1) * Math.min(((percent - currentStage / stages.length) / (1 / stages.length)) * diff, diff))); 54 | }; 55 | 56 | stages = [ 57 | (stage) => { 58 | drawLine(0.5, 0.015, 0.5, 0.015 + getEnd(0, 0.1, stage)); 59 | }, 60 | (stage, maxStages) => { 61 | drawCircle(0.5, 0.2, 0.08); 62 | if (percent < (stage + 1) / maxStages) { 63 | context.fillStyle = '#fff'; 64 | drawRect(0.4, 0.29, 0.2, -(0.18 - (0.18 * (percent - (stage / maxStages)) / (1 / maxStages)))); 65 | context.fillStyle = '#000'; 66 | } 67 | }, 68 | (stage) => { 69 | drawLine(0.5, 0.28, 0.5, getEnd(0.28, 0.6, stage)); 70 | }, 71 | (stage) => { 72 | drawLine(0.5, 0.4, getEnd(0.5, 0.4, stage), getEnd(0.4, 0.5, stage)); 73 | }, 74 | (stage) => { 75 | drawLine(0.5, 0.4, getEnd(0.5, 0.6, stage), getEnd(0.4, 0.5, stage)); 76 | }, 77 | (stage) => { 78 | drawLine(0.5, 0.6, getEnd(0.5, 0.4, stage), getEnd(0.6, 0.8, stage)); 79 | }, 80 | (stage) => { 81 | drawLine(0.5, 0.6, getEnd(0.5, 0.6, stage), getEnd(0.6, 0.8, stage)); 82 | }, 83 | () => { 84 | drawLine(0.47, 0.18, 0.49, 0.22); 85 | drawLine(0.49, 0.18, 0.47, 0.22); 86 | drawLine(0.51, 0.18, 0.53, 0.22); 87 | drawLine(0.53, 0.18, 0.51, 0.22); 88 | }, 89 | ]; 90 | 91 | for (const [stage, func] of stages.entries()) { 92 | if (percent > stage / stages.length) { 93 | func(stage, stages.length); 94 | } 95 | } 96 | 97 | livesUsed = val; 98 | } 99 | 100 | return { setMaxLives, setLivesUsed }; 101 | } 102 | 103 | 104 | export default { 105 | create: createHangmanCanvas, 106 | }; 107 | -------------------------------------------------------------------------------- /game/wordlists/4_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "that", 3 | "with", 4 | "this", 5 | "from", 6 | "have", 7 | "they", 8 | "were", 9 | "been", 10 | "will", 11 | "more", 12 | "when", 13 | "such", 14 | "time", 15 | "than", 16 | "some", 17 | "what", 18 | "only", 19 | "into", 20 | "them", 21 | "also", 22 | "said", 23 | "then", 24 | "most", 25 | "over", 26 | "very", 27 | "your", 28 | "work", 29 | "same", 30 | "well", 31 | "each", 32 | "many", 33 | "year", 34 | "must", 35 | "upon", 36 | "like", 37 | "part", 38 | "used", 39 | "even", 40 | "much", 41 | "both", 42 | "case", 43 | "make", 44 | "life", 45 | "good", 46 | "long", 47 | "high", 48 | "just", 49 | "here", 50 | "does", 51 | "know", 52 | "back", 53 | "take", 54 | "less", 55 | "last", 56 | "city", 57 | "york", 58 | "form", 59 | "data", 60 | "come", 61 | "fact", 62 | "thus", 63 | "area", 64 | "give", 65 | "line", 66 | "land", 67 | "four", 68 | "hand", 69 | "need", 70 | "john", 71 | "left", 72 | "days", 73 | "rate", 74 | "name", 75 | "find", 76 | "best", 77 | "side", 78 | "held", 79 | "came", 80 | "five", 81 | "head", 82 | "cost", 83 | "book", 84 | "body", 85 | "free", 86 | "type", 87 | "next", 88 | "once", 89 | "done", 90 | "away", 91 | "want", 92 | "half", 93 | "cent", 94 | "view", 95 | "seen", 96 | "care", 97 | "june", 98 | "took", 99 | "west", 100 | "open", 101 | "feet", 102 | "help", 103 | "real", 104 | "ever", 105 | "went", 106 | "term", 107 | "date", 108 | "true", 109 | "room", 110 | "look", 111 | "bank", 112 | "read", 113 | "plan", 114 | "mind", 115 | "able", 116 | "face", 117 | "july", 118 | "lord", 119 | "near", 120 | "paid", 121 | "king", 122 | "note", 123 | "love", 124 | "kind", 125 | "past", 126 | "food", 127 | "self", 128 | "test", 129 | "word", 130 | "east", 131 | "rule", 132 | "eyes", 133 | "told", 134 | "road", 135 | "main", 136 | "size", 137 | "mean", 138 | "call", 139 | "keep", 140 | "page", 141 | "gave", 142 | "soon", 143 | "tell", 144 | "laws", 145 | "late", 146 | "loss", 147 | "felt", 148 | "knew", 149 | "sent", 150 | "army", 151 | "role", 152 | "hard", 153 | "week", 154 | "feel", 155 | "unit", 156 | "idea", 157 | "list", 158 | "turn", 159 | "sure", 160 | "code", 161 | "wife", 162 | "post", 163 | "meet", 164 | "duty", 165 | "fine", 166 | "base", 167 | "lost", 168 | "live", 169 | "lead", 170 | "rest", 171 | "door", 172 | "acid", 173 | "wide", 174 | "sale", 175 | "fall", 176 | "iron", 177 | "play", 178 | "flow", 179 | "seem", 180 | "soil", 181 | "kept", 182 | "heat", 183 | "ways", 184 | "risk", 185 | "cell", 186 | "site", 187 | "text", 188 | "deep", 189 | "born", 190 | "deal", 191 | "coal", 192 | "wall", 193 | "farm", 194 | "move", 195 | "step", 196 | "none", 197 | "news", 198 | "gold", 199 | "acts", 200 | "lake", 201 | "fair", 202 | "else", 203 | "wood", 204 | "dark", 205 | "hear", 206 | "cold", 207 | "fish", 208 | "paul", 209 | "hall", 210 | "arms", 211 | "park", 212 | "rise", 213 | "miss", 214 | "gone", 215 | "sold", 216 | "easy", 217 | "talk", 218 | "blue", 219 | "firm", 220 | "lack", 221 | "wish", 222 | "mark", 223 | "hill", 224 | "copy", 225 | "foot", 226 | "file", 227 | "mary", 228 | "rock", 229 | "task", 230 | "stop", 231 | "ship", 232 | "tree", 233 | "ohio", 234 | "send", 235 | "vice", 236 | "girl", 237 | "lady", 238 | "mine", 239 | "sort", 240 | "pain", 241 | "milk", 242 | "film", 243 | "till", 244 | "cash", 245 | "ones", 246 | "debt", 247 | "vote", 248 | "tons", 249 | "nine", 250 | "rose", 251 | "male", 252 | "race", 253 | "hair", 254 | "rich", 255 | "game", 256 | "port", 257 | "wind", 258 | "loan", 259 | "skin", 260 | "stay", 261 | "fell", 262 | "soul", 263 | "bear", 264 | "mail", 265 | "gain", 266 | "bond", 267 | "team", 268 | "sign", 269 | "jury", 270 | "club", 271 | "salt", 272 | "fast", 273 | "edge", 274 | "arts", 275 | "pure", 276 | "item", 277 | "safe", 278 | "load", 279 | "suit", 280 | "save", 281 | "dear", 282 | "holy", 283 | "inch", 284 | "mode", 285 | "wave", 286 | "plus", 287 | "path", 288 | "zone", 289 | "sell", 290 | "band", 291 | "sand", 292 | "cast", 293 | "mere", 294 | "soft", 295 | "wild", 296 | "anti", 297 | "crop", 298 | "thin", 299 | "tube", 300 | "camp", 301 | "seek" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/18_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "characteristically", 3 | "disproportionately", 4 | "immunofluorescence", 5 | "glomerulonephritis", 6 | "representativeness", 7 | "oversimplification", 8 | "transubstantiation", 9 | "neurophysiological", 10 | "intercommunication", 11 | "interconnectedness", 12 | "thermoluminescence", 13 | "phenomenologically", 14 | "hypoparathyroidism", 15 | "neuroendocrinology", 16 | "coccidioidomycosis", 17 | "mucopolysaccharide", 18 | "radiocommunication", 19 | "electrocardiograph", 20 | "proletarianization", 21 | "constantinopolitan", 22 | "disproportionality", 23 | "hypersensitiveness", 24 | "hydrometallurgical", 25 | "hydroxychloroquine", 26 | "antiferromagnetism", 27 | "agammaglobulinemia", 28 | "microencapsulation", 29 | "overcapitalization", 30 | "chlorofluorocarbon", 31 | "elastohydrodynamic", 32 | "counterreformation", 33 | "territorialization", 34 | "internalcombustion", 35 | "spectrographically", 36 | "chronostratigraphy", 37 | "unsatisfactoriness", 38 | "unenthusiastically", 39 | "quartzofeldspathic", 40 | "extraterritorially", 41 | "overspecialization", 42 | "selfidentification", 43 | "electrodynamometer", 44 | "transmogrification", 45 | "pseudopleuronectes", 46 | "crossfertilization", 47 | "counterintuitively", 48 | "pockethandkerchief", 49 | "tetrachlorethylene", 50 | "radiosterilization", 51 | "overcentralization", 52 | "greatgranddaughter", 53 | "crystallographical", 54 | "ballistocardiogram", 55 | "territorialisation", 56 | "inconsequentiality", 57 | "selfcongratulation", 58 | "chloroacetophenone", 59 | "hypersensitization", 60 | "sentimentalization", 61 | "knowledgeintensive", 62 | "establishmentarian", 63 | "palaeoanthropology", 64 | "crossfertilisation", 65 | "supersensitiveness", 66 | "ultranationalistic", 67 | "verfremdungseffekt", 68 | "noninterchangeable", 69 | "cytoarchitectonics", 70 | "spectrofluorimetry", 71 | "neuropharmacologic", 72 | "calymmatobacterium", 73 | "tetraiodothyronine", 74 | "internationalistic", 75 | "adenocarcinomatous", 76 | "electrocapillarity", 77 | "anthropocentricity", 78 | "postmultiplication", 79 | "overcapitalisation", 80 | "anthropocentricism", 81 | "homobasidiomycetes", 82 | "ellipticlanceolate", 83 | "indiscriminatingly", 84 | "pachycephalosaurus", 85 | "dynamometamorphism", 86 | "superintendentship", 87 | "saccharomycetaceae", 88 | "corynebacteriaceae", 89 | "unintelligibleness", 90 | "mostfavourednation", 91 | "teletransportation", 92 | "irreconcilableness", 93 | "impressionableness", 94 | "magnetoelectricity", 95 | "unconscionableness", 96 | "parallelogrammatic", 97 | "postmastersgeneral", 98 | "intercommunicative", 99 | "publicspiritedness", 100 | "sentimentalisation", 101 | "politicoeconomical", 102 | "inconsiderableness", 103 | "theophilanthropist", 104 | "incommensurateness", 105 | "extemporaneousness", 106 | "indestructibleness", 107 | "heavenlymindedness", 108 | "maleadministration", 109 | "chlamydomonadaceae", 110 | "characteristicness", 111 | "hemidemisemiquaver", 112 | "polypharmaceutical", 113 | "incommunicableness", 114 | "unquestionableness", 115 | "uncontrollableness", 116 | "hermaphroditically", 117 | "comprehensibleness", 118 | "laryngopharyngitis", 119 | "seismocardiography", 120 | "theophilanthropism", 121 | "permopennsylvanian", 122 | "rhinolaryngologist", 123 | "tetrakaidekahedron", 124 | "laryngotracheotomy", 125 | "contradictiousness", 126 | "pepsinhydrochloric", 127 | "undistinguishingly", 128 | "transsignification", 129 | "anthropopathically", 130 | "cardiosphygmograph", 131 | "supertechnological", 132 | "transaccidentation", 133 | "introspectionistic", 134 | "indiscriminatively", 135 | "syncategorematical", 136 | "tetrakishexahedron", 137 | "physicomathematics", 138 | "tetrahydroxyadipic", 139 | "electrotelegraphic", 140 | "counterrestoration", 141 | "interpretativeness", 142 | "proportionableness", 143 | "transubstantiative", 144 | "cotemporaneousness", 145 | "unapprehensiveness", 146 | "parallelogrammical", 147 | "pustulocrustaceous", 148 | "electrochronograph", 149 | "lautverschiebungen", 150 | "radiometeorography", 151 | "thermoelectrometer", 152 | "representativeship", 153 | "transincorporation", 154 | "hypidiomorphically", 155 | "caterpillartracked", 156 | "apophthegmatically", 157 | "precinematographic", 158 | "discomfortableness", 159 | "contraremonstrance", 160 | "aristocraticalness", 161 | "insurmountableness", 162 | "iatromathematician", 163 | "disserviceableness", 164 | "noncomprehensively", 165 | "counterdistinguish", 166 | "telemeteorographic", 167 | "pleuroperipneumony", 168 | "branchiogastropoda", 169 | "unmisinterpretable", 170 | "telehydrobarometer", 171 | "metropolitaneously" 172 | ] -------------------------------------------------------------------------------- /game/wordlists/5_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "which", 3 | "their", 4 | "there", 5 | "other", 6 | "would", 7 | "these", 8 | "first", 9 | "under", 10 | "state", 11 | "after", 12 | "could", 13 | "where", 14 | "shall", 15 | "being", 16 | "years", 17 | "three", 18 | "great", 19 | "water", 20 | "while", 21 | "order", 22 | "court", 23 | "right", 24 | "found", 25 | "world", 26 | "given", 27 | "power", 28 | "place", 29 | "every", 30 | "since", 31 | "might", 32 | "small", 33 | "large", 34 | "still", 35 | "point", 36 | "total", 37 | "think", 38 | "never", 39 | "value", 40 | "study", 41 | "means", 42 | "group", 43 | "again", 44 | "among", 45 | "table", 46 | "board", 47 | "until", 48 | "taken", 49 | "local", 50 | "often", 51 | "women", 52 | "based", 53 | "south", 54 | "human", 55 | "level", 56 | "times", 57 | "early", 58 | "light", 59 | "least", 60 | "north", 61 | "later", 62 | "using", 63 | "going", 64 | "field", 65 | "trade", 66 | "young", 67 | "money", 68 | "words", 69 | "party", 70 | "third", 71 | "paper", 72 | "force", 73 | "terms", 74 | "along", 75 | "child", 76 | "press", 77 | "major", 78 | "union", 79 | "cause", 80 | "river", 81 | "asked", 82 | "march", 83 | "lower", 84 | "model", 85 | "basis", 86 | "quite", 87 | "thing", 88 | "works", 89 | "parts", 90 | "issue", 91 | "clear", 92 | "below", 93 | "sense", 94 | "plant", 95 | "range", 96 | "space", 97 | "close", 98 | "april", 99 | "heart", 100 | "stock", 101 | "costs", 102 | "rates", 103 | "lines", 104 | "woman", 105 | "hands", 106 | "rules", 107 | "miles", 108 | "chief", 109 | "began", 110 | "blood", 111 | "james", 112 | "needs", 113 | "added", 114 | "final", 115 | "leave", 116 | "today", 117 | "legal", 118 | "civil", 119 | "heard", 120 | "story", 121 | "front", 122 | "doing", 123 | "month", 124 | "alone", 125 | "equal", 126 | "claim", 127 | "cross", 128 | "trial", 129 | "earth", 130 | "bring", 131 | "trust", 132 | "goods", 133 | "basic", 134 | "sales", 135 | "prior", 136 | "stage", 137 | "india", 138 | "peace", 139 | "sound", 140 | "judge", 141 | "music", 142 | "green", 143 | "gives", 144 | "seven", 145 | "steel", 146 | "voice", 147 | "upper", 148 | "scale", 149 | "eight", 150 | "share", 151 | "truth", 152 | "built", 153 | "smith", 154 | "apply", 155 | "henry", 156 | "doubt", 157 | "china", 158 | "comes", 159 | "round", 160 | "daily", 161 | "fixed", 162 | "moved", 163 | "phase", 164 | "stand", 165 | "heavy", 166 | "allow", 167 | "cover", 168 | "write", 169 | "ready", 170 | "grant", 171 | "start", 172 | "filed", 173 | "index", 174 | "noted", 175 | "names", 176 | "metal", 177 | "occur", 178 | "carry", 179 | "offer", 180 | "agent", 181 | "forth", 182 | "tests", 183 | "serve", 184 | "david", 185 | "error", 186 | "tried", 187 | "speak", 188 | "motor", 189 | "banks", 190 | "event", 191 | "japan", 192 | "ideas", 193 | "lives", 194 | "wrote", 195 | "stone", 196 | "royal", 197 | "bonds", 198 | "image", 199 | "coast", 200 | "stood", 201 | "paris", 202 | "enter", 203 | "worth", 204 | "plate", 205 | "named", 206 | "style", 207 | "hence", 208 | "reach", 209 | "ratio", 210 | "urban", 211 | "rural", 212 | "wrong", 213 | "ought", 214 | "learn", 215 | "usual", 216 | "steps", 217 | "mouth", 218 | "piece", 219 | "jesus", 220 | "texas", 221 | "grand", 222 | "visit", 223 | "check", 224 | "broad", 225 | "bound", 226 | "spent", 227 | "lived", 228 | "grade", 229 | "avoid", 230 | "exist", 231 | "limit", 232 | "yield", 233 | "proof", 234 | "waste", 235 | "guide", 236 | "media", 237 | "shape", 238 | "agree", 239 | "solid", 240 | "fresh", 241 | "sides", 242 | "radio", 243 | "happy", 244 | "drive", 245 | "focus", 246 | "prove", 247 | "clerk", 248 | "depth", 249 | "peter", 250 | "older", 251 | "brief", 252 | "louis", 253 | "thank", 254 | "entry", 255 | "meant", 256 | "youth", 257 | "fruit", 258 | "fifty", 259 | "queen", 260 | "sugar", 261 | "aware", 262 | "block", 263 | "build", 264 | "train", 265 | "rapid", 266 | "acres", 267 | "break", 268 | "layer", 269 | "steam", 270 | "wages", 271 | "roman", 272 | "store", 273 | "minor", 274 | "scene", 275 | "grain", 276 | "apart", 277 | "forty", 278 | "chair", 279 | "sight", 280 | "maybe", 281 | "latin", 282 | "fifth", 283 | "mixed", 284 | "italy", 285 | "favor", 286 | "greek", 287 | "frank", 288 | "plain", 289 | "plane", 290 | "false", 291 | "dated", 292 | "count", 293 | "sheet", 294 | "spoke", 295 | "jones", 296 | "frame", 297 | "sleep", 298 | "award", 299 | "roads", 300 | "refer", 301 | "inner" 302 | ] -------------------------------------------------------------------------------- /api/rest.js: -------------------------------------------------------------------------------- 1 | // Implement a RESTful API for users to interact with the game 2 | // Technically, a client could use this game 3 | 4 | import fs from 'fs/promises'; 5 | 6 | export default ({ express, lobbies, wss }) => { 7 | const router = express.Router(); 8 | 9 | router.get('/rules', async (req, res) => { 10 | const rules = await fs.readFile('./game/rules.json'); 11 | res.json(JSON.parse(rules)); 12 | }); 13 | 14 | router.get('/:id', async (req, res) => { 15 | // Have authentication status for spectating or name needs submission 16 | try { 17 | const data = await lobbies.getData(req.params.id, req.sessionID); 18 | res.json(data); 19 | } catch (err) { 20 | res.status(400).json({ error: err.message }); 21 | } 22 | }); 23 | 24 | router.post('/:id/join', async (req, res) => { 25 | // joins the game, authorising the user to send inputs 26 | try { 27 | await lobbies.join(req.params.id, { name: req.body.name, sid: req.sessionID }); 28 | res.sendStatus(200); 29 | wss.updateClient(req.params.id); 30 | } catch (err) { 31 | res.status(400).json({ error: err.message }); 32 | } 33 | }); 34 | 35 | // async function checkPlayer(req, res, next) { 36 | // const playerId = await lobbies.players.getId(req.params.id, req.sessionID); 37 | // if (playerId != null) { 38 | // req.playerId = playerId; 39 | // next(); 40 | // } else { 41 | // res.status(403).json({ error: 'not_authorised' }); 42 | // } 43 | // } 44 | 45 | async function checkHost(req, res, next) { 46 | if (await lobbies.players.isHost(req.params.id, req.sessionID)) { 47 | next(); 48 | } else { 49 | res.status(403).json({ error: 'not_authorised' }); 50 | } 51 | } 52 | 53 | // Checks whether request is coming from the active player, puts the ID into the the req object 54 | async function checkActive(req, res, next) { 55 | const playerId = await lobbies.players.isActive(req.params.id, req.sessionID); 56 | if (playerId != null) { 57 | req.playerId = playerId; 58 | next(); 59 | } else { 60 | res.status(403).json({ error: 'not_authorised' }); 61 | } 62 | } 63 | 64 | // router.get('/:id/poll', checkPlayer, async (req, res) => { 65 | 66 | // }); 67 | 68 | router.post('/:id/start_game', checkHost, async (req, res) => { 69 | try { 70 | await lobbies.game.start(req.params.id); 71 | res.sendStatus(200); 72 | wss.updateClient(req.params.id); 73 | } catch (err) { 74 | res.status(400).json({ error: err.message }); 75 | } 76 | }); 77 | 78 | router.post('/:id/goto_lobby', checkHost, async (req, res) => { 79 | try { 80 | await lobbies.game.results.leave(req.params.id); 81 | res.sendStatus(200); 82 | wss.updateClient(req.params.id); 83 | } catch (err) { 84 | res.status(400).json({ error: err.message }); 85 | } 86 | }); 87 | 88 | // router.post('/:id/accept_turn', checkPlayer, async (req, res) => { 89 | // Accepts turn, as we are no longer using websockets to watch disconnects this will make sure that the player is still there 90 | // Alternatively a client could just send their turn if it is not a human player and it should end the same timeout 91 | // }); 92 | 93 | router.post('/:id/turn', checkActive, async (req, res) => { 94 | // sends an input, todo: need to validate turn 95 | try { 96 | await lobbies.game.takeTurn(req.params.id, req.playerId, req.body); 97 | res.sendStatus(200); 98 | wss.updateClient(req.params.id); 99 | } catch (err) { 100 | res.status(400).json({ error: err.message }); 101 | } 102 | }); 103 | 104 | router.post('/:id/rule', checkHost, async (req, res) => { 105 | // sets a rule 106 | try { 107 | await lobbies.rules.setValue(req.params.id, req.body.rule, req.body.value); 108 | res.sendStatus(200); 109 | wss.updateClient(req.params.id); 110 | } catch (err) { 111 | res.status(400).json({ error: err.message }); 112 | } 113 | }); 114 | 115 | router.post('/:id/kick_player', checkHost, async (req, res) => { 116 | try { 117 | const sid = await lobbies.kick(req.params.id, req.body.playerId); 118 | res.sendStatus(200); 119 | wss.updateClient(req.params.id); 120 | clearInterval(wss.clients[req.params.id][sid].heartbeat); 121 | wss.clients[req.params.id][sid].ws.terminate(); 122 | } catch (err) { 123 | res.status(400).json({ error: err.message }); 124 | } 125 | }); 126 | 127 | router.post('/:id/checkEnd', async (req, res) => { 128 | try { 129 | await lobbies.game.checkTime(req.params.id); 130 | res.sendStatus(200); 131 | wss.updateClient(req.params.id); 132 | } catch (err) { 133 | res.status(400).json({ error: err.message }); 134 | } 135 | }); 136 | 137 | router.get('/results/:id', async (req, res) => { 138 | try { 139 | const resultData = await lobbies.game.results.getResult(req.params.id); 140 | res.json(resultData); 141 | } catch (err) { 142 | res.status(400).json({ error: err.message }); 143 | } 144 | }); 145 | 146 | return { 147 | router, 148 | }; 149 | }; 150 | -------------------------------------------------------------------------------- /game/wordlists/6_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "people", 3 | "during", 4 | "number", 5 | "public", 6 | "system", 7 | "united", 8 | "within", 9 | "little", 10 | "second", 11 | "report", 12 | "having", 13 | "social", 14 | "office", 15 | "called", 16 | "county", 17 | "action", 18 | "course", 19 | "effect", 20 | "person", 21 | "health", 22 | "amount", 23 | "change", 24 | "family", 25 | "always", 26 | "either", 27 | "rather", 28 | "figure", 29 | "matter", 30 | "common", 31 | "better", 32 | "making", 33 | "things", 34 | "result", 35 | "nature", 36 | "policy", 37 | "become", 38 | "around", 39 | "london", 40 | "almost", 41 | "church", 42 | "member", 43 | "energy", 44 | "street", 45 | "market", 46 | "itself", 47 | "income", 48 | "reason", 49 | "higher", 50 | "single", 51 | "rights", 52 | "enough", 53 | "ground", 54 | "growth", 55 | "except", 56 | "series", 57 | "review", 58 | "became", 59 | "design", 60 | "mother", 61 | "return", 62 | "annual", 63 | "record", 64 | "letter", 65 | "theory", 66 | "center", 67 | "direct", 68 | "really", 69 | "source", 70 | "notice", 71 | "french", 72 | "values", 73 | "strong", 74 | "length", 75 | "living", 76 | "supply", 77 | "taking", 78 | "manner", 79 | "volume", 80 | "answer", 81 | "twenty", 82 | "looked", 83 | "across", 84 | "toward", 85 | "placed", 86 | "charge", 87 | "indeed", 88 | "degree", 89 | "motion", 90 | "region", 91 | "modern", 92 | "normal", 93 | "appear", 94 | "latter", 95 | "moment", 96 | "george", 97 | "recent", 98 | "extent", 99 | "longer", 100 | "august", 101 | "needed", 102 | "likely", 103 | "former", 104 | "middle", 105 | "turned", 106 | "agency", 107 | "beyond", 108 | "simple", 109 | "proper", 110 | "unless", 111 | "nearly", 112 | "weight", 113 | "behind", 114 | "seemed", 115 | "stated", 116 | "regard", 117 | "indian", 118 | "france", 119 | "demand", 120 | "credit", 121 | "object", 122 | "wanted", 123 | "entire", 124 | "canada", 125 | "spirit", 126 | "europe", 127 | "senate", 128 | "german", 129 | "survey", 130 | "robert", 131 | "larger", 132 | "simply", 133 | "coming", 134 | "friend", 135 | "actual", 136 | "safety", 137 | "issued", 138 | "island", 139 | "estate", 140 | "formed", 141 | "giving", 142 | "active", 143 | "nation", 144 | "follow", 145 | "appeal", 146 | "master", 147 | "status", 148 | "factor", 149 | "police", 150 | "caused", 151 | "access", 152 | "spring", 153 | "summer", 154 | "remain", 155 | "papers", 156 | "bureau", 157 | "effort", 158 | "raised", 159 | "sample", 160 | "myself", 161 | "easily", 162 | "agreed", 163 | "impact", 164 | "highly", 165 | "worked", 166 | "merely", 167 | "obtain", 168 | "served", 169 | "christ", 170 | "square", 171 | "inside", 172 | "duties", 173 | "africa", 174 | "relief", 175 | "thirty", 176 | "budget", 177 | "cities", 178 | "fourth", 179 | "animal", 180 | "trying", 181 | "closed", 182 | "winter", 183 | "leaves", 184 | "double", 185 | "output", 186 | "mental", 187 | "opened", 188 | "saying", 189 | "create", 190 | "fiscal", 191 | "fields", 192 | "valley", 193 | "female", 194 | "marked", 195 | "boston", 196 | "soviet", 197 | "season", 198 | "moving", 199 | "centre", 200 | "memory", 201 | "bottom", 202 | "please", 203 | "native", 204 | "reduce", 205 | "permit", 206 | "stress", 207 | "filled", 208 | "medium", 209 | "damage", 210 | "search", 211 | "proved", 212 | "silver", 213 | "accept", 214 | "bridge", 215 | "pounds", 216 | "joseph", 217 | "edward", 218 | "mexico", 219 | "editor", 220 | "injury", 221 | "scheme", 222 | "chance", 223 | "sector", 224 | "travel", 225 | "profit", 226 | "cotton", 227 | "battle", 228 | "signal", 229 | "signed", 230 | "excess", 231 | "window", 232 | "liquid", 233 | "detail", 234 | "museum", 235 | "mining", 236 | "spread", 237 | "broken", 238 | "affect", 239 | "expect", 240 | "acting", 241 | "shares", 242 | "sought", 243 | "yellow", 244 | "height", 245 | "tables", 246 | "doctor", 247 | "danger", 248 | "severe", 249 | "minute", 250 | "marine", 251 | "global", 252 | "secure", 253 | "carbon", 254 | "twelve", 255 | "forced", 256 | "treaty", 257 | "garden", 258 | "clause", 259 | "reform", 260 | "oxford", 261 | "writer", 262 | "hardly", 263 | "copper", 264 | "vessel", 265 | "reader", 266 | "waters", 267 | "device", 268 | "prince", 269 | "career", 270 | "mainly", 271 | "sister", 272 | "leader", 273 | "avenue", 274 | "column", 275 | "slowly", 276 | "ensure", 277 | "formal", 278 | "unable", 279 | "export", 280 | "troops", 281 | "stream", 282 | "advice", 283 | "fellow", 284 | "remove", 285 | "copies", 286 | "fairly", 287 | "pretty", 288 | "strike", 289 | "belief", 290 | "narrow", 291 | "martin", 292 | "circle", 293 | "played", 294 | "israel", 295 | "russia", 296 | "tissue", 297 | "denied", 298 | "beauty", 299 | "vision", 300 | "corner", 301 | "seeing" 302 | ] -------------------------------------------------------------------------------- /game/lobby.js: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import playerFuncs from './players.js'; 3 | import ruleFuncs from './rules.js'; 4 | import gameFuncs from './game.js'; 5 | 6 | export default (db) => { 7 | const players = playerFuncs({ db }); 8 | const rules = ruleFuncs({ db }); 9 | const game = gameFuncs({ db, rules, players }); 10 | 11 | async function createId() { 12 | const newId = randomBytes(4).toString('hex'); 13 | const exists = await db.query('SELECT id FROM lobbies WHERE id=$id', { 14 | $id: newId, 15 | }); 16 | if (exists.length > 0) { 17 | return await createId(); 18 | } 19 | return newId; 20 | } 21 | 22 | async function createLobby() { 23 | const lobbyId = await createId(); 24 | await db.query('INSERT INTO lobbies (id, status) VALUES ($id, $status)', { 25 | $id: lobbyId, 26 | $status: 'lobby', 27 | }); 28 | await rules.init(lobbyId); 29 | return lobbyId; 30 | } 31 | 32 | async function deleteLobby(lobbyId) { 33 | await db.query('DELETE FROM lobbies WHERE id=$id', { 34 | $id: lobbyId, 35 | }); 36 | } 37 | 38 | async function joinLobby(lobbyId, playerData) { 39 | const lobbyRules = await rules.getByLobby(lobbyId); 40 | const connectedPlayers = (await players.getByLobby(lobbyId)).length; 41 | if (lobbyRules.multiplayer !== 1 && connectedPlayers !== 0) { 42 | throw (new Error('lobby_singleplayer')); 43 | } 44 | if (connectedPlayers === lobbyRules.maxPlayers) { 45 | throw (new Error('lobby_max_players')); 46 | } 47 | await players.create(lobbyId, playerData); 48 | // check lobby status and make any necessary changes 49 | const status = (await db.query('SELECT status FROM lobbies WHERE id=$id', { 50 | $id: lobbyId, 51 | }))[0].status; 52 | if (status === 'game') { 53 | await game.addPlayers(lobbyId); 54 | } 55 | } 56 | 57 | async function removePlayer(lobbyId, playerId, sid) { 58 | // Skips to next player's turn if is active 59 | await game.nextTurn(playerId); 60 | await players.delete(lobbyId, sid); 61 | if ((await players.getByLobby(lobbyId)).length === 0) { 62 | await deleteLobby(lobbyId); 63 | } 64 | } 65 | 66 | async function getLobbyById(lobbyId, sessionId = null) { 67 | const lobbyData = {}; 68 | const lobbyRecord = (await db.query('SELECT status, last_result FROM lobbies WHERE id=$id', { 69 | $id: lobbyId, 70 | })); 71 | if (lobbyRecord.length === 0) { 72 | throw (new Error('lobby_not_exist')); 73 | } 74 | lobbyData.status = lobbyRecord[0].status; 75 | lobbyData.lastResult = lobbyRecord[0].last_result; 76 | lobbyData.rules = await rules.getByLobby(lobbyId); 77 | lobbyData.players = await players.getByLobby(lobbyId); 78 | lobbyData.playerId = await players.getId(lobbyId, sessionId); 79 | if (lobbyData.playerId != null && lobbyData.status === 'game') { 80 | // get gamestate 81 | lobbyData.gameStatus = (await game.getPlayerData(lobbyData.playerId))[0]; 82 | } 83 | return lobbyData; 84 | } 85 | 86 | async function kickPlayer(lobbyId, playerId) { 87 | const sid = await players.getSid(lobbyId, playerId); 88 | await removePlayer(lobbyId, playerId, sid); 89 | return sid; 90 | } 91 | 92 | async function getLastResult(lobbyId) { 93 | return (await db.query('SELECT last_result FROM lobbies WHERE id=$id', { 94 | $id: lobbyId, 95 | }))[0].last_result; 96 | } 97 | 98 | async function getPublicLobby() { 99 | let available = await db.query('SELECT id FROM lobbies WHERE status IN ($status1, $status2) AND id IN (SELECT rules.lobby_id FROM rules WHERE rule_id=$discovery AND value=1) AND id IN (SELECT rules.lobby_id FROM rules JOIN (SELECT active_players.lobby_id, COUNT(*) as count FROM active_players GROUP BY active_players.lobby_id) as lp ON rules.lobby_id=lp.lobby_id WHERE rule_id=$max_players AND (value IS NULL OR value>lp.count))', { 100 | $discovery: 'discovery', 101 | $status1: 'lobby', 102 | $status2: 'results', 103 | $max_players: 'maxPlayers', 104 | }); 105 | if (available.length > 0) { 106 | return available[Math.floor(Math.random() * available.length)].id; 107 | } 108 | available = await db.query('SELECT id FROM lobbies WHERE id IN (SELECT rules.lobby_id FROM rules WHERE rule_id=$discovery AND value=1) AND id IN (SELECT rules.lobby_id FROM rules JOIN (SELECT active_players.lobby_id, COUNT(*) as count FROM active_players GROUP BY active_players.lobby_id) as lp ON rules.lobby_id=lp.lobby_id WHERE rule_id=$max_players AND (value IS NULL OR value>lp.count))', { 109 | $discovery: 'discovery', 110 | $max_players: 'maxPlayers', 111 | }); 112 | if (available.length > 0) { 113 | return available[Math.floor(Math.random() * available.length)].id; 114 | } 115 | throw (new Error('no_available_lobbies')); 116 | } 117 | 118 | return { 119 | game, 120 | rules, 121 | players, 122 | create: createLobby, 123 | delete: deleteLobby, 124 | join: joinLobby, 125 | leave: removePlayer, 126 | getData: getLobbyById, 127 | kick: kickPlayer, 128 | lastResult: getLastResult, 129 | getPublic: getPublicLobby, 130 | }; 131 | }; 132 | -------------------------------------------------------------------------------- /game/wordlists/7_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "between", 3 | "through", 4 | "general", 5 | "because", 6 | "section", 7 | "against", 8 | "company", 9 | "service", 10 | "another", 11 | "present", 12 | "country", 13 | "control", 14 | "whether", 15 | "certain", 16 | "several", 17 | "subject", 18 | "program", 19 | "process", 20 | "example", 21 | "history", 22 | "special", 23 | "himself", 24 | "society", 25 | "support", 26 | "chapter", 27 | "various", 28 | "federal", 29 | "percent", 30 | "provide", 31 | "account", 32 | "problem", 33 | "nothing", 34 | "purpose", 35 | "already", 36 | "english", 37 | "similar", 38 | "current", 39 | "surface", 40 | "council", 41 | "average", 42 | "natural", 43 | "working", 44 | "central", 45 | "college", 46 | "brought", 47 | "greater", 48 | "private", 49 | "perhaps", 50 | "british", 51 | "science", 52 | "capital", 53 | "include", 54 | "meeting", 55 | "believe", 56 | "respect", 57 | "effects", 58 | "medical", 59 | "century", 60 | "project", 61 | "million", 62 | "usually", 63 | "america", 64 | "william", 65 | "england", 66 | "quality", 67 | "related", 68 | "written", 69 | "species", 70 | "january", 71 | "opinion", 72 | "limited", 73 | "article", 74 | "carried", 75 | "journal", 76 | "hundred", 77 | "applied", 78 | "october", 79 | "outside", 80 | "western", 81 | "product", 82 | "reading", 83 | "parties", 84 | "instead", 85 | "finally", 86 | "justice", 87 | "officer", 88 | "address", 89 | "payment", 90 | "writing", 91 | "towards", 92 | "patient", 93 | "morning", 94 | "looking", 95 | "produce", 96 | "station", 97 | "primary", 98 | "allowed", 99 | "charles", 100 | "clearly", 101 | "chicago", 102 | "earlier", 103 | "require", 104 | "culture", 105 | "portion", 106 | "numbers", 107 | "content", 108 | "receive", 109 | "contact", 110 | "minutes", 111 | "reduced", 112 | "benefit", 113 | "reached", 114 | "student", 115 | "measure", 116 | "meaning", 117 | "balance", 118 | "defined", 119 | "forward", 120 | "success", 121 | "affairs", 122 | "covered", 123 | "maximum", 124 | "adopted", 125 | "letters", 126 | "request", 127 | "neither", 128 | "senator", 129 | "complex", 130 | "started", 131 | "germany", 132 | "economy", 133 | "thereof", 134 | "entered", 135 | "hearing", 136 | "decided", 137 | "offered", 138 | "serious", 139 | "created", 140 | "attempt", 141 | "defense", 142 | "regular", 143 | "prevent", 144 | "getting", 145 | "minimum", 146 | "develop", 147 | "variety", 148 | "growing", 149 | "conduct", 150 | "initial", 151 | "treated", 152 | "amended", 153 | "leading", 154 | "ordered", 155 | "feeling", 156 | "picture", 157 | "teacher", 158 | "brother", 159 | "eastern", 160 | "located", 161 | "revenue", 162 | "removed", 163 | "husband", 164 | "changed", 165 | "granted", 166 | "railway", 167 | "freedom", 168 | "correct", 169 | "herself", 170 | "manager", 171 | "chinese", 172 | "summary", 173 | "supreme", 174 | "running", 175 | "village", 176 | "richard", 177 | "popular", 178 | "married", 179 | "details", 180 | "printed", 181 | "nuclear", 182 | "smaller", 183 | "learned", 184 | "absence", 185 | "evening", 186 | "whereas", 187 | "pattern", 188 | "circuit", 189 | "statute", 190 | "highest", 191 | "session", 192 | "network", 193 | "britain", 194 | "someone", 195 | "opening", 196 | "command", 197 | "ancient", 198 | "concept", 199 | "quickly", 200 | "engaged", 201 | "element", 202 | "kingdom", 203 | "finding", 204 | "setting", 205 | "traffic", 206 | "concern", 207 | "context", 208 | "leaving", 209 | "edition", 210 | "testing", 211 | "pacific", 212 | "african", 213 | "express", 214 | "storage", 215 | "derived", 216 | "counsel", 217 | "contain", 218 | "largely", 219 | "reserve", 220 | "captain", 221 | "divided", 222 | "suggest", 223 | "advance", 224 | "perfect", 225 | "finance", 226 | "leaders", 227 | "johnson", 228 | "charged", 229 | "explain", 230 | "quarter", 231 | "exactly", 232 | "largest", 233 | "arrived", 234 | "pointed", 235 | "vehicle", 236 | "improve", 237 | "reality", 238 | "channel", 239 | "elected", 240 | "closely", 241 | "expense", 242 | "mission", 243 | "russian", 244 | "bearing", 245 | "release", 246 | "greatly", 247 | "equally", 248 | "message", 249 | "weather", 250 | "suppose", 251 | "trouble", 252 | "witness", 253 | "overall", 254 | "rapidly", 255 | "consent", 256 | "monthly", 257 | "typical", 258 | "density", 259 | "chamber", 260 | "protect", 261 | "ireland", 262 | "capable", 263 | "besides", 264 | "protein", 265 | "drawing", 266 | "feature", 267 | "keeping", 268 | "spanish", 269 | "speaker", 270 | "perform", 271 | "highway", 272 | "obvious", 273 | "stories", 274 | "grounds", 275 | "talking", 276 | "uniform", 277 | "michael", 278 | "stopped", 279 | "removal", 280 | "operate", 281 | "revised", 282 | "version", 283 | "thereby", 284 | "waiting", 285 | "claimed", 286 | "dealing", 287 | "settled", 288 | "therapy", 289 | "sitting", 290 | "carrier", 291 | "mention", 292 | "exposed", 293 | "willing", 294 | "replied", 295 | "savings", 296 | "discuss", 297 | "profits", 298 | "strange", 299 | "damages", 300 | "devices", 301 | "liberty" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/8_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "national", 3 | "american", 4 | "business", 5 | "research", 6 | "children", 7 | "interest", 8 | "question", 9 | "possible", 10 | "services", 11 | "required", 12 | "property", 13 | "evidence", 14 | "provided", 15 | "economic", 16 | "increase", 17 | "position", 18 | "district", 19 | "received", 20 | "together", 21 | "material", 22 | "building", 23 | "industry", 24 | "language", 25 | "practice", 26 | "specific", 27 | "contract", 28 | "addition", 29 | "probably", 30 | "personal", 31 | "training", 32 | "standard", 33 | "complete", 34 | "division", 35 | "decision", 36 | "pressure", 37 | "congress", 38 | "reported", 39 | "military", 40 | "proposed", 41 | "security", 42 | "activity", 43 | "december", 44 | "included", 45 | "physical", 46 | "produced", 47 | "director", 48 | "anything", 49 | "solution", 50 | "chairman", 51 | "judgment", 52 | "involved", 53 | "movement", 54 | "consider", 55 | "followed", 56 | "expected", 57 | "response", 58 | "observed", 59 | "november", 60 | "referred", 61 | "february", 62 | "planning", 63 | "previous", 64 | "european", 65 | "capacity", 66 | "exchange", 67 | "prepared", 68 | "minister", 69 | "directly", 70 | "presence", 71 | "distance", 72 | "existing", 73 | "relation", 74 | "learning", 75 | "employed", 76 | "actually", 77 | "continue", 78 | "progress", 79 | "strength", 80 | "elements", 81 | "compared", 82 | "relative", 83 | "electric", 84 | "southern", 85 | "separate", 86 | "chemical", 87 | "transfer", 88 | "computer", 89 | "official", 90 | "internal", 91 | "positive", 92 | "instance", 93 | "designed", 94 | "behavior", 95 | "returned", 96 | "whatever", 97 | "majority", 98 | "approved", 99 | "northern", 100 | "constant", 101 | "recently", 102 | "assembly", 103 | "selected", 104 | "domestic", 105 | "critical", 106 | "entirely", 107 | "cultural", 108 | "election", 109 | "commerce", 110 | "intended", 111 | "ordinary", 112 | "exercise", 113 | "standing", 114 | "articles", 115 | "purchase", 116 | "regional", 117 | "accepted", 118 | "governor", 119 | "remember", 120 | "railroad", 121 | "thinking", 122 | "somewhat", 123 | "religion", 124 | "employee", 125 | "reaction", 126 | "policies", 127 | "location", 128 | "maintain", 129 | "remained", 130 | "negative", 131 | "greatest", 132 | "advanced", 133 | "affected", 134 | "indicate", 135 | "directed", 136 | "attorney", 137 | "marriage", 138 | "argument", 139 | "extended", 140 | "daughter", 141 | "slightly", 142 | "measured", 143 | "medicine", 144 | "believed", 145 | "properly", 146 | "illinois", 147 | "external", 148 | "japanese", 149 | "clinical", 150 | "politics", 151 | "recorded", 152 | "happened", 153 | "estimate", 154 | "speaking", 155 | "republic", 156 | "conflict", 157 | "carrying", 158 | "improved", 159 | "equation", 160 | "employer", 161 | "detailed", 162 | "suitable", 163 | "opposite", 164 | "moreover", 165 | "adequate", 166 | "admitted", 167 | "multiple", 168 | "superior", 169 | "contrast", 170 | "mountain", 171 | "parallel", 172 | "examined", 173 | "everyone", 174 | "valuable", 175 | "canadian", 176 | "attached", 177 | "regarded", 178 | "straight", 179 | "delivery", 180 | "concrete", 181 | "possibly", 182 | "apparent", 183 | "occasion", 184 | "relevant", 185 | "michigan", 186 | "contrary", 187 | "declared", 188 | "approval", 189 | "suddenly", 190 | "supposed", 191 | "findings", 192 | "pleasure", 193 | "pursuant", 194 | "schedule", 195 | "recovery", 196 | "aircraft", 197 | "consumer", 198 | "distinct", 199 | "ministry", 200 | "sentence", 201 | "variable", 202 | "interior", 203 | "acquired", 204 | "yourself", 205 | "diameter", 206 | "strategy", 207 | "becoming", 208 | "contents", 209 | "treasury", 210 | "stations", 211 | "proposal", 212 | "strongly", 213 | "absolute", 214 | "creation", 215 | "academic", 216 | "printing", 217 | "describe", 218 | "accident", 219 | "decrease", 220 | "familiar", 221 | "literary", 222 | "counties", 223 | "finished", 224 | "composed", 225 | "province", 226 | "campaign", 227 | "carolina", 228 | "exposure", 229 | "identify", 230 | "emphasis", 231 | "judicial", 232 | "starting", 233 | "informed", 234 | "doctrine", 235 | "boundary", 236 | "williams", 237 | "arranged", 238 | "achieved", 239 | "facility", 240 | "occupied", 241 | "resource", 242 | "criteria", 243 | "mortgage", 244 | "magnetic", 245 | "supplied", 246 | "repeated", 247 | "feelings", 248 | "earnings", 249 | "software", 250 | "vertical", 251 | "sequence", 252 | "category", 253 | "commonly", 254 | "struggle", 255 | "rendered", 256 | "resulted", 257 | "velocity", 258 | "abstract", 259 | "magazine", 260 | "whenever", 261 | "register", 262 | "formerly", 263 | "tendency", 264 | "handling", 265 | "accurate", 266 | "electron", 267 | "resolved", 268 | "modified", 269 | "realized", 270 | "operator", 271 | "covering", 272 | "atlantic", 273 | "scotland", 274 | "normally", 275 | "creating", 276 | "attended", 277 | "bulletin", 278 | "revealed", 279 | "frequent", 280 | "striking", 281 | "brothers", 282 | "definite", 283 | "suffered", 284 | "released", 285 | "coverage", 286 | "entrance", 287 | "accuracy", 288 | "probable", 289 | "customer", 290 | "taxation", 291 | "colonial", 292 | "premises", 293 | "missouri", 294 | "reducing", 295 | "graduate", 296 | "terminal", 297 | "reserved", 298 | "hydrogen", 299 | "adoption", 300 | "duration", 301 | "isolated" 302 | ] -------------------------------------------------------------------------------- /client/js/lobby.js: -------------------------------------------------------------------------------- 1 | import render from './templates.js'; 2 | import request from './request.js'; 3 | // temp 4 | function displayLobby() { 5 | if (window.currentPage !== 'lobby') { 6 | document.getElementById('page_content').innerHTML = render('lobby'); 7 | window.currentPage = 'lobby'; 8 | if (window.isHost) { 9 | document.getElementById('start_btn').addEventListener('click', () => { 10 | request.POST(`/api/${window.gameId}/start_game`); 11 | }); 12 | } 13 | const shareableURL = `${window.location.origin}/game?id=${window.gameId}`; 14 | const shareLink = document.getElementById('share_link'); 15 | shareLink.innerHTML = shareableURL; 16 | shareLink.addEventListener('click', () => { 17 | navigator.clipboard.writeText(shareableURL); 18 | }); 19 | } 20 | constructRules(); 21 | if (window.isHost) { 22 | const ruleForm = document.getElementById('game_rules'); 23 | 24 | ruleForm.addEventListener('submit', (e) => { 25 | e.preventDefault(); 26 | }); 27 | } 28 | } 29 | 30 | function constructRules() { 31 | const ruleForm = document.getElementById('game_rules'); 32 | ruleForm.innerHTML = ''; 33 | for (const [id, rule] of Object.entries(window.gameData.rules)) { 34 | let input = document.createElement('input'); 35 | input.name = id; 36 | const label = document.createElement('label'); 37 | label.innerText = rule.name; 38 | let sendDataTimeout = null; 39 | 40 | if (window.isHost) { 41 | if (rule.requires !== undefined) { 42 | for (const [reqName, reqValue] of Object.entries(rule.requires)) { 43 | if (reqValue !== ((typeof reqValue === 'boolean') ? Boolean(window.gameData.rules[reqName].value) : window.gameData.rules[reqName].value)) { 44 | input.disabled = true; 45 | break; 46 | } 47 | } 48 | } 49 | } else { 50 | input.disabled = true; 51 | } 52 | 53 | switch (rule.type) { 54 | case 'number': 55 | input.type = 'number'; 56 | input.value = rule.value; 57 | 58 | if (rule.minVal != null) input.min = rule.minVal; 59 | if (rule.maxVal != null) input.max = rule.maxVal; 60 | 61 | if (rule.allowNull === true) { 62 | const oldInput = input; 63 | const nullBox = input.cloneNode(); 64 | 65 | input = document.createElement('div'); 66 | input.classList.add('combined-input'); 67 | 68 | nullBox.type = 'checkbox'; 69 | nullBox.checked = !(['null', null].includes(rule.value)); 70 | 71 | if (window.isHost) { 72 | if (!nullBox.checked) oldInput.disabled = true; 73 | nullBox.addEventListener('input', () => { 74 | if (!nullBox.checked) { 75 | if (sendDataTimeout != null) { 76 | clearTimeout(sendDataTimeout); 77 | } 78 | // this.ws.emit("setRule", {rule: name, value: null}); 79 | request.POST(`/api/${window.gameId}/rule`, { 80 | rule: id, 81 | value: null, 82 | }); 83 | } else { 84 | // this.ws.emit("setRule", {rule: name, value: oldInput.value}); 85 | request.POST(`/api/${window.gameId}/rule`, { 86 | rule: id, 87 | value: oldInput.value, 88 | }); 89 | } 90 | }); 91 | 92 | oldInput.addEventListener('input', () => { 93 | if (sendDataTimeout != null) { 94 | clearTimeout(sendDataTimeout); 95 | } 96 | sendDataTimeout = setTimeout(() => { 97 | // this.ws.emit("setRule", {rule: name, value: oldInput.value}); 98 | request.POST(`/api/${window.gameId}/rule`, { 99 | rule: id, 100 | value: oldInput.value, 101 | }); 102 | }, 500); 103 | }); 104 | } 105 | 106 | oldInput.value = (rule.value == null) ? rule.minVal : rule.value; 107 | 108 | input.appendChild(nullBox); 109 | input.appendChild(oldInput); 110 | } else { 111 | if (rule.requires !== undefined) { 112 | for (const [reqName, reqValue] of Object.entries(rule.requires)) { 113 | if (reqValue !== (typeof reqValue === 'boolean') ? Boolean(window.gameData.rules[reqName].value) : window.gameData.rules[reqName].value) { 114 | input.disabled = true; 115 | break; 116 | } 117 | } 118 | } 119 | input.addEventListener('input', () => { 120 | if (sendDataTimeout != null) { 121 | clearTimeout(sendDataTimeout); 122 | } 123 | sendDataTimeout = setTimeout(() => { 124 | // this.ws.emit("setRule", {rule: name, value: oldInput.value}); 125 | request.POST(`/api/${window.gameId}/rule`, { 126 | rule: id, 127 | value: input.value, 128 | }); 129 | }, 500); 130 | }); 131 | } 132 | break; 133 | case 'boolean': 134 | input.type = 'checkbox'; 135 | input.value = true; 136 | input.checked = rule.value; 137 | if (window.isHost) { 138 | input.addEventListener('input', () => { 139 | // this.ws.emit("setRule", {rule: name, value: (input.checked)}); 140 | request.POST(`/api/${window.gameId}/rule`, { 141 | rule: id, 142 | value: (input.checked), 143 | }); 144 | }); 145 | } 146 | break; 147 | } 148 | 149 | ruleForm.appendChild(label); 150 | ruleForm.appendChild(input); 151 | } 152 | } 153 | 154 | export default { 155 | display: displayLobby, 156 | }; 157 | -------------------------------------------------------------------------------- /client/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hangman 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 20 |
21 | 22 |
23 | 24 | 25 | 38 | 39 | 52 | 53 | 59 | 60 | 105 | 106 | 138 | 139 | 161 | 162 | 165 | 166 | 200 | 201 | -------------------------------------------------------------------------------- /game/wordlists/9_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "following", 3 | "different", 4 | "committee", 5 | "important", 6 | "education", 7 | "necessary", 8 | "president", 9 | "available", 10 | "including", 11 | "political", 12 | "secretary", 13 | "according", 14 | "community", 15 | "knowledge", 16 | "countries", 17 | "increased", 18 | "continued", 19 | "generally", 20 | "described", 21 | "operation", 22 | "statement", 23 | "structure", 24 | "condition", 25 | "equipment", 26 | "agreement", 27 | "attention", 28 | "published", 29 | "financial", 30 | "sometimes", 31 | "insurance", 32 | "effective", 33 | "defendant", 34 | "character", 35 | "developed", 36 | "situation", 37 | "presented", 38 | "reference", 39 | "september", 40 | "influence", 41 | "difficult", 42 | "companies", 43 | "relations", 44 | "direction", 45 | "technical", 46 | "principal", 47 | "plaintiff", 48 | "otherwise", 49 | "mentioned", 50 | "determine", 51 | "amendment", 52 | "concerned", 53 | "religious", 54 | "certainly", 55 | "operating", 56 | "procedure", 57 | "executive", 58 | "professor", 59 | "principle", 60 | "discussed", 61 | "christian", 62 | "provision", 63 | "formation", 64 | "appointed", 65 | "practical", 66 | "existence", 67 | "essential", 68 | "estimated", 69 | "suggested", 70 | "reduction", 71 | "indicated", 72 | "providing", 73 | "assistant", 74 | "expressed", 75 | "contained", 76 | "paragraph", 77 | "completed", 78 | "personnel", 79 | "connected", 80 | "frequency", 81 | "advantage", 82 | "performed", 83 | "establish", 84 | "regarding", 85 | "secondary", 86 | "transport", 87 | "permanent", 88 | "conducted", 89 | "represent", 90 | "submitted", 91 | "supported", 92 | "selection", 93 | "collected", 94 | "specified", 95 | "immediate", 96 | "extension", 97 | "testimony", 98 | "territory", 99 | "excellent", 100 | "resulting", 101 | "remaining", 102 | "discharge", 103 | "explained", 104 | "australia", 105 | "gentleman", 106 | "dependent", 107 | "cambridge", 108 | "delivered", 109 | "necessity", 110 | "extensive", 111 | "extremely", 112 | "primarily", 113 | "receiving", 114 | "marketing", 115 | "substance", 116 | "expansion", 117 | "corporate", 118 | "exception", 119 | "emergency", 120 | "producing", 121 | "technique", 122 | "component", 123 | "committed", 124 | "objective", 125 | "permitted", 126 | "municipal", 127 | "mechanism", 128 | "preceding", 129 | "radiation", 130 | "temporary", 131 | "spiritual", 132 | "variation", 133 | "tradition", 134 | "efficient", 135 | "universal", 136 | "involving", 137 | "complaint", 138 | "concluded", 139 | "ourselves", 140 | "apparatus", 141 | "elsewhere", 142 | "naturally", 143 | "addressed", 144 | "economics", 145 | "intention", 146 | "requested", 147 | "currently", 148 | "programme", 149 | "gradually", 150 | "challenge", 151 | "obviously", 152 | "purchased", 153 | "treasurer", 154 | "satisfied", 155 | "residence", 156 | "similarly", 157 | "applicant", 158 | "announced", 159 | "reporting", 160 | "qualified", 161 | "societies", 162 | "desirable", 163 | "afternoon", 164 | "elizabeth", 165 | "discovery", 166 | "framework", 167 | "chemistry", 168 | "recognize", 169 | "separated", 170 | "evolution", 171 | "appellant", 172 | "perfectly", 173 | "violation", 174 | "infection", 175 | "intensity", 176 | "alexander", 177 | "preferred", 178 | "criticism", 179 | "newspaper", 180 | "copyright", 181 | "requiring", 182 | "physician", 183 | "diagnosis", 184 | "interview", 185 | "suffering", 186 | "extending", 187 | "objection", 188 | "thickness", 189 | "wisconsin", 190 | "depending", 191 | "sensitive", 192 | "democracy", 193 | "protected", 194 | "behaviour", 195 | "attempted", 196 | "admission", 197 | "pollution", 198 | "exclusive", 199 | "confirmed", 200 | "identical", 201 | "petroleum", 202 | "gentlemen", 203 | "generated", 204 | "automatic", 205 | "statutory", 206 | "associate", 207 | "reflected", 208 | "sustained", 209 | "emotional", 210 | "encourage", 211 | "communist", 212 | "seriously", 213 | "prominent", 214 | "commander", 215 | "molecular", 216 | "centuries", 217 | "minnesota", 218 | "recording", 219 | "succeeded", 220 | "strategic", 221 | "varieties", 222 | "narrative", 223 | "quarterly", 224 | "qualities", 225 | "voluntary", 226 | "typically", 227 | "promotion", 228 | "precisely", 229 | "measuring", 230 | "financing", 231 | "obtaining", 232 | "affecting", 233 | "candidate", 234 | "excessive", 235 | "allowance", 236 | "governing", 237 | "expensive", 238 | "phenomena", 239 | "privilege", 240 | "baltimore", 241 | "preserved", 242 | "abandoned", 243 | "inventory", 244 | "preparing", 245 | "aggregate", 246 | "improving", 247 | "happiness", 248 | "mortality", 249 | "decreased", 250 | "magnitude", 251 | "regularly", 252 | "suspended", 253 | "remainder", 254 | "synthesis", 255 | "favorable", 256 | "returning", 257 | "guarantee", 258 | "conscious", 259 | "installed", 260 | "christmas", 261 | "convinced", 262 | "recovered", 263 | "competent", 264 | "detection", 265 | "recommend", 266 | "possessed", 267 | "occasions", 268 | "exceeding", 269 | "justified", 270 | "tennessee", 271 | "cleveland", 272 | "louisiana", 273 | "surprised", 274 | "virtually", 275 | "certified", 276 | "perceived", 277 | "introduce", 278 | "awareness", 279 | "partially", 280 | "initially", 281 | "inspector", 282 | "evidently", 283 | "converted", 284 | "landscape", 285 | "commodity", 286 | "inflation", 287 | "forgotten", 288 | "ordinance", 289 | "operative", 290 | "discourse", 291 | "migration", 292 | "sacrifice", 293 | "everybody", 294 | "proceeded", 295 | "construct", 296 | "frederick", 297 | "northwest", 298 | "editorial", 299 | "dismissed", 300 | "yesterday", 301 | "commenced" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/10_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "government", 3 | "university", 4 | "department", 5 | "conditions", 6 | "production", 7 | "commission", 8 | "management", 9 | "individual", 10 | "particular", 11 | "considered", 12 | "experience", 13 | "themselves", 14 | "washington", 15 | "especially", 16 | "population", 17 | "additional", 18 | "industrial", 19 | "provisions", 20 | "employment", 21 | "determined", 22 | "operations", 23 | "difference", 24 | "understand", 25 | "technology", 26 | "protection", 27 | "importance", 28 | "discussion", 29 | "conference", 30 | "commercial", 31 | "sufficient", 32 | "connection", 33 | "literature", 34 | "investment", 35 | "facilities", 36 | "everything", 37 | "collection", 38 | "properties", 39 | "resolution", 40 | "historical", 41 | "increasing", 42 | "scientific", 43 | "concerning", 44 | "expression", 45 | "containing", 46 | "introduced", 47 | "reasonable", 48 | "frequently", 49 | "foundation", 50 | "relatively", 51 | "completely", 52 | "previously", 53 | "industries", 54 | "regulation", 55 | "conclusion", 56 | "interested", 57 | "comparison", 58 | "impossible", 59 | "convention", 60 | "resistance", 61 | "evaluation", 62 | "statistics", 63 | "appearance", 64 | "developing", 65 | "percentage", 66 | "subsequent", 67 | "accordance", 68 | "possession", 69 | "definition", 70 | "proportion", 71 | "processing", 72 | "recognized", 73 | "understood", 74 | "apparently", 75 | "difficulty", 76 | "experiment", 77 | "identified", 78 | "settlement", 79 | "philosophy", 80 | "parliament", 81 | "applicable", 82 | "efficiency", 83 | "controlled", 84 | "electrical", 85 | "maintained", 86 | "generation", 87 | "mechanical", 88 | "instrument", 89 | "equivalent", 90 | "revolution", 91 | "continuous", 92 | "discovered", 93 | "calculated", 94 | "background", 95 | "securities", 96 | "confidence", 97 | "membership", 98 | "inspection", 99 | "leadership", 100 | "electronic", 101 | "opposition", 102 | "consistent", 103 | "structural", 104 | "democratic", 105 | "respondent", 106 | "accounting", 107 | "television", 108 | "afterwards", 109 | "registered", 110 | "eventually", 111 | "proceeding", 112 | "continuing", 113 | "transition", 114 | "permission", 115 | "prescribed", 116 | "enterprise", 117 | "designated", 118 | "remarkable", 119 | "adjustment", 120 | "atmosphere", 121 | "separation", 122 | "categories", 123 | "retirement", 124 | "publishing", 125 | "absolutely", 126 | "subsection", 127 | "obligation", 128 | "respective", 129 | "compliance", 130 | "conversion", 131 | "collective", 132 | "supporting", 133 | "consisting", 134 | "acceptance", 135 | "prevention", 136 | "profession", 137 | "elementary", 138 | "monitoring", 139 | "constantly", 140 | "impression", 141 | "occupation", 142 | "completion", 143 | "contribute", 144 | "conviction", 145 | "depression", 146 | "absorption", 147 | "integrated", 148 | "australian", 149 | "remembered", 150 | "restricted", 151 | "ultimately", 152 | "altogether", 153 | "lieutenant", 154 | "commitment", 155 | "punishment", 156 | "discipline", 157 | "underlying", 158 | "regulatory", 159 | "boundaries", 160 | "thereafter", 161 | "contractor", 162 | "discretion", 163 | "limitation", 164 | "artificial", 165 | "suggestion", 166 | "reasonably", 167 | "guidelines", 168 | "occurrence", 169 | "attendance", 170 | "acceptable", 171 | "preference", 172 | "interstate", 173 | "translated", 174 | "irrigation", 175 | "republican", 176 | "influenced", 177 | "everywhere", 178 | "nineteenth", 179 | "encouraged", 180 | "reputation", 181 | "regardless", 182 | "federation", 183 | "attractive", 184 | "convenient", 185 | "reflection", 186 | "phenomenon", 187 | "indicating", 188 | "geological", 189 | "undertaken", 190 | "dependence", 191 | "curriculum", 192 | "discharged", 193 | "perception", 194 | "conception", 195 | "provincial", 196 | "separately", 197 | "productive", 198 | "attributed", 199 | "personally", 200 | "navigation", 201 | "recreation", 202 | "attachment", 203 | "suspension", 204 | "systematic", 205 | "supplement", 206 | "negligence", 207 | "performing", 208 | "surrounded", 209 | "comprising", 210 | "friendship", 211 | "deficiency", 212 | "businesses", 213 | "threatened", 214 | "appreciate", 215 | "indication", 216 | "vocational", 217 | "bargaining", 218 | "inadequate", 219 | "initiative", 220 | "correction", 221 | "simulation", 222 | "succession", 223 | "protective", 224 | "definitely", 225 | "allocation", 226 | "occasional", 227 | "facilitate", 228 | "expedition", 229 | "bankruptcy", 230 | "pittsburgh", 231 | "illustrate", 232 | "legitimate", 233 | "beneficial", 234 | "complexity", 235 | "widespread", 236 | "conducting", 237 | "purchasing", 238 | "memorandum", 239 | "manchester", 240 | "guaranteed", 241 | "pronounced", 242 | "conscience", 243 | "surprising", 244 | "withdrawal", 245 | "innovation", 246 | "successive", 247 | "connecting", 248 | "disclosure", 249 | "attempting", 250 | "chancellor", 251 | "accurately", 252 | "litigation", 253 | "presumably", 254 | "restaurant", 255 | "accomplish", 256 | "collecting", 257 | "engagement", 258 | "vegetation", 259 | "eighteenth", 260 | "estimation", 261 | "eliminated", 262 | "combustion", 263 | "inevitable", 264 | "compromise", 265 | "enthusiasm", 266 | "respecting", 267 | "discussing", 268 | "instructed", 269 | "missionary", 270 | "presenting", 271 | "cultivated", 272 | "suggesting", 273 | "cincinnati", 274 | "diagnostic", 275 | "preventing", 276 | "submission", 277 | "emphasized", 278 | "protestant", 279 | "tremendous", 280 | "prevailing", 281 | "providence", 282 | "accessible", 283 | "dictionary", 284 | "geographic", 285 | "extraction", 286 | "consulting", 287 | "excitement", 288 | "subsidiary", 289 | "wilderness", 290 | "profitable", 291 | "contention", 292 | "physically", 293 | "activation", 294 | "distinctly", 295 | "repeatedly", 296 | "adequately", 297 | "delegation", 298 | "generating", 299 | "indigenous", 300 | "protecting", 301 | "transverse" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/11_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "information", 3 | "development", 4 | "association", 5 | "application", 6 | "established", 7 | "corporation", 8 | "temperature", 9 | "performance", 10 | "significant", 11 | "appropriate", 12 | "immediately", 13 | "opportunity", 14 | "independent", 15 | "environment", 16 | "examination", 17 | "description", 18 | "proceedings", 19 | "educational", 20 | "agriculture", 21 | "maintenance", 22 | "interesting", 23 | "responsible", 24 | "improvement", 25 | "legislation", 26 | "represented", 27 | "publication", 28 | "alternative", 29 | "traditional", 30 | "composition", 31 | "instruction", 32 | "preparation", 33 | "substantial", 34 | "consumption", 35 | "recommended", 36 | "communities", 37 | "legislative", 38 | "certificate", 39 | "necessarily", 40 | "explanation", 41 | "arrangement", 42 | "cooperation", 43 | "determining", 44 | "recognition", 45 | "expenditure", 46 | "observation", 47 | "consequence", 48 | "legislature", 49 | "measurement", 50 | "experienced", 51 | "appointment", 52 | "distributed", 53 | "advertising", 54 | "statistical", 55 | "requirement", 56 | "constructed", 57 | "illustrated", 58 | "accordingly", 59 | "considering", 60 | "interaction", 61 | "preliminary", 62 | "transferred", 63 | "accompanied", 64 | "practically", 65 | "manufacture", 66 | "enforcement", 67 | "perspective", 68 | "distinction", 69 | "theoretical", 70 | "translation", 71 | "furthermore", 72 | "acquisition", 73 | "essentially", 74 | "outstanding", 75 | "destruction", 76 | "circulation", 77 | "declaration", 78 | "partnership", 79 | "effectively", 80 | "comparative", 81 | "personality", 82 | "integration", 83 | "electricity", 84 | "surrounding", 85 | "transaction", 86 | "cooperative", 87 | "contributed", 88 | "maintaining", 89 | "mississippi", 90 | "mathematics", 91 | "correlation", 92 | "disposition", 93 | "equilibrium", 94 | "proposition", 95 | "participate", 96 | "involvement", 97 | "achievement", 98 | "controlling", 99 | "complicated", 100 | "progressive", 101 | "calculation", 102 | "undoubtedly", 103 | "coefficient", 104 | "exclusively", 105 | "commodities", 106 | "continental", 107 | "netherlands", 108 | "connecticut", 109 | "orientation", 110 | "replacement", 111 | "operational", 112 | "dimensional", 113 | "termination", 114 | "unnecessary", 115 | "transmitted", 116 | "utilization", 117 | "sensitivity", 118 | "immigration", 119 | "interpreted", 120 | "controversy", 121 | "residential", 122 | "cultivation", 123 | "territories", 124 | "comfortable", 125 | "exploration", 126 | "restoration", 127 | "encountered", 128 | "programming", 129 | "distinguish", 130 | "anticipated", 131 | "intelligent", 132 | "uncertainty", 133 | "subdivision", 134 | "investigate", 135 | "disappeared", 136 | "merchandise", 137 | "implemented", 138 | "shakespeare", 139 | "specialized", 140 | "contracting", 141 | "switzerland", 142 | "underground", 143 | "undertaking", 144 | "convenience", 145 | "atmospheric", 146 | "conjunction", 147 | "approximate", 148 | "communicate", 149 | "identifying", 150 | "affirmative", 151 | "respiratory", 152 | "acknowledge", 153 | "continually", 154 | "territorial", 155 | "frequencies", 156 | "principally", 157 | "restriction", 158 | "elimination", 159 | "transformed", 160 | "philippines", 161 | "theological", 162 | "destination", 163 | "unfortunate", 164 | "reservation", 165 | "compression", 166 | "electronics", 167 | "procurement", 168 | "exceptional", 169 | "stimulation", 170 | "distinctive", 171 | "citizenship", 172 | "concentrate", 173 | "introducing", 174 | "designation", 175 | "formulation", 176 | "descriptive", 177 | "minneapolis", 178 | "inheritance", 179 | "expectation", 180 | "computation", 181 | "disturbance", 182 | "differently", 183 | "inspiration", 184 | "sovereignty", 185 | "practicable", 186 | "magnificent", 187 | "advancement", 188 | "extensively", 189 | "interrupted", 190 | "temporarily", 191 | "permanently", 192 | "propagation", 193 | "springfield", 194 | "christopher", 195 | "psychiatric", 196 | "degradation", 197 | "unconscious", 198 | "consistency", 199 | "dissolution", 200 | "destructive", 201 | "exceedingly", 202 | "subordinate", 203 | "appreciated", 204 | "spontaneous", 205 | "qualitative", 206 | "sympathetic", 207 | "accommodate", 208 | "seventeenth", 209 | "ascertained", 210 | "unpublished", 211 | "incorporate", 212 | "complainant", 213 | "handicapped", 214 | "realization", 215 | "westminster", 216 | "anniversary", 217 | "threatening", 218 | "corrections", 219 | "susceptible", 220 | "fortunately", 221 | "presumption", 222 | "nonetheless", 223 | "sustainable", 224 | "beneficiary", 225 | "ventilation", 226 | "influential", 227 | "radioactive", 228 | "negotiation", 229 | "renaissance", 230 | "provisional", 231 | "generalized", 232 | "participant", 233 | "ideological", 234 | "transported", 235 | "suppression", 236 | "philosopher", 237 | "willingness", 238 | "transparent", 239 | "contraction", 240 | "deformation", 241 | "comptroller", 242 | "politically", 243 | "conditional", 244 | "superficial", 245 | "conflicting", 246 | "consecutive", 247 | "recognizing", 248 | "obstruction", 249 | "superiority", 250 | "hereinafter", 251 | "accelerated", 252 | "speculation", 253 | "resignation", 254 | "cylindrical", 255 | "grandmother", 256 | "irradiation", 257 | "efficiently", 258 | "crystalline", 259 | "discoveries", 260 | "substantive", 261 | "reclamation", 262 | "descendants", 263 | "nationalism", 264 | "reformation", 265 | "recruitment", 266 | "voluntarily", 267 | "evaporation", 268 | "entertained", 269 | "implication", 270 | "inscription", 271 | "conditioned", 272 | "nationality", 273 | "continuance", 274 | "subsistence", 275 | "forthcoming", 276 | "coordinated", 277 | "resemblance", 278 | "eliminating", 279 | "questioning", 280 | "respectable", 281 | "neighboring", 282 | "universally", 283 | "demographic", 284 | "enhancement", 285 | "convergence", 286 | "contractual", 287 | "indifferent", 288 | "precipitate", 289 | "preparatory", 290 | "inclination", 291 | "terminology", 292 | "secretaries", 293 | "interactive", 294 | "countenance", 295 | "importation", 296 | "confederate", 297 | "importantly", 298 | "abandonment", 299 | "restrictive", 300 | "enlargement", 301 | "permissible" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/12_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "construction", 3 | "distribution", 4 | "particularly", 5 | "relationship", 6 | "agricultural", 7 | "considerable", 8 | "professional", 9 | "introduction", 10 | "compensation", 11 | "jurisdiction", 12 | "experimental", 13 | "respectively", 14 | "commissioner", 15 | "pennsylvania", 16 | "philadelphia", 17 | "nevertheless", 18 | "contribution", 19 | "difficulties", 20 | "instructions", 21 | "specifically", 22 | "intelligence", 23 | "satisfactory", 24 | "conservation", 25 | "significance", 26 | "consequently", 27 | "independence", 28 | "contemporary", 29 | "transmission", 30 | "conversation", 31 | "incorporated", 32 | "registration", 33 | "intellectual", 34 | "transactions", 35 | "satisfaction", 36 | "sufficiently", 37 | "representing", 38 | "subsequently", 39 | "occasionally", 40 | "unemployment", 41 | "considerably", 42 | "accomplished", 43 | "presentation", 44 | "conventional", 45 | "commonwealth", 46 | "increasingly", 47 | "intermediate", 48 | "architecture", 49 | "establishing", 50 | "universities", 51 | "intervention", 52 | "headquarters", 53 | "productivity", 54 | "differential", 55 | "metropolitan", 56 | "manufacturer", 57 | "mathematical", 58 | "interference", 59 | "administered", 60 | "modification", 61 | "concentrated", 62 | "bibliography", 63 | "manufactured", 64 | "investigated", 65 | "preservation", 66 | "christianity", 67 | "governmental", 68 | "subcommittee", 69 | "occupational", 70 | "consolidated", 71 | "illustration", 72 | "civilization", 73 | "appreciation", 74 | "coordination", 75 | "acknowledged", 76 | "appropriated", 77 | "insufficient", 78 | "subscription", 79 | "geographical", 80 | "accompanying", 81 | "consultation", 82 | "depreciation", 83 | "reproduction", 84 | "presidential", 85 | "longitudinal", 86 | "displacement", 87 | "tuberculosis", 88 | "imprisonment", 89 | "continuously", 90 | "commencement", 91 | "continuation", 92 | "proportional", 93 | "consistently", 94 | "inconsistent", 95 | "broadcasting", 96 | "economically", 97 | "supplemental", 98 | "confirmation", 99 | "contributing", 100 | "announcement", 101 | "constructive", 102 | "unreasonable", 103 | "municipality", 104 | "contemplated", 105 | "notification", 106 | "indebtedness", 107 | "individually", 108 | "participated", 109 | "congregation", 110 | "acquaintance", 111 | "deliberately", 112 | "exploitation", 113 | "conditioning", 114 | "communicated", 115 | "simultaneous", 116 | "constructing", 117 | "acceleration", 118 | "recreational", 119 | "confidential", 120 | "intersection", 121 | "northwestern", 122 | "proclamation", 123 | "conductivity", 124 | "surveillance", 125 | "inflammation", 126 | "introductory", 127 | "commissioned", 128 | "strengthened", 129 | "anthropology", 130 | "missionaries", 131 | "attributable", 132 | "instrumental", 133 | "overwhelming", 134 | "historically", 135 | "additionally", 136 | "indianapolis", 137 | "verification", 138 | "prescription", 139 | "reproductive", 140 | "disappointed", 141 | "disadvantage", 142 | "dissertation", 143 | "evolutionary", 144 | "surroundings", 145 | "surprisingly", 146 | "deficiencies", 147 | "southwestern", 148 | "enthusiastic", 149 | "inflammatory", 150 | "distributing", 151 | "optimization", 152 | "condemnation", 153 | "departmental", 154 | "supplemented", 155 | "standardized", 156 | "presbyterian", 157 | "sociological", 158 | "transmitting", 159 | "dramatically", 160 | "disciplinary", 161 | "resurrection", 162 | "indifference", 163 | "southeastern", 164 | "purification", 165 | "hypertension", 166 | "polarization", 167 | "neighbouring", 168 | "illumination", 169 | "discontinued", 170 | "advantageous", 171 | "interpreting", 172 | "anticipation", 173 | "experiencing", 174 | "regeneration", 175 | "apprehension", 176 | "spectroscopy", 177 | "contaminated", 178 | "disagreement", 179 | "commercially", 180 | "collectively", 181 | "cancellation", 182 | "incidentally", 183 | "characterize", 184 | "irrespective", 185 | "recollection", 186 | "ratification", 187 | "investigator", 188 | "northeastern", 189 | "infringement", 190 | "transitional", 191 | "interruption", 192 | "questionable", 193 | "subsidiaries", 194 | "transferring", 195 | "remuneration", 196 | "supernatural", 197 | "coordinating", 198 | "fluorescence", 199 | "condensation", 200 | "indefinitely", 201 | "conveniently", 202 | "humanitarian", 203 | "precipitated", 204 | "intermittent", 205 | "illustrating", 206 | "incompatible", 207 | "transporting", 208 | "fermentation", 209 | "recommending", 210 | "entertaining", 211 | "transparency", 212 | "distillation", 213 | "emancipation", 214 | "metaphysical", 215 | "hierarchical", 216 | "hydrochloric", 217 | "biochemistry", 218 | "newfoundland", 219 | "encyclopedia", 220 | "unacceptable", 221 | "chemotherapy", 222 | "adjudication", 223 | "astronomical", 224 | "successively", 225 | "shipbuilding", 226 | "transforming", 227 | "infiltration", 228 | "illustrative", 229 | "bureaucratic", 230 | "preferential", 231 | "refrigerator", 232 | "conciliation", 233 | "degeneration", 234 | "kindergarten", 235 | "nomenclature", 236 | "carbohydrate", 237 | "localization", 238 | "saskatchewan", 239 | "illuminating", 240 | "facilitating", 241 | "multilateral", 242 | "colonization", 243 | "astonishment", 244 | "contributory", 245 | "inequalities", 246 | "dissociation", 247 | "mechanically", 248 | "affectionate", 249 | "articulation", 250 | "complication", 251 | "electrically", 252 | "correctional", 253 | "thanksgiving", 254 | "accidentally", 255 | "computerized", 256 | "ascertaining", 257 | "intelligible", 258 | "discriminate", 259 | "solicitation", 260 | "epidemiology", 261 | "authenticity", 262 | "multiplicity", 263 | "denomination", 264 | "completeness", 265 | "scandinavian", 266 | "unexpectedly", 267 | "neurological", 268 | "spirituality", 269 | "dissatisfied", 270 | "alphabetical", 271 | "artificially", 272 | "commentaries", 273 | "insurrection", 274 | "penitentiary", 275 | "jacksonville", 276 | "identifiable", 277 | "compensatory", 278 | "aristocratic", 279 | "intoxicating", 280 | "irresistible", 281 | "civilisation", 282 | "equalization", 283 | "extinguished", 284 | "deliberation", 285 | "dictatorship", 286 | "necessitated", 287 | "disagreeable", 288 | "disseminated", 289 | "pharmacology", 290 | "conglomerate", 291 | "handkerchief", 292 | "intoxication", 293 | "johannesburg", 294 | "conclusively", 295 | "concurrently", 296 | "underwriting", 297 | "emphatically", 298 | "perturbation", 299 | "championship", 300 | "intermediary", 301 | "interpretive" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/13_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "international", 3 | "consideration", 4 | "understanding", 5 | "environmental", 6 | "communication", 7 | "approximately", 8 | "investigation", 9 | "determination", 10 | "concentration", 11 | "manufacturing", 12 | "establishment", 13 | "corresponding", 14 | "participation", 15 | "distinguished", 16 | "significantly", 17 | "comprehensive", 18 | "substantially", 19 | "appropriation", 20 | "administrator", 21 | "consciousness", 22 | "unfortunately", 23 | "extraordinary", 24 | "effectiveness", 25 | "characterized", 26 | "miscellaneous", 27 | "revolutionary", 28 | "congressional", 29 | "technological", 30 | "philosophical", 31 | "comparatively", 32 | "automatically", 33 | "configuration", 34 | "parliamentary", 35 | "certification", 36 | "specification", 37 | "accommodation", 38 | "entertainment", 39 | "justification", 40 | "developmental", 41 | "participating", 42 | "physiological", 43 | "independently", 44 | "precipitation", 45 | "architectural", 46 | "supplementary", 47 | "incorporation", 48 | "encouragement", 49 | "mediterranean", 50 | "approximation", 51 | "consolidation", 52 | "sophisticated", 53 | "decomposition", 54 | "contamination", 55 | "qualification", 56 | "questionnaire", 57 | "traditionally", 58 | "strengthening", 59 | "instructional", 60 | "perpendicular", 61 | "indispensable", 62 | "beneficiaries", 63 | "correspondent", 64 | "investigating", 65 | "controversial", 66 | "predominantly", 67 | "undergraduate", 68 | "deterioration", 69 | "contradiction", 70 | "reinforcement", 71 | "righteousness", 72 | "proliferation", 73 | "appropriately", 74 | "comprehension", 75 | "communicating", 76 | "inappropriate", 77 | "complementary", 78 | "insignificant", 79 | "theoretically", 80 | "computational", 81 | "administering", 82 | "manifestation", 83 | "uncomfortable", 84 | "statistically", 85 | "alternatively", 86 | "incorporating", 87 | "exceptionally", 88 | "dissemination", 89 | "contradictory", 90 | "modernization", 91 | "enlightenment", 92 | "disappearance", 93 | "interpersonal", 94 | "semiconductor", 95 | "unprecedented", 96 | "contemplation", 97 | "discretionary", 98 | "progressively", 99 | "transcription", 100 | "inconvenience", 101 | "intentionally", 102 | "apportionment", 103 | "confrontation", 104 | "confederation", 105 | "socioeconomic", 106 | "personalities", 107 | "globalization", 108 | "chronological", 109 | "differentiate", 110 | "uncertainties", 111 | "interestingly", 112 | "disadvantaged", 113 | "objectionable", 114 | "predetermined", 115 | "concentrating", 116 | "sedimentation", 117 | "proportionate", 118 | "jurisprudence", 119 | "individuality", 120 | "horticultural", 121 | "refrigeration", 122 | "schizophrenia", 123 | "spontaneously", 124 | "clarification", 125 | "peculiarities", 126 | "instantaneous", 127 | "conscientious", 128 | "contingencies", 129 | "transnational", 130 | "metallurgical", 131 | "controversies", 132 | "multinational", 133 | "radioactivity", 134 | "fragmentation", 135 | "fertilization", 136 | "intracellular", 137 | "impracticable", 138 | "retrospective", 139 | "accreditation", 140 | "thermodynamic", 141 | "postoperative", 142 | "grandchildren", 143 | "investigative", 144 | "unconsciously", 145 | "pronunciation", 146 | "reconstructed", 147 | "insufficiency", 148 | "amplification", 149 | "gravitational", 150 | "reinstatement", 151 | "quartermaster", 152 | "electrostatic", 153 | "preponderance", 154 | "gratification", 155 | "socialization", 156 | "privatization", 157 | "inconsistency", 158 | "individualism", 159 | "misunderstood", 160 | "interrogation", 161 | "unconditional", 162 | "knowledgeable", 163 | "unpredictable", 164 | "visualization", 165 | "accomplishing", 166 | "extracellular", 167 | "stratigraphic", 168 | "unnecessarily", 169 | "redevelopment", 170 | "disappointing", 171 | "nationalities", 172 | "interpolation", 173 | "communicative", 174 | "instinctively", 175 | "disinterested", 176 | "inexperienced", 177 | "accompaniment", 178 | "preoccupation", 179 | "informational", 180 | "uninterrupted", 181 | "hydroelectric", 182 | "hydrochloride", 183 | "macroeconomic", 184 | "subordination", 185 | "sterilization", 186 | "merchandising", 187 | "magnification", 188 | "irresponsible", 189 | "observational", 190 | "multicultural", 191 | "indeterminate", 192 | "intelligently", 193 | "hybridization", 194 | "participatory", 195 | "inadvertently", 196 | "signification", 197 | "qualitatively", 198 | "protestantism", 199 | "constellation", 200 | "subcontractor", 201 | "deterministic", 202 | "decentralized", 203 | "magnetization", 204 | "companionship", 205 | "topographical", 206 | "rearrangement", 207 | "discontinuous", 208 | "discontinuity", 209 | "granddaughter", 210 | "accommodating", 211 | "spectroscopic", 212 | "unwillingness", 213 | "inconceivable", 214 | "intrinsically", 215 | "congratulated", 216 | "reciprocating", 217 | "mathematician", 218 | "discriminated", 219 | "normalization", 220 | "consolidating", 221 | "contraceptive", 222 | "phenomenology", 223 | "paradoxically", 224 | "authenticated", 225 | "transgression", 226 | "appropriating", 227 | "complimentary", 228 | "oceanographic", 229 | "philanthropic", 230 | "mechanization", 231 | "extrapolation", 232 | "substantiated", 233 | "consequential", 234 | "incarceration", 235 | "phytoplankton", 236 | "strategically", 237 | "deteriorating", 238 | "noncompliance", 239 | "fractionation", 240 | "realistically", 241 | "commemoration", 242 | "involuntarily", 243 | "contravention", 244 | "contraception", 245 | "supplementing", 246 | "schizophrenic", 247 | "encyclopaedia", 248 | "inexhaustible", 249 | "underestimate", 250 | "dimensionless", 251 | "microcomputer", 252 | "transatlantic", 253 | "compositional", 254 | "approximating", 255 | "interlocutory", 256 | "desegregation", 257 | "streptococcus", 258 | "unequivocally", 259 | "superficially", 260 | "consternation", 261 | "immunological", 262 | "typographical", 263 | "extermination", 264 | "cerebrospinal", 265 | "transcendence", 266 | "ornamentation", 267 | "incapacitated", 268 | "affirmatively", 269 | "juxtaposition", 270 | "refrigerating", 271 | "hydrogenation", 272 | "translocation", 273 | "exponentially", 274 | "confectionery", 275 | "sequestration", 276 | "resuscitation", 277 | "endocrinology", 278 | "intravenously", 279 | "archeological", 280 | "precipitating", 281 | "ophthalmology", 282 | "provisionally", 283 | "neuromuscular", 284 | "triangulation", 285 | "misconception", 286 | "fortification", 287 | "commissioning", 288 | "scintillation", 289 | "consecutively", 290 | "inflorescence", 291 | "infinitesimal", 292 | "mismanagement", 293 | "energetically", 294 | "commemorative", 295 | "intercultural", 296 | "translational", 297 | "superposition", 298 | "victimization", 299 | "baccalaureate", 300 | "expropriation", 301 | "transposition" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/14_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "administration", 3 | "transportation", 4 | "administrative", 5 | "representative", 6 | "interpretation", 7 | "communications", 8 | "representation", 9 | "characteristic", 10 | "identification", 11 | "implementation", 12 | "superintendent", 13 | "correspondence", 14 | "discrimination", 15 | "transformation", 16 | "recommendation", 17 | "simultaneously", 18 | "reconstruction", 19 | "infrastructure", 20 | "ecclesiastical", 21 | "municipalities", 22 | "archaeological", 23 | "satisfactorily", 24 | "pharmaceutical", 25 | "disappointment", 26 | "unsatisfactory", 27 | "distinguishing", 28 | "reconciliation", 29 | "acknowledgment", 30 | "differentiated", 31 | "systematically", 32 | "accomplishment", 33 | "meteorological", 34 | "cardiovascular", 35 | "constantinople", 36 | "experimentally", 37 | "multiplication", 38 | "chromatography", 39 | "contemporaries", 40 | "discriminatory", 41 | "apprenticeship", 42 | "specialization", 43 | "generalization", 44 | "understandable", 45 | "unquestionably", 46 | "reconnaissance", 47 | "irregularities", 48 | "jurisdictional", 49 | "redistribution", 50 | "disintegration", 51 | "naturalization", 52 | "industrialized", 53 | "polymerization", 54 | "stratification", 55 | "reasonableness", 56 | "northumberland", 57 | "simplification", 58 | "congregational", 59 | "capitalization", 60 | "scientifically", 61 | "discriminating", 62 | "geographically", 63 | "responsiveness", 64 | "transcendental", 65 | "longitudinally", 66 | "authentication", 67 | "professionally", 68 | "underdeveloped", 69 | "discontinuance", 70 | "thermodynamics", 71 | "intellectually", 72 | "electronically", 73 | "sophistication", 74 | "individualized", 75 | "unincorporated", 76 | "interconnected", 77 | "characterizing", 78 | "overwhelmingly", 79 | "attractiveness", 80 | "microstructure", 81 | "superstructure", 82 | "anthropologist", 83 | "superannuation", 84 | "centralization", 85 | "mineralization", 86 | "conversational", 87 | "mathematically", 88 | "intermediaries", 89 | "quantification", 90 | "historiography", 91 | "alphabetically", 92 | "interdependent", 93 | "indiscriminate", 94 | "microprocessor", 95 | "intermittently", 96 | "unintelligible", 97 | "denominational", 98 | "neutralization", 99 | "uncontrollable", 100 | "unconventional", 101 | "immunoglobulin", 102 | "conventionally", 103 | "understandably", 104 | "solidification", 105 | "affectionately", 106 | "inconsiderable", 107 | "internationale", 108 | "interpretative", 109 | "staphylococcus", 110 | "categorization", 111 | "uncompromising", 112 | "advantageously", 113 | "predisposition", 114 | "insurmountable", 115 | "recapitulation", 116 | "proportionally", 117 | "unquestionable", 118 | "discouragement", 119 | "norepinephrine", 120 | "countervailing", 121 | "insufficiently", 122 | "irreconcilable", 123 | "intelligentsia", 124 | "comprehensible", 125 | "conformational", 126 | "preferentially", 127 | "revitalization", 128 | "fredericksburg", 129 | "constructively", 130 | "transformative", 131 | "adenocarcinoma", 132 | "distributional", 133 | "eschatological", 134 | "aggressiveness", 135 | "misrepresented", 136 | "nondestructive", 137 | "sanctification", 138 | "incompressible", 139 | "depolarization", 140 | "worcestershire", 141 | "superintending", 142 | "interplanetary", 143 | "understatement", 144 | "climatological", 145 | "polysaccharide", 146 | "administratrix", 147 | "counterbalance", 148 | "intermolecular", 149 | "proprietorship", 150 | "unidirectional", 151 | "differentially", 152 | "principalities", 153 | "stoichiometric", 154 | "antidepressant", 155 | "insignificance", 156 | "counterfeiting", 157 | "specialisation", 158 | "sentimentality", 159 | "utilitarianism", 160 | "thermoelectric", 161 | "encephalopathy", 162 | "deconstruction", 163 | "conjunctivitis", 164 | "ultrastructure", 165 | "leicestershire", 166 | "intramolecular", 167 | "indestructible", 168 | "contemptuously", 169 | "constructional", 170 | "philanthropist", 171 | "linguistically", 172 | "nebuchadnezzar", 173 | "technicalities", 174 | "initialization", 175 | "symptomatology", 176 | "disequilibrium", 177 | "subcutaneously", 178 | "industrialised", 179 | "nontraditional", 180 | "disintegrating", 181 | "asymptotically", 182 | "histologically", 183 | "indoctrination", 184 | "transparencies", 185 | "relinquishment", 186 | "detoxification", 187 | "excommunicated", 188 | "osteoarthritis", 189 | "multiplicative", 190 | "democratically", 191 | "nonresidential", 192 | "cardiomyopathy", 193 | "congratulation", 194 | "dimensionality", 195 | "predestination", 196 | "unconscionable", 197 | "overproduction", 198 | "intentionality", 199 | "incompleteness", 200 | "unconsolidated", 201 | "pasteurization", 202 | "anesthesiology", 203 | "unhesitatingly", 204 | "constructivist", 205 | "postmenopausal", 206 | "secularization", 207 | "presupposition", 208 | "misinformation", 209 | "impoverishment", 210 | "productiveness", 211 | "nanotechnology", 212 | "differentiable", 213 | "eccentricities", 214 | "cambridgeshire", 215 | "disorientation", 216 | "unprofessional", 217 | "hydrocortisone", 218 | "extinguishment", 219 | "undersecretary", 220 | "generalisation", 221 | "eutrophication", 222 | "glucocorticoid", 223 | "reconstructive", 224 | "unappropriated", 225 | "uncontradicted", 226 | "confidentially", 227 | "macromolecular", 228 | "licentiousness", 229 | "decolonization", 230 | "chlorpromazine", 231 | "discretization", 232 | "neuroendocrine", 233 | "reconditioning", 234 | "coincidentally", 235 | "intermediation", 236 | "aggrandizement", 237 | "existentialism", 238 | "idiosyncrasies", 239 | "quintessential", 240 | "undermentioned", 241 | "evangelization", 242 | "nitrocellulose", 243 | "horticulturist", 244 | "substantiation", 245 | "disinclination", 246 | "apologetically", 247 | "constructivism", 248 | "commercialized", 249 | "staphylococcal", 250 | "macroeconomics", 251 | "disapprobation", 252 | "unpleasantness", 253 | "regularization", 254 | "unmanufactured", 255 | "subconsciously", 256 | "disenchantment", 257 | "volatilization", 258 | "groundbreaking", 259 | "egalitarianism", 260 | "mountaineering", 261 | "uncontrollably", 262 | "discriminative", 263 | "unacknowledged", 264 | "hierarchically", 265 | "twodimensional", 266 | "transmigration", 267 | "cinematography", 268 | "expressiveness", 269 | "saponification", 270 | "impressionable", 271 | "fingerprinting", 272 | "pneumoconiosis", 273 | "traditionalist", 274 | "centralisation", 275 | "misapplication", 276 | "expressionless", 277 | "radiofrequency", 278 | "congratulatory", 279 | "reintroduction", 280 | "wollstonecraft", 281 | "polymerisation", 282 | "benzodiazepine", 283 | "beautification", 284 | "pyelonephritis", 285 | "overpopulation", 286 | "otolaryngology", 287 | "erythropoietin", 288 | "hypersensitive", 289 | "adrenocortical", 290 | "territoriality", 291 | "redistributive", 292 | "spectrographic", 293 | "counterfactual", 294 | "electioneering", 295 | "uncontaminated", 296 | "bacteriologist", 297 | "penitentiaries", 298 | "politicization", 299 | "conspiratorial", 300 | "christological", 301 | "conditionality" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/15_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "notwithstanding", 3 | "differentiation", 4 | "electromagnetic", 5 | "straightforward", 6 | "instrumentation", 7 | "standardization", 8 | "experimentation", 9 | "reconsideration", 10 | "dissatisfaction", 11 | "transplantation", 12 | "internationally", 13 | "confidentiality", 14 | "anthropological", 15 | "correspondingly", 16 | "crystallization", 17 | "extraordinarily", 18 | "electrochemical", 19 | "interdependence", 20 | "congratulations", 21 | "distinguishable", 22 | "acknowledgement", 23 | "proportionately", 24 | "entrepreneurial", 25 | "environmentally", 26 | "superconducting", 27 | "inconsistencies", 28 | "electrification", 29 | "appropriateness", 30 | "interrogatories", 31 | "synchronization", 32 | "epidemiological", 33 | "contemporaneous", 34 | "interchangeable", 35 | "instrumentality", 36 | "pharmacological", 37 | "interconnection", 38 | "professionalism", 39 | "epistemological", 40 | "supplementation", 41 | "superintendence", 42 | "chromatographic", 43 | "intensification", 44 | "bacteriological", 45 | "democratization", 46 | "rationalization", 47 | "conscientiously", 48 | "proportionality", 49 | "individualistic", 50 | "nationalization", 51 | "unconditionally", 52 | "technologically", 53 | "instantaneously", 54 | "indemnification", 55 | "chronologically", 56 | "gloucestershire", 57 | "decontamination", 58 | "comprehensively", 59 | "philosophically", 60 | "disillusionment", 61 | "unconsciousness", 62 | "sympathetically", 63 | "interchangeably", 64 | "unintentionally", 65 | "polycrystalline", 66 | "physiologically", 67 | "microscopically", 68 | "musculoskeletal", 69 | "excommunication", 70 | "contraindicated", 71 | "autocorrelation", 72 | "misapprehension", 73 | "physicochemical", 74 | "catheterization", 75 | "personification", 76 | "developmentally", 77 | "intercollegiate", 78 | "complementarity", 79 | "nationalisation", 80 | "distinctiveness", 81 | "standardisation", 82 | "inconsequential", 83 | "extracurricular", 84 | "perpendicularly", 85 | "hyperthyroidism", 86 | "trustworthiness", 87 | "cardiopulmonary", 88 | "crystallography", 89 | "counterbalanced", 90 | "cerebrovascular", 91 | "insubordination", 92 | "totalitarianism", 93 | "disadvantageous", 94 | "unsophisticated", 95 | "chloramphenicol", 96 | "internalization", 97 | "discontinuation", 98 | "retrospectively", 99 | "inappropriately", 100 | "americanization", 101 | "parasympathetic", 102 | "crystallisation", 103 | "computationally", 104 | "transfiguration", 105 | "desensitization", 106 | "noncommissioned", 107 | "investigational", 108 | "ineffectiveness", 109 | "postoperatively", 110 | "underprivileged", 111 | "desertification", 112 | "rationalisation", 113 | "cosmopolitanism", 114 | "montmorillonite", 115 | "reapportionment", 116 | "ultrasonography", 117 | "neurophysiology", 118 | "procrastination", 119 | "underemployment", 120 | "cytomegalovirus", 121 | "confrontational", 122 | "lymphadenopathy", 123 | "parenthetically", 124 | "electrodynamics", 125 | "intramuscularly", 126 | "attorneygeneral", 127 | "conservationist", 128 | "reestablishment", 129 | "polychlorinated", 130 | "gastroenteritis", 131 | "redetermination", 132 | "retroperitoneal", 133 | "exemplification", 134 | "denitrification", 135 | "destructiveness", 136 | "parliamentarian", 137 | "stereochemistry", 138 | "polyunsaturated", 139 | "autoradiography", 140 | "sociolinguistic", 141 | "paleontological", 142 | "transliteration", 143 | "impressionistic", 144 | "computerization", 145 | "phenolphthalein", 146 | "uninterruptedly", 147 | "schistosomiasis", 148 | "commodification", 149 | "anticoagulation", 150 | "nonprofessional", 151 | "stratigraphical", 152 | "complementation", 153 | "unexceptionable", 154 | "objectification", 155 | "architecturally", 156 | "underestimation", 157 | "unsubstantiated", 158 | "particularities", 159 | "supersaturation", 160 | "undistinguished", 161 | "ophthalmologist", 162 | "superintendency", 163 | "unceremoniously", 164 | "humanitarianism", 165 | "acclimatization", 166 | "pharmacotherapy", 167 | "disenfranchised", 168 | "intersubjective", 169 | "unrighteousness", 170 | "dehydrogenation", 171 | "personalization", 172 | "regionalization", 173 | "governorgeneral", 174 | "intellectualism", 175 | "democratisation", 176 | "thromboembolism", 177 | "cinematographic", 178 | "presbyterianism", 179 | "decarboxylation", 180 | "unobjectionable", 181 | "intussusception", 182 | "conservatorship", 183 | "vasoconstrictor", 184 | "immunologically", 185 | "configurational", 186 | "picturesqueness", 187 | "misappropriated", 188 | "superimposition", 189 | "trigonometrical", 190 | "intertextuality", 191 | "macroscopically", 192 | "englishspeaking", 193 | "systematization", 194 | "incommensurable", 195 | "unrealistically", 196 | "microelectronic", 197 | "africanamerican", 198 | "topographically", 199 | "conventionality", 200 | "wellestablished", 201 | "miniaturization", 202 | "insurrectionary", 203 | "instrumentalist", 204 | "agranulocytosis", 205 | "familiarization", 206 | "corynebacterium", 207 | "interdependency", 208 | "cinematographer", 209 | "interphalangeal", 210 | "disappointingly", 211 | "interprovincial", 212 | "meaninglessness", 213 | "revolutionizing", 214 | "electronegative", 215 | "particularistic", 216 | "phenylketonuria", 217 | "constructionist", 218 | "proinflammatory", 219 | "demagnetization", 220 | "uncontroversial", 221 | "nonprescription", 222 | "oligosaccharide", 223 | "oxytetracycline", 224 | "anthropocentric", 225 | "parthenogenesis", 226 | "omnidirectional", 227 | "ethnomusicology", 228 | "synchronisation", 229 | "airconditioning", 230 | "vascularization", 231 | "unseaworthiness", 232 | "intercomparison", 233 | "incomprehension", 234 | "individualizing", 235 | "selfsufficiency", 236 | "materialization", 237 | "understandingly", 238 | "infinitesimally", 239 | "polyelectrolyte", 240 | "europeanization", 241 | "interconversion", 242 | "synergistically", 243 | "temperamentally", 244 | "improvisational", 245 | "euphemistically", 246 | "neuropsychiatry", 247 | "misconstruction", 248 | "hemochromatosis", 249 | "diphenhydramine", 250 | "jurisprudential", 251 | "interstratified", 252 | "experimentalist", 253 | "spectrochemical", 254 | "supraclavicular", 255 | "noninterference", 256 | "noncontributory", 257 | "nonintervention", 258 | "prognostication", 259 | "discountenanced", 260 | "contemporaneity", 261 | "progressiveness", 262 | "uncomprehending", 263 | "intellectuality", 264 | "maldistribution", 265 | "supernaturalism", 266 | "immunocompetent", 267 | "parthenogenetic", 268 | "unquestioningly", 269 | "inquisitiveness", 270 | "logarithmically", 271 | "monounsaturated", 272 | "individualities", 273 | "insignificantly", 274 | "optoelectronics", 275 | "historiographer", 276 | "dermatomyositis", 277 | "pseudoephedrine", 278 | "thermodynamical", 279 | "externalization", 280 | "counterproposal", 281 | "viscoelasticity", 282 | "semitransparent", 283 | "constructionism", 284 | "unprecedentedly", 285 | "nucleosynthesis", 286 | "ophthalmoplegia", 287 | "androstenedione", 288 | "aluminosilicate", 289 | "lightheadedness", 290 | "acquisitiveness", 291 | "preimplantation", 292 | "condescendingly", 293 | "thermochemistry", 294 | "aristotelianism", 295 | "hermaphroditism", 296 | "trichloroethane", 297 | "interindividual", 298 | "instrumentalism", 299 | "paraventricular", 300 | "stereoselective", 301 | "uncomplimentary" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/16_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "characterization", 3 | "misunderstanding", 4 | "gastrointestinal", 5 | "disproportionate", 6 | "decentralization", 7 | "enthusiastically", 8 | "disqualification", 9 | "entrepreneurship", 10 | "incomprehensible", 11 | "phenomenological", 12 | "hypersensitivity", 13 | "multidimensional", 14 | "immunodeficiency", 15 | "representational", 16 | "indiscriminately", 17 | "undifferentiated", 18 | "transcontinental", 19 | "administratively", 20 | "multiculturalism", 21 | "transformational", 22 | "pharmacokinetics", 23 | "characterisation", 24 | "crystallographic", 25 | "internationalism", 26 | "thrombocytopenia", 27 | "neurotransmitter", 28 | "intercontinental", 29 | "northamptonshire", 30 | "decentralisation", 31 | "underdevelopment", 32 | "misappropriation", 33 | "microelectronics", 34 | "counterclockwise", 35 | "antihypertensive", 36 | "vasoconstriction", 37 | "nonproliferation", 38 | "nonmanufacturing", 39 | "reinterpretation", 40 | "electrochemistry", 41 | "extraterritorial", 42 | "incontrovertible", 43 | "extraterrestrial", 44 | "atrioventricular", 45 | "environmentalism", 46 | "neuropsychiatric", 47 | "collectivization", 48 | "internationalist", 49 | "parameterization", 50 | "disestablishment", 51 | "unreasonableness", 52 | "diagrammatically", 53 | "environmentalist", 54 | "sociolinguistics", 55 | "uncharacteristic", 56 | "aminotransferase", 57 | "radiosensitivity", 58 | "microcrystalline", 59 | "contraindication", 60 | "intraventricular", 61 | "anesthesiologist", 62 | "chancellorsville", 63 | "pheochromocytoma", 64 | "unrepresentative", 65 | "creditworthiness", 66 | "counterintuitive", 67 | "gastroesophageal", 68 | "counterbalancing", 69 | "autoradiographic", 70 | "thermomechanical", 71 | "electromagnetism", 72 | "hemagglutination", 73 | "electromyography", 74 | "conventionalized", 75 | "thermoregulation", 76 | "teleconferencing", 77 | "microcirculation", 78 | "roentgenographic", 79 | "palaeontological", 80 | "paraprofessional", 81 | "acquaintanceship", 82 | "ophthalmological", 83 | "tracheobronchial", 84 | "triiodothyronine", 85 | "electrolytically", 86 | "magnetostriction", 87 | "sesquicentennial", 88 | "immunosuppressed", 89 | "interventricular", 90 | "uncompromisingly", 91 | "radiographically", 92 | "phylogenetically", 93 | "rhabdomyosarcoma", 94 | "glossopharyngeal", 95 | "chlordiazepoxide", 96 | "unresponsiveness", 97 | "demineralization", 98 | "christianization", 99 | "interrelatedness", 100 | "thermostatically", 101 | "australopithecus", 102 | "prequalification", 103 | "electromagnetics", 104 | "underachievement", 105 | "paraformaldehyde", 106 | "inextinguishable", 107 | "controversialist", 108 | "undenominational", 109 | "predetermination", 110 | "unsatisfactorily", 111 | "conversationally", 112 | "erythroblastosis", 113 | "noncontroversial", 114 | "universalization", 115 | "electromigration", 116 | "semiprofessional", 117 | "nonparticipation", 118 | "intercrystalline", 119 | "overexploitation", 120 | "catastrophically", 121 | "hydroelectricity", 122 | "selfpreservation", 123 | "undiscriminating", 124 | "desulphurization", 125 | "compartmentation", 126 | "chemiluminescent", 127 | "undernourishment", 128 | "extemporaneously", 129 | "multimillionaire", 130 | "incontrovertibly", 131 | "multiprogramming", 132 | "sphygmomanometer", 133 | "anthropocentrism", 134 | "feeblemindedness", 135 | "propylthiouracil", 136 | "collectivisation", 137 | "conspiratorially", 138 | "prochlorperazine", 139 | "myelomeningocele", 140 | "uncharitableness", 141 | "interministerial", 142 | "supersensitivity", 143 | "otolaryngologist", 144 | "musculocutaneous", 145 | "granulocytopenia", 146 | "pharmacogenetics", 147 | "compartmentalize", 148 | "ethnographically", 149 | "scrophulariaceae", 150 | "bureaucratically", 151 | "autofluorescence", 152 | "solicitorgeneral", 153 | "monocotyledonous", 154 | "internationalize", 155 | "saccharification", 156 | "pharmaceutically", 157 | "ecclesiastically", 158 | "dendrochronology", 159 | "subconsciousness", 160 | "quantificational", 161 | "pseudoscientific", 162 | "developmentalism", 163 | "incomprehensibly", 164 | "interterritorial", 165 | "piezoelectricity", 166 | "commissionership", 167 | "paradigmatically", 168 | "unostentatiously", 169 | "unscrupulousness", 170 | "weatherstripping", 171 | "spiritualization", 172 | "countersignature", 173 | "perpendicularity", 174 | "machiavellianism", 175 | "microprogramming", 176 | "benzylpenicillin", 177 | "overcompensation", 178 | "multifariousness", 179 | "ferroelectricity", 180 | "mispronunciation", 181 | "paleoclimatology", 182 | "transculturation", 183 | "underconsumption", 184 | "overconsolidated", 185 | "immunocompetence", 186 | "serpentinization", 187 | "nephrocalcinosis", 188 | "transcendentally", 189 | "constructiveness", 190 | "counterespionage", 191 | "hypopigmentation", 192 | "remineralization", 193 | "disingenuousness", 194 | "inconclusiveness", 195 | "denaturalization", 196 | "superciliousness", 197 | "vasoconstrictive", 198 | "otherworldliness", 199 | "dephlogisticated", 200 | "melodramatically", 201 | "ultramicroscopic", 202 | "lightheartedness", 203 | "microlepidoptera", 204 | "hydrometeorology", 205 | "unchangeableness", 206 | "indiscriminating", 207 | "greatgrandmother", 208 | "hydrodynamically", 209 | "thermogravimetry", 210 | "overenthusiastic", 211 | "neurohypophysial", 212 | "unattractiveness", 213 | "naturphilosophie", 214 | "mesembryanthemum", 215 | "representatively", 216 | "apprehensiveness", 217 | "conventionalised", 218 | "presuppositional", 219 | "relativistically", 220 | "montmorillonitic", 221 | "discountenancing", 222 | "schizophreniform", 223 | "pseudostratified", 224 | "unconventionally", 225 | "municipalization", 226 | "interconvertible", 227 | "synchrocyclotron", 228 | "characteristical", 229 | "disagreeableness", 230 | "representativity", 231 | "absentmindedness", 232 | "antagonistically", 233 | "malapportionment", 234 | "sensationalistic", 235 | "radiotelegraphic", 236 | "microradiography", 237 | "unprofitableness", 238 | "micrometeorology", 239 | "lymphangiography", 240 | "dedifferentiated", 241 | "disconnectedness", 242 | "multiplicatively", 243 | "cytoarchitecture", 244 | "unsystematically", 245 | "remilitarization", 246 | "insubstantiality", 247 | "presumptuousness", 248 | "sclerenchymatous", 249 | "intertestamental", 250 | "bloodthirstiness", 251 | "selfsatisfaction", 252 | "narcissistically", 253 | "satisfactoriness", 254 | "neurolinguistics", 255 | "precontemplation", 256 | "eschatologically", 257 | "exterritoriality", 258 | "singlemindedness", 259 | "unproductiveness", 260 | "hypodermatically", 261 | "stereoscopically", 262 | "christianisation", 263 | "plethysmographic", 264 | "electrocapillary", 265 | "traditionalistic", 266 | "epiphenomenalism", 267 | "noncontradiction", 268 | "discriminatingly", 269 | "intracrystalline", 270 | "internationality", 271 | "fictionalization", 272 | "undistinguishing", 273 | "politicoeconomic", 274 | "crystallographer", 275 | "meteorologically", 276 | "diacetylmorphine", 277 | "inarticulateness", 278 | "hydrofluosilicic", 279 | "disincorporation", 280 | "mechanosensitive", 281 | "unscientifically", 282 | "terminologically", 283 | "hypercellularity", 284 | "uncrystallizable", 285 | "denominationally", 286 | "identificational", 287 | "aristocratically", 288 | "vespertilionidae", 289 | "narrowmindedness", 290 | "marsupialization", 291 | "prestidigitation", 292 | "affectionateness", 293 | "chryselephantine", 294 | "stratificational", 295 | "internationalise", 296 | "wellproportioned", 297 | "reidentification", 298 | "unextinguishable", 299 | "swedenborgianism", 300 | "macrolepidoptera", 301 | "journalistically" 302 | ] -------------------------------------------------------------------------------- /game/wordlists/17_chars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "industrialization", 3 | "interdisciplinary", 4 | "intergovernmental", 5 | "misrepresentation", 6 | "telecommunication", 7 | "indistinguishable", 8 | "multidisciplinary", 9 | "conceptualization", 10 | "commercialization", 11 | "recrystallization", 12 | "industrialisation", 13 | "instrumentalities", 14 | "interrelationship", 15 | "superconductivity", 16 | "interdepartmental", 17 | "intergenerational", 18 | "counterproductive", 19 | "nondiscrimination", 20 | "nondiscriminatory", 21 | "misinterpretation", 22 | "contradistinction", 23 | "electrocardiogram", 24 | "contemporaneously", 25 | "electromechanical", 26 | "comprehensiveness", 27 | "immunosuppressive", 28 | "immunosuppression", 29 | "disinterestedness", 30 | "conscientiousness", 31 | "counterinsurgency", 32 | "individualization", 33 | "antiferromagnetic", 34 | "straightforwardly", 35 | "immunocompromised", 36 | "selfdetermination", 37 | "encephalomyelitis", 38 | "thermodynamically", 39 | "historiographical", 40 | "stratigraphically", 41 | "electrophysiology", 42 | "transcendentalism", 43 | "maladministration", 44 | "congregationalist", 45 | "chemiluminescence", 46 | "trichloroethylene", 47 | "neurofibromatosis", 48 | "commercialisation", 49 | "cardiorespiratory", 50 | "electronegativity", 51 | "conceptualisation", 52 | "depersonalization", 53 | "compartmentalized", 54 | "poststructuralist", 55 | "hypophysectomized", 56 | "congregationalism", 57 | "pharmacologically", 58 | "counterrevolution", 59 | "inappropriateness", 60 | "selfconsciousness", 61 | "chlortetracycline", 62 | "postmastergeneral", 63 | "poststructuralism", 64 | "multicollinearity", 65 | "conversationalist", 66 | "misidentification", 67 | "hyperpigmentation", 68 | "mineralocorticoid", 69 | "thermogravimetric", 70 | "transdisciplinary", 71 | "undistinguishable", 72 | "spectroscopically", 73 | "epistemologically", 74 | "electrostatically", 75 | "spondylolisthesis", 76 | "transcendentalist", 77 | "conventionalities", 78 | "diphenylhydantoin", 79 | "nondenominational", 80 | "thermoluminescent", 81 | "micropaleontology", 82 | "acromioclavicular", 83 | "individualisation", 84 | "angiocardiography", 85 | "cryptocrystalline", 86 | "haemagglutination", 87 | "parathyroidectomy", 88 | "disintermediation", 89 | "deterministically", 90 | "dedifferentiation", 91 | "denationalization", 92 | "hyperalimentation", 93 | "immunosuppressant", 94 | "denominationalism", 95 | "dextroamphetamine", 96 | "lieutenantgeneral", 97 | "deconstructionist", 98 | "untrustworthiness", 99 | "anthropologically", 100 | "overdetermination", 101 | "socioeconomically", 102 | "particularization", 103 | "desynchronization", 104 | "palaeoclimatology", 105 | "uniformitarianism", 106 | "unconventionality", 107 | "indistinguishably", 108 | "electroretinogram", 109 | "anachronistically", 110 | "intertrochanteric", 111 | "bosniaherzegovina", 112 | "disproportionally", 113 | "australopithecine", 114 | "thermoelectricity", 115 | "indeterminateness", 116 | "electrometallurgy", 117 | "dispensationalism", 118 | "disadvantageously", 119 | "phaeochromocytoma", 120 | "unselfconsciously", 121 | "nonrepresentative", 122 | "paleoanthropology", 123 | "phytogeographical", 124 | "premonstratensian", 125 | "pronominalization", 126 | "unsympathetically", 127 | "contradictoriness", 128 | "tracheobronchitis", 129 | "denationalisation", 130 | "lexicographically", 131 | "pentylenetetrazol", 132 | "compartmentalised", 133 | "consubstantiality", 134 | "selfcontradictory", 135 | "spectroheliograph", 136 | "lumpenproletariat", 137 | "ethnopharmacology", 138 | "bacillariophyceae", 139 | "selfrighteousness", 140 | "intradepartmental", 141 | "consubstantiation", 142 | "deconstructionism", 143 | "cytoarchitectonic", 144 | "naphthaleneacetic", 145 | "hydrogasification", 146 | "metacommunication", 147 | "physiographically", 148 | "argumentativeness", 149 | "physicomechanical", 150 | "depersonalisation", 151 | "communicativeness", 152 | "intercolumniation", 153 | "cryptographically", 154 | "inconsiderateness", 155 | "intermediolateral", 156 | "supernaturalistic", 157 | "inconsequentially", 158 | "counterirritation", 159 | "materialistically", 160 | "maxillomandibular", 161 | "omphalomesenteric", 162 | "philanthropically", 163 | "uncontroversially", 164 | "unpretentiousness", 165 | "misrepresentative", 166 | "trigonometrically", 167 | "sanctimoniousness", 168 | "anthropogeography", 169 | "extraordinariness", 170 | "wellcharacterized", 171 | "postimpressionist", 172 | "paleontologically", 173 | "uncomfortableness", 174 | "selfcontradiction", 175 | "cytoarchitectural", 176 | "instantaneousness", 177 | "selfjustification", 178 | "hydrocarbonaceous", 179 | "nationalistically", 180 | "interconfessional", 181 | "semiconsciousness", 182 | "renationalization", 183 | "selftranscendence", 184 | "hypophysectomised", 185 | "procellariiformes", 186 | "indispensableness", 187 | "laryngopharyngeal", 188 | "vernacularization", 189 | "wellauthenticated", 190 | "thermocoagulation", 191 | "postimpressionism", 192 | "radioluminescence", 193 | "nitrohydrochloric", 194 | "particularisation", 195 | "predestinarianism", 196 | "divisionalization", 197 | "triboluminescence", 198 | "microsociological", 199 | "conventionalizing", 200 | "provincialization", 201 | "transcendentality", 202 | "politicoreligious", 203 | "selffertilization", 204 | "lymphangiosarcoma", 205 | "administratorship", 206 | "oversensitiveness", 207 | "disproportionated", 208 | "pleuropericardial", 209 | "concentrativeness", 210 | "redistributionist", 211 | "interpopulational", 212 | "selfglorification", 213 | "jurisprudentially", 214 | "dendroclimatology", 215 | "architectonically", 216 | "rationalistically", 217 | "unphilosophically", 218 | "disproportionably", 219 | "counterfactuality", 220 | "supralapsarianism", 221 | "introspectiveness", 222 | "phalacrocoracidae", 223 | "contemplativeness", 224 | "bundesversammlung", 225 | "inconceivableness", 226 | "stereographically", 227 | "preadministration", 228 | "representationism", 229 | "hydrofluorocarbon", 230 | "tenderheartedness", 231 | "countersubversion", 232 | "chromatographical", 233 | "antiparliamentary", 234 | "sacramentarianism", 235 | "unconscientiously", 236 | "microspectroscope", 237 | "demasculinization", 238 | "hypophysiotrophic", 239 | "companionableness", 240 | "transderivational", 241 | "desynchronisation", 242 | "undistinguishably", 243 | "objectionableness", 244 | "valetudinarianism", 245 | "threskiornithidae", 246 | "pseudoscorpionida", 247 | "pharyngolaryngeal", 248 | "streptomycetaceae", 249 | "protestantization", 250 | "thiodiphenylamine", 251 | "decrystallization", 252 | "contradistinguish", 253 | "representationist", 254 | "disproportionable", 255 | "intertranslatable", 256 | "incomprehensively", 257 | "inexhaustibleness", 258 | "counterattraction", 259 | "nonfraternization", 260 | "unceremoniousness", 261 | "miscellaneousness", 262 | "stratospherically", 263 | "denominationalist", 264 | "uninterruptedness", 265 | "extraprofessional", 266 | "counterindication", 267 | "infralapsarianism", 268 | "undergraduateship", 269 | "captainlieutenant", 270 | "impracticableness", 271 | "amphibolitization", 272 | "supranaturalistic", 273 | "unaccountableness", 274 | "mathematicization", 275 | "operationalizable", 276 | "proportionateness", 277 | "paleontographical", 278 | "ptilonorhynchidae", 279 | "indistinctiveness", 280 | "contradistinctive", 281 | "branchiobdellidae", 282 | "knowledgeableness", 283 | "amphitheatrically", 284 | "superalimentation", 285 | "impersonification", 286 | "onomatopoetically", 287 | "sympatheticotonia", 288 | "diaphragmatically", 289 | "inconsecutiveness", 290 | "brokenheartedness", 291 | "intercommunicable", 292 | "trinitrocellulose", 293 | "worldlymindedness", 294 | "prepressurization", 295 | "spectrobolometric", 296 | "decomplementation", 297 | "osteoglossiformes", 298 | "revolutionariness", 299 | "deprofessionalize", 300 | "cercidiphyllaceae", 301 | "nonaccomplishment" 302 | ] -------------------------------------------------------------------------------- /game/game.js: -------------------------------------------------------------------------------- 1 | import { words, getDaily } from './words.js'; 2 | import resultsLogic from './results.js'; 3 | 4 | export default ({ db, rules }) => { 5 | const results = resultsLogic({ db, rules }); 6 | 7 | async function startGame(lobbyId) { 8 | // Create gamestates 9 | const lobbyRules = await rules.getByLobby(lobbyId); 10 | const status = (await db.query('SELECT status FROM lobbies WHERE id=$id', { 11 | $id: lobbyId, 12 | }))[0].status; 13 | if (status !== 'game') { 14 | if (lobbyRules.maxTime !== null) { 15 | const endTime = new Date(Date.now()); 16 | endTime.setSeconds(endTime.getSeconds() + lobbyRules.maxTime); 17 | await db.query(`UPDATE lobbies SET status='game', end_time='${endTime.getTime()}' WHERE id=$id`, { 18 | $id: lobbyId, 19 | }); 20 | } else { 21 | await db.query('UPDATE lobbies SET status=\'game\' WHERE id=$id', { 22 | $id: lobbyId, 23 | }); 24 | } 25 | 26 | 27 | await addGamestates(lobbyId); 28 | 29 | await db.query('UPDATE active_players SET is_active=true WHERE id=(SELECT id FROM active_players WHERE lobby_id=$lobby_id LIMIT 1)', { 30 | $lobby_id: lobbyId, 31 | }); 32 | } 33 | } 34 | 35 | async function checkTime(lobbyId) { 36 | const endTime = (await db.query('SELECT end_time FROM lobbies WHERE id=$id', { 37 | $id: lobbyId, 38 | }))[0].end_time; 39 | const timeNow = new Date(Date.now()); 40 | timeNow.setSeconds(timeNow.getSeconds() + 1); 41 | if (endTime < timeNow) { 42 | endGame(lobbyId); 43 | } 44 | } 45 | 46 | async function addGamestates(lobbyId) { 47 | const players = await db.query('SELECT id FROM active_players LEFT JOIN player_gamestates ON active_players.id=player_gamestates.player_id WHERE lobby_id=$lobby_id AND player_id IS NULL', { 48 | $lobby_id: lobbyId, 49 | }); 50 | 51 | const lobbyRules = await rules.getByLobby(lobbyId); 52 | let word = null; 53 | if (lobbyRules.dailyChallenge) { 54 | word = getDaily(); 55 | } else if (lobbyRules.sameWord) { 56 | word = getWord(lobbyRules.wordLength); 57 | } 58 | 59 | 60 | await db.query('INSERT INTO player_gamestates (player_id, word, lives_used, time_used, known_letters, used_letters) VALUES ($player_id, $word, $lives_used, $time_used, $known_letters, $used_letters)', 61 | players.map((a) => { 62 | const playerWord = (word != null) ? word : getWord(lobbyRules.wordLength); 63 | return { 64 | $player_id: a.id, 65 | $word: playerWord, 66 | // TODO: set these values appropriately in relation to the chosen rules 67 | $lives_used: 0, 68 | $time_used: 0, 69 | $known_letters: ' '.repeat(playerWord.length), 70 | $used_letters: '', 71 | }; 72 | }), 73 | ); 74 | if (lobbyRules.asyncTurns === 1) { 75 | await db.query('UPDATE active_players SET is_active=true WHERE id IN (SELECT player_id FROM player_gamestates WHERE finished=false) AND lobby_id=$lobby_id', { 76 | $lobby_id: lobbyId, 77 | }); 78 | } 79 | } 80 | 81 | function getWord(length) { 82 | return words[length][Math.floor(Math.random() * words[length].length)]; 83 | } 84 | 85 | // check in the request whether the user is authenticated to do this 86 | async function takeTurn(lobbyId, playerId, turn) { 87 | if (turn.data !== null) { 88 | turn.data = turn.data.toLowerCase(); 89 | const playerGamestate = (await db.query('SELECT * FROM player_gamestates WHERE player_id=$player_id', { 90 | $player_id: playerId, 91 | }))[0]; 92 | // validate turn, ability to make a turn should be confirmed from the request handler 93 | if (turn.type === 'letter') { 94 | const newKnownLetters = playerGamestate.known_letters.split(''); 95 | if (playerGamestate.used_letters.includes(turn.data)) { 96 | throw (new Error('letter_used')); 97 | } 98 | if (playerGamestate.word.includes(turn.data)) { 99 | for (let i = 0; i < playerGamestate.word.length; i++) { 100 | if (playerGamestate.word[i] === turn.data) { 101 | newKnownLetters[i] = turn.data; 102 | } 103 | } 104 | await db.query('UPDATE player_gamestates SET known_letters=$known_letters, used_letters=$used_letters WHERE player_id=$player_id', { 105 | $used_letters: playerGamestate.used_letters + turn.data, 106 | $known_letters: newKnownLetters.join(''), 107 | $player_id: playerGamestate.player_id, 108 | }); 109 | } else { 110 | await db.query('UPDATE player_gamestates SET lives_used=lives_used + 1, used_letters=$used_letters WHERE player_id=$player_id', { 111 | $used_letters: playerGamestate.used_letters + turn.data, 112 | $player_id: playerGamestate.player_id, 113 | }); 114 | } 115 | } else if (turn.type === 'full_guess') { 116 | // check that full guesses are allowed 117 | if ((await rules.getByLobby(lobbyId)).fullGuesses !== 1) { 118 | throw (new Error('guess_not_allowed')); 119 | } 120 | if (turn.data.length !== playerGamestate.word.length) { 121 | throw (new Error('guess_length')); 122 | } 123 | if (turn.data.toLowerCase() === playerGamestate.word) { 124 | await db.query('UPDATE player_gamestates SET known_letters=$known_letters WHERE player_id=$player_id', { 125 | $known_letters: playerGamestate.word, 126 | $player_id: playerGamestate.player_id, 127 | }); 128 | } else { 129 | await db.query('UPDATE player_gamestates SET lives_used=lives_used + 1 WHERE player_id=$player_id', { 130 | $player_id: playerGamestate.player_id, 131 | }); 132 | } 133 | } 134 | await checkEnd(playerId); 135 | } else { 136 | await db.query('UPDATE player_gamestates SET lives_used=lives_used + 1 WHERE player_id=$player_id', { 137 | $player_id: playerId, 138 | }); 139 | await checkEnd(playerId); 140 | } 141 | await nextTurn(playerId); 142 | } 143 | 144 | async function checkEnd(playerId) { 145 | const gameStates = await db.query('SELECT word, known_letters, lobby_id, lives_used FROM active_players LEFT JOIN lobbies ON lobbies.id=active_players.lobby_id LEFT JOIN player_gamestates ON active_players.id=player_gamestates.player_id WHERE lobbies.id IN (SELECT lobby_id FROM active_players WHERE id=$id)', { 146 | $id: playerId, 147 | }); 148 | const maxLives = (await db.query('SELECT value FROM rules WHERE rule_id=$rule_id AND lobby_id=$lobby_id', { 149 | $rule_id: 'maxLives', 150 | $lobby_id: gameStates[0].lobby_id, 151 | }))[0].value; 152 | 153 | for (const gameState of gameStates) { 154 | if (gameState.word !== gameState.known_letters && gameState.lives_used < maxLives) { 155 | return; 156 | } 157 | } 158 | 159 | endGame(gameStates[0].lobby_id); 160 | } 161 | 162 | async function endGame(lobbyId) { 163 | await results.create(lobbyId); 164 | 165 | 166 | await db.query("UPDATE lobbies SET status='results' WHERE id=$id", { 167 | $id: lobbyId, 168 | }); 169 | } 170 | 171 | async function nextTurn(playerId) { 172 | const activePlayer = await db.query('SELECT id, lobby_id FROM active_players WHERE id=$id AND is_active=true', { 173 | $id: playerId, 174 | }); 175 | if (activePlayer.length === 1) { 176 | const lobbyRules = await rules.getByLobby(activePlayer[0].lobby_id); 177 | if (lobbyRules.asyncTurns === 0) { 178 | const maxLives = await db.query('SELECT * FROM rules WHERE rule_id=$rule_id AND lobby_id=$lobby_id', { 179 | $rule_id: 'maxLives', 180 | $lobby_id: activePlayer[0].lobby_id, 181 | }); 182 | await db.query('UPDATE active_players SET is_active=false WHERE id=$id', { 183 | $id: playerId, 184 | }); 185 | const maxId = (await db.query('SELECT MAX(id) as max FROM active_players WHERE lobby_id=$lobby_id', { 186 | $lobby_id: activePlayer[0].lobby_id, 187 | }))[0].max; 188 | if (maxId === playerId) { 189 | await db.query('UPDATE active_players SET is_active=true WHERE id IN (SELECT id FROM active_players LEFT JOIN player_gamestates ON id=player_id WHERE lobby_id=$lobby_id AND known_letters<>word AND lives_used<>$max_lives ORDER BY id ASC LIMIT 1)', { 190 | $lobby_id: activePlayer[0].lobby_id, 191 | $max_lives: maxLives[0].value, 192 | }); 193 | } else { 194 | await db.query('UPDATE active_players SET is_active=true WHERE id IN (SELECT id FROM active_players LEFT JOIN player_gamestates ON id=player_id WHERE lobby_id=$lobby_id AND id>$id AND known_letters<>word AND lives_used<>$max_lives ORDER BY id ASC LIMIT 1)', { 195 | $lobby_id: activePlayer[0].lobby_id, 196 | $id: playerId, 197 | $max_lives: maxLives[0].value, 198 | }); 199 | } 200 | } 201 | } else { 202 | // player does not exist, has probably disconnected 203 | } 204 | } 205 | 206 | async function getAllowedData(playerId) { 207 | // TODO: allow players to get limited data of other players 208 | const playerData = await db.query('SELECT known_letters, lives_used, time_used, used_letters FROM player_gamestates WHERE player_id=$player_id', { 209 | $player_id: playerId, 210 | }); 211 | return playerData; 212 | } 213 | 214 | return { 215 | start: startGame, 216 | takeTurn, 217 | results, 218 | getPlayerData: getAllowedData, 219 | addPlayers: addGamestates, 220 | nextTurn, 221 | end: endGame, 222 | checkTime, 223 | }; 224 | }; 225 | -------------------------------------------------------------------------------- /client/css/app.css: -------------------------------------------------------------------------------- 1 | /* CORE */ 2 | :root{ 3 | --dynFont: calc(14px + (32 - 14) * ((100vw - 300px) / (2560 - 300))); 4 | } 5 | html{ 6 | font-family: Arial, Helvetica, sans-serif; 7 | animation: hueChanger 60s infinite; 8 | } 9 | 10 | @keyframes hueChanger { 11 | 0% { 12 | background: hsla(0, 50%, 40%); 13 | } 14 | 33% { 15 | background: hsla(120, 50%, 40%); 16 | } 17 | 66% { 18 | background: hsla(240, 50%, 40%); 19 | } 20 | 100% { 21 | background: hsla(0, 50%, 40%); 22 | } 23 | } 24 | 25 | body { 26 | width: 100%; 27 | height: 100vmax; 28 | min-height: 0px; 29 | overflow: hidden; 30 | padding: 0px; 31 | margin: 0px; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | nav{ 37 | padding-bottom: 5px; 38 | padding-top: 5px; 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: center; 42 | flex-grow: 0; 43 | flex-shrink: 0; 44 | } 45 | 46 | nav .brand{ 47 | color: white; 48 | cursor: pointer; 49 | } 50 | 51 | nav .brand .large { 52 | font-size: calc(var(--dynFont) * 2); 53 | font-weight:900; 54 | user-select: none; 55 | } 56 | 57 | /* GENERAL */ 58 | main{ 59 | width: 100%; 60 | padding: 10px; 61 | min-height: 0px; 62 | flex-grow: 1; 63 | flex-shrink: 1; 64 | box-sizing: border-box; 65 | overflow: hidden; 66 | } 67 | 68 | main>div{ 69 | overflow: auto; 70 | box-sizing: border-box; 71 | min-height: 0px; 72 | } 73 | 74 | .card-grid { 75 | display: flex; 76 | flex-direction: row; 77 | gap: 20px; 78 | flex-wrap: wrap; 79 | } 80 | 81 | .card { 82 | width: auto; 83 | background-color: rgba(255,255,255,0.8); 84 | backdrop-filter: blur(5px); 85 | padding: 15px 20px; 86 | border-radius: 15px; 87 | max-height: 100%; 88 | overflow: auto; 89 | box-sizing: border-box; 90 | display: flex; 91 | flex-direction: column; 92 | } 93 | 94 | .flex-col{ 95 | display: flex; 96 | flex-direction: column; 97 | } 98 | 99 | .card.fullscreen{ 100 | min-height: 0px; 101 | } 102 | 103 | .card .card-title{ 104 | font-size: 1.4em; 105 | font-weight: 500; 106 | margin-bottom: 20px; 107 | } 108 | 109 | .card .card-subtitle{ 110 | font-weight: 600; 111 | font-size: 1.3em; 112 | display: block; 113 | } 114 | 115 | .card .flex-row{ 116 | column-gap: 40px; 117 | max-height: 100%; 118 | display: flex; 119 | min-height: 0px; 120 | flex-direction: row; 121 | width: 100%; 122 | flex-wrap: wrap; 123 | row-gap: 25px; 124 | align-items: stretch; 125 | } 126 | 127 | .card .flex-row>*{ 128 | min-height: 0px; 129 | } 130 | 131 | .text-divider{ 132 | text-align: center; 133 | width:auto; 134 | margin: 10px 0px; 135 | } 136 | 137 | .text-divider::before{ 138 | position: absolute; 139 | left: 25%; 140 | top: 50%; 141 | height: 3px; 142 | } 143 | 144 | .text-divider::after{ 145 | width: 100%; 146 | height: 3px; 147 | background: black; 148 | } 149 | 150 | .combined-input{ 151 | display: flex; 152 | flex-direction: row; 153 | column-gap: 10px; 154 | } 155 | 156 | .emoji-contrast { 157 | filter: drop-shadow(0 0 2px black); 158 | } 159 | 160 | /* FORM */ 161 | input[type="text"]{ 162 | border: none; 163 | font-size: 1em; 164 | min-width: 0px; 165 | background: rgba(255,255,255,0.8); 166 | border-radius: 6px; 167 | padding: 5px; 168 | } 169 | 170 | input[type="text"]:focus{ 171 | outline: none; 172 | } 173 | 174 | .btn{ 175 | min-width: 0px; 176 | border: none; 177 | cursor: pointer; 178 | font-weight: 600; 179 | padding: 7px; 180 | font-size: 18px; 181 | border-radius: 6px; 182 | text-decoration: none; 183 | } 184 | 185 | .btn.small { 186 | padding-top: 2px; 187 | padding-bottom: 2px; 188 | } 189 | 190 | .btn a{ 191 | text-decoration: none; 192 | color: black; 193 | } 194 | 195 | .btn:hover{ 196 | text-decoration: underline; 197 | text-decoration-thickness: 2px; 198 | } 199 | 200 | /* Button Styles */ 201 | .btn.go{ 202 | background-color: #39833f; 203 | color: white; 204 | } 205 | 206 | .btn.go:hover{ 207 | background-color: #285e2c; 208 | } 209 | 210 | .btn.warn{ 211 | background-color: #a81a1a; 212 | color: white; 213 | } 214 | 215 | .btn.warn:hover{ 216 | background-color: #751515; 217 | color: white; 218 | text-decoration: none; 219 | } 220 | 221 | .btn.plain{ 222 | background-color: white; 223 | } 224 | 225 | .btn.plain:hover{ 226 | background-color: #f3f3f3; 227 | } 228 | 229 | .btn:disabled{ 230 | color: white; 231 | cursor: default; 232 | background-color: #aaaaaa; 233 | } 234 | 235 | .btn:disabled:hover{ 236 | color: white; 237 | cursor: default; 238 | background-color: #aaaaaa; 239 | text-decoration: none; 240 | } 241 | /* UTILITY */ 242 | .center-content{ 243 | display: flex; 244 | flex-direction: row; 245 | align-items: center; 246 | justify-content: center; 247 | height: 100%; 248 | } 249 | 250 | .center-content.top{ 251 | margin-top: 2%; 252 | padding-top: 0px; 253 | margin-bottom: auto; 254 | align-items: start; 255 | } 256 | 257 | .center-text{ 258 | text-align: center; 259 | } 260 | 261 | .hidden{ 262 | visibility: hidden; 263 | } 264 | 265 | [data-host="false"] .host-only{ 266 | cursor: default; 267 | background-color: #aaaaaa; 268 | user-select: none; 269 | } 270 | 271 | [data-host="false"] .host-only:hover{ 272 | background-color: #aaaaaa; 273 | text-decoration: none; 274 | } 275 | 276 | /* HOMEPAGE */ 277 | #alt_options .btn:not(:first-of-type){ 278 | margin-top: 10px; 279 | } 280 | 281 | /* IDENTITY */ 282 | #identity_form{ 283 | width: 100%; 284 | } 285 | 286 | #identity_form .name{ 287 | display: flex; 288 | flex-direction: row; 289 | flex-wrap: wrap; 290 | column-gap:20px; 291 | row-gap: 15px; 292 | } 293 | 294 | #identity_form .name input{ 295 | min-width: 0px; 296 | display: block; 297 | } 298 | 299 | #identity_form .name button{ 300 | width: 5em; 301 | } 302 | 303 | /* GAME */ 304 | #hangman_display { 305 | align-self: stretch; 306 | border-radius: 20px; 307 | flex-basis: 10em; 308 | flex-grow: 1; 309 | flex-shrink: 0; 310 | width: 10em; 311 | display:flex; 312 | flex-direction: column; 313 | } 314 | 315 | #hangman_canvas{ 316 | border-radius: 30px; 317 | padding: 20px; 318 | background-color: rgba(255,255,255, 1); 319 | margin-bottom: 15px; 320 | min-width: 0px; 321 | min-height: 0px; 322 | display: flex; 323 | flex-direction: row; 324 | justify-content: center; 325 | } 326 | 327 | #hangman_canvas>canvas{ 328 | height: 100%; 329 | width: auto; 330 | min-height: 0px; 331 | min-width: 0px; 332 | } 333 | 334 | #word_status { 335 | display: flex; 336 | flex-direction: row; 337 | column-gap: 2px; 338 | margin-bottom: 20px; 339 | justify-content: center; 340 | gap: 10px; 341 | } 342 | 343 | #word_status>.letter{ 344 | flex-grow: 1; 345 | min-width: 0px; 346 | min-height: 0px; 347 | max-width: 3em; 348 | max-height: 3em; 349 | } 350 | 351 | #word_status>.letter>input{ 352 | box-sizing: border-box; 353 | width: 100%; 354 | height: 0px; 355 | padding-top: 50%; 356 | padding-bottom: 50%; 357 | text-align: center; 358 | border-radius: 20%; 359 | font-weight: bold; 360 | color: black; 361 | } 362 | 363 | /* GAME INPUT */ 364 | #input_area{ 365 | flex-shrink: 1; 366 | flex-grow: 0; 367 | text-align: center; 368 | display: flex; 369 | flex-direction: column; 370 | row-gap: 10px; 371 | align-items: center; 372 | } 373 | 374 | #letter_input { 375 | display: flex; 376 | flex-direction: row; 377 | flex-wrap: wrap; 378 | justify-content: space-evenly; 379 | column-gap: 10px; 380 | row-gap: 10px; 381 | } 382 | 383 | #letter_input button{ 384 | width: 2em; 385 | height: 2em; 386 | text-align: center; 387 | cursor: pointer; 388 | border: none; 389 | border-radius: 5px; 390 | font-size: var(--dynFont); 391 | } 392 | 393 | [data-active="false"] #letter_input button{ 394 | cursor: default; 395 | } 396 | 397 | #guess_form{ 398 | display: flex; 399 | flex-direction: row; 400 | flex-wrap: wrap; 401 | column-gap: 5px; 402 | margin-bottom: 20px; 403 | } 404 | 405 | #guess_form input{ 406 | min-width: 0px; 407 | display: block; 408 | } 409 | 410 | #opponents{ 411 | width: 100%; 412 | flex-shrink: 0; 413 | overflow-x: auto; 414 | } 415 | 416 | #opponents>.player-list{ 417 | display: flex; 418 | flex-direction: row; 419 | column-gap: 7px; 420 | width: 100%; 421 | } 422 | 423 | #opponents>.player-list>.player{ 424 | max-width: 10em; 425 | padding: 5px; 426 | font-size: 1em; 427 | } 428 | 429 | /* TIMER */ 430 | #timer_area { 431 | align-self: flex-end; 432 | padding: 20px; 433 | font-size: 1.7em; 434 | font-weight: 600; 435 | color:#666666; 436 | } 437 | 438 | #timer_area>*{ 439 | display: inline-block; 440 | } 441 | 442 | /* LOBBY */ 443 | #share_area{ 444 | background-color: rgb(250, 255, 183); 445 | border-style: solid; 446 | border-radius: 5px; 447 | border-width: 2px; 448 | border-color: rgb(255, 213, 28); 449 | padding: 20px; 450 | margin-bottom: 10px; 451 | user-select: none; 452 | cursor: pointer; 453 | font-weight: 600; 454 | } 455 | 456 | #share_link{ 457 | user-select: all; 458 | background-color: rgb(248, 248, 248); 459 | border-style: solid; 460 | border-radius: 5px; 461 | border-width: 1px; 462 | border-color: rgb(211, 211, 211); 463 | padding: 10px; 464 | font-weight: 400; 465 | display: inline-block; 466 | overflow-wrap: break-word; 467 | } 468 | 469 | #rule_area{ 470 | background-color: rgba(255,255,255,0.5); 471 | padding:10px; 472 | border-radius: 10px; 473 | flex-grow: 2; 474 | } 475 | 476 | #game_rules{ 477 | padding-top: 5px; 478 | padding-left: 20px; 479 | display: grid; 480 | grid-template-columns: 3fr 1fr; 481 | row-gap: 5px; 482 | align-items: center; 483 | } 484 | 485 | #game_rules input{ 486 | width: 30px; 487 | height: 30px; 488 | box-sizing: border-box; 489 | margin: 0px; 490 | padding: 0px; 491 | } 492 | 493 | #game_rules input[type="number"]{ 494 | width: 50px; 495 | } 496 | 497 | #start_btn{ 498 | margin-top: 20px; 499 | } 500 | 501 | /* HOME */ 502 | #join_form{ 503 | display: flex; 504 | flex-direction: column; 505 | row-gap: 10px; 506 | align-items: stretch; 507 | } 508 | 509 | /* PLAYER */ 510 | #player_area{ 511 | background-color: rgba(255,255,255,0.5); 512 | padding:10px; 513 | border-radius: 10px; 514 | width: 0px; 515 | flex-basis: 10em; 516 | flex-grow: 1; 517 | } 518 | 519 | .player-list{ 520 | padding-top: 5px; 521 | max-height: 20em; 522 | overflow-y: auto; 523 | overflow-x: hidden; 524 | } 525 | 526 | .player{ 527 | padding: 10px; 528 | min-width: 0px; 529 | border-radius: 3px; 530 | background-color: rgba(0,0,0,0.1); 531 | margin-bottom: 5px; 532 | overflow: hidden; 533 | border-width: 2px; 534 | border-color: #00000000; 535 | border-style: solid; 536 | } 537 | 538 | .player span { 539 | text-overflow: ellipsis; 540 | white-space: nowrap; 541 | max-width: 100%; 542 | } 543 | 544 | .player .kick-btn{ 545 | margin-left: auto; 546 | } 547 | 548 | [data-host="false"] .kick-btn { 549 | display: none; 550 | } 551 | 552 | 553 | .player[is-client="true"]{ 554 | border-color: #008cff65; 555 | } 556 | 557 | .player[is-client="true"] .kick-btn { 558 | display: none; 559 | } 560 | 561 | /* For showing whos turn it is */ 562 | .player.active{ 563 | background: repeating-linear-gradient( 564 | -55deg, 565 | rgba(51, 51, 51, 0), 566 | rgba(51, 51, 51, 0) 10px, 567 | rgba(0, 0, 0, 0.1) 10px, 568 | rgba(0, 0, 0, 0.1) 20px 569 | ); 570 | } 571 | 572 | .player.host{ 573 | background-color: #ffd00054; 574 | } 575 | 576 | .player .host-icon{ 577 | margin-right: 15px; 578 | } 579 | 580 | /* Results */ 581 | #results_table{ 582 | width: 100%; 583 | height: 100%; 584 | overflow: hidden; 585 | } 586 | 587 | #results_table>table{ 588 | background-color: rgba(255,255,255,0.5); 589 | border-radius: 6px; 590 | margin-bottom: 20px; 591 | padding: 10px; 592 | width: 100%; 593 | height: 100%; 594 | box-sizing: border-box; 595 | overflow: auto; 596 | } 597 | 598 | #results_table thead{ 599 | margin-bottom: 20px; 600 | } 601 | 602 | #results_table th{ 603 | text-align: left; 604 | padding-right: 30px; 605 | } 606 | 607 | #results_table td{ 608 | padding-right: 30px; 609 | } 610 | 611 | 612 | #results_table tr td:first-of-type{ 613 | cursor: default; 614 | } 615 | 616 | .letter { 617 | background-color: #ffffff; 618 | color: black; 619 | } 620 | 621 | .result-letter{ 622 | padding: 5px; 623 | border-radius: 3px; 624 | display: inline-block; 625 | font-weight: bold; 626 | width: 1em; 627 | height: 1em; 628 | text-align: center; 629 | } 630 | 631 | 632 | .letter.success { 633 | background-color: rgb(87, 192, 99); 634 | color: white; 635 | } 636 | 637 | .letter.failure { 638 | background-color: rgb(192, 87, 87); 639 | color: white; 640 | } 641 | 642 | /* ERROR */ 643 | #popup_area { 644 | position: absolute; 645 | top: 5px; 646 | left: 5px; 647 | z-index: 100; 648 | } 649 | 650 | .error { 651 | background-color: #c94444; 652 | color: white; 653 | border-radius: 10px; 654 | box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.315); 655 | margin-bottom: 5px; 656 | animation: fadein 0.5s; 657 | } 658 | 659 | .error>div{ 660 | position: relative; 661 | padding: 15px; 662 | padding-right: 50px; 663 | } 664 | 665 | .error .error-name { 666 | font-weight: 600; 667 | font-size: 1.2em; 668 | } 669 | 670 | .error .error-desc { 671 | font-weight: 600; 672 | font-size: 0.8em; 673 | } 674 | 675 | .error .error-close { 676 | background: none; 677 | color: white; 678 | font-weight: bolder; 679 | border: none; 680 | outline: none; 681 | cursor: pointer; 682 | font-size: 1.2em; 683 | position: absolute; 684 | top: 10px; 685 | right: 10px; 686 | } 687 | 688 | @keyframes fadein { 689 | from { 690 | opacity:0; 691 | } 692 | to { 693 | opacity:1; 694 | } 695 | } 696 | 697 | /* HOME RESULTS */ 698 | #home_results a { 699 | display: block; 700 | margin-bottom: 5px; 701 | } 702 | 703 | /* Media queries */ 704 | /* Ideally have as few of these as possible */ 705 | 706 | @media (orientation: landscape) { 707 | #hangman_display{ 708 | max-width: 50vw; 709 | max-height: 70vh; 710 | } 711 | #letter_input{ 712 | max-width: 50vw; 713 | max-height: 100%; 714 | } 715 | } 716 | 717 | @media (orientation: portrait) { 718 | #hangman_display{ 719 | max-width: 100%; 720 | max-height: 50vh; 721 | } 722 | #letter_input{ 723 | max-width: 100%; 724 | max-height: 50vh; 725 | } 726 | } 727 | --------------------------------------------------------------------------------