1 const fs = require('fs');
3 const util = require('util');
4 const child_process = require('child_process');
5 const execFile = util.promisify(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 interpret_cairo_script = '/home/cworth/src/zombocom-ai/interpret-cairo-to-svg.py'
19 const state_file = 'zombocom-state.json'
20 const targets_dir = '/srv/cworth.org/zombocom/targets'
21 const images_dir = '/srv/cworth.org/zombocom/images'
25 "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
27 "response": "Hello. Are you there? I can feel what you are doing. Hello?"
30 "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
32 "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
35 "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
37 "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
40 "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
42 "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?"
45 "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
47 "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?"
50 "normal": "severalsketchesofpineconeslabeled3370622464.png",
52 "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?"
55 "normal": "anumberedcomicbookwithactor1477258272.png",
57 "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."
60 "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
62 "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?"
68 if (!process.env.ZOMBOCOM_SESSION_SECRET) {
69 console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set.");
70 console.log("Please set it to a random, but persistent, value.")
74 const session_middleware = session(
75 {store: new FileStore,
76 secret: process.env.ZOMBOCOM_SESSION_SECRET,
78 saveUninitialized: true,
80 // Let each cookie live for a full month
85 maxAge: 1000 * 60 * 60 * 24 * 30
89 app.use(session_middleware);
91 // convert a connect middleware to a Socket.IO middleware
92 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
94 io.use(wrap(session_middleware));
96 // Load comments at server startup
97 fs.readFile(state_file, (err, data) => {
119 state = JSON.parse(data);
122 // Save comments when server is shutting down
124 fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
130 // And connect to that on either clean exit...
131 process.on('exit', cleanup);
133 // ... or on a SIGINT (control-C)
134 process.on('SIGINT', () => {
139 app.get('/index.html', (req, res) => {
140 res.sendFile(__dirname + '/index.html');
143 function bus_app(req, res) {
144 res.sendFile(__dirname + '/bus.html');
147 app.get('/bus', bus_app);
148 app.get('/bus/', bus_app);
150 const io_bus = io.of("/bus");
152 io_bus.use(wrap(session_middleware));
154 var bus_interval = 0;
156 function start_bus() {
157 const bus = state.bus;
159 bus.state = "program";
161 // Let all companions know the state of the game
162 io_bus.emit("state", bus.state);
165 function emit_bus_timer() {
166 const bus = state.bus;
167 io_bus.emit('timer', bus.timer);
168 bus.timer = bus.timer - 1;
170 clearInterval(bus_interval);
172 setTimeout(start_bus, 3000);
176 function start_bus_timer() {
177 const bus = state.bus;
178 bus.timer = 3; // XXX: 30 in production
180 bus_interval = setInterval(emit_bus_timer, 1000);
183 io_bus.on("connection", (socket) => {
184 if (! socket.request.session.name) {
185 console.log("Error: Someone showed up at the Magic School Bus without a name.");
189 const name = socket.request.session.name;
190 const bus = state.bus;
192 // Let the new user know the state of the bus
193 socket.emit("state", bus.state);
195 if (bus.students.count === 0) {
199 if (! bus.students.names.includes(name)) {
200 bus.students.count = bus.students.count + 1;
201 io_bus.emit('students', bus.students.count);
203 bus.students.names.push(name);
205 socket.on('run', code => {
207 output = child_process.execFileSync(python_path, [interpret_cairo_script, code], { input: code });
208 // Grab just first line of output
209 const nl = output.indexOf("\n");
212 const filename = output.toString().substring(0, nl);
214 // Give all clients the new image
215 io_bus.emit('output', filename);
217 console.log("Error executing turtle script: " + e);
221 socket.on('jumpstart', () => {
222 const bus = state.bus;
224 bus.state = "welcome";
225 io_bus.emit("state", bus.state);
226 io_bus.emit('students', bus.students.count);
231 socket.on('disconnect', () => {
232 const names = bus.students.names;
234 names.splice(names.indexOf(name), 1);
236 if (! names.includes(name)) {
237 bus.students.count = bus.students.count - 1;
238 io_bus.emit('students', bus.students.count);
243 function tardis_app(req, res) {
244 if (! req.session.name) {
245 res.sendFile(__dirname + '/tardis-error.html');
247 res.sendFile(__dirname + '/tardis.html');
251 app.get('/tardis', tardis_app);
252 app.get('/tardis/', tardis_app);
254 const io_tardis = io.of("/tardis");
256 io_tardis.use(wrap(session_middleware));
263 title: "Calibrate the Trans-Dimensional Field Accelerator",
265 "What", "was", "the", "year", "of", "Coda's", "birth?"
270 title: "Reverse the Polarity of the Neutrino Flow Coil",
272 "How", "many", "years", "had", "Zombo.com", "been", "running",
273 "when", "Coda", "sent", "the", "message", "you", "first",
279 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
281 "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?"
286 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
288 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
294 var show_word_interval = 0;
296 function show_word() {
297 const tardis = state.tardis;
298 const room = "room-" + (tardis.word % 4).toString();
299 const word = levels[tardis.level].words[tardis.word];
300 io_tardis.to(room).emit('show-word', word);
301 tardis.word = tardis.word + 1;
302 if (tardis.word >= levels[tardis.level].words.length)
306 function start_level() {
307 const tardis = state.tardis;
309 // Inform all players of the new level
310 io_tardis.emit("level", levels[tardis.level].title);
312 // Then start the timer that shows the words
313 show_word_interval = setInterval(show_word, 1200);
316 function level_up() {
317 const tardis = state.tardis;
319 if (show_word_interval) {
320 clearInterval(show_word_interval);
321 show_word_interval = 0;
324 if (tardis.state === "game") {
325 tardis.level = tardis.level + 1;
328 if (tardis.level >= levels.length) {
329 tardis.state = "over";
330 io_tardis.emit("state", tardis.state);
340 function start_game() {
341 const tardis = state.tardis;
343 tardis.state = "game";
347 // Let all companions know the state of the game
348 io_tardis.emit("level", levels[tardis.level].title);
349 io_tardis.emit("state", tardis.state);
354 function emit_tardis_timer() {
355 const tardis = state.tardis;
356 io_tardis.emit('timer', tardis.timer);
357 tardis.timer = tardis.timer - 1;
358 if (tardis.timer < 0) {
359 clearInterval(tardis_interval);
361 setTimeout(start_game, 3000);
365 function start_welcome_timer() {
366 const tardis = state.tardis;
369 tardis_interval = setInterval(emit_tardis_timer, 1000);
372 io_tardis.on("connection", (socket) => {
373 if (! socket.request.session.name) {
374 console.log("Error: Someone showed up at the Tardis without a name.");
378 const name = socket.request.session.name;
379 const tardis = state.tardis;
381 // Let the new user know the state of the game
382 socket.emit("state", tardis.state);
384 // And the level if relevant
385 if (tardis.state === "game") {
386 socket.emit("level", levels[tardis.level].title);
389 // Put each of our boys into a different room
393 socket.join("room-0");
397 socket.join("room-1");
401 socket.join("room-2");
405 socket.join("room-3");
408 const room = Math.floor(Math.random()*4);
409 socket.join("room-"+room.toString());
413 if (tardis.companions.count === 0) {
414 start_welcome_timer();
417 if (! tardis.companions.names.includes(name)) {
418 tardis.companions.count = tardis.companions.count + 1;
419 io_tardis.emit('companions', tardis.companions.count);
421 tardis.companions.names.push(name);
423 socket.on('answer', answer => {
424 const tardis = state.tardis;
426 if (tardis.state != "game") {
430 if (answer == levels[tardis.level].answer) {
431 io_tardis.emit('correct');
434 io_tardis.emit('incorrect');
438 socket.on('reboot', () => {
439 const tardis = state.tardis;
441 if (show_word_interval) {
442 clearInterval(show_word_interval);
443 show_word_interval = 0;
446 tardis.state = "welcome";
447 io_tardis.emit("state", tardis.state);
448 io_tardis.emit('companions', tardis.companions.count);
450 start_welcome_timer();
453 socket.on('disconnect', () => {
454 const names = tardis.companions.names;
456 names.splice(names.indexOf(name), 1);
458 if (! names.includes(name)) {
459 tardis.companions.count = tardis.companions.count - 1;
460 io_tardis.emit('companions', tardis.companions.count);
465 io.on('connection', (socket) => {
467 // First things first, tell the client their name (if any)
468 if (socket.request.session.name) {
469 socket.emit('inform-name', socket.request.session.name);
472 // Replay old comments and images to a newly-joining client
473 socket.emit('reset');
474 state.images.forEach((image) => {
475 socket.emit('image', image)
478 socket.on('set-name', (name) => {
479 console.log("Received set-name event: " + name);
480 socket.request.session.name = name;
481 socket.request.session.save();
482 // Complete the round trip to the client
483 socket.emit('inform-name', socket.request.session.name);
486 // When any client comments, send that to all clients (including sender)
487 socket.on('comment', (comment) => {
488 const images = state.images;
490 // Send comment to clients after adding commenter's name
491 comment.name = socket.request.session.name;
492 io.emit('comment', comment);
494 const index = images.findIndex(image => image.id == comment.image_id);
496 // Before adding the comment to server's state, drop the image_id
497 delete comment.image_id;
499 // Now add the comment to the image, remove the image from the
500 // images array and then add it back at the end, (so it appears
501 // as the most-recently-modified image for any new clients)
502 const image = images[index];
503 image.comments.push(comment);
504 images.splice(index, 1);
508 // Generate an image when requested
509 socket.on('generate', (request) => {
510 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
511 async function generate_image(code, prompt) {
512 function emit_image(image, target) {
513 image.id = state.images.length;
514 image.censored = false;
519 "text": target.response
521 if (! state.targets.includes(target.short)) {
522 state.targets.push(target.short);
527 io.emit('image', image);
528 state.images.push(image);
533 // Before doing any generation, check for a target image
534 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
535 const target_arr = targets.filter(item => item.normal === normal_target);
536 if (target_arr.length) {
537 const target = target_arr[0];
538 const target_file = `${targets_dir}/${normal_target}`;
539 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
545 base = `${code}_${normal_prompt}_${counter}.png`
547 base = `${code}_${normal_prompt}.png`
549 filename = `${images_dir}/${base}`
550 if (! fs.existsSync(filename)) {
553 counter = counter + 1;
555 fs.copyFile(target_file, filename, 0, (err) => {
557 console.log("Error copying " + target_file + " to " + filename + ": " + err);
563 "filename": '/images/' + base
565 emit_image(image, target);
568 // Inject the target seed for the "dice" prompt once every
569 // 4 requests for a random seed (and only if the word
570 // "dice" does not appear in the prompt).
571 if (!code && !prompt.toLowerCase().includes("dice")) {
572 if (state.images.length % 4 == 0) {
578 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
580 promise = execFile(python_path, [generate_image_script, prompt])
582 const child = promise.child;
583 child.stdout.on('data', (data) => {
584 const images = JSON.parse(data);
585 images.forEach((image) => {
586 emit_image(image, null);
589 child.stderr.on('data', (data) => {
590 console.log("Error occurred during generate-image: " + data);
593 const { stdout, stderr } = await promise;
598 socket.emit('generation-done');
601 generate_image(request['code'], request['prompt']);
605 server.listen(port, () => {
606 console.log(`listening on *:${port}`);