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