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);
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 console.log("Error executing turtle script: " + e);
307 socket.on('jumpstart', () => {
308 const bus = state.bus;
310 bus.state = "welcome";
311 io_bus.emit("state", bus.state);
312 io_bus.emit('students', bus.students.count);
317 socket.on('disconnect', () => {
318 const names = bus.students.names;
320 names.splice(names.indexOf(name), 1);
322 if (! names.includes(name)) {
323 bus.students.count = bus.students.count - 1;
324 io_bus.emit('students', bus.students.count);
329 function tardis_app(req, res) {
330 if (! req.session.name) {
331 res.sendFile(__dirname + '/tardis-error.html');
333 res.sendFile(__dirname + '/tardis.html');
337 app.get('/tardis', tardis_app);
338 app.get('/tardis/', tardis_app);
340 const io_tardis = io.of("/tardis");
342 io_tardis.use(wrap(session_middleware));
349 title: "Calibrate the Trans-Dimensional Field Accelerator",
351 "What", "was", "the", "year", "of", "Coda's", "birth?"
356 title: "Reverse the Polarity of the Neutrino Flow Coil",
358 "How", "many", "years", "had", "Zombo.com", "been", "running",
359 "when", "Coda", "sent", "the", "message", "you", "first",
365 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
367 "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?"
372 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
374 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
380 var show_word_interval = 0;
382 function show_word() {
383 const tardis = state.tardis;
384 const room = "room-" + (tardis.word % 4).toString();
385 const word = levels[tardis.level].words[tardis.word];
386 io_tardis.to(room).emit('show-word', word);
387 tardis.word = tardis.word + 1;
388 if (tardis.word >= levels[tardis.level].words.length)
392 function start_level() {
393 const tardis = state.tardis;
395 // Inform all players of the new level
396 io_tardis.emit("level", levels[tardis.level].title);
398 // Then start the timer that shows the words
399 show_word_interval = setInterval(show_word, 1200);
402 function level_up() {
403 const tardis = state.tardis;
405 if (show_word_interval) {
406 clearInterval(show_word_interval);
407 show_word_interval = 0;
410 if (tardis.state === "game") {
411 tardis.level = tardis.level + 1;
414 if (tardis.level >= levels.length) {
415 tardis.state = "over";
416 io_tardis.emit("state", tardis.state);
426 function start_game() {
427 const tardis = state.tardis;
429 tardis.state = "game";
433 // Let all companions know the state of the game
434 io_tardis.emit("level", levels[tardis.level].title);
435 io_tardis.emit("state", tardis.state);
440 function emit_tardis_timer() {
441 const tardis = state.tardis;
442 io_tardis.emit('timer', tardis.timer);
443 tardis.timer = tardis.timer - 1;
444 if (tardis.timer < 0) {
445 clearInterval(tardis_interval);
447 setTimeout(start_game, 3000);
451 function start_welcome_timer() {
452 const tardis = state.tardis;
455 tardis_interval = setInterval(emit_tardis_timer, 1000);
458 io_tardis.on("connection", (socket) => {
459 if (! socket.request.session.name) {
460 console.log("Error: Someone showed up at the Tardis without a name.");
464 const name = socket.request.session.name;
465 const tardis = state.tardis;
467 // Let the new user know the state of the game
468 socket.emit("state", tardis.state);
470 // And the level if relevant
471 if (tardis.state === "game") {
472 socket.emit("level", levels[tardis.level].title);
475 // Put each of our boys into a different room
479 socket.join("room-0");
483 socket.join("room-1");
487 socket.join("room-2");
491 socket.join("room-3");
494 const room = Math.floor(Math.random()*4);
495 socket.join("room-"+room.toString());
499 if (tardis.companions.count === 0) {
500 start_welcome_timer();
503 if (! tardis.companions.names.includes(name)) {
504 tardis.companions.count = tardis.companions.count + 1;
505 io_tardis.emit('companions', tardis.companions.count);
507 tardis.companions.names.push(name);
509 socket.on('answer', answer => {
510 const tardis = state.tardis;
512 if (tardis.state != "game") {
516 if (answer == levels[tardis.level].answer) {
517 io_tardis.emit('correct');
520 io_tardis.emit('incorrect');
524 socket.on('reboot', () => {
525 const tardis = state.tardis;
527 if (show_word_interval) {
528 clearInterval(show_word_interval);
529 show_word_interval = 0;
532 tardis.state = "welcome";
533 io_tardis.emit("state", tardis.state);
534 io_tardis.emit('companions', tardis.companions.count);
536 start_welcome_timer();
539 socket.on('disconnect', () => {
540 const names = tardis.companions.names;
542 names.splice(names.indexOf(name), 1);
544 if (! names.includes(name)) {
545 tardis.companions.count = tardis.companions.count - 1;
546 io_tardis.emit('companions', tardis.companions.count);
551 io.on('connection', (socket) => {
553 // First things first, tell the client their name (if any)
554 if (socket.request.session.name) {
555 socket.emit('inform-name', socket.request.session.name);
558 // Replay old comments and images to a newly-joining client
559 socket.emit('reset');
560 state.images.forEach((image) => {
561 socket.emit('image', image)
564 socket.on('set-name', (name) => {
565 console.log("Received set-name event: " + name);
566 socket.request.session.name = name;
567 socket.request.session.save();
568 // Complete the round trip to the client
569 socket.emit('inform-name', socket.request.session.name);
572 // When any client comments, send that to all clients (including sender)
573 socket.on('comment', (comment) => {
574 const images = state.images;
576 // Send comment to clients after adding commenter's name
577 comment.name = socket.request.session.name;
578 io.emit('comment', comment);
580 const index = images.findIndex(image => image.id == comment.image_id);
582 // Before adding the comment to server's state, drop the image_id
583 delete comment.image_id;
585 // Now add the comment to the image, remove the image from the
586 // images array and then add it back at the end, (so it appears
587 // as the most-recently-modified image for any new clients)
588 const image = images[index];
589 image.comments.push(comment);
590 images.splice(index, 1);
594 // Generate an image when requested
595 socket.on('generate', (request) => {
596 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
597 async function generate_image(code, prompt) {
598 function emit_image(image, target) {
599 image.id = state.images.length;
600 image.censored = false;
605 "text": target.response
607 if (! state.targets.includes(target.short)) {
608 state.targets.push(target.short);
613 io.emit('image', image);
614 state.images.push(image);
619 // Before doing any generation, check for a target image
620 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
621 const target_arr = targets.filter(item => item.normal === normal_target);
622 if (target_arr.length) {
623 const target = target_arr[0];
624 const target_file = `${targets_dir}/${normal_target}`;
625 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
631 base = `${code}_${normal_prompt}_${counter}.png`
633 base = `${code}_${normal_prompt}.png`
635 filename = `${images_dir}/${base}`
636 if (! fs.existsSync(filename)) {
639 counter = counter + 1;
641 fs.copyFile(target_file, filename, 0, (err) => {
643 console.log("Error copying " + target_file + " to " + filename + ": " + err);
649 "filename": '/images/' + base
651 emit_image(image, target);
654 // Inject the target seed for the "dice" prompt once every
655 // 4 requests for a random seed (and only if the word
656 // "dice" does not appear in the prompt).
657 if (!code && !prompt.toLowerCase().includes("dice")) {
658 if (state.images.length % 4 == 0) {
664 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
666 promise = execFile(python_path, [generate_image_script, prompt])
668 const child = promise.child;
669 child.stdout.on('data', (data) => {
670 const images = JSON.parse(data);
671 images.forEach((image) => {
672 emit_image(image, null);
675 child.stderr.on('data', (data) => {
676 console.log("Error occurred during generate-image: " + data);
679 const { stdout, stderr } = await promise;
684 socket.emit('generation-done');
687 generate_image(request['code'], request['prompt']);
691 server.listen(port, () => {
692 console.log(`listening on *:${port}`);