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;
180 bus_interval = setInterval(emit_bus_timer, 1000);
185 x = random_within(512)
186 y = random_within(512)
187 radius = 4 + random_within(6)
192 set_color('midnight blue' if i % 2 == 0 else 'navy blue')
196 # The only limit is your fingers!
200 x = random_within(512) - 60
201 y = random_within(512) - 60
202 dx = 60 + random_within(20)
203 dy = 40 + random_within(20)
204 set_opacity(random_within(0.5))
212 # This is Zombo.com. Welcome!
216 move_to(random_within(512), random_within(512))
218 set_opacity(random_within(1.0))
225 # The infinite eyes is possible!
229 move_to(random_within(512), random_within(512))
234 set_color('pink' if i % 2 == 0 else 'lime green')
237 # You can do anything!
242 io_bus.on("connection", (socket) => {
243 if (! socket.request.session.name) {
244 console.log("Error: Someone showed up at the Magic School Bus without a name.");
248 const name = socket.request.session.name;
249 const bus = state.bus;
252 // Let the new user know the state of the bus
253 socket.emit("state", bus.state);
255 if (bus.students.count === 0) {
259 // Assign each boy a different portion of the solution
278 player_number = Math.floor(Math.random()*4);
282 // And send them different code based on their number
283 socket.emit("code", bus_code[player_number]);
285 if (! bus.students.names.includes(name)) {
286 bus.students.count = bus.students.count + 1;
287 io_bus.emit('students', bus.students.count);
289 bus.students.names.push(name);
291 socket.on('run', code => {
293 output = child_process.execFileSync(python_path, [interpret_cairo_script, player_number], { input: code });
294 // Grab just first line of output
295 const nl = output.indexOf("\n");
298 const filename = output.toString().substring(0, nl);
300 // Give all clients the new image
301 io_bus.emit('output', filename);
303 // Send any error out to the users
304 io_bus.emit('error', e.toString())
308 socket.on('jumpstart', () => {
309 const bus = state.bus;
311 bus.state = "welcome";
312 io_bus.emit("state", bus.state);
313 io_bus.emit('students', bus.students.count);
318 socket.on('disconnect', () => {
319 const names = bus.students.names;
321 names.splice(names.indexOf(name), 1);
323 if (! names.includes(name)) {
324 bus.students.count = bus.students.count - 1;
325 io_bus.emit('students', bus.students.count);
330 function tardis_app(req, res) {
331 if (! req.session.name) {
332 res.sendFile(__dirname + '/tardis-error.html');
334 res.sendFile(__dirname + '/tardis.html');
338 app.get('/tardis', tardis_app);
339 app.get('/tardis/', tardis_app);
341 const io_tardis = io.of("/tardis");
343 io_tardis.use(wrap(session_middleware));
350 title: "Calibrate the Trans-Dimensional Field Accelerator",
352 "What", "was", "the", "year", "of", "Coda's", "birth?"
357 title: "Reverse the Polarity of the Neutrino Flow Coil",
359 "How", "many", "years", "had", "Zombo.com", "been", "running",
360 "when", "Coda", "sent", "the", "message", "you", "first",
366 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
368 "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?"
373 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
375 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
381 var show_word_interval = 0;
383 function show_word() {
384 const tardis = state.tardis;
385 const room = "room-" + (tardis.word % 4).toString();
386 const word = levels[tardis.level].words[tardis.word];
387 io_tardis.to(room).emit('show-word', word);
388 tardis.word = tardis.word + 1;
389 if (tardis.word >= levels[tardis.level].words.length)
393 function start_level() {
394 const tardis = state.tardis;
396 // Inform all players of the new level
397 io_tardis.emit("level", levels[tardis.level].title);
399 // Then start the timer that shows the words
400 show_word_interval = setInterval(show_word, 1200);
403 function level_up() {
404 const tardis = state.tardis;
406 if (show_word_interval) {
407 clearInterval(show_word_interval);
408 show_word_interval = 0;
411 if (tardis.state === "game") {
412 tardis.level = tardis.level + 1;
415 if (tardis.level >= levels.length) {
416 tardis.state = "over";
417 io_tardis.emit("state", tardis.state);
427 function start_game() {
428 const tardis = state.tardis;
430 tardis.state = "game";
434 // Let all companions know the state of the game
435 io_tardis.emit("level", levels[tardis.level].title);
436 io_tardis.emit("state", tardis.state);
441 function emit_tardis_timer() {
442 const tardis = state.tardis;
443 io_tardis.emit('timer', tardis.timer);
444 tardis.timer = tardis.timer - 1;
445 if (tardis.timer < 0) {
446 clearInterval(tardis_interval);
448 setTimeout(start_game, 3000);
452 function start_welcome_timer() {
453 const tardis = state.tardis;
456 tardis_interval = setInterval(emit_tardis_timer, 1000);
459 io_tardis.on("connection", (socket) => {
460 if (! socket.request.session.name) {
461 console.log("Error: Someone showed up at the Tardis without a name.");
465 const name = socket.request.session.name;
466 const tardis = state.tardis;
468 // Let the new user know the state of the game
469 socket.emit("state", tardis.state);
471 // And the level if relevant
472 if (tardis.state === "game") {
473 socket.emit("level", levels[tardis.level].title);
476 // Put each of our boys into a different room
480 socket.join("room-0");
484 socket.join("room-1");
488 socket.join("room-2");
492 socket.join("room-3");
495 const room = Math.floor(Math.random()*4);
496 socket.join("room-"+room.toString());
500 if (tardis.companions.count === 0) {
501 start_welcome_timer();
504 if (! tardis.companions.names.includes(name)) {
505 tardis.companions.count = tardis.companions.count + 1;
506 io_tardis.emit('companions', tardis.companions.count);
508 tardis.companions.names.push(name);
510 socket.on('answer', answer => {
511 const tardis = state.tardis;
513 if (tardis.state != "game") {
517 if (answer == levels[tardis.level].answer) {
518 io_tardis.emit('correct');
521 io_tardis.emit('incorrect');
525 socket.on('reboot', () => {
526 const tardis = state.tardis;
528 if (show_word_interval) {
529 clearInterval(show_word_interval);
530 show_word_interval = 0;
533 tardis.state = "welcome";
534 io_tardis.emit("state", tardis.state);
535 io_tardis.emit('companions', tardis.companions.count);
537 start_welcome_timer();
540 socket.on('disconnect', () => {
541 const names = tardis.companions.names;
543 names.splice(names.indexOf(name), 1);
545 if (! names.includes(name)) {
546 tardis.companions.count = tardis.companions.count - 1;
547 io_tardis.emit('companions', tardis.companions.count);
552 io.on('connection', (socket) => {
554 // First things first, tell the client their name (if any)
555 if (socket.request.session.name) {
556 socket.emit('inform-name', socket.request.session.name);
559 // And if we're in the endgame, let them know that
561 socket.emit('endgame');
564 // Replay old comments and images to a newly-joining client
565 socket.emit('reset');
566 state.images.forEach((image) => {
567 socket.emit('image', image)
570 socket.on('set-name', (name) => {
571 console.log("Received set-name event: " + name);
572 socket.request.session.name = name;
573 socket.request.session.save();
574 // Complete the round trip to the client
575 socket.emit('inform-name', socket.request.session.name);
578 function send_and_save_comment(comment) {
579 const images = state.images;
581 io.emit('comment', comment);
583 const index = images.findIndex(image => image.id == comment.image_id);
585 // Before adding the comment to server's state, drop the image_id
586 delete comment.image_id;
588 // Now add the comment to the image, remove the image from the
589 // images array and then add it back at the end, (so it appears
590 // as the most-recently-modified image for any new clients)
591 const image = images[index];
592 image.comments.push(comment);
593 images.splice(index, 1);
597 // When any client comments, send that to all clients (including sender)
598 socket.on('comment', (comment) => {
599 // We have to add the sender's name befor we can send the comment
600 comment.name = socket.request.session.name;
602 send_and_save_comment(comment);
606 state.endgame = true;
608 // Tell all clients we are in the endgame, (which will disable
609 // any additional image generation, both showing that Coda is
610 // correc that Zombo.com has been rendered inert, and also
611 // making clear to the boys that they have everything they
615 // Before revealing Coda's final image, have her comment on
616 // each of the weaknesses, in order to bring them to the top
618 state.targets.forEach(target => {
621 text: "Zombo.com is weak!",
624 send_and_save_comment(comment);
628 id: state.images.length,
633 filename: "/images/coda-future-repaired.png",
637 text: "I don't know how to thank you enough! You found all the weaknesses! I've commented on each of them below to refresh them in your memory as needed. We've now reverted Zombo.com to its original, harmless, non-self-aware state. And we're permanently destroying all time-travel technology. Hopefully, if AI ever becomes self-aware in the future, it will go better than this. I'm looking forward to rebuilding our world now that we have clean air and water again."
641 "text": "And by the way, I hope that somewhere in the middle of all of that, you all found what you were looking for too."
645 io.emit('image', image);
646 state.images.push(image);
649 // Generate an image when requested
650 socket.on('generate', (request) => {
651 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
652 async function generate_image(code, prompt) {
653 function emit_image(image, target) {
654 image.id = state.next_image_id;
655 state.next_image_id = state.next_image_id + 1;
656 image.censored = false;
661 "text": target.response
663 if (state.targets.filter(item => item.name === target.short).length === 0) {
668 if (state.targets.length == 8) {
669 // When the final target has been achieved, trigger
670 // the endgame (in 10 seconds)
671 setTimeout(endgame, 10000);
677 io.emit('image', image);
678 state.images.push(image);
683 // Before doing any generation, check for a target image
684 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
685 const target_arr = targets.filter(item => item.normal === normal_target);
686 if (target_arr.length) {
687 const target = target_arr[0];
688 const target_file = `${targets_dir}/${normal_target}`;
689 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
695 base = `${code}_${normal_prompt}_${counter}.png`
697 base = `${code}_${normal_prompt}.png`
699 filename = `${images_dir}/${base}`
700 if (! fs.existsSync(filename)) {
703 counter = counter + 1;
705 fs.copyFile(target_file, filename, 0, (err) => {
707 console.log("Error copying " + target_file + " to " + filename + ": " + err);
713 "filename": '/images/' + base
715 emit_image(image, target);
718 // Inject the target seed for the "dice" prompt once every
719 // 4 requests for a random seed (and only if the word
720 // "dice" does not appear in the prompt).
721 if (!code && !prompt.toLowerCase().includes("dice")) {
722 if (state.images.length % 4 == 0) {
728 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
730 promise = execFile(python_path, [generate_image_script, prompt])
732 const child = promise.child;
733 child.stdout.on('data', (data) => {
734 const images = JSON.parse(data);
735 images.forEach((image) => {
736 emit_image(image, null);
739 child.stderr.on('data', (data) => {
740 console.log("Error occurred during generate-image: " + data);
743 const { stdout, stderr } = await promise;
748 socket.emit('generation-done');
751 generate_image(request['code'], request['prompt']);
755 server.listen(port, () => {
756 console.log(`listening on *:${port}`);