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')
197 x = random_within(512) - 60
198 y = random_within(512) - 60
199 dx = 60 + random_within(20)
200 dy = 40 + random_within(20)
201 set_opacity(random_within(0.5))
210 move_to(random_within(512), random_within(512))
212 set_opacity(random_within(1.0))
220 move_to(random_within(512), random_within(512))
225 set_color('pink' if i % 2 == 0 else 'lime green')
229 io_bus.on("connection", (socket) => {
230 if (! socket.request.session.name) {
231 console.log("Error: Someone showed up at the Magic School Bus without a name.");
235 const name = socket.request.session.name;
236 const bus = state.bus;
239 // Let the new user know the state of the bus
240 socket.emit("state", bus.state);
242 if (bus.students.count === 0) {
246 // Assign each boy a different portion of the solution
265 player_number = Math.floor(Math.random()*4);
269 // And send them different code based on their number
270 socket.emit("code", bus_code[player_number]);
272 if (! bus.students.names.includes(name)) {
273 bus.students.count = bus.students.count + 1;
274 io_bus.emit('students', bus.students.count);
276 bus.students.names.push(name);
278 socket.on('run', code => {
280 output = child_process.execFileSync(python_path, [interpret_cairo_script, player_number], { input: code });
281 // Grab just first line of output
282 const nl = output.indexOf("\n");
285 const filename = output.toString().substring(0, nl);
287 // Give all clients the new image
288 io_bus.emit('output', filename);
290 console.log("Error executing turtle script: " + e);
294 socket.on('jumpstart', () => {
295 const bus = state.bus;
297 bus.state = "welcome";
298 io_bus.emit("state", bus.state);
299 io_bus.emit('students', bus.students.count);
304 socket.on('disconnect', () => {
305 const names = bus.students.names;
307 names.splice(names.indexOf(name), 1);
309 if (! names.includes(name)) {
310 bus.students.count = bus.students.count - 1;
311 io_bus.emit('students', bus.students.count);
316 function tardis_app(req, res) {
317 if (! req.session.name) {
318 res.sendFile(__dirname + '/tardis-error.html');
320 res.sendFile(__dirname + '/tardis.html');
324 app.get('/tardis', tardis_app);
325 app.get('/tardis/', tardis_app);
327 const io_tardis = io.of("/tardis");
329 io_tardis.use(wrap(session_middleware));
336 title: "Calibrate the Trans-Dimensional Field Accelerator",
338 "What", "was", "the", "year", "of", "Coda's", "birth?"
343 title: "Reverse the Polarity of the Neutrino Flow Coil",
345 "How", "many", "years", "had", "Zombo.com", "been", "running",
346 "when", "Coda", "sent", "the", "message", "you", "first",
352 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
354 "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?"
359 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
361 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
367 var show_word_interval = 0;
369 function show_word() {
370 const tardis = state.tardis;
371 const room = "room-" + (tardis.word % 4).toString();
372 const word = levels[tardis.level].words[tardis.word];
373 io_tardis.to(room).emit('show-word', word);
374 tardis.word = tardis.word + 1;
375 if (tardis.word >= levels[tardis.level].words.length)
379 function start_level() {
380 const tardis = state.tardis;
382 // Inform all players of the new level
383 io_tardis.emit("level", levels[tardis.level].title);
385 // Then start the timer that shows the words
386 show_word_interval = setInterval(show_word, 1200);
389 function level_up() {
390 const tardis = state.tardis;
392 if (show_word_interval) {
393 clearInterval(show_word_interval);
394 show_word_interval = 0;
397 if (tardis.state === "game") {
398 tardis.level = tardis.level + 1;
401 if (tardis.level >= levels.length) {
402 tardis.state = "over";
403 io_tardis.emit("state", tardis.state);
413 function start_game() {
414 const tardis = state.tardis;
416 tardis.state = "game";
420 // Let all companions know the state of the game
421 io_tardis.emit("level", levels[tardis.level].title);
422 io_tardis.emit("state", tardis.state);
427 function emit_tardis_timer() {
428 const tardis = state.tardis;
429 io_tardis.emit('timer', tardis.timer);
430 tardis.timer = tardis.timer - 1;
431 if (tardis.timer < 0) {
432 clearInterval(tardis_interval);
434 setTimeout(start_game, 3000);
438 function start_welcome_timer() {
439 const tardis = state.tardis;
442 tardis_interval = setInterval(emit_tardis_timer, 1000);
445 io_tardis.on("connection", (socket) => {
446 if (! socket.request.session.name) {
447 console.log("Error: Someone showed up at the Tardis without a name.");
451 const name = socket.request.session.name;
452 const tardis = state.tardis;
454 // Let the new user know the state of the game
455 socket.emit("state", tardis.state);
457 // And the level if relevant
458 if (tardis.state === "game") {
459 socket.emit("level", levels[tardis.level].title);
462 // Put each of our boys into a different room
466 socket.join("room-0");
470 socket.join("room-1");
474 socket.join("room-2");
478 socket.join("room-3");
481 const room = Math.floor(Math.random()*4);
482 socket.join("room-"+room.toString());
486 if (tardis.companions.count === 0) {
487 start_welcome_timer();
490 if (! tardis.companions.names.includes(name)) {
491 tardis.companions.count = tardis.companions.count + 1;
492 io_tardis.emit('companions', tardis.companions.count);
494 tardis.companions.names.push(name);
496 socket.on('answer', answer => {
497 const tardis = state.tardis;
499 if (tardis.state != "game") {
503 if (answer == levels[tardis.level].answer) {
504 io_tardis.emit('correct');
507 io_tardis.emit('incorrect');
511 socket.on('reboot', () => {
512 const tardis = state.tardis;
514 if (show_word_interval) {
515 clearInterval(show_word_interval);
516 show_word_interval = 0;
519 tardis.state = "welcome";
520 io_tardis.emit("state", tardis.state);
521 io_tardis.emit('companions', tardis.companions.count);
523 start_welcome_timer();
526 socket.on('disconnect', () => {
527 const names = tardis.companions.names;
529 names.splice(names.indexOf(name), 1);
531 if (! names.includes(name)) {
532 tardis.companions.count = tardis.companions.count - 1;
533 io_tardis.emit('companions', tardis.companions.count);
538 io.on('connection', (socket) => {
540 // First things first, tell the client their name (if any)
541 if (socket.request.session.name) {
542 socket.emit('inform-name', socket.request.session.name);
545 // Replay old comments and images to a newly-joining client
546 socket.emit('reset');
547 state.images.forEach((image) => {
548 socket.emit('image', image)
551 socket.on('set-name', (name) => {
552 console.log("Received set-name event: " + name);
553 socket.request.session.name = name;
554 socket.request.session.save();
555 // Complete the round trip to the client
556 socket.emit('inform-name', socket.request.session.name);
559 // When any client comments, send that to all clients (including sender)
560 socket.on('comment', (comment) => {
561 const images = state.images;
563 // Send comment to clients after adding commenter's name
564 comment.name = socket.request.session.name;
565 io.emit('comment', comment);
567 const index = images.findIndex(image => image.id == comment.image_id);
569 // Before adding the comment to server's state, drop the image_id
570 delete comment.image_id;
572 // Now add the comment to the image, remove the image from the
573 // images array and then add it back at the end, (so it appears
574 // as the most-recently-modified image for any new clients)
575 const image = images[index];
576 image.comments.push(comment);
577 images.splice(index, 1);
581 // Generate an image when requested
582 socket.on('generate', (request) => {
583 console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
584 async function generate_image(code, prompt) {
585 function emit_image(image, target) {
586 image.id = state.images.length;
587 image.censored = false;
592 "text": target.response
594 if (! state.targets.includes(target.short)) {
595 state.targets.push(target.short);
600 io.emit('image', image);
601 state.images.push(image);
606 // Before doing any generation, check for a target image
607 const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
608 const target_arr = targets.filter(item => item.normal === normal_target);
609 if (target_arr.length) {
610 const target = target_arr[0];
611 const target_file = `${targets_dir}/${normal_target}`;
612 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
618 base = `${code}_${normal_prompt}_${counter}.png`
620 base = `${code}_${normal_prompt}.png`
622 filename = `${images_dir}/${base}`
623 if (! fs.existsSync(filename)) {
626 counter = counter + 1;
628 fs.copyFile(target_file, filename, 0, (err) => {
630 console.log("Error copying " + target_file + " to " + filename + ": " + err);
636 "filename": '/images/' + base
638 emit_image(image, target);
641 // Inject the target seed for the "dice" prompt once every
642 // 4 requests for a random seed (and only if the word
643 // "dice" does not appear in the prompt).
644 if (!code && !prompt.toLowerCase().includes("dice")) {
645 if (state.images.length % 4 == 0) {
651 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
653 promise = execFile(python_path, [generate_image_script, prompt])
655 const child = promise.child;
656 child.stdout.on('data', (data) => {
657 const images = JSON.parse(data);
658 images.forEach((image) => {
659 emit_image(image, null);
662 child.stderr.on('data', (data) => {
663 console.log("Error occurred during generate-image: " + data);
666 const { stdout, stderr } = await promise;
671 socket.emit('generation-done');
674 generate_image(request['code'], request['prompt']);
678 server.listen(port, () => {
679 console.log(`listening on *:${port}`);