]> git.cworth.org Git - zombocom-ai/blob - index.js
2f990748b218d9bc37b4cc5872aa6e793bf83197
[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 = 3; // XXX: 30 in production
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             console.log("Error executing turtle script: " + e);
304         }
305     });
306
307     socket.on('jumpstart', () => {
308         const bus = state.bus;
309
310         bus.state = "welcome";
311         io_bus.emit("state", bus.state);
312         io_bus.emit('students', bus.students.count);
313
314         start_bus_timer();
315     });
316
317     socket.on('disconnect', () => {
318         const names = bus.students.names;
319
320         names.splice(names.indexOf(name), 1);
321
322         if (! names.includes(name)) {
323             bus.students.count = bus.students.count - 1;
324             io_bus.emit('students', bus.students.count);
325         }
326     });
327 });
328
329 function tardis_app(req, res) {
330     if (! req.session.name) {
331         res.sendFile(__dirname + '/tardis-error.html');
332     } else {
333         res.sendFile(__dirname + '/tardis.html');
334     }
335 }
336
337 app.get('/tardis', tardis_app);
338 app.get('/tardis/', tardis_app);
339
340 const io_tardis = io.of("/tardis");
341
342 io_tardis.use(wrap(session_middleware));
343
344 var tardis_interval;
345 var game_timer;
346
347 const levels = [
348     {
349         title: "Calibrate the Trans-Dimensional Field Accelerator",
350         words: [
351             "What", "was", "the", "year", "of", "Coda's", "birth?"
352         ],
353         answer: 2098
354     },
355     {
356         title: "Reverse the Polarity of the Neutrino Flow Coil",
357         words: [
358             "How", "many", "years", "had", "Zombo.com", "been", "running",
359             "when", "Coda", "sent", "the", "message", "you", "first",
360             "received?"
361         ],
362         answer: 123
363     },
364     {
365         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
366         words: [
367             "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?"
368         ],
369         answer: 111
370     },
371     {
372         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
373         words: [
374             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
375         ],
376         answer: 28643994
377     }
378 ];
379
380 var show_word_interval = 0;
381
382 function show_word() {
383     const tardis = state.tardis;
384     const room = "room-" + (tardis.word % 4).toString();
385     const word = levels[tardis.level].words[tardis.word];
386     io_tardis.to(room).emit('show-word', word);
387     tardis.word = tardis.word + 1;
388     if (tardis.word >= levels[tardis.level].words.length)
389         tardis.word = 0;
390 }
391
392 function start_level() {
393     const tardis = state.tardis;
394
395     // Inform all players of the new level
396     io_tardis.emit("level", levels[tardis.level].title);
397
398     // Then start the timer that shows the words
399     show_word_interval = setInterval(show_word, 1200);
400 }
401
402 function level_up() {
403     const tardis = state.tardis;
404
405     if (show_word_interval) {
406         clearInterval(show_word_interval);
407         show_word_interval = 0;
408     }
409
410     if (tardis.state === "game") {
411         tardis.level = tardis.level + 1;
412         tardis.word = 0;
413
414         if (tardis.level >= levels.length) {
415             tardis.state = "over";
416             io_tardis.emit("state", tardis.state);
417         } else {
418             setTimeout(() => {
419                 start_level();
420             }, 2000);
421         }
422
423     }
424 }
425
426 function start_game() {
427     const tardis = state.tardis;
428
429     tardis.state = "game";
430     tardis.level = 0;
431     tardis.word = 0;
432
433     // Let all companions know the state of the game
434     io_tardis.emit("level", levels[tardis.level].title);
435     io_tardis.emit("state", tardis.state);
436
437     start_level();
438 }
439
440 function emit_tardis_timer() {
441     const tardis = state.tardis;
442     io_tardis.emit('timer', tardis.timer);
443     tardis.timer = tardis.timer - 1;
444     if (tardis.timer < 0) {
445         clearInterval(tardis_interval);
446         tardis.timer = 30;
447         setTimeout(start_game, 3000);
448     }
449 }
450
451 function start_welcome_timer() {
452     const tardis = state.tardis;
453     tardis.timer = 30;
454     emit_tardis_timer();
455     tardis_interval = setInterval(emit_tardis_timer, 1000);
456 }
457
458 io_tardis.on("connection", (socket) => {
459     if (! socket.request.session.name) {
460         console.log("Error: Someone showed up at the Tardis without a name.");
461         return;
462     }
463
464     const name = socket.request.session.name;
465     const tardis = state.tardis;
466
467     // Let the new user know the state of the game
468     socket.emit("state", tardis.state);
469
470     // And the level if relevant
471     if (tardis.state === "game") {
472         socket.emit("level", levels[tardis.level].title);
473     }
474
475     // Put each of our boys into a different room
476     switch (name[0]) {
477     case 'C':
478     case 'c':
479         socket.join("room-0");
480         break;
481     case 'H':
482     case' h':
483         socket.join("room-1");
484         break;
485     case 'A':
486     case 'a':
487         socket.join("room-2");
488         break;
489     case 'S':
490     case 's':
491         socket.join("room-3");
492         break;
493     default:
494         const room = Math.floor(Math.random()*4);
495         socket.join("room-"+room.toString());
496         break;
497     }
498
499     if (tardis.companions.count === 0) {
500         start_welcome_timer();
501     }
502
503     if (! tardis.companions.names.includes(name)) {
504         tardis.companions.count = tardis.companions.count + 1;
505         io_tardis.emit('companions', tardis.companions.count);
506     }
507     tardis.companions.names.push(name);
508
509     socket.on('answer', answer => {
510         const tardis = state.tardis;
511
512         if (tardis.state != "game") {
513             return;
514         }
515
516         if (answer == levels[tardis.level].answer) {
517             io_tardis.emit('correct');
518             level_up();
519         } else {
520             io_tardis.emit('incorrect');
521         }
522     });
523
524     socket.on('reboot', () => {
525         const tardis = state.tardis;
526
527         if (show_word_interval) {
528             clearInterval(show_word_interval);
529             show_word_interval = 0;
530         }
531
532         tardis.state = "welcome";
533         io_tardis.emit("state", tardis.state);
534         io_tardis.emit('companions', tardis.companions.count);
535
536         start_welcome_timer();
537     });
538
539     socket.on('disconnect', () => {
540         const names = tardis.companions.names;
541
542         names.splice(names.indexOf(name), 1);
543
544         if (! names.includes(name)) {
545             tardis.companions.count = tardis.companions.count - 1;
546             io_tardis.emit('companions', tardis.companions.count);
547         }
548     });
549 });
550
551 io.on('connection', (socket) => {
552
553     // First things first, tell the client their name (if any)
554     if (socket.request.session.name) {
555         socket.emit('inform-name', socket.request.session.name);
556     }
557
558     // Replay old comments and images to a newly-joining client
559     socket.emit('reset');
560     state.images.forEach((image) => {
561         socket.emit('image', image)
562     });
563
564     socket.on('set-name', (name) => {
565         console.log("Received set-name event: " + name);
566         socket.request.session.name = name;
567         socket.request.session.save();
568         // Complete the round trip to the client
569         socket.emit('inform-name', socket.request.session.name);
570     });
571
572     // When any client comments, send that to all clients (including sender)
573     socket.on('comment', (comment) => {
574         const images = state.images;
575
576         // Send comment to clients after adding commenter's name
577         comment.name = socket.request.session.name;
578         io.emit('comment', comment);
579
580         const index = images.findIndex(image => image.id == comment.image_id);
581
582         // Before adding the comment to server's state, drop the image_id
583         delete comment.image_id;
584
585         // Now add the comment to the image, remove the image from the
586         // images array and then add it back at the end, (so it appears
587         // as the most-recently-modified image for any new clients)
588         const image = images[index];
589         image.comments.push(comment);
590         images.splice(index, 1);
591         images.push(image);
592     });
593
594     // Generate an image when requested
595     socket.on('generate', (request) => {
596         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
597         async function generate_image(code, prompt) {
598             function emit_image(image, target) {
599                 image.id = state.images.length;
600                 image.censored = false;
601                 image.link = "";
602                 if (target) {
603                     image.comments = [{
604                         "name": "ZomboCom",
605                         "text": target.response
606                     }];
607                     if (! state.targets.includes(target.short)) {
608                         state.targets.push(target.short);
609                     }
610                 } else {
611                     image.comments = [];
612                 }
613                 io.emit('image', image);
614                 state.images.push(image);
615             }
616
617             var promise;
618
619             // Before doing any generation, check for a target image
620             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
621             const target_arr = targets.filter(item => item.normal === normal_target);
622             if (target_arr.length) {
623                 const target = target_arr[0];
624                 const target_file = `${targets_dir}/${normal_target}`;
625                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
626                 var counter = 1;
627                 var base;
628                 var filename;
629                 while (true) {
630                     if (counter > 1) {
631                         base = `${code}_${normal_prompt}_${counter}.png`
632                     } else {
633                         base = `${code}_${normal_prompt}.png`
634                     }
635                     filename = `${images_dir}/${base}`
636                     if (! fs.existsSync(filename)) {
637                         break;
638                     }
639                     counter = counter + 1;
640                 }
641                 fs.copyFile(target_file, filename, 0, (err) => {
642                     if (err) {
643                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
644                     }
645                 });
646                 const image = {
647                     "code": code,
648                     "prompt": prompt,
649                     "filename": '/images/' + base
650                 };
651                 emit_image(image, target);
652             } else {
653
654                 // Inject the target seed for the "dice" prompt once every
655                 // 4 requests for a random seed (and only if the word
656                 // "dice" does not appear in the prompt).
657                 if (!code && !prompt.toLowerCase().includes("dice")) {
658                     if (state.images.length % 4 == 0) {
659                         code = 319630254;
660                     }
661                 }
662
663                 if (code) {
664                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
665                 } else {
666                     promise = execFile(python_path, [generate_image_script, prompt])
667                 }
668                 const child = promise.child;
669                 child.stdout.on('data', (data) => {
670                     const images = JSON.parse(data);
671                     images.forEach((image) => {
672                         emit_image(image, null);
673                     });
674                 });
675                 child.stderr.on('data', (data) => {
676                     console.log("Error occurred during generate-image: " + data);
677                 });
678                 try {
679                     const { stdout, stderr } = await promise;
680                 } catch(e) {
681                     console.error(e);
682                 }
683             }
684             socket.emit('generation-done');
685         }
686
687         generate_image(request['code'], request['prompt']);
688     });
689 });
690
691 server.listen(port, () => {
692     console.log(`listening on *:${port}`);
693 });