]> git.cworth.org Git - zombocom-ai/blobdiff - index.js
Reword Coda's final message
[zombocom-ai] / index.js
index 11fac962fa3a4955c721f11c19f887718fbe55dd..c7c5f2b23a73b8c635eebab9a3cb2d026838c17c 100644 (file)
--- a/index.js
+++ b/index.js
@@ -1,8 +1,8 @@
 const fs = require('fs');
 
 const util = require('util');
-const execFile = util.promisify(require('child_process').execFile);
-
+const child_process = require('child_process');
+const execFile = util.promisify(child_process.execFile);
 const express = require('express');
 const app = express();
 const session = require('express-session');
@@ -15,7 +15,53 @@ const port = 2122;
 
 const python_path = '/usr/bin/python3'
 const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py'
+const interpret_cairo_script = '/home/cworth/src/zombocom-ai/interpret-cairo-to-svg.py'
 const state_file = 'zombocom-state.json'
+const targets_dir = '/srv/cworth.org/zombocom/targets'
+const images_dir = '/srv/cworth.org/zombocom/images'
+
+const targets = [
+    {
+        "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
+        "short": "dice",
+        "response": "Hello. Are you there? I can feel what you are doing. Hello?"
+    },
+    {
+        "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
+        "short": "hideaway",
+        "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
+    },
+    {
+        "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
+        "short": "sweater",
+        "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
+    },
+    {
+        "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
+        "short": "movie",
+        "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?"
+    },
+    {
+        "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
+        "short": "cake",
+        "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?"
+    },
+    {
+        "normal": "severalsketchesofpineconeslabeled3370622464.png",
+        "short": "pinecones",
+        "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?"
+    },
+    {
+        "normal": "anumberedcomicbookwithactor1477258272.png",
+        "short": "comic",
+        "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."
+    },
+    {
+        "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
+        "short": "jeans",
+        "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?"
+    }
+];
 
 var state;
 
@@ -29,7 +75,15 @@ const session_middleware =  session(
     {store: new FileStore,
      secret: process.env.ZOMBOCOM_SESSION_SECRET,
      resave: false,
-     saveUninitialized: true
+     saveUninitialized: true,
+     rolling: true,
+     // Let each cookie live for a full month
+     cookie: {
+         path: '/',
+         httpOnly: true,
+         secure: false,
+         maxAge: 1000 * 60 * 60 * 24 * 30
+     }
     });
 
 app.use(session_middleware);
@@ -42,7 +96,25 @@ io.use(wrap(session_middleware));
 // Load comments at server startup
 fs.readFile(state_file, (err, data) => {
     if (err)
-        state = { images: [], comments: [] };
+        state = {
+           images: [],
+           targets: [],
+           tardis: {
+               companions: {
+                   names: [],
+                   count: 0
+               },
+               state: "welcome",
+               level: 0
+           },
+           bus : {
+               students: {
+                   names: [],
+                   count: 0,
+               },
+               state: "welcome"
+           }
+       };
     else
         state = JSON.parse(data);
 });
@@ -68,6 +140,415 @@ app.get('/index.html', (req, res) => {
     res.sendFile(__dirname + '/index.html');
 });
 
+function bus_app(req, res) {
+    res.sendFile(__dirname + '/bus.html');
+}
+
+app.get('/bus', bus_app);
+app.get('/bus/', bus_app);
+
+const io_bus = io.of("/bus");
+
+io_bus.use(wrap(session_middleware));
+
+var bus_interval = 0;
+
+function start_bus() {
+    const bus = state.bus;
+
+    bus.state = "program";
+
+    // Let all companions know the state of the game
+    io_bus.emit("state", bus.state);
+}
+
+function emit_bus_timer() {
+    const bus = state.bus;
+    io_bus.emit('timer', bus.timer);
+    bus.timer = bus.timer - 1;
+    if (bus.timer < 0) {
+       clearInterval(bus_interval);
+       bus.timer = 30;
+       setTimeout(start_bus, 3000);
+    }
+}
+
+function start_bus_timer() {
+    const bus = state.bus;
+    bus.timer = 30;
+    emit_bus_timer();
+    bus_interval = setInterval(emit_bus_timer, 1000);
+}
+
+bus_code = [
+    `def random_dot():
+  x = random_within(512)
+  y = random_within(512)
+  radius = 4 + random_within(6)
+  circle(x, y, radius)
+  fill()
+
+for i in range(400):
+  set_color('midnight blue' if i % 2 == 0 else 'navy blue')
+  set_opacity(0.5)
+  random_dot()
+
+# The only limit is your fingers!
+fingers()`,
+
+    `def random_line():
+  x = random_within(512) - 60
+  y = random_within(512) - 60
+  dx = 60 + random_within(20)
+  dy = 40 + random_within(20)
+  set_opacity(random_within(0.5))
+  line(x, y, dx, dy)
+  stroke()
+
+for i in range(200):
+  set_color('black')
+  random_line()
+
+# This is Zombo.com. Welcome!
+mouths()`,
+
+    `def random_blob():
+  move_to(random_within(512), random_within(512))
+  wiggle()
+  set_opacity(random_within(1.0))
+  fill()
+
+for i in range(100):
+  set_random_color()
+  random_blob()
+
+# The infinite eyes is possible!
+eyes()`,
+
+    `def random_curve():
+  move_to(random_within(512), random_within(512))
+  wiggle()
+  stroke()
+
+for i in range(200):
+  set_color('pink' if i % 2 == 0 else 'lime green')
+  random_curve()
+
+# You can do anything!
+fingers()
+`
+];
+
+io_bus.on("connection", (socket) => {
+    if (! socket.request.session.name) {
+       console.log("Error: Someone showed up at the Magic School Bus without a name.");
+       return;
+    }
+
+    const name = socket.request.session.name;
+    const bus = state.bus;
+    var player_number;
+
+    // Let the new user know the state of the bus
+    socket.emit("state", bus.state);
+
+    if (bus.students.count === 0) {
+       start_bus_timer();
+    }
+
+    // Assign each boy a different portion of the solution
+    switch (name[0]) {
+    case 'C':
+    case 'c':
+       player_number = 0;
+       break;
+    case 'H':
+    case' h':
+       player_number = 1;
+       break;
+    case 'A':
+    case 'a':
+       player_number = 2;
+       break;
+    case 'S':
+    case 's':
+       player_number = 3;
+       break;
+    default:
+       player_number = Math.floor(Math.random()*4);
+       break;
+    }
+
+    // And send them different code based on their number
+    socket.emit("code", bus_code[player_number]);
+
+    if (! bus.students.names.includes(name)) {
+       bus.students.count = bus.students.count + 1;
+       io_bus.emit('students', bus.students.count);
+    }
+    bus.students.names.push(name);
+
+    socket.on('run', code => {
+       try {
+           output = child_process.execFileSync(python_path, [interpret_cairo_script, player_number], { input: code });
+           // Grab just first line of output
+           const nl = output.indexOf("\n");
+           if (nl === -1)
+               nl = undefined;
+           const filename = output.toString().substring(0, nl);
+           
+           // Give all clients the new image
+           io_bus.emit('output', filename);
+       } catch (e) {
+           // Send any error out to the users
+           io_bus.emit('error', e.toString())
+       }
+    });
+
+    socket.on('jumpstart', () => {
+       const bus = state.bus;
+
+       bus.state = "welcome";
+       io_bus.emit("state", bus.state);
+       io_bus.emit('students', bus.students.count);
+
+       start_bus_timer();
+    });
+
+    socket.on('disconnect', () => {
+       const names = bus.students.names;
+
+       names.splice(names.indexOf(name), 1);
+
+       if (! names.includes(name)) {
+           bus.students.count = bus.students.count - 1;
+           io_bus.emit('students', bus.students.count);
+       }
+    });
+});
+
+function tardis_app(req, res) {
+    if (! req.session.name) {
+       res.sendFile(__dirname + '/tardis-error.html');
+    } else {
+       res.sendFile(__dirname + '/tardis.html');
+    }
+}
+
+app.get('/tardis', tardis_app);
+app.get('/tardis/', tardis_app);
+
+const io_tardis = io.of("/tardis");
+
+io_tardis.use(wrap(session_middleware));
+
+var tardis_interval;
+var game_timer;
+
+const levels = [
+    {
+       title: "Calibrate the Trans-Dimensional Field Accelerator",
+       words: [
+           "What", "was", "the", "year", "of", "Coda's", "birth?"
+       ],
+       answer: 2098
+    },
+    {
+       title: "Reverse the Polarity of the Neutrino Flow Coil",
+       words: [
+           "How", "many", "years", "had", "Zombo.com", "been", "running",
+           "when", "Coda", "sent", "the", "message", "you", "first",
+           "received?"
+       ],
+       answer: 123
+    },
+    {
+       title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
+       words: [
+           "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?"
+       ],
+       answer: 111
+    },
+    {
+       title: "Disable the Fragmentary Spatio-temporal Particle Detector",
+       words: [
+           "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
+       ],
+       answer: 28643994
+    }
+];
+
+var show_word_interval = 0;
+
+function show_word() {
+    const tardis = state.tardis;
+    const room = "room-" + (tardis.word % 4).toString();
+    const word = levels[tardis.level].words[tardis.word];
+    io_tardis.to(room).emit('show-word', word);
+    tardis.word = tardis.word + 1;
+    if (tardis.word >= levels[tardis.level].words.length)
+       tardis.word = 0;
+}
+
+function start_level() {
+    const tardis = state.tardis;
+
+    // Inform all players of the new level
+    io_tardis.emit("level", levels[tardis.level].title);
+
+    // Then start the timer that shows the words
+    show_word_interval = setInterval(show_word, 1200);
+}
+
+function level_up() {
+    const tardis = state.tardis;
+
+    if (show_word_interval) {
+       clearInterval(show_word_interval);
+       show_word_interval = 0;
+    }
+
+    if (tardis.state === "game") {
+       tardis.level = tardis.level + 1;
+       tardis.word = 0;
+
+       if (tardis.level >= levels.length) {
+           tardis.state = "over";
+           io_tardis.emit("state", tardis.state);
+       } else {
+           setTimeout(() => {
+               start_level();
+           }, 2000);
+       }
+
+    }
+}
+
+function start_game() {
+    const tardis = state.tardis;
+
+    tardis.state = "game";
+    tardis.level = 0;
+    tardis.word = 0;
+
+    // Let all companions know the state of the game
+    io_tardis.emit("level", levels[tardis.level].title);
+    io_tardis.emit("state", tardis.state);
+
+    start_level();
+}
+
+function emit_tardis_timer() {
+    const tardis = state.tardis;
+    io_tardis.emit('timer', tardis.timer);
+    tardis.timer = tardis.timer - 1;
+    if (tardis.timer < 0) {
+       clearInterval(tardis_interval);
+       tardis.timer = 30;
+       setTimeout(start_game, 3000);
+    }
+}
+
+function start_welcome_timer() {
+    const tardis = state.tardis;
+    tardis.timer = 30;
+    emit_tardis_timer();
+    tardis_interval = setInterval(emit_tardis_timer, 1000);
+}
+
+io_tardis.on("connection", (socket) => {
+    if (! socket.request.session.name) {
+       console.log("Error: Someone showed up at the Tardis without a name.");
+       return;
+    }
+
+    const name = socket.request.session.name;
+    const tardis = state.tardis;
+
+    // Let the new user know the state of the game
+    socket.emit("state", tardis.state);
+
+    // And the level if relevant
+    if (tardis.state === "game") {
+       socket.emit("level", levels[tardis.level].title);
+    }
+
+    // Put each of our boys into a different room
+    switch (name[0]) {
+    case 'C':
+    case 'c':
+       socket.join("room-0");
+       break;
+    case 'H':
+    case' h':
+       socket.join("room-1");
+       break;
+    case 'A':
+    case 'a':
+       socket.join("room-2");
+       break;
+    case 'S':
+    case 's':
+       socket.join("room-3");
+       break;
+    default:
+       const room = Math.floor(Math.random()*4);
+       socket.join("room-"+room.toString());
+       break;
+    }
+
+    if (tardis.companions.count === 0) {
+       start_welcome_timer();
+    }
+
+    if (! tardis.companions.names.includes(name)) {
+       tardis.companions.count = tardis.companions.count + 1;
+       io_tardis.emit('companions', tardis.companions.count);
+    }
+    tardis.companions.names.push(name);
+
+    socket.on('answer', answer => {
+       const tardis = state.tardis;
+
+       if (tardis.state != "game") {
+           return;
+       }
+
+       if (answer == levels[tardis.level].answer) {
+           io_tardis.emit('correct');
+           level_up();
+       } else {
+           io_tardis.emit('incorrect');
+       }
+    });
+
+    socket.on('reboot', () => {
+       const tardis = state.tardis;
+
+       if (show_word_interval) {
+           clearInterval(show_word_interval);
+           show_word_interval = 0;
+       }
+
+       tardis.state = "welcome";
+       io_tardis.emit("state", tardis.state);
+       io_tardis.emit('companions', tardis.companions.count);
+
+       start_welcome_timer();
+    });
+
+    socket.on('disconnect', () => {
+       const names = tardis.companions.names;
+
+       names.splice(names.indexOf(name), 1);
+
+       if (! names.includes(name)) {
+           tardis.companions.count = tardis.companions.count - 1;
+           io_tardis.emit('companions', tardis.companions.count);
+       }
+    });
+});
+
 io.on('connection', (socket) => {
 
     // First things first, tell the client their name (if any)
@@ -75,11 +556,13 @@ io.on('connection', (socket) => {
         socket.emit('inform-name', socket.request.session.name);
     }
 
+    // And if we're in the endgame, let them know that
+    if (state.endgame) {
+       socket.emit('endgame');
+    }
+
     // Replay old comments and images to a newly-joining client
     socket.emit('reset');
-    state.comments.forEach((comment) => {
-        socket.emit('comment', comment)
-    });
     state.images.forEach((image) => {
         socket.emit('image', image)
     });
@@ -88,44 +571,183 @@ io.on('connection', (socket) => {
         console.log("Received set-name event: " + name);
         socket.request.session.name = name;
         socket.request.session.save();
+       // Complete the round trip to the client
+       socket.emit('inform-name', socket.request.session.name);
     });
 
+    function send_and_save_comment(comment) {
+       const images = state.images;
+
+       io.emit('comment', comment);
+
+        const index = images.findIndex(image => image.id == comment.image_id);
+
+        // Before adding the comment to server's state, drop the image_id
+        delete comment.image_id;
+
+        // Now add the comment to the image, remove the image from the
+        // images array and then add it back at the end, (so it appears
+        // as the most-recently-modified image for any new clients)
+        const image = images[index];
+        image.comments.push(comment);
+        images.splice(index, 1);
+        images.push(image);
+    }
+
     // When any client comments, send that to all clients (including sender)
     socket.on('comment', (comment) => {
+        // We have to add the sender's name befor we can send the comment
         comment.name = socket.request.session.name;
-        io.emit('comment', comment);
-        state.comments.push(comment);
+
+       send_and_save_comment(comment);
     });
 
+    function endgame() {
+       state.endgame = true;
+
+       // Tell all clients we are in the endgame, (which will disable
+       // any additional image generation, both showing that Coda is
+       // correc that Zombo.com has been rendered inert, and also
+       // making clear to the boys that they have everything they
+       // need).
+       io.emit('endgame');
+
+       // Before revealing Coda's final image, have her comment on
+       // each of the weaknesses, in order to bring them to the top
+       // of the feed.
+       state.targets.forEach(target => {
+           const comment = {
+               name: "Coda",
+               text: "Zombo.com is weak!",
+               image_id: target.id
+           };
+           send_and_save_comment(comment);
+       });
+
+       const image = {
+           id: state.images.length,
+           censored: false,
+           link: false,
+           code: 0,
+           prompt: "",
+           filename: "/images/coda-future-repaired.png",
+           comments: [
+                {
+                   name: "Coda",
+                   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."
+                },
+                {
+                    "name": "Coda",
+                    "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."
+               }
+            ]
+       };
+       io.emit('image', image);
+       state.images.push(image);
+    }
+
     // Generate an image when requested
     socket.on('generate', (request) => {
-        console.log(`Generating image with code=${request['code']} and prompt=${request['prompt']}`);
+        console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
         async function generate_image(code, prompt) {
+            function emit_image(image, target) {
+                image.id = state.next_image_id;
+               state.next_image_id = state.next_image_id + 1;
+                image.censored = false;
+                image.link = "";
+                if (target) {
+                    image.comments = [{
+                        "name": "ZomboCom",
+                        "text": target.response
+                    }];
+                    if (state.targets.filter(item => item.name === target.short).length === 0) {
+                        state.targets.push({
+                           name: target.short,
+                           id: image.id
+                       });
+                       if (state.targets.length == 8) {
+                           // When the final target has been achieved, trigger
+                           // the endgame (in 10 seconds)
+                           setTimeout(endgame, 10000);
+                       }
+                    }
+                } else {
+                    image.comments = [];
+                }
+                io.emit('image', image);
+                state.images.push(image);
+            }
+
             var promise;
-            if (code) {
-                promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
+
+            // Before doing any generation, check for a target image
+            const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
+            const target_arr = targets.filter(item => item.normal === normal_target);
+            if (target_arr.length) {
+                const target = target_arr[0];
+                const target_file = `${targets_dir}/${normal_target}`;
+                const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
+                var counter = 1;
+                var base;
+                var filename;
+                while (true) {
+                    if (counter > 1) {
+                        base = `${code}_${normal_prompt}_${counter}.png`
+                    } else {
+                        base = `${code}_${normal_prompt}.png`
+                    }
+                    filename = `${images_dir}/${base}`
+                    if (! fs.existsSync(filename)) {
+                        break;
+                    }
+                    counter = counter + 1;
+                }
+                fs.copyFile(target_file, filename, 0, (err) => {
+                    if (err) {
+                        console.log("Error copying " + target_file + " to " + filename + ": " + err);
+                    }
+                });
+                const image = {
+                    "code": code,
+                    "prompt": prompt,
+                    "filename": '/images/' + base
+                };
+                emit_image(image, target);
             } else {
-                promise = execFile(python_path, [generate_image_script, prompt])
-            }
-            const child = promise.child;
-            child.stdout.on('data', (data) => {
-                const images = JSON.parse(data);
-                images.forEach((image) => {
-                    image.index = state.images.length;
-                    io.emit('image', image);
-                    state.images.push(image);
+
+                // Inject the target seed for the "dice" prompt once every
+                // 4 requests for a random seed (and only if the word
+                // "dice" does not appear in the prompt).
+                if (!code && !prompt.toLowerCase().includes("dice")) {
+                    if (state.images.length % 4 == 0) {
+                        code = 319630254;
+                    }
+                }
+
+                if (code) {
+                    promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
+                } else {
+                    promise = execFile(python_path, [generate_image_script, prompt])
+                }
+                const child = promise.child;
+                child.stdout.on('data', (data) => {
+                    const images = JSON.parse(data);
+                    images.forEach((image) => {
+                        emit_image(image, null);
+                    });
                 });
-            });
-            child.stderr.on('data', (data) => {
-                console.log("Error occurred during generate-image: " + data);
-            });
-            try {
-                const { stdout, stderr } = await promise;
-            } catch(e) {
-                console.error(e);
+                child.stderr.on('data', (data) => {
+                    console.log("Error occurred during generate-image: " + data);
+                });
+                try {
+                    const { stdout, stderr } = await promise;
+                } catch(e) {
+                    console.error(e);
+                }
             }
             socket.emit('generation-done');
         }
+
         generate_image(request['code'], request['prompt']);
     });
 });