--- /dev/null
+<!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>
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');
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'
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');
--- /dev/null
+#!/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);
+
+
});
socket.on('timer', (value) => {
- console.log("Receiving timer value of " + value);
timer_div.style.visibility = "visible";
timer.textContent = value.toString();