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 io_tardis.on("connection", (socket) => {
254 if (! socket.request.session.name) {
255 console.log("Error: Someone showed up at the Tardis without a name.");
259 const name = socket.request.session.name;
260 const tardis = state.tardis;
262 // Let the new user know the state of the game
263 socket.emit("state", tardis.state);
265 // Put each of our boys into a different room
269 socket.join("room-0");
273 socket.join("room-1");
277 socket.join("room-2");
281 socket.join("room-3");
284 const room = Math.floor(Math.random()*4);
285 socket.join("room-"+room.toString());
289 if (tardis.companions.count === 0) {
292 tardis_interval = setInterval(emit_tardis_timer, 1000);
295 if (! tardis.companions.names.includes(name)) {
296 tardis.companions.count = tardis.companions.count + 1;
297 io_tardis.emit('companions', tardis.companions.count);
299 tardis.companions.names.push(name);
301 socket.on('answer', answer => {
302 const tardis = state.tardis;
304 if (tardis.state != "game") {
308 if (answer == levels[tardis.level].answer) {
309 io_tardis.emit('correct');
312 io_tardis.emit('incorrect');
316 socket.on('disconnect', () => {
317 const names = tardis.companions.names;
319 names.splice(names.indexOf(name), 1);
321 if (! names.includes(name)) {
322 tardis.companions.count = tardis.companions.count - 1;
323 io_tardis.emit('companions', tardis.companions.count);
328 io.on('connection', (socket) => {
330 // First things first, tell the client their name (if any)
331 if (socket.request.session.name) {
332 socket.emit('inform-name', socket.request.session.name);
335 // Replay old comments and images to a newly-joining client
336 socket.emit('reset');
337 state.images.forEach((image) => {
338 socket.emit('image', image)
341 socket.on('set-name', (name) => {
342 console.log("Received set-name event: " + name);
343 socket.request.session.name = name;
344 socket.request.session.save();
345 // Complete the round trip to the client
346 socket.emit('inform-name', socket.request.session.name);
349 // When any client comments, send that to all clients (including sender)
350 socket.on('comment', (comment) => {
351 const images = state.images;
353 // Send comment to clients after adding commenter's name
354 comment.name = socket.request.session.name;
355 io.emit('comment', comment);
357 const index = images.findIndex(image => image.id == comment.image_id);
359 // Before adding the comment to server's state, drop the image_id
360 delete comment.image_id;
362 // Now add the comment to the image, remove the image from the
363 // images array and then add it back at the end, (so it appears
364 // as the most-recently-modified image for any new clients)
365 const image = images[index];
366 image.comments.push(comment);
367 images.splice(index, 1);
371 // Generate an image when requested
372 socket.on('generate', (request) => {
373 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
374 async function generate_image(code, prompt) {
375 function emit_image(image, target) {
376 image.id = state.images.length;
377 image.censored = false;
382 "text": target.response
384 if (! state.targets.includes(target.short)) {
385 state.targets.push(target.short);
390 io.emit('image', image);
391 state.images.push(image);
396 // Before doing any generation, check for a target image
397 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
398 const target_arr = targets.filter(item => item.normal === normal_target);
399 if (target_arr.length) {
400 const target = target_arr[0];
401 const target_file = `${targets_dir}/${normal_target}`;
402 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
408 base = `${code}_${normal_prompt}_${counter}.png`
410 base = `${code}_${normal_prompt}.png`
412 filename = `${images_dir}/${base}`
413 if (! fs.existsSync(filename)) {
416 counter = counter + 1;
418 fs.copyFile(target_file, filename, 0, (err) => {
420 console.log("Error copying " + target_file + " to " + filename + ": " + err);
426 "filename": '/images/' + base
428 emit_image(image, target);
431 // Inject the target seed for the "dice" prompt once every
432 // 4 requests for a random seed (and only if the word
433 // "dice" does not appear in the prompt).
434 if (!code && !prompt.toLowerCase().includes("dice")) {
435 if (state.images.length % 4 == 0) {
441 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
443 promise = execFile(python_path, [generate_image_script, prompt])
445 const child = promise.child;
446 child.stdout.on('data', (data) => {
447 const images = JSON.parse(data);
448 images.forEach((image) => {
449 emit_image(image, null);
452 child.stderr.on('data', (data) => {
453 console.log("Error occurred during generate-image: " + data);
456 const { stdout, stderr } = await promise;
461 socket.emit('generation-done');
464 generate_image(request['code'], request['prompt']);
468 server.listen(port, () => {
469 console.log(`listening on *:${port}`);