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 res.sendFile(__dirname + '/tardis.html');
139 app.get('/tardis', tardis_app);
140 app.get('/tardis/', tardis_app);
142 const io_tardis = io.of("/tardis");
144 io_tardis.use(wrap(session_middleware));
151 title: "Calibrate the Trans-Dimensional Field Accelerator",
153 "What", "was", "the", "year", "of", "Coda's", "birth?"
158 title: "Reverse the Polarity of the Neutrino Flow Coil",
160 "How", "many", "years", "had", "Zombo.com", "been", "running",
161 "when", "Coda", "sent", "the", "message", "you", "first",
167 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
169 "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?"
174 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
176 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
182 var show_word_interval = 0;
184 function show_word() {
185 const tardis = state.tardis;
186 const room = "room-" + (tardis.word % 4).toString();
187 const word = levels[tardis.level].words[tardis.word];
188 io_tardis.to(room).emit('show-word', word);
189 tardis.word = tardis.word + 1;
190 if (tardis.word >= levels[tardis.level].words.length)
194 function start_level() {
195 const tardis = state.tardis;
197 // Inform all players of the new level
198 io_tardis.emit("level", levels[tardis.level].title);
200 // Then start the timer that shows the words
201 show_word_interval = setInterval(show_word, 1200);
204 function level_up() {
205 const tardis = state.tardis;
207 if (show_word_interval) {
208 clearInterval(show_word_interval);
209 show_word_interval = 0;
212 if (tardis.state === "game") {
213 tardis.level = tardis.level + 1;
216 if (tardis.level >= levels.length) {
217 tardis.state = "over";
218 io_tardis.emit("state", tardis.state);
228 function start_game() {
229 const tardis = state.tardis;
231 tardis.state = "game";
235 // Let all companions know the state of the game
236 io_tardis.emit("level", levels[tardis.level].title);
237 io_tardis.emit("state", tardis.state);
242 function emit_tardis_timer() {
243 const tardis = state.tardis;
244 io_tardis.emit('timer', tardis.timer);
245 tardis.timer = tardis.timer - 1;
246 if (tardis.timer < 0) {
247 clearInterval(tardis_interval);
249 setTimeout(start_game, 3000);
253 function start_welcome_timer() {
254 const tardis = state.tardis;
257 tardis_interval = setInterval(emit_tardis_timer, 1000);
260 io_tardis.on("connection", (socket) => {
261 if (! socket.request.session.name) {
262 console.log("Error: Someone showed up at the Tardis without a name.");
266 const name = socket.request.session.name;
267 const tardis = state.tardis;
269 // Let the new user know the state of the game
270 socket.emit("state", tardis.state);
272 // And the level if relevant
273 if (tardis.state === "game") {
274 socket.emit("level", levels[tardis.level].title);
277 // Put each of our boys into a different room
281 socket.join("room-0");
285 socket.join("room-1");
289 socket.join("room-2");
293 socket.join("room-3");
296 const room = Math.floor(Math.random()*4);
297 socket.join("room-"+room.toString());
301 if (tardis.companions.count === 0) {
302 start_welcome_timer();
305 if (! tardis.companions.names.includes(name)) {
306 tardis.companions.count = tardis.companions.count + 1;
307 io_tardis.emit('companions', tardis.companions.count);
309 tardis.companions.names.push(name);
311 socket.on('answer', answer => {
312 const tardis = state.tardis;
314 if (tardis.state != "game") {
318 if (answer == levels[tardis.level].answer) {
319 io_tardis.emit('correct');
322 io_tardis.emit('incorrect');
326 socket.on('reboot', () => {
327 const tardis = state.tardis;
329 if (show_word_interval) {
330 clearInterval(show_word_interval);
331 show_word_interval = 0;
334 tardis.state = "welcome";
335 io_tardis.emit("state", tardis.state);
336 io_tardis.emit('companions', tardis.companions.count);
338 start_welcome_timer();
341 socket.on('disconnect', () => {
342 const names = tardis.companions.names;
344 names.splice(names.indexOf(name), 1);
346 if (! names.includes(name)) {
347 tardis.companions.count = tardis.companions.count - 1;
348 io_tardis.emit('companions', tardis.companions.count);
353 io.on('connection', (socket) => {
355 // First things first, tell the client their name (if any)
356 if (socket.request.session.name) {
357 socket.emit('inform-name', socket.request.session.name);
360 // Replay old comments and images to a newly-joining client
361 socket.emit('reset');
362 state.images.forEach((image) => {
363 socket.emit('image', image)
366 socket.on('set-name', (name) => {
367 console.log("Received set-name event: " + name);
368 socket.request.session.name = name;
369 socket.request.session.save();
370 // Complete the round trip to the client
371 socket.emit('inform-name', socket.request.session.name);
374 // When any client comments, send that to all clients (including sender)
375 socket.on('comment', (comment) => {
376 const images = state.images;
378 // Send comment to clients after adding commenter's name
379 comment.name = socket.request.session.name;
380 io.emit('comment', comment);
382 const index = images.findIndex(image => image.id == comment.image_id);
384 // Before adding the comment to server's state, drop the image_id
385 delete comment.image_id;
387 // Now add the comment to the image, remove the image from the
388 // images array and then add it back at the end, (so it appears
389 // as the most-recently-modified image for any new clients)
390 const image = images[index];
391 image.comments.push(comment);
392 images.splice(index, 1);
396 // Generate an image when requested
397 socket.on('generate', (request) => {
398 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
399 async function generate_image(code, prompt) {
400 function emit_image(image, target) {
401 image.id = state.images.length;
402 image.censored = false;
407 "text": target.response
409 if (! state.targets.includes(target.short)) {
410 state.targets.push(target.short);
415 io.emit('image', image);
416 state.images.push(image);
421 // Before doing any generation, check for a target image
422 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
423 const target_arr = targets.filter(item => item.normal === normal_target);
424 if (target_arr.length) {
425 const target = target_arr[0];
426 const target_file = `${targets_dir}/${normal_target}`;
427 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
433 base = `${code}_${normal_prompt}_${counter}.png`
435 base = `${code}_${normal_prompt}.png`
437 filename = `${images_dir}/${base}`
438 if (! fs.existsSync(filename)) {
441 counter = counter + 1;
443 fs.copyFile(target_file, filename, 0, (err) => {
445 console.log("Error copying " + target_file + " to " + filename + ": " + err);
451 "filename": '/images/' + base
453 emit_image(image, target);
456 // Inject the target seed for the "dice" prompt once every
457 // 4 requests for a random seed (and only if the word
458 // "dice" does not appear in the prompt).
459 if (!code && !prompt.toLowerCase().includes("dice")) {
460 if (state.images.length % 4 == 0) {
466 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
468 promise = execFile(python_path, [generate_image_script, prompt])
470 const child = promise.child;
471 child.stdout.on('data', (data) => {
472 const images = JSON.parse(data);
473 images.forEach((image) => {
474 emit_image(image, null);
477 child.stderr.on('data', (data) => {
478 console.log("Error occurred during generate-image: " + data);
481 const { stdout, stderr } = await promise;
486 socket.emit('generation-done');
489 generate_image(request['code'], request['prompt']);
493 server.listen(port, () => {
494 console.log(`listening on *:${port}`);