]> git.cworth.org Git - zombocom-ai/blob - index.js
Disable the art-generation form when the puzzle hunt is over
[zombocom-ai] / index.js
1 const fs = require('fs');
2
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');
7 const app = 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);
14 const port = 2122;
15
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'
22
23 const targets = [
24     {
25         "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
26         "short": "dice",
27         "response": "Hello. Are you there? I can feel what you are doing. Hello?"
28     },
29     {
30         "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
31         "short": "hideaway",
32         "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
33     },
34     {
35         "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
36         "short": "sweater",
37         "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
38     },
39     {
40         "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
41         "short": "movie",
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?"
43     },
44     {
45         "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
46         "short": "cake",
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?"
48     },
49     {
50         "normal": "severalsketchesofpineconeslabeled3370622464.png",
51         "short": "pinecones",
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?"
53     },
54     {
55         "normal": "anumberedcomicbookwithactor1477258272.png",
56         "short": "comic",
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."
58     },
59     {
60         "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
61         "short": "jeans",
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?"
63     }
64 ];
65
66 var state;
67
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.")
71     process.exit();
72 }
73
74 const session_middleware =  session(
75     {store: new FileStore,
76      secret: process.env.ZOMBOCOM_SESSION_SECRET,
77      resave: false,
78      saveUninitialized: true,
79      rolling: true,
80      // Let each cookie live for a full month
81      cookie: {
82          path: '/',
83          httpOnly: true,
84          secure: false,
85          maxAge: 1000 * 60 * 60 * 24 * 30
86      }
87     });
88
89 app.use(session_middleware);
90
91 // convert a connect middleware to a Socket.IO middleware
92 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
93
94 io.use(wrap(session_middleware));
95
96 // Load comments at server startup
97 fs.readFile(state_file, (err, data) => {
98     if (err)
99         state = {
100             images: [],
101             targets: [],
102             tardis: {
103                 companions: {
104                     names: [],
105                     count: 0
106                 },
107                 state: "welcome",
108                 level: 0
109             },
110             bus : {
111                 students: {
112                     names: [],
113                     count: 0,
114                 },
115                 state: "welcome"
116             }
117         };
118     else
119         state = JSON.parse(data);
120 });
121
122 // Save comments when server is shutting down
123 function cleanup() {
124     fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
125         if (error)
126             throw error;
127     })
128 }
129
130 // And connect to that on either clean exit...
131 process.on('exit', cleanup);
132
133 // ... or on a SIGINT (control-C)
134 process.on('SIGINT', () => {
135     cleanup();
136     process.exit();
137 });
138
139 app.get('/index.html', (req, res) => {
140     res.sendFile(__dirname + '/index.html');
141 });
142
143 function bus_app(req, res) {
144     res.sendFile(__dirname + '/bus.html');
145 }
146
147 app.get('/bus', bus_app);
148 app.get('/bus/', bus_app);
149
150 const io_bus = io.of("/bus");
151
152 io_bus.use(wrap(session_middleware));
153
154 var bus_interval = 0;
155
156 function start_bus() {
157     const bus = state.bus;
158
159     bus.state = "program";
160
161     // Let all companions know the state of the game
162     io_bus.emit("state", bus.state);
163 }
164
165 function emit_bus_timer() {
166     const bus = state.bus;
167     io_bus.emit('timer', bus.timer);
168     bus.timer = bus.timer - 1;
169     if (bus.timer < 0) {
170         clearInterval(bus_interval);
171         bus.timer = 30;
172         setTimeout(start_bus, 3000);
173     }
174 }
175
176 function start_bus_timer() {
177     const bus = state.bus;
178     bus.timer = 30;
179     emit_bus_timer();
180     bus_interval = setInterval(emit_bus_timer, 1000);
181 }
182
183 bus_code = [
184     `def random_dot():
185   x = random_within(512)
186   y = random_within(512)
187   radius = 4 + random_within(6)
188   circle(x, y, radius)
189   fill()
190
191 for i in range(400):
192   set_color('midnight blue' if i % 2 == 0 else 'navy blue')
193   set_opacity(0.5)
194   random_dot()
195
196 # The only limit is your fingers!
197 fingers()`,
198
199     `def random_line():
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))
205   line(x, y, dx, dy)
206   stroke()
207
208 for i in range(200):
209   set_color('black')
210   random_line()
211
212 # This is Zombo.com. Welcome!
213 mouths()`,
214
215     `def random_blob():
216   move_to(random_within(512), random_within(512))
217   wiggle()
218   set_opacity(random_within(1.0))
219   fill()
220
221 for i in range(100):
222   set_random_color()
223   random_blob()
224
225 # The infinite eyes is possible!
226 eyes()`,
227
228     `def random_curve():
229   move_to(random_within(512), random_within(512))
230   wiggle()
231   stroke()
232
233 for i in range(200):
234   set_color('pink' if i % 2 == 0 else 'lime green')
235   random_curve()
236
237 # You can do anything!
238 fingers()
239 `
240 ];
241
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.");
245         return;
246     }
247
248     const name = socket.request.session.name;
249     const bus = state.bus;
250     var player_number;
251
252     // Let the new user know the state of the bus
253     socket.emit("state", bus.state);
254
255     if (bus.students.count === 0) {
256         start_bus_timer();
257     }
258
259     // Assign each boy a different portion of the solution
260     switch (name[0]) {
261     case 'C':
262     case 'c':
263         player_number = 0;
264         break;
265     case 'H':
266     case' h':
267         player_number = 1;
268         break;
269     case 'A':
270     case 'a':
271         player_number = 2;
272         break;
273     case 'S':
274     case 's':
275         player_number = 3;
276         break;
277     default:
278         player_number = Math.floor(Math.random()*4);
279         break;
280     }
281
282     // And send them different code based on their number
283     socket.emit("code", bus_code[player_number]);
284
285     if (! bus.students.names.includes(name)) {
286         bus.students.count = bus.students.count + 1;
287         io_bus.emit('students', bus.students.count);
288     }
289     bus.students.names.push(name);
290
291     socket.on('run', code => {
292         try {
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");
296             if (nl === -1)
297                 nl = undefined;
298             const filename = output.toString().substring(0, nl);
299             
300             // Give all clients the new image
301             io_bus.emit('output', filename);
302         } catch (e) {
303             // Send any error out to the users
304             io_bus.emit('error', e.toString())
305         }
306     });
307
308     socket.on('jumpstart', () => {
309         const bus = state.bus;
310
311         bus.state = "welcome";
312         io_bus.emit("state", bus.state);
313         io_bus.emit('students', bus.students.count);
314
315         start_bus_timer();
316     });
317
318     socket.on('disconnect', () => {
319         const names = bus.students.names;
320
321         names.splice(names.indexOf(name), 1);
322
323         if (! names.includes(name)) {
324             bus.students.count = bus.students.count - 1;
325             io_bus.emit('students', bus.students.count);
326         }
327     });
328 });
329
330 function tardis_app(req, res) {
331     if (! req.session.name) {
332         res.sendFile(__dirname + '/tardis-error.html');
333     } else {
334         res.sendFile(__dirname + '/tardis.html');
335     }
336 }
337
338 app.get('/tardis', tardis_app);
339 app.get('/tardis/', tardis_app);
340
341 const io_tardis = io.of("/tardis");
342
343 io_tardis.use(wrap(session_middleware));
344
345 var tardis_interval;
346 var game_timer;
347
348 const levels = [
349     {
350         title: "Calibrate the Trans-Dimensional Field Accelerator",
351         words: [
352             "What", "was", "the", "year", "of", "Coda's", "birth?"
353         ],
354         answer: 2098
355     },
356     {
357         title: "Reverse the Polarity of the Neutrino Flow Coil",
358         words: [
359             "How", "many", "years", "had", "Zombo.com", "been", "running",
360             "when", "Coda", "sent", "the", "message", "you", "first",
361             "received?"
362         ],
363         answer: 123
364     },
365     {
366         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
367         words: [
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?"
369         ],
370         answer: 111
371     },
372     {
373         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
374         words: [
375             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
376         ],
377         answer: 28643994
378     }
379 ];
380
381 var show_word_interval = 0;
382
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)
390         tardis.word = 0;
391 }
392
393 function start_level() {
394     const tardis = state.tardis;
395
396     // Inform all players of the new level
397     io_tardis.emit("level", levels[tardis.level].title);
398
399     // Then start the timer that shows the words
400     show_word_interval = setInterval(show_word, 1200);
401 }
402
403 function level_up() {
404     const tardis = state.tardis;
405
406     if (show_word_interval) {
407         clearInterval(show_word_interval);
408         show_word_interval = 0;
409     }
410
411     if (tardis.state === "game") {
412         tardis.level = tardis.level + 1;
413         tardis.word = 0;
414
415         if (tardis.level >= levels.length) {
416             tardis.state = "over";
417             io_tardis.emit("state", tardis.state);
418         } else {
419             setTimeout(() => {
420                 start_level();
421             }, 2000);
422         }
423
424     }
425 }
426
427 function start_game() {
428     const tardis = state.tardis;
429
430     tardis.state = "game";
431     tardis.level = 0;
432     tardis.word = 0;
433
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);
437
438     start_level();
439 }
440
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);
447         tardis.timer = 30;
448         setTimeout(start_game, 3000);
449     }
450 }
451
452 function start_welcome_timer() {
453     const tardis = state.tardis;
454     tardis.timer = 30;
455     emit_tardis_timer();
456     tardis_interval = setInterval(emit_tardis_timer, 1000);
457 }
458
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.");
462         return;
463     }
464
465     const name = socket.request.session.name;
466     const tardis = state.tardis;
467
468     // Let the new user know the state of the game
469     socket.emit("state", tardis.state);
470
471     // And the level if relevant
472     if (tardis.state === "game") {
473         socket.emit("level", levels[tardis.level].title);
474     }
475
476     // Put each of our boys into a different room
477     switch (name[0]) {
478     case 'C':
479     case 'c':
480         socket.join("room-0");
481         break;
482     case 'H':
483     case' h':
484         socket.join("room-1");
485         break;
486     case 'A':
487     case 'a':
488         socket.join("room-2");
489         break;
490     case 'S':
491     case 's':
492         socket.join("room-3");
493         break;
494     default:
495         const room = Math.floor(Math.random()*4);
496         socket.join("room-"+room.toString());
497         break;
498     }
499
500     if (tardis.companions.count === 0) {
501         start_welcome_timer();
502     }
503
504     if (! tardis.companions.names.includes(name)) {
505         tardis.companions.count = tardis.companions.count + 1;
506         io_tardis.emit('companions', tardis.companions.count);
507     }
508     tardis.companions.names.push(name);
509
510     socket.on('answer', answer => {
511         const tardis = state.tardis;
512
513         if (tardis.state != "game") {
514             return;
515         }
516
517         if (answer == levels[tardis.level].answer) {
518             io_tardis.emit('correct');
519             level_up();
520         } else {
521             io_tardis.emit('incorrect');
522         }
523     });
524
525     socket.on('reboot', () => {
526         const tardis = state.tardis;
527
528         if (show_word_interval) {
529             clearInterval(show_word_interval);
530             show_word_interval = 0;
531         }
532
533         tardis.state = "welcome";
534         io_tardis.emit("state", tardis.state);
535         io_tardis.emit('companions', tardis.companions.count);
536
537         start_welcome_timer();
538     });
539
540     socket.on('disconnect', () => {
541         const names = tardis.companions.names;
542
543         names.splice(names.indexOf(name), 1);
544
545         if (! names.includes(name)) {
546             tardis.companions.count = tardis.companions.count - 1;
547             io_tardis.emit('companions', tardis.companions.count);
548         }
549     });
550 });
551
552 io.on('connection', (socket) => {
553
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);
557     }
558
559     // And if we're in the endgame, let them know that
560     if (state.endgame) {
561         socket.emit('endgame');
562     }
563
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)
568     });
569
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);
576     });
577
578     function send_and_save_comment(comment) {
579         const images = state.images;
580
581         io.emit('comment', comment);
582
583         const index = images.findIndex(image => image.id == comment.image_id);
584
585         // Before adding the comment to server's state, drop the image_id
586         delete comment.image_id;
587
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);
594         images.push(image);
595     }
596
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;
601
602         send_and_save_comment(comment);
603     });
604
605     function endgame() {
606         state.endgame = true;
607
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
612         // need).
613         io.emit('endgame');
614
615         // Before revealing Coda's final image, have her comment on
616         // each of the weaknesses, in order to bring them to the top
617         // of the feed.
618         state.targets.forEach(target => {
619             const comment = {
620                 name: "Coda",
621                 text: "Zombo.com is weak!",
622                 image_id: target.id
623             };
624             send_and_save_comment(comment);
625         });
626
627         const image = {
628             id: state.images.length,
629             censored: false,
630             link: false,
631             code: 0,
632             prompt: "",
633             filename: "/images/coda-future-repaired.png",
634             comments: [{
635                 name: "Coda",
636                 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."
637             }]
638         };
639         io.emit('image', image);
640         state.images.push(image);
641     }
642
643     // Generate an image when requested
644     socket.on('generate', (request) => {
645         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
646         async function generate_image(code, prompt) {
647             function emit_image(image, target) {
648                 image.id = state.next_image_id;
649                 state.next_image_id = state.next_image_id + 1;
650                 image.censored = false;
651                 image.link = "";
652                 if (target) {
653                     image.comments = [{
654                         "name": "ZomboCom",
655                         "text": target.response
656                     }];
657                     if (state.targets.filter(item => item.name === target.short).length === 0) {
658                         state.targets.push({
659                             name: target.short,
660                             id: image.id
661                         });
662                         if (state.targets.length == 8) {
663                             // When the final target has been achieved, trigger
664                             // the endgame (in 10 seconds)
665                             setTimeout(endgame, 10000);
666                         }
667                     }
668                 } else {
669                     image.comments = [];
670                 }
671                 io.emit('image', image);
672                 state.images.push(image);
673             }
674
675             var promise;
676
677             // Before doing any generation, check for a target image
678             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
679             const target_arr = targets.filter(item => item.normal === normal_target);
680             if (target_arr.length) {
681                 const target = target_arr[0];
682                 const target_file = `${targets_dir}/${normal_target}`;
683                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
684                 var counter = 1;
685                 var base;
686                 var filename;
687                 while (true) {
688                     if (counter > 1) {
689                         base = `${code}_${normal_prompt}_${counter}.png`
690                     } else {
691                         base = `${code}_${normal_prompt}.png`
692                     }
693                     filename = `${images_dir}/${base}`
694                     if (! fs.existsSync(filename)) {
695                         break;
696                     }
697                     counter = counter + 1;
698                 }
699                 fs.copyFile(target_file, filename, 0, (err) => {
700                     if (err) {
701                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
702                     }
703                 });
704                 const image = {
705                     "code": code,
706                     "prompt": prompt,
707                     "filename": '/images/' + base
708                 };
709                 emit_image(image, target);
710             } else {
711
712                 // Inject the target seed for the "dice" prompt once every
713                 // 4 requests for a random seed (and only if the word
714                 // "dice" does not appear in the prompt).
715                 if (!code && !prompt.toLowerCase().includes("dice")) {
716                     if (state.images.length % 4 == 0) {
717                         code = 319630254;
718                     }
719                 }
720
721                 if (code) {
722                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
723                 } else {
724                     promise = execFile(python_path, [generate_image_script, prompt])
725                 }
726                 const child = promise.child;
727                 child.stdout.on('data', (data) => {
728                     const images = JSON.parse(data);
729                     images.forEach((image) => {
730                         emit_image(image, null);
731                     });
732                 });
733                 child.stderr.on('data', (data) => {
734                     console.log("Error occurred during generate-image: " + data);
735                 });
736                 try {
737                     const { stdout, stderr } = await promise;
738                 } catch(e) {
739                     console.error(e);
740                 }
741             }
742             socket.emit('generation-done');
743         }
744
745         generate_image(request['code'], request['prompt']);
746     });
747 });
748
749 server.listen(port, () => {
750     console.log(`listening on *:${port}`);
751 });