]> git.cworth.org Git - zombocom-ai/blob - index.js
Add an endgame to the hunt
[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     // Replay old comments and images to a newly-joining client
560     socket.emit('reset');
561     state.images.forEach((image) => {
562         socket.emit('image', image)
563     });
564
565     socket.on('set-name', (name) => {
566         console.log("Received set-name event: " + name);
567         socket.request.session.name = name;
568         socket.request.session.save();
569         // Complete the round trip to the client
570         socket.emit('inform-name', socket.request.session.name);
571     });
572
573     function send_and_save_comment(comment) {
574         const images = state.images;
575
576         io.emit('comment', comment);
577
578         const index = images.findIndex(image => image.id == comment.image_id);
579
580         // Before adding the comment to server's state, drop the image_id
581         delete comment.image_id;
582
583         // Now add the comment to the image, remove the image from the
584         // images array and then add it back at the end, (so it appears
585         // as the most-recently-modified image for any new clients)
586         const image = images[index];
587         image.comments.push(comment);
588         images.splice(index, 1);
589         images.push(image);
590     }
591
592     // When any client comments, send that to all clients (including sender)
593     socket.on('comment', (comment) => {
594         // We have to add the sender's name befor we can send the comment
595         comment.name = socket.request.session.name;
596
597         send_and_save_comment(comment);
598     });
599
600     function endgame() {
601         // Before revealing Coda's final image, have her comment on
602         // each of the weaknesses, in order to bring them to the top
603         // of the feed.
604         state.targets.forEach(target => {
605             const comment = {
606                 name: "Coda",
607                 text: "Zombo.com is weak!",
608                 image_id: target.id
609             };
610             send_and_save_comment(comment);
611         });
612
613         const image = {
614             id: state.images.length,
615             censored: false,
616             link: false,
617             code: 0,
618             prompt: "",
619             filename: "/images/coda-future-repaired.png",
620             comments: [{
621                 name: "Coda",
622                 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."
623             }]
624         };
625         io.emit('image', image);
626         state.images.push(image);
627     }
628
629     // Generate an image when requested
630     socket.on('generate', (request) => {
631         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
632         async function generate_image(code, prompt) {
633             function emit_image(image, target) {
634                 image.id = state.next_image_id;
635                 state.next_image_id = state.next_image_id + 1;
636                 image.censored = false;
637                 image.link = "";
638                 if (target) {
639                     image.comments = [{
640                         "name": "ZomboCom",
641                         "text": target.response
642                     }];
643                     if (state.targets.filter(item => item.name === target.short).length === 0) {
644                         state.targets.push({
645                             name: target.short,
646                             id: image.id
647                         });
648                         if (state.targets.length == 8) {
649                             // When the final target has been achieved, trigger
650                             // the endgame (in 10 seconds)
651                             setTimeout(endgame, 10000);
652                         }
653                     }
654                 } else {
655                     image.comments = [];
656                 }
657                 io.emit('image', image);
658                 state.images.push(image);
659             }
660
661             var promise;
662
663             // Before doing any generation, check for a target image
664             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
665             const target_arr = targets.filter(item => item.normal === normal_target);
666             if (target_arr.length) {
667                 const target = target_arr[0];
668                 const target_file = `${targets_dir}/${normal_target}`;
669                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
670                 var counter = 1;
671                 var base;
672                 var filename;
673                 while (true) {
674                     if (counter > 1) {
675                         base = `${code}_${normal_prompt}_${counter}.png`
676                     } else {
677                         base = `${code}_${normal_prompt}.png`
678                     }
679                     filename = `${images_dir}/${base}`
680                     if (! fs.existsSync(filename)) {
681                         break;
682                     }
683                     counter = counter + 1;
684                 }
685                 fs.copyFile(target_file, filename, 0, (err) => {
686                     if (err) {
687                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
688                     }
689                 });
690                 const image = {
691                     "code": code,
692                     "prompt": prompt,
693                     "filename": '/images/' + base
694                 };
695                 emit_image(image, target);
696             } else {
697
698                 // Inject the target seed for the "dice" prompt once every
699                 // 4 requests for a random seed (and only if the word
700                 // "dice" does not appear in the prompt).
701                 if (!code && !prompt.toLowerCase().includes("dice")) {
702                     if (state.images.length % 4 == 0) {
703                         code = 319630254;
704                     }
705                 }
706
707                 if (code) {
708                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
709                 } else {
710                     promise = execFile(python_path, [generate_image_script, prompt])
711                 }
712                 const child = promise.child;
713                 child.stdout.on('data', (data) => {
714                     const images = JSON.parse(data);
715                     images.forEach((image) => {
716                         emit_image(image, null);
717                     });
718                 });
719                 child.stderr.on('data', (data) => {
720                     console.log("Error occurred during generate-image: " + data);
721                 });
722                 try {
723                     const { stdout, stderr } = await promise;
724                 } catch(e) {
725                     console.error(e);
726                 }
727             }
728             socket.emit('generation-done');
729         }
730
731         generate_image(request['code'], request['prompt']);
732     });
733 });
734
735 server.listen(port, () => {
736     console.log(`listening on *:${port}`);
737 });