]> git.cworth.org Git - zombocom-ai/commitdiff
Add initial structure of a Magic School Bus puzzle
authorCarl Worth <cworth@cworth.org>
Fri, 23 Dec 2022 01:13:55 +0000 (17:13 -0800)
committerCarl Worth <cworth@cworth.org>
Fri, 23 Dec 2022 19:51:10 +0000 (11:51 -0800)
This has the same sort of introduction as the TARDIS puzzle (zooming
into the time machine), but this time it's the Magic School Bus, not
the TARDIS, of course.

Then, there's an interactive form for making some turtle art, and an
image showing the result. Currently, the result is hard-coded inside
the script, but it should be simple to make it consume the form input,
(which is already being sent to the server at least).

bus.html [new file with mode: 0644]
index.js
run-turtle.py [new file with mode: 0755]
tardis.html

diff --git a/bus.html b/bus.html
new file mode 100644 (file)
index 0000000..845b048
--- /dev/null
+++ b/bus.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <title>ZOMBO</title>
+  <link href="/zombo.css" rel="stylesheet" type="text/css">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="HandheldFriendly" content="true">
+
+  <style>
+    @keyframes zoom {
+       from {
+           opacity: 100%;
+           transform: scale(1);
+       }
+        to {
+           opacity: 0%;
+           transform: scale(30);
+        }
+    }
+    .zoom-into {
+        animation-name: zoom;
+        animation-duration: 2s;
+        animation-timing-function: ease-in;
+       animation-fill-mode: forwards;
+    }
+    @keyframes fade {
+       from {
+           opacity: 100%;
+       }
+        to {
+           opacity: 0%;
+        }
+    }
+    .fade-out {
+       animation-name: fade;
+       animation-duration: 0.8s;
+       animation-timing-function: ease-in;
+       animation-fill-mode: forwards;
+    }
+    #welcome {
+       position: fixed;
+       width: 100%;
+       top: 50%;
+       left: 50%;
+       transform: translate(-50%, -50%);
+    }
+    #game {
+       position: relative;
+    }
+    #word {
+       text-align: center;
+       font-size: 400%;
+       font-weight: bold;
+    }
+    #interface {
+       width: 100%;
+    }
+    #input {
+       display: block;
+       margin-left: auto;
+       margin-right: auto;
+       width: 60%;
+       font-size: 200%;
+       border-width: 5px;
+    }
+    #jumpstart {
+       position: fixed;
+       right: 5px;
+       bottom: 5px;
+    }
+    #welcome-message {
+       font-size: 120%;
+       font-weight: bold;
+       text-align: center;
+    }
+    #result {
+       position: fixed;
+       width: 100%;
+       top: 50%;
+       left: 50%;
+       transform: translate(-50%, -50%);
+       font-size: 500%;
+       text-align: center;
+    }
+    #form {
+       width: 100%;
+    }
+    #code {
+       width: 95%;
+    }
+    #output {
+       width: 100%;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="content">
+
+    <div id="welcome" style="visibility: hidden">
+      <div id="header" align="center">
+       <p>
+          <img src="images/1403010940_The_Magic_School_Bus_in_the_apocalypse.png">
+       </p>
+      </div>
+
+      <div id="welcome-message">
+       <div id="timer_div" style="visibility: hidden">
+         Entering Magic School Bus in <span id="timer"></span> seconds.
+       </div>
+       <br>
+       Students present: <span id="students">1</span>/4
+      </div>
+    </div>
+
+    <div id="program" style="visibility: hidden">
+      <h1>
+       Magic School Bus Central Processor
+      </h1>
+      <form id="form">
+       <textarea id="code" rows="10" width="100%">t.forward(100);
+t.right(90);
+t.forward(100);
+       </textarea>
+       <button type="submit">Run code</button>
+      </form>
+      <img id="output"></img>
+    </div>
+
+    <button id="jumpstart">
+      Jumpstart Magic School Bus
+    </button>
+
+  </div>
+
+  <script src="/socket.io/socket.io.js"></script>
+  <script>
+    const socket = io("/bus");
+
+    const welcome = document.getElementById("welcome");
+    const program = document.getElementById("program");
+    const header = document.getElementById("header");
+    const companions = document.getElementById("students");
+    const timer_div = document.getElementById("timer_div");
+    const timer = document.getElementById("timer");
+    const form = document.getElementById("form");
+    const code = document.getElementById("code");
+    const welcome_message = document.getElementById("welcome-message");
+    const jumpstart = document.getElementById("jumpstart");
+    const output = document.getElementById("output");
+
+    function fade_element(elt) {
+       elt.style.opacity = "100%";
+       elt.className = "fade-out";
+       // Arrange to clear the class name when the animation is over
+       // This will allow for it to be restarted on the next word.
+       setTimeout(() => {
+           elt.style.opacity = "0%";
+           elt.className = "";
+       }, 900);
+    }
+
+    jumpstart.addEventListener('click', event => {
+       socket.emit('jumpstart');
+    });
+
+    form.addEventListener('submit', event => {
+       event.preventDefault();
+       console.log("Submitted form with code: " + code.value);
+       socket.emit('run', code.value);
+    });
+
+    socket.on('students', (count) => {
+       students.textContent = count.toString();
+    });
+
+    socket.on('timer', (value) => {
+       timer_div.style.visibility = "visible";
+       timer.textContent = value.toString();
+
+       if (value === 0) {
+           welcome_message.style.visibility = "hidden";
+           timer_div.style.visibility = "hidden";
+           header.className = "zoom-into";
+           // Clear that className after the animation is complete
+           setTimeout(() => {
+               header.style.visibility = "hidden"
+               header.className = "";
+           }, 2200);
+       }
+    });
+
+    socket.on('state', (state) => {
+       if (state === "program") {
+           welcome.style.visibility = "hidden";
+           program.style.visibility = "visible";
+       } else if (state === "welcome") {
+           welcome.style.visibility = "visible";
+           welcome_message.style.visibility = "visible";
+           header.style.opacity = "100%";
+           header.style.transform = "scale(1)";
+           header.style.visibility = "visible";
+           program.style.visibility = "hidden";
+           output.src = "";
+       }
+    });
+
+    socket.on('output', (filename) => {
+       output.src = filename;
+    });
+
+  </script>
+</body>
+</html>
index cb46c203bb3f63e28f1c292bbee6726321c62398..9b7270b7673e03e1ae78c803b68c6af054551b9f 100644 (file)
--- a/index.js
+++ b/index.js
@@ -1,8 +1,8 @@
 const fs = require('fs');
 
 const util = require('util');
-const execFile = util.promisify(require('child_process').execFile);
-
+const child_process = require('child_process');
+const execFile = util.promisify(child_process.execFile);
 const express = require('express');
 const app = express();
 const session = require('express-session');
@@ -15,6 +15,7 @@ const port = 2122;
 
 const python_path = '/usr/bin/python3'
 const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py'
+const run_turtle_script = '/home/cworth/src/zombocom-ai/run-turtle.py'
 const state_file = 'zombocom-state.json'
 const targets_dir = '/srv/cworth.org/zombocom/targets'
 const images_dir = '/srv/cworth.org/zombocom/images'
@@ -132,6 +133,106 @@ app.get('/index.html', (req, res) => {
     res.sendFile(__dirname + '/index.html');
 });
 
+function bus_app(req, res) {
+    res.sendFile(__dirname + '/bus.html');
+}
+
+app.get('/bus', bus_app);
+app.get('/bus/', bus_app);
+
+const io_bus = io.of("/bus");
+
+io_bus.use(wrap(session_middleware));
+
+var bus_interval = 0;
+
+function start_bus() {
+    const bus = state.bus;
+
+    bus.state = "program";
+
+    // Let all companions know the state of the game
+    io_bus.emit("state", bus.state);
+}
+
+function emit_bus_timer() {
+    const bus = state.bus;
+    io_bus.emit('timer', bus.timer);
+    bus.timer = bus.timer - 1;
+    if (bus.timer < 0) {
+       clearInterval(bus_interval);
+       bus.timer = 30;
+       setTimeout(start_bus, 3000);
+    }
+}
+
+function start_bus_timer() {
+    const bus = state.bus;
+    bus.timer = 3; // XXX: 30 in production
+    emit_bus_timer();
+    bus_interval = setInterval(emit_bus_timer, 1000);
+}
+
+io_bus.on("connection", (socket) => {
+    if (! socket.request.session.name) {
+       console.log("Error: Someone showed up at the Magic School Bus without a name.");
+       return;
+    }
+
+    const name = socket.request.session.name;
+    const bus = state.bus;
+
+    // Let the new user know the state of the bus
+    socket.emit("state", bus.state);
+
+    if (bus.students.count === 0) {
+       start_bus_timer();
+    }
+
+    if (! bus.students.names.includes(name)) {
+       bus.students.count = bus.students.count + 1;
+       io_bus.emit('students', bus.students.count);
+    }
+    bus.students.names.push(name);
+
+    socket.on('run', code => {
+       try {
+           output = child_process.execFileSync(python_path, [run_turtle_script, code], { input: code });
+           // Grab just first line of output
+           const nl = output.indexOf("\n");
+           if (nl === -1)
+               nl = undefined;
+           const filename = output.toString().substring(0, nl);
+           
+           // Give all clients the new image
+           io_bus.emit('output', filename);
+       } catch (e) {
+           console.log("Error executing turtle script: " + e);
+       }
+    });
+
+    socket.on('jumpstart', () => {
+       const bus = state.bus;
+
+       bus.state = "welcome";
+       io_bus.emit("state", bus.state);
+       io_bus.emit('students', bus.students.count);
+
+       start_bus_timer();
+    });
+
+    socket.on('disconnect', () => {
+       const names = bus.students.names;
+
+       names.splice(names.indexOf(name), 1);
+
+       if (! names.includes(name)) {
+           bus.students.count = bus.students.count - 1;
+           io_bus.emit('students', bus.students.count);
+       }
+    });
+});
+
 function tardis_app(req, res) {
     if (! req.session.name) {
        res.sendFile(__dirname + '/tardis-error.html');
diff --git a/run-turtle.py b/run-turtle.py
new file mode 100755 (executable)
index 0000000..fb74b02
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+from svg_turtle import SvgTurtle
+import tempfile
+import os
+
+OUTPUT_DIR_PREFIX='/srv/cworth.org/zombocom'
+OUTPUT_DIR="{}/busart".format(OUTPUT_DIR_PREFIX)
+
+t = SvgTurtle(512,512);
+
+t.pencolor('red');
+
+t.penup();
+t.right(180);
+t.forward(200);
+t.right(180);
+t.pendown();
+
+for i in range(50):
+    t.forward(100);
+    t.left(123);
+
+(fd, filename) = tempfile.mkstemp(suffix=".svg", prefix="busart", dir=OUTPUT_DIR);
+os.close(fd)
+
+t.save_as(filename);
+os.chmod(filename, 0o644);
+
+web_file = filename.removeprefix(OUTPUT_DIR_PREFIX);
+
+print(web_file);
+
+
index 250ec3d211f880390ed28abfbd124e6080efd095..06091926b20c19f14b550d452bda3201594c335e 100644 (file)
     });
 
     socket.on('timer', (value) => {
-       console.log("Receiving timer value of " + value);
        timer_div.style.visibility = "visible";
        timer.textContent = value.toString();