]> git.cworth.org Git - zombocom-ai/blobdiff - index.js
Add an endgame to the hunt
[zombocom-ai] / index.js
index babe27068b4d83094c451b633da72367ce4d296d..29f304bd13e2b3015c32b54af60fd94eee5227de 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,6 +15,7 @@ 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'
@@ -74,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);
@@ -87,7 +96,25 @@ io.use(wrap(session_middleware));
 // Load comments at server startup
 fs.readFile(state_file, (err, data) => {
     if (err)
-        state = { images: [], targets: [] };
+        state = {
+           images: [],
+           targets: [],
+           tardis: {
+               companions: {
+                   names: [],
+                   count: 0
+               },
+               state: "welcome",
+               level: 0
+           },
+           bus : {
+               students: {
+                   names: [],
+                   count: 0,
+               },
+               state: "welcome"
+           }
+       };
     else
         state = JSON.parse(data);
 });
@@ -113,16 +140,413 @@ app.get('/index.html', (req, res) => {
     res.sendFile(__dirname + '/index.html');
 });
 
-app.get('/tardis', (req, res) => {
-    res.sendFile(__dirname + '/tardis.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;
 
-app.get('/tardis/', (req, res) => {
-    res.sendFile(__dirname + '/tardis.html');
+    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);
+       }
+    });
 });
 
-app.get('/tardis/index.html', (req, res) => {
-    res.sendFile(__dirname + '/tardis.html');
+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) => {
@@ -146,13 +570,10 @@ io.on('connection', (socket) => {
        socket.emit('inform-name', socket.request.session.name);
     });
 
-    // When any client comments, send that to all clients (including sender)
-    socket.on('comment', (comment) => {
-        const images = state.images;
+    function send_and_save_comment(comment) {
+       const images = state.images;
 
-        // Send comment to clients after adding commenter's name
-        comment.name = socket.request.session.name;
-        io.emit('comment', comment);
+       io.emit('comment', comment);
 
         const index = images.findIndex(image => image.id == comment.image_id);
 
@@ -166,14 +587,52 @@ io.on('connection', (socket) => {
         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;
+
+       send_and_save_comment(comment);
     });
 
+    function 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 necessary for us to defeat Zombo.com, (as I've commented below as you'll see the next time you look). It's now been rendered harmless and inert throughout the entire timeline. I'm going to leave to get back to rebuilding our world. I hope you've found everything you were looking for along the way as well."
+           }]
+       };
+       io.emit('image', image);
+       state.images.push(image);
+    }
+
     // Generate an image when requested
     socket.on('generate', (request) => {
         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.images.length;
+                image.id = state.next_image_id;
+               state.next_image_id = state.next_image_id + 1;
                 image.censored = false;
                 image.link = "";
                 if (target) {
@@ -181,8 +640,16 @@ io.on('connection', (socket) => {
                         "name": "ZomboCom",
                         "text": target.response
                     }];
-                    if (! state.targets.includes(target.short)) {
-                        state.targets.push(target.short);
+                    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 = [];