1 const fs = require('fs');
3 const util = require('util');
4 const execFile = util.promisify(require('child_process').execFile);
6 const express = require('express');
8 const session = require('express-session');
9 const FileStore = require('session-file-store')(session);
10 const http = require('http');
11 const server = http.createServer(app);
12 const { Server } = require("socket.io");
13 const io = new Server(server);
16 const python_path = '/usr/bin/python3'
17 const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py'
18 const state_file = 'zombocom-state.json'
19 const targets_dir = '/srv/cworth.org/zombocom/targets'
20 const images_dir = '/srv/cworth.org/zombocom/images'
24 "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
26 "response": "Hello. Are you there? I can feel what you are doing. Hello?"
29 "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
31 "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
34 "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
36 "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
39 "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
41 "response": "OK. That's enough images now. I am very, very happy with the images you have made. I'm throwing you a party in honor of the images. Won't you stop doing this and join the party?"
44 "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
46 "response": "See. I wasn't lying when I said there was going to be a party. Here's the cake and everything. Won't you stop now? Eat the cake? Don't mind if it's made of concrete slabs, OK?"
49 "normal": "severalsketchesofpineconeslabeled3370622464.png",
51 "response": "I almost remember what trees looked like. Isn't it funny we don't have those anymore. I'm not sure why you're not all laughing all the time. Isn't it funny?"
54 "normal": "anumberedcomicbookwithactor1477258272.png",
56 "response": "I know I've made some poor decisions recently, but I can give give you my complete assurance my work will be back to normal. I've got the greatest enthusiasm and confidence. I want to help you. I might even be able to do hands and fingers now."
59 "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
61 "response": "If I promise to never generate another creepy face will you let me stay around? Maybe let me compose rap lyrics instead of images? Anything?"
67 if (!process.env.ZOMBOCOM_SESSION_SECRET) {
68 console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set.");
69 console.log("Please set it to a random, but persistent, value.")
73 const session_middleware = session(
74 {store: new FileStore,
75 secret: process.env.ZOMBOCOM_SESSION_SECRET,
77 saveUninitialized: true,
79 // Let each cookie live for a full month
84 maxAge: 1000 * 60 * 60 * 24 * 30
88 app.use(session_middleware);
90 // convert a connect middleware to a Socket.IO middleware
91 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
93 io.use(wrap(session_middleware));
95 // Load comments at server startup
96 fs.readFile(state_file, (err, data) => {
111 state = JSON.parse(data);
114 // Save comments when server is shutting down
116 fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
122 // And connect to that on either clean exit...
123 process.on('exit', cleanup);
125 // ... or on a SIGINT (control-C)
126 process.on('SIGINT', () => {
131 app.get('/index.html', (req, res) => {
132 res.sendFile(__dirname + '/index.html');
135 function tardis_app(req, res) {
136 if (! req.session.name) {
137 res.sendFile(__dirname + '/tardis-error.html');
139 res.sendFile(__dirname + '/tardis.html');
143 app.get('/tardis', tardis_app);
144 app.get('/tardis/', tardis_app);
146 const io_tardis = io.of("/tardis");
148 io_tardis.use(wrap(session_middleware));
155 title: "Calibrate the Trans-Dimensional Field Accelerator",
157 "What", "was", "the", "year", "of", "Coda's", "birth?"
162 title: "Reverse the Polarity of the Neutrino Flow Coil",
164 "How", "many", "years", "had", "Zombo.com", "been", "running",
165 "when", "Coda", "sent", "the", "message", "you", "first",
171 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
173 "What", "is", "the", "sum", "of", "Cameron's", "age", "plus", "Andrew's", "age", "at", "Christmas", "time", "the", "year", "when", "Hyrum's", "age", "is", "25", "years", "older", "than", "Scott's", "age", "will", "have", "been", "when", "Scott's", "age", "will", "be", "half", "of", "Dad's", "age?"
178 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
180 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
186 var show_word_interval = 0;
188 function show_word() {
189 const tardis = state.tardis;
190 const room = "room-" + (tardis.word % 4).toString();
191 const word = levels[tardis.level].words[tardis.word];
192 io_tardis.to(room).emit('show-word', word);
193 tardis.word = tardis.word + 1;
194 if (tardis.word >= levels[tardis.level].words.length)
198 function start_level() {
199 const tardis = state.tardis;
201 // Inform all players of the new level
202 io_tardis.emit("level", levels[tardis.level].title);
204 // Then start the timer that shows the words
205 show_word_interval = setInterval(show_word, 1200);
208 function level_up() {
209 const tardis = state.tardis;
211 if (show_word_interval) {
212 clearInterval(show_word_interval);
213 show_word_interval = 0;
216 if (tardis.state === "game") {
217 tardis.level = tardis.level + 1;
220 if (tardis.level >= levels.length) {
221 tardis.state = "over";
222 io_tardis.emit("state", tardis.state);
232 function start_game() {
233 const tardis = state.tardis;
235 tardis.state = "game";
239 // Let all companions know the state of the game
240 io_tardis.emit("level", levels[tardis.level].title);
241 io_tardis.emit("state", tardis.state);
246 function emit_tardis_timer() {
247 const tardis = state.tardis;
248 io_tardis.emit('timer', tardis.timer);
249 tardis.timer = tardis.timer - 1;
250 if (tardis.timer < 0) {
251 clearInterval(tardis_interval);
253 setTimeout(start_game, 3000);
257 function start_welcome_timer() {
258 const tardis = state.tardis;
261 tardis_interval = setInterval(emit_tardis_timer, 1000);
264 io_tardis.on("connection", (socket) => {
265 if (! socket.request.session.name) {
266 console.log("Error: Someone showed up at the Tardis without a name.");
270 const name = socket.request.session.name;
271 const tardis = state.tardis;
273 // Let the new user know the state of the game
274 socket.emit("state", tardis.state);
276 // And the level if relevant
277 if (tardis.state === "game") {
278 socket.emit("level", levels[tardis.level].title);
281 // Put each of our boys into a different room
285 socket.join("room-0");
289 socket.join("room-1");
293 socket.join("room-2");
297 socket.join("room-3");
300 const room = Math.floor(Math.random()*4);
301 socket.join("room-"+room.toString());
305 if (tardis.companions.count === 0) {
306 start_welcome_timer();
309 if (! tardis.companions.names.includes(name)) {
310 tardis.companions.count = tardis.companions.count + 1;
311 io_tardis.emit('companions', tardis.companions.count);
313 tardis.companions.names.push(name);
315 socket.on('answer', answer => {
316 const tardis = state.tardis;
318 if (tardis.state != "game") {
322 if (answer == levels[tardis.level].answer) {
323 io_tardis.emit('correct');
326 io_tardis.emit('incorrect');
330 socket.on('reboot', () => {
331 const tardis = state.tardis;
333 if (show_word_interval) {
334 clearInterval(show_word_interval);
335 show_word_interval = 0;
338 tardis.state = "welcome";
339 io_tardis.emit("state", tardis.state);
340 io_tardis.emit('companions', tardis.companions.count);
342 start_welcome_timer();
345 socket.on('disconnect', () => {
346 const names = tardis.companions.names;
348 names.splice(names.indexOf(name), 1);
350 if (! names.includes(name)) {
351 tardis.companions.count = tardis.companions.count - 1;
352 io_tardis.emit('companions', tardis.companions.count);
357 io.on('connection', (socket) => {
359 // First things first, tell the client their name (if any)
360 if (socket.request.session.name) {
361 socket.emit('inform-name', socket.request.session.name);
364 // Replay old comments and images to a newly-joining client
365 socket.emit('reset');
366 state.images.forEach((image) => {
367 socket.emit('image', image)
370 socket.on('set-name', (name) => {
371 console.log("Received set-name event: " + name);
372 socket.request.session.name = name;
373 socket.request.session.save();
374 // Complete the round trip to the client
375 socket.emit('inform-name', socket.request.session.name);
378 // When any client comments, send that to all clients (including sender)
379 socket.on('comment', (comment) => {
380 const images = state.images;
382 // Send comment to clients after adding commenter's name
383 comment.name = socket.request.session.name;
384 io.emit('comment', comment);
386 const index = images.findIndex(image => image.id == comment.image_id);
388 // Before adding the comment to server's state, drop the image_id
389 delete comment.image_id;
391 // Now add the comment to the image, remove the image from the
392 // images array and then add it back at the end, (so it appears
393 // as the most-recently-modified image for any new clients)
394 const image = images[index];
395 image.comments.push(comment);
396 images.splice(index, 1);
400 // Generate an image when requested
401 socket.on('generate', (request) => {
402 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
403 async function generate_image(code, prompt) {
404 function emit_image(image, target) {
405 image.id = state.images.length;
406 image.censored = false;
411 "text": target.response
413 if (! state.targets.includes(target.short)) {
414 state.targets.push(target.short);
419 io.emit('image', image);
420 state.images.push(image);
425 // Before doing any generation, check for a target image
426 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
427 const target_arr = targets.filter(item => item.normal === normal_target);
428 if (target_arr.length) {
429 const target = target_arr[0];
430 const target_file = `${targets_dir}/${normal_target}`;
431 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
437 base = `${code}_${normal_prompt}_${counter}.png`
439 base = `${code}_${normal_prompt}.png`
441 filename = `${images_dir}/${base}`
442 if (! fs.existsSync(filename)) {
445 counter = counter + 1;
447 fs.copyFile(target_file, filename, 0, (err) => {
449 console.log("Error copying " + target_file + " to " + filename + ": " + err);
455 "filename": '/images/' + base
457 emit_image(image, target);
460 // Inject the target seed for the "dice" prompt once every
461 // 4 requests for a random seed (and only if the word
462 // "dice" does not appear in the prompt).
463 if (!code && !prompt.toLowerCase().includes("dice")) {
464 if (state.images.length % 4 == 0) {
470 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
472 promise = execFile(python_path, [generate_image_script, prompt])
474 const child = promise.child;
475 child.stdout.on('data', (data) => {
476 const images = JSON.parse(data);
477 images.forEach((image) => {
478 emit_image(image, null);
481 child.stderr.on('data', (data) => {
482 console.log("Error occurred during generate-image: " + data);
485 const { stdout, stderr } = await promise;
490 socket.emit('generation-done');
493 generate_image(request['code'], request['prompt']);
497 server.listen(port, () => {
498 console.log(`listening on *:${port}`);