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) => {
112 state = JSON.parse(data);
115 // Save comments when server is shutting down
117 fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
123 // And connect to that on either clean exit...
124 process.on('exit', cleanup);
126 // ... or on a SIGINT (control-C)
127 process.on('SIGINT', () => {
132 app.get('/index.html', (req, res) => {
133 res.sendFile(__dirname + '/index.html');
136 function bus_app(req, res) {
137 res.sendFile(__dirname + '/bus.html');
140 app.get('/bus', bus_app);
141 app.get('/bus/', bus_app);
143 const io_bus = io.of("/bus");
145 io_bus.use(wrap(session_middleware));
147 var bus_interval = 0;
149 function start_bus() {
150 const bus = state.bus;
152 bus.state = "program";
154 // Let all companions know the state of the game
155 io_bus.emit("state", bus.state);
158 function emit_bus_timer() {
159 const bus = state.bus;
160 io_bus.emit('timer', bus.timer);
161 bus.timer = bus.timer - 1;
163 clearInterval(bus_interval);
165 setTimeout(start_bus, 3000);
169 function start_bus_timer() {
170 const bus = state.bus;
171 bus.timer = 3; // XXX: 30 in production
173 bus_interval = setInterval(emit_bus_timer, 1000);
176 io_bus.on("connection", (socket) => {
177 if (! socket.request.session.name) {
178 console.log("Error: Someone showed up at the Magic School Bus without a name.");
182 const name = socket.request.session.name;
183 const bus = state.bus;
185 // Let the new user know the state of the bus
186 socket.emit("state", bus.state);
188 if (bus.students.count === 0) {
192 if (! bus.students.names.includes(name)) {
193 bus.students.count = bus.students.count + 1;
194 io_bus.emit('students', bus.students.count);
196 bus.students.names.push(name);
198 socket.on('run', code => {
200 output = child_process.execFileSync(python_path, [interpret_cairo_script, code], { input: code });
201 // Grab just first line of output
202 const nl = output.indexOf("\n");
205 const filename = output.toString().substring(0, nl);
207 // Give all clients the new image
208 io_bus.emit('output', filename);
210 console.log("Error executing turtle script: " + e);
214 socket.on('jumpstart', () => {
215 const bus = state.bus;
217 bus.state = "welcome";
218 io_bus.emit("state", bus.state);
219 io_bus.emit('students', bus.students.count);
224 socket.on('disconnect', () => {
225 const names = bus.students.names;
227 names.splice(names.indexOf(name), 1);
229 if (! names.includes(name)) {
230 bus.students.count = bus.students.count - 1;
231 io_bus.emit('students', bus.students.count);
236 function tardis_app(req, res) {
237 if (! req.session.name) {
238 res.sendFile(__dirname + '/tardis-error.html');
240 res.sendFile(__dirname + '/tardis.html');
244 app.get('/tardis', tardis_app);
245 app.get('/tardis/', tardis_app);
247 const io_tardis = io.of("/tardis");
249 io_tardis.use(wrap(session_middleware));
256 title: "Calibrate the Trans-Dimensional Field Accelerator",
258 "What", "was", "the", "year", "of", "Coda's", "birth?"
263 title: "Reverse the Polarity of the Neutrino Flow Coil",
265 "How", "many", "years", "had", "Zombo.com", "been", "running",
266 "when", "Coda", "sent", "the", "message", "you", "first",
272 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
274 "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?"
279 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
281 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
287 var show_word_interval = 0;
289 function show_word() {
290 const tardis = state.tardis;
291 const room = "room-" + (tardis.word % 4).toString();
292 const word = levels[tardis.level].words[tardis.word];
293 io_tardis.to(room).emit('show-word', word);
294 tardis.word = tardis.word + 1;
295 if (tardis.word >= levels[tardis.level].words.length)
299 function start_level() {
300 const tardis = state.tardis;
302 // Inform all players of the new level
303 io_tardis.emit("level", levels[tardis.level].title);
305 // Then start the timer that shows the words
306 show_word_interval = setInterval(show_word, 1200);
309 function level_up() {
310 const tardis = state.tardis;
312 if (show_word_interval) {
313 clearInterval(show_word_interval);
314 show_word_interval = 0;
317 if (tardis.state === "game") {
318 tardis.level = tardis.level + 1;
321 if (tardis.level >= levels.length) {
322 tardis.state = "over";
323 io_tardis.emit("state", tardis.state);
333 function start_game() {
334 const tardis = state.tardis;
336 tardis.state = "game";
340 // Let all companions know the state of the game
341 io_tardis.emit("level", levels[tardis.level].title);
342 io_tardis.emit("state", tardis.state);
347 function emit_tardis_timer() {
348 const tardis = state.tardis;
349 io_tardis.emit('timer', tardis.timer);
350 tardis.timer = tardis.timer - 1;
351 if (tardis.timer < 0) {
352 clearInterval(tardis_interval);
354 setTimeout(start_game, 3000);
358 function start_welcome_timer() {
359 const tardis = state.tardis;
362 tardis_interval = setInterval(emit_tardis_timer, 1000);
365 io_tardis.on("connection", (socket) => {
366 if (! socket.request.session.name) {
367 console.log("Error: Someone showed up at the Tardis without a name.");
371 const name = socket.request.session.name;
372 const tardis = state.tardis;
374 // Let the new user know the state of the game
375 socket.emit("state", tardis.state);
377 // And the level if relevant
378 if (tardis.state === "game") {
379 socket.emit("level", levels[tardis.level].title);
382 // Put each of our boys into a different room
386 socket.join("room-0");
390 socket.join("room-1");
394 socket.join("room-2");
398 socket.join("room-3");
401 const room = Math.floor(Math.random()*4);
402 socket.join("room-"+room.toString());
406 if (tardis.companions.count === 0) {
407 start_welcome_timer();
410 if (! tardis.companions.names.includes(name)) {
411 tardis.companions.count = tardis.companions.count + 1;
412 io_tardis.emit('companions', tardis.companions.count);
414 tardis.companions.names.push(name);
416 socket.on('answer', answer => {
417 const tardis = state.tardis;
419 if (tardis.state != "game") {
423 if (answer == levels[tardis.level].answer) {
424 io_tardis.emit('correct');
427 io_tardis.emit('incorrect');
431 socket.on('reboot', () => {
432 const tardis = state.tardis;
434 if (show_word_interval) {
435 clearInterval(show_word_interval);
436 show_word_interval = 0;
439 tardis.state = "welcome";
440 io_tardis.emit("state", tardis.state);
441 io_tardis.emit('companions', tardis.companions.count);
443 start_welcome_timer();
446 socket.on('disconnect', () => {
447 const names = tardis.companions.names;
449 names.splice(names.indexOf(name), 1);
451 if (! names.includes(name)) {
452 tardis.companions.count = tardis.companions.count - 1;
453 io_tardis.emit('companions', tardis.companions.count);
458 io.on('connection', (socket) => {
460 // First things first, tell the client their name (if any)
461 if (socket.request.session.name) {
462 socket.emit('inform-name', socket.request.session.name);
465 // Replay old comments and images to a newly-joining client
466 socket.emit('reset');
467 state.images.forEach((image) => {
468 socket.emit('image', image)
471 socket.on('set-name', (name) => {
472 console.log("Received set-name event: " + name);
473 socket.request.session.name = name;
474 socket.request.session.save();
475 // Complete the round trip to the client
476 socket.emit('inform-name', socket.request.session.name);
479 // When any client comments, send that to all clients (including sender)
480 socket.on('comment', (comment) => {
481 const images = state.images;
483 // Send comment to clients after adding commenter's name
484 comment.name = socket.request.session.name;
485 io.emit('comment', comment);
487 const index = images.findIndex(image => image.id == comment.image_id);
489 // Before adding the comment to server's state, drop the image_id
490 delete comment.image_id;
492 // Now add the comment to the image, remove the image from the
493 // images array and then add it back at the end, (so it appears
494 // as the most-recently-modified image for any new clients)
495 const image = images[index];
496 image.comments.push(comment);
497 images.splice(index, 1);
501 // Generate an image when requested
502 socket.on('generate', (request) => {
503 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
504 async function generate_image(code, prompt) {
505 function emit_image(image, target) {
506 image.id = state.images.length;
507 image.censored = false;
512 "text": target.response
514 if (! state.targets.includes(target.short)) {
515 state.targets.push(target.short);
520 io.emit('image', image);
521 state.images.push(image);
526 // Before doing any generation, check for a target image
527 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
528 const target_arr = targets.filter(item => item.normal === normal_target);
529 if (target_arr.length) {
530 const target = target_arr[0];
531 const target_file = `${targets_dir}/${normal_target}`;
532 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
538 base = `${code}_${normal_prompt}_${counter}.png`
540 base = `${code}_${normal_prompt}.png`
542 filename = `${images_dir}/${base}`
543 if (! fs.existsSync(filename)) {
546 counter = counter + 1;
548 fs.copyFile(target_file, filename, 0, (err) => {
550 console.log("Error copying " + target_file + " to " + filename + ": " + err);
556 "filename": '/images/' + base
558 emit_image(image, target);
561 // Inject the target seed for the "dice" prompt once every
562 // 4 requests for a random seed (and only if the word
563 // "dice" does not appear in the prompt).
564 if (!code && !prompt.toLowerCase().includes("dice")) {
565 if (state.images.length % 4 == 0) {
571 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
573 promise = execFile(python_path, [generate_image_script, prompt])
575 const child = promise.child;
576 child.stdout.on('data', (data) => {
577 const images = JSON.parse(data);
578 images.forEach((image) => {
579 emit_image(image, null);
582 child.stderr.on('data', (data) => {
583 console.log("Error occurred during generate-image: " + data);
586 const { stdout, stderr } = await promise;
591 socket.emit('generation-done');
594 generate_image(request['code'], request['prompt']);
598 server.listen(port, () => {
599 console.log(`listening on *:${port}`);