├── run.sh ├── bots ├── shell │ ├── bots.sh │ ├── look.sh │ ├── say.sh │ ├── clear.sh │ ├── color.sh │ ├── register.sh │ ├── line.sh │ ├── smiley.sh │ ├── README.md │ └── _utils.sh ├── clojure │ ├── README.md │ ├── deps.edn │ └── src │ │ └── paintbots │ │ └── bot.clj ├── typescript │ ├── .gitignore │ ├── src │ │ ├── types │ │ │ ├── Pixel.ts │ │ │ ├── Bot.ts │ │ │ ├── BotCommand.ts │ │ │ └── Color.ts │ │ ├── util.ts │ │ ├── index.ts │ │ └── Api.ts │ ├── .editorconfig │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── node │ ├── package.json │ ├── README.md │ └── bot.mjs ├── java │ ├── .settings │ │ ├── org.eclipse.jdt.apt.core.prefs │ │ ├── org.eclipse.m2e.core.prefs │ │ └── org.eclipse.jdt.core.prefs │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── README.md │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── paintbots │ │ │ ├── Main.java │ │ │ ├── Bot.java │ │ │ └── BotClient.java │ ├── .project │ ├── pom.xml │ ├── .classpath │ ├── mvnw.cmd │ └── mvnw ├── prolog │ ├── Dockerfile │ ├── README.md │ └── bot.pl ├── python │ ├── .gitignore │ ├── requirements.txt │ ├── README.md │ ├── util.py │ ├── main.py │ ├── bot.py │ └── api.py ├── dotnet │ ├── PaintBots │ │ ├── PaintBots │ │ │ ├── PaintBots.csproj │ │ │ ├── Program.cs │ │ │ ├── Bot.cs │ │ │ └── BotClient.cs │ │ └── PaintBots.sln │ ├── README.md │ └── .gitignore └── README.md ├── resources ├── input.css └── public │ ├── logo.png │ ├── favicon.ico │ └── client │ ├── swipl-bundle.js.gz │ └── logo.pl ├── dev-src └── user.clj ├── ffmpeg.sh ├── .gitignore ├── azure ├── delete_group.sh ├── build.sh ├── create_registry.sh ├── create_group.sh ├── upgrade.sh ├── config_sample.sh ├── README.md └── create_webapp.sh ├── Dockerfile ├── package.json ├── tailwind.config.js ├── deps.edn ├── config.edn ├── src └── paintbots │ ├── video.clj │ ├── png.clj │ ├── client.clj │ ├── state.clj │ └── main.clj ├── LICENSE ├── README.md └── logo.md /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | clojure -M:run 4 | -------------------------------------------------------------------------------- /bots/shell/bots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | bots 5 | -------------------------------------------------------------------------------- /bots/shell/look.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | look 5 | -------------------------------------------------------------------------------- /bots/shell/say.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | say $@ 5 | -------------------------------------------------------------------------------- /bots/clojure/README.md: -------------------------------------------------------------------------------- 1 | # Example Clojure bot 2 | 3 | Draws a dragon curve 4 | -------------------------------------------------------------------------------- /bots/shell/clear.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | 5 | clear 6 | -------------------------------------------------------------------------------- /bots/shell/color.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | 5 | color $1 6 | -------------------------------------------------------------------------------- /bots/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .idea 4 | botConfig.cfg* 5 | -------------------------------------------------------------------------------- /bots/shell/register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | 5 | register $1 6 | -------------------------------------------------------------------------------- /resources/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /bots/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "node-fetch": "^3.3.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /dev-src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (defn go [] 4 | ((requiring-resolve 'paintbots.main/-main))) 5 | -------------------------------------------------------------------------------- /resources/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/paintbots/master/resources/public/logo.png -------------------------------------------------------------------------------- /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/paintbots/master/resources/public/favicon.ico -------------------------------------------------------------------------------- /bots/java/.settings/org.eclipse.jdt.apt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.apt.aptEnabled=false 3 | -------------------------------------------------------------------------------- /bots/node/README.md: -------------------------------------------------------------------------------- 1 | # Node JS bot 2 | 3 | Simple bot using node-fetch to issue commands. 4 | 5 | run with `node bot.mjs` 6 | -------------------------------------------------------------------------------- /bots/java/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/paintbots/master/bots/java/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /bots/prolog/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swipl:latest 2 | COPY bot.pl /app/bot.pl 3 | CMD ["swipl", "-l", "/app/bot.pl", "-g", "start_repl"] 4 | -------------------------------------------------------------------------------- /resources/public/client/swipl-bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/paintbots/master/resources/public/client/swipl-bundle.js.gz -------------------------------------------------------------------------------- /bots/java/.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /bots/java/README.md: -------------------------------------------------------------------------------- 1 | # Simple Java skeleton 2 | 3 | To run: 4 | * compile with `mvn compile` 5 | * run with `mvn exec:java -Dexec.args="JavaBot"` 6 | -------------------------------------------------------------------------------- /bots/clojure/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {http-kit/http-kit {:mvn/version "2.5.1"} 3 | io.github.bortexz/resocket {:mvn/version "0.1.0"}}} 4 | -------------------------------------------------------------------------------- /ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Example of how to convert images to video 4 | 5 | ffmpeg -framerate 30 -i art_%06d.png -c:v libx264 -pix_fmt yuv420p art.mp4 6 | -------------------------------------------------------------------------------- /bots/shell/line.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | 5 | DIR=$1 6 | AMOUNT=$2 7 | 8 | for ((i=0;i<$AMOUNT;i++)) 9 | do 10 | move $DIR 11 | paint 12 | done 13 | -------------------------------------------------------------------------------- /bots/typescript/src/types/Pixel.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./Color"; 2 | 3 | export interface Pixel { 4 | color: Color; 5 | position: { 6 | x: number; 7 | y: number; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /bots/python/.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | python.iml 11 | botConfig.cfg 12 | venv/ -------------------------------------------------------------------------------- /bots/typescript/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | insert_final_newline = true 4 | 5 | [*.ts] 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [package.json] 10 | indent_style = space 11 | indent_size = 2 -------------------------------------------------------------------------------- /bots/typescript/src/types/Bot.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./Color"; 2 | 3 | export interface Bot { 4 | id: string; 5 | name: string; 6 | color?: Color; 7 | position?: { 8 | x: number; 9 | y: number; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.clj-kondo/ 3 | /.cpcache/ 4 | /.lsp/ 5 | /.nrepl-port 6 | /node_modules/ 7 | /package-lock.json 8 | /*.png 9 | /*.mp4 10 | /bots/node/node_modules/ 11 | /bots/java/target/ 12 | /bots/clojure/.cpcache/ 13 | /bots/clojure/.nrepl-port 14 | /azure/config.sh 15 | -------------------------------------------------------------------------------- /azure/delete_group.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az group delete --name "$PAINTBOTS_AZURE_RG" 10 | -------------------------------------------------------------------------------- /bots/python/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.1.0 2 | aiohttp==3.8.4 3 | aiosignal==1.3.1 4 | async-timeout==4.0.2 5 | asyncio==3.4.3 6 | attrs==23.1.0 7 | certifi==2022.12.7 8 | charset-normalizer==3.1.0 9 | frozenlist==1.3.3 10 | idna==3.4 11 | multidict==6.0.4 12 | urllib3==1.26.15 13 | yarl==1.8.2 14 | -------------------------------------------------------------------------------- /bots/typescript/src/types/BotCommand.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./Color"; 2 | 3 | export interface BotCommand { 4 | id: string; 5 | color?: Color; 6 | move?: string; 7 | paint?: string; 8 | clear?: string; 9 | look?: string; 10 | msg?: string; 11 | bye?: string; 12 | bots?: string; 13 | } 14 | -------------------------------------------------------------------------------- /bots/dotnet/PaintBots/PaintBots/PaintBots.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /azure/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az acr build --file Dockerfile --registry "$PAINTBOTS_AZURE_REGISTRY" --image "$PAINTBOTS_AZURE_IMAGE" . 10 | -------------------------------------------------------------------------------- /bots/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Simple dotnet / C# skeleton for a PaintBot 2 | 3 | Design following closely the Java skeleton: https://github.com/tatut/paintbots/tree/master/bots/java 4 | 5 | ## Running 6 | 7 | EIther: 8 | 9 | 1. Open the solution on the IDE of your preference and simply run the project 10 | 2. cd `PaintBots/PaintBots` and `dotnet run` 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM clojure:temurin-17-tools-deps-focal 2 | RUN apt-get update 3 | RUN apt-get install -y ffmpeg 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | COPY deps.edn /usr/src/app/ 7 | COPY src /usr/src/app/src 8 | COPY resources /usr/src/app/resources 9 | COPY config.edn /usr/src/app 10 | RUN clojure -X:deps prep 11 | CMD ["clojure", "-M:run"] 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "daisyui": "^2.17.0", 4 | "postcss-cli": "^8.3.1", 5 | "tailwindcss": "^3.1.4" 6 | }, 7 | "scripts": { 8 | "tailwind": "tailwindcss -i resources/input.css -o resources/public/paintbots.css --watch", 9 | "tailwindprod": "tailwindcss -i resources/input.css -o resources/public/paintbots.css -m" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: { 3 | enabled: true, 4 | content: ['./src/**/*.clj'], 5 | }, 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: "#146a8e" 10 | }, 11 | }, 12 | }, 13 | variants: { 14 | extend: {}, 15 | }, 16 | plugins: [require("daisyui")], 17 | }; 18 | -------------------------------------------------------------------------------- /bots/java/src/main/java/paintbots/Main.java: -------------------------------------------------------------------------------- 1 | package paintbots; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | String name = args[0]; 7 | Bot b = new Bot(name); 8 | for (int i = 0; i < 10; i++) { 9 | b.move(Bot.Dir.LEFT); 10 | b.paint(); 11 | } 12 | b.bye(); 13 | System.exit(0); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /azure/create_registry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az acr create --name "$PAINTBOTS_AZURE_REGISTRY" \ 10 | --resource-group "$PAINTBOTS_AZURE_RG" \ 11 | --sku standard \ 12 | --admin-enabled true 13 | 14 | -------------------------------------------------------------------------------- /azure/create_group.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az group create --name "$PAINTBOTS_AZURE_RG" \ 10 | --location "$PAINTBOTS_AZURE_REGION" \ 11 | --tags Owner="$PAINTBOTS_AZURE_OWNER" DueDate="$PAINTBOTS_AZURE_DUEDATE" 12 | -------------------------------------------------------------------------------- /azure/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az webapp config container set \ 10 | --name "$PAINTBOTS_AZURE_APPNAME" \ 11 | --resource-group "$PAINTBOTS_AZURE_RG" \ 12 | --docker-custom-image-name "${PAINTBOTS_AZURE_IMAGE}:latest" 13 | -------------------------------------------------------------------------------- /bots/shell/smiley.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source _utils.sh 4 | 5 | paint 6 | move "RIGHT" 7 | move "RIGHT" 8 | move "RIGHT" 9 | move "RIGHT" 10 | paint 11 | move "RIGHT" 12 | move "DOWN" 13 | move "DOWN" 14 | paint 15 | move "LEFT" 16 | move "DOWN" 17 | paint 18 | move "LEFT" 19 | move "DOWN" 20 | paint 21 | move "LEFT" 22 | paint 23 | move "LEFT" 24 | paint 25 | move "LEFT" 26 | move "UP" 27 | paint 28 | move "LEFT" 29 | move "UP" 30 | paint 31 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {tatut/ripley {:git/url "https://github.com/tatut/ripley.git" 3 | :sha "59bb1dbb5efae3ab1e06a8c320944f833d321ab0"} 4 | http-kit/http-kit {:mvn/version "2.5.1"} 5 | org.clojure/core.async {:mvn/version "1.6.673"} 6 | org.clojure/clojure {:mvn/version "1.11.1"} 7 | cheshire/cheshire {:mvn/version "5.11.0"}} 8 | :aliases 9 | {:dev {:extra-paths ["dev-src"]} 10 | :run {:main-opts ["-m" "paintbots.main"]}} 11 | } 12 | -------------------------------------------------------------------------------- /bots/java/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 3 | org.eclipse.jdt.core.compiler.compliance=17 4 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 5 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 6 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore 7 | org.eclipse.jdt.core.compiler.processAnnotations=disabled 8 | org.eclipse.jdt.core.compiler.release=disabled 9 | org.eclipse.jdt.core.compiler.source=17 10 | -------------------------------------------------------------------------------- /bots/dotnet/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode/ 10 | 11 | # Rider 12 | .idea/ 13 | 14 | # Visual Studio 15 | .vs/ 16 | 17 | # Fleet 18 | .fleet/ 19 | 20 | # Code Rush 21 | .cr/ 22 | 23 | # User-specific files 24 | *.suo 25 | *.user 26 | *.userosscache 27 | *.sln.docstates 28 | 29 | # Build results 30 | [Dd]ebug/ 31 | [Dd]ebugPublic/ 32 | [Rr]elease/ 33 | [Rr]eleases/ 34 | x64/ 35 | x86/ 36 | build/ 37 | bld/ 38 | [Bb]in/ 39 | [Oo]bj/ 40 | [Oo]ut/ 41 | msbuild.log 42 | msbuild.err 43 | msbuild.wrn 44 | -------------------------------------------------------------------------------- /bots/shell/README.md: -------------------------------------------------------------------------------- 1 | # Simple shell script bot example 2 | 3 | Simple shell scripts that use the paintbots API to draw stuff. 4 | 5 | Bots use the `PAINTBOTS_URL` environment variable to read the URL 6 | of the server. The ID of a registered bot is set to `PAINTBOTS_ID` 7 | environment variable. 8 | 9 | Example: 10 | ```shell 11 | export PAINTBOTS_URL="https://localhost:31173" 12 | export PAINTBOTS_ID=`./register.sh MazeBot` 13 | # Draw a simple maze 14 | ./line.sh RIGHT 2 15 | ./line.sh UP 2 16 | ./line.sh LEFT 4 17 | ./line.sh DOWN 5 18 | ./line.sh RIGHT 6 19 | ./line.sh UP 7 20 | ./line.sh LEFT 8 21 | # ...and so on 22 | ``` 23 | -------------------------------------------------------------------------------- /config.edn: -------------------------------------------------------------------------------- 1 | {;; HTTP port and ip to serve on 2 | :port 31173 3 | :ip "0.0.0.0" 4 | ;; How long does executing a paint command take 5 | :command-duration-ms 1 ; make short for local debug 6 | 7 | ;; Canvas size 8 | :width 320 :height 200 9 | :background-logo? true ; show a very cool logo in the canvas background 10 | 11 | ;; How often to take a PNG snapshot, this affects both web UI and files created 12 | :png {:interval 500} 13 | 14 | ;; Admin config, for the love of all that is good, CHANGE the password before deploying 15 | :admin {:password "letmein"} 16 | 17 | :video {:ffmpeg-executable "/usr/bin/ffmpeg" 18 | :framerate 10} 19 | } 20 | -------------------------------------------------------------------------------- /bots/dotnet/PaintBots/PaintBots/Program.cs: -------------------------------------------------------------------------------- 1 | using PaintBots; 2 | 3 | Console.WriteLine("Hello Bots!"); 4 | const string name = "BotName2"; // TODO From config or command line args 5 | var bot = new Bot(name, new BotClient()); // TODO Pass server url from config or command line 6 | Console.WriteLine($"Initializing bot: {bot.Name}"); 7 | try 8 | { 9 | await bot.Register(); 10 | Console.WriteLine($"Bot registered with ID: {bot.Id}"); 11 | await bot.Color("8"); 12 | Console.WriteLine("Drawing..."); 13 | for (var i = 0; i < 10; i++) 14 | { 15 | await bot.Move(Bot.Dir.Left); 16 | await bot.Paint(); 17 | } 18 | } 19 | finally 20 | { 21 | Console.WriteLine("Bye bye!"); 22 | await bot.Bye(false); 23 | } 24 | -------------------------------------------------------------------------------- /bots/typescript/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint"], 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:import/typescript", 11 | ], 12 | env: { 13 | node: true, 14 | es2022: true, 15 | }, 16 | rules: { 17 | "eol-last": ["error", "always"], 18 | "@typescript-eslint/no-non-null-assertion": 0, 19 | "@typescript-eslint/explicit-module-boundary-types": 0, 20 | }, 21 | settings: { 22 | "import/resolver": { 23 | typescript: { 24 | alwaysTryTypes: true, 25 | }, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /azure/config_sample.sh: -------------------------------------------------------------------------------- 1 | # This will be sourced by other scripts, fill in your own values and copy to config.sh 2 | 3 | # Resource group example: "paintbots-your-name" 4 | PAINTBOTS_AZURE_RG="" 5 | # Owner example: "Your Name" 6 | PAINTBOTS_AZURE_OWNER="" 7 | # Due date example: "2023-05-05" 8 | PAINTBOTS_AZURE_DUEDATE="" 9 | # Alpha numeric characters only and must be between 5 and 50 characters 10 | # Owner example: "paintbotsyourname" 11 | PAINTBOTS_AZURE_REGISTRY="paintbots" 12 | PAINTBOTS_AZURE_IMAGE="paintbots" 13 | PAINTBOTS_AZURE_CONTAINER="paintbots" 14 | PAINTBOTS_AZURE_PLAN="paintbotsplan" 15 | # App name example: "paintbots-city" 16 | PAINTBOTS_AZURE_APPNAME="paintbots-" 17 | PAINTBOTS_AZURE_REGION="westeurope" 18 | -------------------------------------------------------------------------------- /azure/README.md: -------------------------------------------------------------------------------- 1 | # Deploy paintbots to Azure 2 | 3 | First write your config to `config.sh`. It is used by all the scripts. 4 | 5 | ## Deploy 6 | 7 | Paintbots will be available at https://PAINTBOTS_AZURE_APPNAME.azurewebsites.net/ 8 | 9 | ```bash 10 | # Login to azure 11 | az login 12 | 13 | # copy config_sample.sh to config.sh and change your settigns 14 | cp azure/config_sample.sh azure/config.sh 15 | 16 | # Create resource group 17 | ./azure/create_group.sh 18 | 19 | # Create ACR registry 20 | ./azure/create_registry.sh 21 | 22 | # Build 23 | ./azure/build.sh 24 | 25 | # Create and configure webapp 26 | ./azure/create_webapp.sh 27 | 28 | ``` 29 | 30 | ## Upgrade deployment 31 | 32 | ```bash 33 | ./azure/upgrade.sh 34 | 35 | ``` 36 | 37 | ## Delete deployment 38 | 39 | ```bash 40 | ./azure/delete_group.sh 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /azure/create_webapp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | source "$(dirname "$0")"/config.sh 8 | 9 | az appservice plan create \ 10 | --resource-group "$PAINTBOTS_AZURE_RG" \ 11 | --name "$PAINTBOTS_AZURE_PLAN" \ 12 | --is-linux 13 | 14 | az webapp create \ 15 | --resource-group "$PAINTBOTS_AZURE_RG" \ 16 | --plan "$PAINTBOTS_AZURE_PLAN" \ 17 | --name "$PAINTBOTS_AZURE_APPNAME" \ 18 | --deployment-container-image-name "${PAINTBOTS_AZURE_REGISTRY}.azurecr.io/${PAINTBOTS_AZURE_IMAGE}:latest" 19 | 20 | 21 | az webapp config appsettings set \ 22 | --resource-group "$PAINTBOTS_AZURE_RG" \ 23 | --name "$PAINTBOTS_AZURE_APPNAME" \ 24 | --settings WEBSITES_PORT=31173 25 | -------------------------------------------------------------------------------- /src/paintbots/video.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.video 2 | "Generate video (using ffmpeg) from exported PNG images." 3 | (:require [clojure.java.shell :as sh] 4 | [clojure.java.io :as io])) 5 | 6 | (defn generate [{:keys [ffmpeg-executable framerate] 7 | :or {ffmpeg-executable "ffmpeg" 8 | framerate 10}} 9 | canvas-name to] 10 | (let [f (java.io.File/createTempFile canvas-name ".mp4")] 11 | (try 12 | (let [res 13 | (sh/sh "ffmpeg" "-framerate" (str framerate) "-i" (str canvas-name "_%06d.png") 14 | "-c:v" "libx264" "-pix_fmt" "yuv420p" "-y" 15 | (.getAbsolutePath f))] 16 | (when (not= 0 (:exit res)) 17 | (println "Video generation failed: " (:err res)))) 18 | (io/copy f to) 19 | (finally 20 | (io/delete-file f true))))) 21 | -------------------------------------------------------------------------------- /bots/typescript/src/types/Color.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * pico-8 16 color palette from https://www.pixilart.com/palettes/pico-8-51001 3 | * 4 | * 0 "#000000" 5 | * 1 "#1D2B53" 6 | * 2 "#7E2553" 7 | * 3 "#008751" 8 | * 4 "#AB5236" 9 | * 5 "#5F574F" 10 | * 6 "#C2C3C7" 11 | * 7 "#FFF1E8" 12 | * 8 "#FF004D" 13 | * 9 "#FFA300" 14 | * a "#FFEC27" 15 | * b "#00E436" 16 | * c "#29ADFF" 17 | * d "#83769C" 18 | * e "#FF77A8" 19 | * f "#FFCCAA" 20 | */ 21 | export const colors = { 22 | BLACK: "0", 23 | BLUE: "1", 24 | PURPLE: "2", 25 | GREEN: "3", 26 | BROWN: "4", 27 | GREY: "5", 28 | SILVER: "6", 29 | WHITE: "7", 30 | RED: "8", 31 | ORANGE: "9", 32 | YELLOW: "a", 33 | BRIGHT_GREEN: "b", 34 | LIGHT_BLUE: "c", 35 | DARK_GRAY: "d", 36 | PINK: "e", 37 | TAN: "f", 38 | } as const; 39 | 40 | export type ColorName = keyof typeof colors; 41 | export type Color = (typeof colors)[ColorName]; 42 | -------------------------------------------------------------------------------- /bots/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "es2022", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "inlineSourceMap": false, 18 | "inlineSources": false, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization": true, 21 | "preserveConstEnums": true, 22 | "sourceMap": false, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true 26 | }, 27 | "exclude": ["node_modules", "build", "**/*test.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /bots/README.md: -------------------------------------------------------------------------------- 1 | # Simple example bots 2 | 3 | This folder contains example bot implementations. 4 | 5 | * `shell` contains bash scripts using curl 6 | * `python` a python example 7 | * `node` a simple Node JavaScript example 8 | * `typescript` a more complex Node TypeScript example 9 | * `clojure` a Clojure example 10 | * `java` a simple Java skeleton 11 | * `prolog` a Prolog sample 12 | 13 | 14 | Simple scripts that use the paintbots API to draw stuff. 15 | 16 | Bots use the `PAINTBOTS_URL` environment variable to read the URL 17 | of the server. The ID of a registered bot is set to `PAINTBOTS_ID` 18 | environment variable. 19 | 20 | Example: 21 | ```shell 22 | export PAINTBOTS_URL="https://localhost:31173" 23 | export PAINTBOTS_ID=`./register.sh MazeBot` 24 | # Draw a simple maze 25 | ./line.sh RIGHT 2 26 | ./line.sh UP 2 27 | ./line.sh LEFT 4 28 | ./line.sh DOWN 5 29 | ./line.sh RIGHT 6 30 | ./line.sh UP 7 31 | ./line.sh LEFT 8 32 | # ...and so on 33 | ``` 34 | -------------------------------------------------------------------------------- /bots/java/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | javabot 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | 25 | 1681480465435 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|.metadata|archetype-resources|META-INF/maven|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /bots/python/README.md: -------------------------------------------------------------------------------- 1 | # Python bot client 2 | 3 | Tested with python 3.8.10. 4 | 5 | ## Set up the API url and bot name 6 | 7 | 1. If you need to change the API base URL, edit the `API_URL` variable in `api.py` before running the bot 8 | * Or, use PAINTBOTS_URL environment variable when running the python script 9 | 2. Change the bot name in `main.py` 10 | 11 | ## Install deps 12 | 1. Setup venv 13 | 2. Activate venv `$ source /venv/bin/activate` 14 | 3. Install requirements.txt deps in venv: `$ pip install -r requirements.txt` 15 | 16 | ## Running the bot 17 | 18 | Run the script with `$ python -u main.py` 19 | Or, with `$ PAINTBOTS_URL="http://your-url.com" python -u main.py` 20 | 21 | ## Problems with registering the bot? 22 | 23 | If you restart the local paintbots server (or if the cloud server is restarted), you will need to re-register the bot. 24 | In that case, remove the `botConfig.cfg` file you created from your client directory or change the name of the bot in the index.ts file. 25 | to re-register the next time you run the bot script. 26 | -------------------------------------------------------------------------------- /bots/typescript/README.md: -------------------------------------------------------------------------------- 1 | # Node.js bot in TypeScript 2 | 3 | Tested with Node.js version 18.14.0 4 | 5 | ### Install deps 6 | 7 | 1. Install Nodejs 18+ (use for example NVM for this). 8 | 2. Run `$ npm install` 9 | 10 | ## Set up the API url and bot name 11 | 12 | 1. If you need to change the API base URL, edit the `API_URL` variable in `types/Api.ts` before running the bot 13 | * Or, use PAINTBOTS_URL environment variable when running the bot 14 | 2. Change the bot name in `index.ts` 15 | 16 | ## Running the bot 17 | 18 | Run the script with `$ npm start` 19 | Or, with `$ PAINTBOTS_URL="http://your-url.com" npm start` 20 | There are other helpful commands you can use while developing the bot. Check them out in `package.json`. 21 | 22 | ## Problems with registering the bot? 23 | 24 | If you restart the local paintbots server (or if the cloud server is restarted), you will need to re-register the bot. 25 | In that case, remove the `botConfig.cfg` file you created from your client directory or change the name of the bot in the index.ts file. 26 | to re-register the next time you run the bot script. 27 | -------------------------------------------------------------------------------- /bots/dotnet/PaintBots/PaintBots.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaintBots", "PaintBots\PaintBots.csproj", "{6B236CB5-39B2-4F62-BDB7-667EB8C556A1}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{3A9147D0-CC59-409D-8F7B-148F9A823532}" 6 | ProjectSection(SolutionItems) = preProject 7 | ..\README.md = ..\README.md 8 | EndProjectSection 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6B236CB5-39B2-4F62-BDB7-667EB8C556A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {6B236CB5-39B2-4F62-BDB7-667EB8C556A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {6B236CB5-39B2-4F62-BDB7-667EB8C556A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {6B236CB5-39B2-4F62-BDB7-667EB8C556A1}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /bots/java/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /bots/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paintbots-client", 3 | "version": "1.0.0", 4 | "description": "Client for the collaborative canvas drawing tool", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "npm run build && node build/index.js", 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "clean": "rm -rf build dist", 12 | "format:check": "prettier --check .", 13 | "format": "prettier --write .", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix" 16 | }, 17 | "dependencies": { 18 | "axios": "^1.3.5" 19 | }, 20 | "devDependencies": { 21 | "@types/jest": "^29.0.0", 22 | "@types/node": "18.15.11", 23 | "@types/prettier": "2.7.2", 24 | "@typescript-eslint/eslint-plugin": "^5.58.0", 25 | "@typescript-eslint/parser": "^5.58.0", 26 | "eslint": "^8.38.0", 27 | "eslint-import-resolver-typescript": "^3.5.1", 28 | "eslint-plugin-import": "^2.26.0", 29 | "jest": "^29.0.3", 30 | "prettier": "2.8.7", 31 | "ts-jest": "^29.0.1", 32 | "ts-node": "^10.9.1", 33 | "typescript": "^5.0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bots/java/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | paintbots 6 | javabot 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 17 11 | 17 12 | 13 | 14 | 15 | 16 | junit 17 | junit 18 | 4.12 19 | test 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.codehaus.mojo 27 | exec-maven-plugin 28 | 3.0.0 29 | 30 | paintbots.Main 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tatu Tarvainen and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bots/shell/_utils.sh: -------------------------------------------------------------------------------- 1 | check_url() { 2 | if [ -z "$PAINTBOTS_URL" ] 3 | then 4 | echo "No server URL defined with PAINTBOTS_URL environment variable!" 5 | exit 1 6 | fi 7 | } 8 | 9 | check_id() { 10 | if [ -z "$PAINTBOTS_ID" ] 11 | then 12 | echo "No bot id defined with PAINTBOTS_ID environment variable!" 13 | exit 1 14 | fi 15 | } 16 | 17 | post() { 18 | check_url 19 | curl -H "Content-Type: application/x-www-form-urlencoded" -d $1 $PAINTBOTS_URL 20 | } 21 | 22 | register() { 23 | check_url 24 | if [ -z "$1" ] 25 | then 26 | echo "Call register with name!" 27 | exit 1 28 | fi 29 | post "register=$1" 30 | } 31 | 32 | paint() { 33 | check_id 34 | post "id=$PAINTBOTS_ID&paint"; 35 | } 36 | 37 | move() { 38 | check_id 39 | post "id=$PAINTBOTS_ID&move=$1"; 40 | } 41 | 42 | color() { 43 | check_id 44 | post "id=$PAINTBOTS_ID&color=$1"; 45 | } 46 | 47 | clear() { 48 | check_id 49 | post "id=$PAINTBOTS_ID&clear"; 50 | } 51 | 52 | say() { 53 | MSG="$@" 54 | check_id 55 | check_url 56 | curl -H "Content-Type: application/x-www-form-urlencoded" \ 57 | --data-urlencode "id=$PAINTBOTS_ID" \ 58 | --data-urlencode "msg=$MSG" \ 59 | $PAINTBOTS_URL 60 | } 61 | 62 | look() { 63 | post "id=$PAINTBOTS_ID&look" 64 | } 65 | 66 | bots() { 67 | post "id=$PAINTBOTS_ID&bots" 68 | } 69 | -------------------------------------------------------------------------------- /bots/java/src/main/java/paintbots/Bot.java: -------------------------------------------------------------------------------- 1 | package paintbots; 2 | 3 | public class Bot { 4 | private boolean registered; 5 | private String name; 6 | private String id; 7 | 8 | private int x; 9 | private int y; 10 | private String color; 11 | 12 | public String getName() { return name; } 13 | public int getX() { return x; } 14 | public int getY() { return y; } 15 | 16 | public Bot(String name) { 17 | this.name = name; 18 | id = BotClient.register(name); 19 | registered = true; 20 | /* get these with info command */ 21 | x = 0; 22 | y = 0; 23 | color = ""; 24 | } 25 | 26 | private void checkRegistered() { 27 | if(!registered) throw new IllegalStateException("Not registered!"); 28 | } 29 | 30 | private void update(BotClient.BotResponse r) { 31 | this.x = r.x; 32 | this.y = r.y; 33 | this.color = r.color; 34 | } 35 | 36 | public enum Dir { LEFT, RIGHT, UP, DOWN }; 37 | 38 | public void move(Dir d) { 39 | checkRegistered(); 40 | update(BotClient.cmd("id", id, "move", d)); 41 | } 42 | 43 | public void paint() { 44 | checkRegistered(); 45 | update(BotClient.cmd("id", id, "paint", "1")); 46 | } 47 | 48 | public void color(String col) { 49 | checkRegistered(); 50 | update(BotClient.cmd("id", id, "color", col)); 51 | } 52 | 53 | public void bye() { 54 | checkRegistered(); 55 | BotClient.cmd("id", id, "bye", "1"); 56 | registered = false; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /bots/prolog/README.md: -------------------------------------------------------------------------------- 1 | # Prolog bot 2 | 3 | Implementation of drawing bot with Prolog (tested with SWI-Prolog). 4 | 5 | Run with `swipl bot.pl` then evaluate query `circle_demo("Somename").`. 6 | 7 | 8 | ## Toy Logo implementation 9 | 10 | The code also contains an implementation of a Logo-like programming language 11 | that can be used to create programmatic graphics with simple commands. 12 | 13 | Start the REPL by evaluating `logo('botname').` 14 | 15 | The language implements the following commands: 16 | * `fd ` draw line forwards of length `N` 17 | * `rt ` rotate `N` degrees 18 | * `bk ` draw line backwards of length `N` 19 | * `repeat [ ...code... ]` repeat the given code N times 20 | * `for [ ] [ ...code... ]` repeat code with `V` getting each value from `From` to `To` (incremented by `Step` each round). 21 | * `pen ` set pen color (0 - f) 22 | * `randpen` set a random pen color 23 | * `setxy ` move to X,Y coordinates (without drawing) 24 | * `say "message"` set your chat message 25 | 26 | Parameter values can be integer numbers (possibly negative) or variable references prefixed with colon 27 | (eg `:i`). Variables are all single characters. 28 | 29 | Example programs: 30 | 31 | Draw a spiral of lines: 32 | `setxy 80 50 for [i 2 30 3] [randpen fd :i rt 80 fd :i rt 80 fd :i rt 80]` 33 | 34 | Draw a star: 35 | `repeat 5 [ fd 25 rt 144 ]` 36 | 37 | Draw a circle of stars, each with a random color: 38 | `repeat 6 [ randpen repeat 5 [ fd 25 rt 144 ] fd 30 rt 60]` 39 | 40 | 41 | Running with docker: 42 | `docker run --env PAINTBOTS_URL=https:// --env PAINTBOTS_NAME= -it antitadex/paintbots-logo:latest` 43 | -------------------------------------------------------------------------------- /bots/python/util.py: -------------------------------------------------------------------------------- 1 | import aiofiles.os 2 | import api 3 | from bot import Bot 4 | 5 | config_file_path = "botConfig.cfg" 6 | 7 | 8 | async def file_exists(path: str) -> bool: 9 | return await aiofiles.os.path.exists(path) 10 | 11 | 12 | async def store_bot_config(bot_name: str, bot_id: str) -> dict: 13 | async with aiofiles.open(config_file_path, 'w') as f: 14 | await f.write(f"{bot_name}:{bot_id}") 15 | 16 | return {'name': bot_name, 'bot_id': bot_id} 17 | 18 | 19 | async def remove_bot_config(): 20 | exists = await file_exists(config_file_path) 21 | 22 | if exists: 23 | await aiofiles.os.remove(config_file_path) 24 | 25 | return None 26 | 27 | 28 | async def load_bot_config() -> dict or None: 29 | exists = await file_exists(config_file_path) 30 | 31 | if exists: 32 | try: 33 | async with aiofiles.open(config_file_path, 'r') as f: 34 | result = await f.read() 35 | splat = result.split(":") 36 | return {'name': splat[0], 'bot_id': splat[1]} 37 | except Exception as e: 38 | print(e) 39 | 40 | return None 41 | 42 | 43 | async def register_bot(session, bot_name: str) -> Bot: 44 | config = await load_bot_config() 45 | 46 | if config and config['name'] == bot_name: 47 | bot_id = config['bot_id'] 48 | else: 49 | bot_id = await api.register_bot(session, bot_name) 50 | await store_bot_config(bot_name, bot_id) 51 | 52 | print(f"Registered bot: {bot_name} with id: {bot_id}") 53 | 54 | return Bot(session, name=bot_name, bot_id=bot_id) 55 | 56 | 57 | async def deregister_bot(session, bot_id): 58 | await api.bye(session, bot_id) 59 | 60 | # Remove bot_config.cfg if present 61 | await remove_bot_config() 62 | -------------------------------------------------------------------------------- /bots/java/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /bots/dotnet/PaintBots/PaintBots/Bot.cs: -------------------------------------------------------------------------------- 1 | namespace PaintBots; 2 | 3 | public class Bot { 4 | private readonly BotClient _botClient; 5 | public bool Registered { get; set; } 6 | public string Name { get; } 7 | public Guid Id { get; set; } 8 | 9 | public int X { get; set; } 10 | public int Y { get; set; } 11 | public string Colour { get; set; } = ""; 12 | 13 | public Bot(string name, BotClient botClient) { 14 | Name = name; 15 | _botClient = botClient; 16 | } 17 | 18 | public async Task Register() 19 | { 20 | Id = await _botClient.Register(Name); 21 | Registered = true; 22 | } 23 | 24 | private void CheckRegistered() { 25 | if(!Registered) throw new InvalidOperationException("Not registered!"); 26 | } 27 | 28 | private void Update(BotResponse botResponse) { 29 | Console.WriteLine(botResponse); 30 | 31 | X = botResponse.X; 32 | Y = botResponse.Y; 33 | Colour = botResponse.Color; 34 | } 35 | 36 | public enum Dir { Left, Right, Up, Down }; 37 | 38 | public async Task Move(Dir d) 39 | => await Do("move", d.ToString().ToUpper()); 40 | 41 | public async Task Paint() 42 | => await Do("paint", "1"); 43 | 44 | public async Task Color(string color) 45 | => await Do("color", color); 46 | 47 | public async Task Bye(bool checkRegistration = true) 48 | => await Do("bye", "1", checkRegistration); 49 | 50 | private async Task Do(string argument, string value, bool checkRegistration = true) 51 | { 52 | if (checkRegistration) 53 | { 54 | CheckRegistered(); 55 | } 56 | var args = new Dictionary() 57 | { 58 | {"id", Id.ToString()}, 59 | {argument, value} 60 | }; 61 | var updatedBot = await _botClient.PostCommand(args); 62 | if(updatedBot != null) Update(updatedBot); 63 | } 64 | } -------------------------------------------------------------------------------- /bots/typescript/src/util.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import * as api from "./Api"; 3 | import { Bot } from "./types/Bot"; 4 | 5 | const configFilePath = "botConfig.cfg"; 6 | const fileExists = async (path: string) => 7 | !!(await fs.stat(path).catch(() => false)); 8 | 9 | export const storeBotConfig = async ( 10 | botName: string, 11 | id: string 12 | ): Promise => { 13 | await fs.writeFile(configFilePath, `${botName}:${id}`); 14 | 15 | return { 16 | name: botName, 17 | id, 18 | }; 19 | }; 20 | 21 | export const removeBotConfig = async () => { 22 | await fs.unlink(configFilePath); 23 | }; 24 | 25 | export const loadBotConfig = async (): Promise => { 26 | const exists = await fileExists(configFilePath); 27 | 28 | if (exists) { 29 | try { 30 | const result = await fs.readFile(configFilePath, "utf8"); 31 | const splat = result.split(":"); 32 | return { 33 | name: splat[0], 34 | id: splat[1], 35 | }; 36 | } catch (e) { 37 | console.error(e); 38 | } 39 | } 40 | 41 | return; 42 | }; 43 | 44 | export const registerBot = async (botName: string): Promise => { 45 | const config = await loadBotConfig(); 46 | let id; 47 | 48 | // If there is an existing config matching the chosen bot name, return the registered id, 49 | // otherwise register a new bot. 50 | if (config && config.name === botName) { 51 | id = config.id; 52 | } else { 53 | id = await api.registerBot(botName); 54 | await storeBotConfig(botName, id); 55 | } 56 | 57 | console.log(`Registered bot: ${botName} with id: ${id}`); 58 | 59 | return { 60 | name: botName, 61 | id, 62 | }; 63 | }; 64 | 65 | export const deregisterBot = async (bot: Bot) => { 66 | await api.degisterBot(bot); 67 | 68 | // Remove bot config if present 69 | await removeBotConfig(); 70 | }; 71 | 72 | export const moveBot = async ( 73 | bot: Bot, 74 | dir: string, 75 | dist: number 76 | ): Promise => { 77 | for (let i = 0; i < dist; i++) { 78 | bot = await api.moveBot(bot, dir); 79 | } 80 | 81 | return bot; 82 | }; 83 | -------------------------------------------------------------------------------- /bots/python/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import random 4 | from util import register_bot 5 | 6 | # Name to be registered. Must be unique in the drawing board. 7 | bot_name = "MyBot" 8 | # See color palette documentation in api.set_color 9 | bot_color = 6 10 | sayings = [ 11 | "Leoka pystyyn kun tulloo kova paekka, pyssyypähän aenae suu kiinni.", 12 | "Ne tekköö jotka ossoo, jotka ee ossoo ne arvostelloo.", 13 | "Joka ihteesä luottaa, se kykysä tuploo.", 14 | "Anna kaekkes vuan elä periks.", 15 | "Naara itelles ennen ku muut kerkijää.", 16 | "Jos et tiijjä, niin kysy.", 17 | "Jos ymmärrät kaeken, oot varmasti käsittännä viärin.", 18 | "Misteepä sen tietää, mihin pystyy, ennen kun kokkeeloo." 19 | ] 20 | 21 | 22 | async def main(): 23 | async with aiohttp.ClientSession() as session: 24 | bot = await register_bot(session, bot_name) 25 | await bot.set_color(bot_color) 26 | await bot.say(sayings[random.randint(0, len(sayings) - 1)]) 27 | 28 | # Draw some simple rectangles 29 | # Add your own drawing helper functions into bot.py 30 | await bot.draw_rectangle(6) 31 | await bot.move_bot("RIGHT", 4) 32 | await bot.draw_rectangle(2) 33 | await bot.move_bot("RIGHT", 6) 34 | await bot.draw_rectangle(6) 35 | await bot.move_bot("RIGHT", 4) 36 | await bot.draw_rectangle(2) 37 | await bot.move_bot("RIGHT", 8) 38 | 39 | print(f"Current bot position: {bot.x},{bot.y} and current bot color: {bot.color}") 40 | 41 | # Get the current state of all registered bots (json) 42 | # Useful i.e. for bots that want to utilize some swarming behaviour 43 | # print(await bot.bots()) 44 | 45 | # Print the current state of the canvas 46 | # print(await bot.look()) 47 | 48 | # Call 'deregister_bot' if you want to remove your bot from the server and, for example, change your bot name. 49 | # Your bot key is stored in botConfig.cfg after registration, and it is reused when you run this script again. 50 | # The deregister_bot command will remove the botConfig.cfg file automatically. 51 | # await bot.deregister_bot() 52 | 53 | 54 | if __name__ == '__main__': 55 | asyncio.run(main()) 56 | -------------------------------------------------------------------------------- /src/paintbots/png.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.png 2 | "Export PNGs from canvas data. 3 | 4 | The canvas-exporter thread periodically polls the 5 | state and creates byte arrays of all canvases. 6 | 7 | The process also outputs the canvas data to files 8 | so they can be exported into mp4 movies using ffmpeg." 9 | (:require [clojure.java.io :as io] 10 | [clojure.core.async :refer [go-loop bytes 21 | [^BufferedImage img] 22 | (with-open [out (java.io.ByteArrayOutputStream.)] 23 | (ImageIO/write img "png" out) 24 | (.flush out) 25 | (.toByteArray out))) 26 | 27 | (defn listen! 28 | "Start a listener go block that polls state every `interval` 29 | milliseconds and writes a PNG if it has changed." 30 | [state {:keys [interval] 31 | :or {interval 500}}] 32 | (go-loop [canvas-changed {}] 33 | (let [current-state @state 34 | new-canvas-changed 35 | (reduce-kv 36 | (fn [changed-map canvas-name {:keys [lock img changed]}] 37 | (let [{previous-changed :changed i :i 38 | :or {previous-changed 0 i 0}} (get changed-map canvas-name) 39 | changed? (< previous-changed changed)] 40 | (when changed? 41 | ;;(println "Canvas " canvas-name " changed at " changed) 42 | (let [b (locking lock (img->bytes img))] 43 | (swap! image-data assoc canvas-name b) 44 | (io/copy b (io/file (format "%s_%06d.png" canvas-name i))))) 45 | (assoc changed-map canvas-name {:changed changed 46 | :i (if changed? (inc i) i)}))) 47 | canvas-changed (:canvas current-state))] 48 | ( 21 | /// Register name with server 22 | /// 23 | /// 24 | /// Id of the registered bot 25 | public async Task Register(string name) 26 | { 27 | var formContent = new FormUrlEncodedContent(new Dictionary {{"register", name}}); 28 | var result = await _httpClient.PostAsync("", formContent); 29 | return Guid.Parse(await result.Content.ReadAsStringAsync()); 30 | } 31 | 32 | /// 33 | /// POST against the API with any collection of bot arguments: https://github.com/tatut/paintbots#bot-commands 34 | /// 35 | /// Dictionary of key value pairs to be used as arguments for the POST 36 | /// Returns the received state of the 37 | public async Task PostCommand(IDictionary args) 38 | { 39 | var formContent = new FormUrlEncodedContent(args); 40 | var response = await _httpClient.PostAsync("", formContent); 41 | var responseQueryString = await response.Content.ReadAsStringAsync(); 42 | BotResponse? botResponse = null; 43 | if (!string.IsNullOrEmpty(responseQueryString)) 44 | { 45 | var parsed = HttpUtility.ParseQueryString(responseQueryString); 46 | botResponse = new BotResponse(int.Parse(parsed["x"]!), int.Parse(parsed["y"]!), parsed["color"]!); 47 | } 48 | return botResponse; 49 | } 50 | } 51 | 52 | /// 53 | /// Simply record matching the expected response for API requests related to a Bot 54 | /// 55 | /// 56 | /// 57 | /// 58 | public record BotResponse(int X, int Y, string Color); -------------------------------------------------------------------------------- /bots/node/bot.mjs: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import http from 'http'; 3 | import https from 'https'; 4 | 5 | const httpAgent = new http.Agent({ keepAlive: false }); 6 | const httpsAgent = new https.Agent({ keepAlive: false }); 7 | const agent = (_parsedURL) => _parsedURL.protocol == 'http:' ? httpAgent : httpsAgent; 8 | 9 | 10 | const URL = process.argv[2] || "http://localhost:31173"; 11 | console.log("Using URL: "+ URL); 12 | 13 | function params(p) { 14 | let param = new URLSearchParams(); 15 | for(let k in p) param.append(k, p[k]); 16 | return param; 17 | } 18 | 19 | function post(data) { 20 | return fetch(URL, {method: "POST", body: params(data), agent}); 21 | } 22 | 23 | async function register(bot) { 24 | const r = await post({register: bot.name}); 25 | const id = await r.text(); 26 | console.log("ID: ", id); 27 | return {id: id, ...bot}; 28 | } 29 | 30 | // Issue a bot command, returns new bot (updates x/y/color) 31 | async function command(bot, params) { 32 | //console.log("COMMAND, bot: ", bot, " params: ", params); 33 | const r = await post({id: bot.id, ...params}); 34 | const fd = await r.formData(); 35 | return {x: parseInt(fd.get("x")), y: parseInt(fd.get("y")), color: fd.get("color"), ...bot}; 36 | } 37 | 38 | async function move(bot, dir) { 39 | return await command(bot, {move: dir}); 40 | } 41 | async function paint(bot) { 42 | return await command(bot, {paint: "1"}); 43 | } 44 | async function color(bot, col) { 45 | return await command(bot, {color: col}); 46 | } 47 | 48 | async function line(bot, dir, count) { 49 | let b = bot; 50 | for(let i=0; i < count; i++) { 51 | b = await paint(b); 52 | b = await move(b, dir); 53 | } 54 | return b; 55 | } 56 | 57 | // Main entrypoint here, registers a new bot, draws a "maze" 58 | async function main(name) { 59 | let bot = await register({name: name}) 60 | bot = await line(bot, "LEFT", 10); 61 | bot = await line(bot, "DOWN", 9); 62 | bot = await line(bot, "RIGHT", 8); 63 | bot = await line(bot, "UP", 7); 64 | bot = await line(bot, "LEFT", 6); 65 | bot = await line(bot, "DOWN", 5); 66 | bot = await line(bot, "RIGHT", 4); 67 | bot = await line(bot, "UP", 3); 68 | bot = await line(bot, "LEFT", 2); 69 | bot = await line(bot, "DOWN", 1); 70 | } 71 | 72 | main("Mazer"); 73 | -------------------------------------------------------------------------------- /bots/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as api from "./Api"; 2 | import { Bot } from "./types/Bot"; 3 | import { Color, colors } from "./types/Color"; 4 | import { moveBot, registerBot } from "./util"; 5 | 6 | // Name to be registered. Must be unique in the drawing board. 7 | const botName = "MyBot"; 8 | // See color palette documentation in Color.ts 9 | const botColor: Color = colors.RED; 10 | 11 | const sayings = [ 12 | "Kylän kohoralla komiasti, vaikka mettällä vähän kompuroottooki.", 13 | "Kyllä maailma opettaa, jonsei muuta niin hilijaa kävelemähän.", 14 | "Olokaa klopit hilijaa siälä porstuas, nyt tuloo runua!", 15 | "Hyviä neuvoja sateloo niinku rakehia.", 16 | "Minen palijo mitää tee, jos mä jotaki teen, niin mä makaan.", 17 | "Nii on jano, notta sylyki pöläjää. 🍺", 18 | "Kyllä aika piisaa, kun vain järki kestää.", 19 | "Me ei teherä virheitä, vaa ilosii pikku vahinkoi.", 20 | ]; 21 | 22 | /** 23 | * Example helper functions for drawing a simple rectangle using the api calls 24 | * Here we are moving the bot first to a certain direction and then painting a 25 | * pixel with a color that was previously set in the main function. 26 | * @param bot 27 | * @param width 28 | */ 29 | const drawRectangle = async (bot: Bot, width: number): Promise => { 30 | const dirs = ["RIGHT", "DOWN", "LEFT", "UP"]; 31 | 32 | for (const dir of dirs) { 33 | for (let i = 1; i < width; i++) { 34 | bot = await api.moveBot(bot, dir); 35 | bot = await api.paintPixel(bot); 36 | } 37 | } 38 | 39 | return bot; 40 | }; 41 | 42 | export async function main() { 43 | let bot = await registerBot(botName); 44 | bot = await api.setColor(bot, botColor); 45 | bot = await api.say(bot, sayings[Math.floor(Math.random() * sayings.length)]); 46 | 47 | // Draw some simple rectangles for example (make your own helper functions for more complex shapes!) 48 | bot = await drawRectangle(bot, 6); 49 | bot = await moveBot(bot, "RIGHT", 3); 50 | bot = await drawRectangle(bot, 3); 51 | bot = await moveBot(bot, "RIGHT", 6); 52 | bot = await drawRectangle(bot, 6); 53 | bot = await moveBot(bot, "RIGHT", 3); 54 | bot = await drawRectangle(bot, 3); 55 | bot = await moveBot(bot, "RIGHT", 8); 56 | 57 | console.log( 58 | `Current bot position: ${bot.position?.x},${bot.position?.y} and current bot color: ${bot.color}` 59 | ); 60 | 61 | // Print the current state of canvas in ASCII 62 | // console.log(await api.look(bot)) 63 | 64 | // Get the current state of all registered bots (json) 65 | // Useful i.e. for bots that want to utilize some swarming behaviour 66 | // console.log(await api.bots(bot)) 67 | 68 | // Call 'deregisterBot' if you want to remove your bot from the server and, for example, change your bot name. 69 | // Your bot key is stored in botConfig.cfg after registration, and it is reused when you run this script again. 70 | // The deregister_bot command will remove the botConfig.cfg file automatically. 71 | // await deregisterBot(bot) 72 | } 73 | 74 | if (require.main === module) { 75 | main(); 76 | } 77 | -------------------------------------------------------------------------------- /bots/java/src/main/java/paintbots/BotClient.java: -------------------------------------------------------------------------------- 1 | package paintbots; 2 | 3 | import java.net.http.HttpClient; 4 | import java.net.http.HttpRequest; 5 | import java.net.http.HttpResponse; 6 | import java.net.http.HttpResponse.BodyHandlers; 7 | import java.io.IOException; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URI; 10 | import java.net.URLEncoder; 11 | 12 | /** 13 | * HTML interface helper to call paintbots server. 14 | */ 15 | public class BotClient { 16 | static String LOCAL_URL = "http://localhost:31173"; 17 | 18 | static final String url; 19 | static { 20 | String envUrl = System.getenv("PAINTBOTS_URL"); 21 | url = envUrl == null ? LOCAL_URL : envUrl; 22 | } 23 | 24 | private static String enc(Object s) { 25 | if(s == null) return ""; 26 | try { 27 | return URLEncoder.encode(s.toString(), "UTF-8"); 28 | } catch(UnsupportedEncodingException uee) { 29 | throw new RuntimeException(uee); 30 | } 31 | } 32 | 33 | private static String post(Object... data) { 34 | var b = new StringBuilder(); 35 | for(int i=0; i0) b.append("&"); 37 | b.append(enc(data[i*2+0])).append("=").append(enc(data[i*2+1])); 38 | } 39 | var req = HttpRequest.newBuilder() 40 | .uri(URI.create(url)) 41 | .header("Content-Type", "application/x-www-form-urlencoded") 42 | .POST(HttpRequest.BodyPublishers.ofString(b.toString())) 43 | .build(); 44 | try { 45 | HttpResponse resp = HttpClient.newHttpClient().send(req, BodyHandlers.ofString()); 46 | if(resp.statusCode() >= 400) { 47 | throw new RuntimeException("Got unexpected status: "+resp.statusCode()+", with body: "+resp.body()); 48 | } else { 49 | return resp.body(); 50 | } 51 | } catch(IOException ioe) { 52 | throw new RuntimeException("IO Exception in POST", ioe); 53 | } catch(InterruptedException ie) { 54 | throw new RuntimeException("Interrupted in POST", ie); 55 | } 56 | } 57 | 58 | public static class BotResponse { 59 | public int x; 60 | public int y; 61 | public String color; 62 | BotResponse(String s) { 63 | System.out.println("GOT: "+s); 64 | x = 0; y = 0; 65 | for(String fd : s.split("&")) { 66 | String[] kv = fd.split("="); 67 | switch(kv[0]) { 68 | case "x": x = Integer.parseInt(kv[1]); break; 69 | case "y": y = Integer.parseInt(kv[1]); break; 70 | case "color": color = kv[1]; break; 71 | } 72 | } 73 | } 74 | } 75 | 76 | public static BotResponse cmd(Object... args) { 77 | return new BotResponse(post(args)); 78 | } 79 | 80 | /** 81 | * Register name with server, returns id. 82 | */ 83 | public static String register(String name) { 84 | return post("register", name); 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /bots/python/bot.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import api 3 | import util 4 | 5 | 6 | class Bot: 7 | def __init__(self, session: aiohttp.ClientSession, name: str, bot_id: str): 8 | self.session = session 9 | self.name = name 10 | self.bot_id = bot_id 11 | self.color = None 12 | self.x = None 13 | self.y = None 14 | 15 | def __set_position(self, response): 16 | self.x = response['x'] 17 | self.y = response['y'] 18 | 19 | async def deregister_bot(self): 20 | await util.deregister_bot(self.session, self.bot_id) 21 | 22 | return self 23 | 24 | async def set_color(self, color: int): 25 | """ 26 | Sets the color of the bot. This determines in what color the bot will paint in. 27 | 28 | :param color: 29 | :return: 30 | """ 31 | self.color = color 32 | 33 | response = await api.set_color(self.session, self.bot_id, color) 34 | self.__set_position(response) 35 | 36 | return self 37 | 38 | async def paint_pixel(self): 39 | response = await api.paint_pixel(self.session, self.bot_id) 40 | 41 | self.__set_position(response) 42 | 43 | return self 44 | 45 | async def clear_pixel(self): 46 | response = await api.clear_pixel(self.session, self.bot_id) 47 | 48 | self.__set_position(response) 49 | 50 | return self 51 | 52 | async def say(self, msg: str): 53 | response = await api.say(self.session, self.bot_id, msg) 54 | 55 | self.__set_position(response) 56 | 57 | return self 58 | 59 | async def move_bot(self, direction: str, dist: int): 60 | response = None 61 | for i in range(dist): 62 | response = await api.move_bot(self.session, self.bot_id, direction) 63 | 64 | self.__set_position(response) 65 | 66 | return self 67 | 68 | async def look(self): 69 | response = await api.look(self.session, self.bot_id) 70 | 71 | # Returns the ASCII representation of the current canvas 72 | return response 73 | 74 | async def bots(self): 75 | """ 76 | :return: (JSON) information about all registered bots 77 | """ 78 | response = await api.bots(self.session, self.bot_id) 79 | 80 | return response 81 | 82 | # !! Add your own drawing functions here !! 83 | 84 | async def draw_rectangle(self, width: int): 85 | """ 86 | Example helper function for drawing a simple rectangle using the api calls 87 | Here, we are moving the bot first to a certain direction and then painting a 88 | single pixel with a color that was previously set in the main function for the bot. 89 | 90 | :param width: 91 | :return: 92 | """ 93 | dirs = ["RIGHT", "DOWN", "LEFT", "UP"] 94 | 95 | for direction in dirs: 96 | for i in range(width): 97 | # Move bot and paint a pixel one pixel at time 98 | await api.move_bot(self.session, self.bot_id, direction) 99 | await self.paint_pixel() 100 | 101 | return self 102 | -------------------------------------------------------------------------------- /src/paintbots/client.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.client 2 | "UI to include Logo toy interpreter client on the page." 3 | (:require [ripley.html :as h])) 4 | 5 | (defn client-head [] 6 | (h/html 7 | [:script {:src "/client/swipl-bundle.js"}]) 8 | (h/html 9 | [:script 10 | "new SWIPL({}).then((ok,_) => { 11 | P = ok.prolog; 12 | let l = window.location; 13 | let url = l.protocol+'//'+l.host+'/client/logo.pl'; 14 | P.consult(url) 15 | }); 16 | 17 | function pb_post(form_data) { 18 | let fd = form_data.map(c=>{ 19 | let a = c.arguments(); 20 | return encodeURIComponent(a[0]) + \"=\" + encodeURIComponent(a[1]); 21 | }).join(\"&\"); 22 | let l = window.location; 23 | let url = l.protocol+'//'+l.host+l.pathname; 24 | return fetch(url, { 25 | method: \"POST\", 26 | headers: {\"Content-Type\": \"application/x-www-form-urlencoded\"}, 27 | body: fd}); 28 | } 29 | 30 | window._promises = {}; 31 | 32 | function get_input() { 33 | document.querySelector('#botname').disabled = true; 34 | document.querySelector('#regbtn').style.visibility = 'hidden'; 35 | return new Promise((resolve) => { window._promises.input = resolve }); 36 | } 37 | 38 | function send_input() { 39 | let input = document.querySelector(\"#logo\").value; 40 | let p = window._promises.input; 41 | delete window._promises.input; 42 | p(input); 43 | } 44 | function maybe_send_input(event) { 45 | if(event.code=='Enter' && event.ctrlKey) send_input(); 46 | } 47 | function register() { 48 | P.forEach('start_repl.'); 49 | } 50 | 51 | function log(msg) { 52 | let l = document.querySelector('#logs'); 53 | l.innerHTML += `
${msg}
`; 54 | l.scrollTop = l.scrollHeight; 55 | } 56 | 57 | function botinfo(X,Y,C,Ang) { 58 | document.querySelector('#X').innerHTML = X; 59 | document.querySelector('#Y').innerText = Y; 60 | document.querySelector('#C').innerText = C; 61 | document.querySelector('#Ang').innerText = Ang; 62 | } 63 | function get_bot_name() { return document.querySelector(\"#botname\").value.trim(); } 64 | "])) 65 | 66 | (defn client-ui [] 67 | (h/html 68 | [:div.bot 69 | [:div 70 | [:b "Bot name: "] 71 | [:input#botname {:placeholder "mybot123"}] 72 | [:button#regbtn.btn.btn-sm {:on-click "register()"} "Register"]] 73 | [:div.flex.w-full 74 | [:div.grid.flex-grow.card 75 | [:textarea#logo {:style "font-family: monospace,monospace;" :rows 7 :cols 80 76 | :on-key-press "maybe_send_input(event)"} "repeat 5 [fd 25 rt 144]"] 77 | [:div 78 | [:button.btn.btn-sm {:on-click "send_input()"} "Execute"] 79 | " (ctrl-enter)"]] 80 | [:div.divider.divider-horizontal] 81 | [:div#logs.flex-grow.card.font-mono {:style "max-width: 50%; max-height: 200px; overflow-y: scroll;"}] 82 | ] 83 | [:div.font-mono 84 | [:div.inline.mx-2 [:b "X: "] [:span#X "N/A"]] 85 | [:div.inline.mx-2 [:b "Y: "] [:span#Y "N/A"]] 86 | [:div.inline.mx-2 [:b "C: "] [:span#C "N/A"]] 87 | [:div.inline.mx-2 [:b "A: "] [:span#Ang "N/A"]]]])) 88 | -------------------------------------------------------------------------------- /bots/python/api.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | import os 3 | 4 | API_URL = os.getenv('PAINTBOTS_URL', 'http://localhost:31173/') 5 | headers = {'content-type': 'application/x-www-form-urlencoded'} 6 | 7 | 8 | async def parse_text_response(response): 9 | return await response.text() 10 | 11 | 12 | async def parse_json_response(response): 13 | return await response.json() 14 | 15 | 16 | async def parse_position_response(response): 17 | r = await parse_text_response(response) 18 | params = dict(urllib.parse.parse_qsl(r)) 19 | color = params.get('color') 20 | x = params.get('x') 21 | y = params.get('y') 22 | 23 | color = int(color) if color else None 24 | x = int(params.get('x')) if x else None 25 | y = int(params.get('y')) if y else None 26 | 27 | return {'color': color, 'x': x, 'y': y} 28 | 29 | 30 | async def api_command(session, command, error_msg, parse_response=parse_position_response, debug=False): 31 | try: 32 | async with session.post(API_URL, data=command, headers=headers) as resp: 33 | response = await resp.text() 34 | if debug: 35 | print(command, response) 36 | 37 | resp.raise_for_status() 38 | 39 | return await parse_response(resp) 40 | except Exception as e: 41 | msg = response or e 42 | 43 | raise Exception(f"{error_msg}: {msg}") 44 | 45 | 46 | async def register_bot(session, name): 47 | return await api_command(session, {'register': name}, "Failed to register bot", parse_text_response) 48 | 49 | 50 | async def look(session, bot_id): 51 | """ 52 | Note: Returns an ascii representation of the current canvas. 53 | """ 54 | 55 | return await api_command(session, {'id': bot_id, 'look': ''}, "Failed to look", parse_text_response) 56 | 57 | 58 | async def bots(session, bot_id): 59 | """ 60 | Return (JSON) information about all registered bots 61 | """ 62 | 63 | return await api_command(session, {'id': bot_id, 'bots': ''}, "Failed to fetch bots state", parse_json_response) 64 | 65 | 66 | async def move_bot(session, bot_id, direction): 67 | return await api_command(session, {'id': bot_id, 'move': direction}, "Failed to move bot") 68 | 69 | 70 | # pico-8 16 color palette from https://www.pixilart.com/palettes/pico-8-51001 71 | # 0 "#000000" 72 | # 1 "#1D2B53" 73 | # 2 "#7E2553" 74 | # 3 "#008751" 75 | # 4 "#AB5236" 76 | # 5 "#5F574F" 77 | # 6 "#C2C3C7" 78 | # 7 "#FFF1E8" 79 | # 8 "#FF004D" 80 | # 9 "#FFA300" 81 | # 10 "#FFEC27" 82 | # 11 "#00E436" 83 | # 12 "#29ADFF" 84 | # 13 "#83769C" 85 | # 14 "#FF77A8" 86 | # 15 "#FFCCAA" 87 | 88 | async def set_color(session, bot_id, color): 89 | return await api_command(session, {'id': bot_id, 'color': color}, "Failed to set color") 90 | 91 | 92 | async def paint_pixel(session, bot_id): 93 | return await api_command(session, {'id': bot_id, 'paint': ''}, "Failed to paint") 94 | 95 | 96 | async def clear_pixel(session, bot_id): 97 | return await api_command(session, {'id': bot_id, 'clear': ''}, "Failed to clear a pixel") 98 | 99 | 100 | async def say(session, bot_id, msg): 101 | return await api_command(session, {'id': bot_id, 'msg': msg}, "Failed to say a message") 102 | 103 | 104 | async def bye(session, bot_id): 105 | return await api_command(session, {'id': bot_id, 'bye': ''}, "Failed to deregister the bot") 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paintbots! 2 | 3 | A collaborative canvas where participants program "bots" that move on the canvas and color in cells. 4 | The server contains a web UI to watch the canvas in (near) realtime to see how the art is being generated. 5 | 6 | The server will also periodically save a PNG of the canvas that can be turned into a video at the end of 7 | a session. 8 | 9 | ## Running 10 | 11 | Start by running `clojure -m paintbots.main`. Then open browser at http://localhost:31173 12 | 13 | See bots folder for sample bots using simple bash scripts. 14 | 15 | Alternatively, you can run local server using Docker without needing to install Clojure: 16 | `docker run -p 31173:31173 antitadex/paintbots:latest` 17 | 18 | ## Bot commands 19 | 20 | Each bot must be registered to use. All bot commands are POSTed using simple form encoding to 21 | the server. See bots folder `_utils.sh` on how it uses curl to post commands. 22 | 23 | | Command | Parameters | Description | 24 | |----------|-----------------|-----------------------------------------------------------------------------------------| 25 | | register | register=name | register a bot with the given name (if not already registered), returns id | 26 | | info | id=ID&info | no-op command that just returns bots current | 27 | | move | id=ID&move=DIR | move from current to position to direction DIR, which is one of LEFT, RIGHT, UP or DOWN | 28 | | paint | id=ID&paint | paint the current position with the current color | 29 | | color | id=ID&color=COL | set the current color to COL, which is one of 0-f (16 color palette) | 30 | | msg | id=ID&msg=MSG | say MSG, displays the message along with your name in the UI | 31 | | clear | id=ID&clear | clear the pixel at current position | 32 | | look | id=ID&look | look around, returns ascii containing the current image (with colors as above) | 33 | | bye | id=ID&bye | deregister this bot (id no longer is usable and name can be reused) | 34 | | bots | id=ID&bots | return (JSON) information about all registered bots | 35 | 36 | Register command returns just the ID (as plain text) for future use. All other commands return your 37 | bot's current position and color. 38 | 39 | ### Using WebSockets 40 | 41 | If you prefer, you can also connect via WebSocket. The API is the same, but instead of sending each 42 | command as a separate HTTP request, you connect with the canvas URL. The server expects a text message with 43 | the same form encoded format and sends the response back as a text message. 44 | 45 | The commands are the same, but register only returns "OK" instead of an id. Id parameter is not required 46 | as the id is implicit in the WebSocket connection. The bot is automatically deregistered when the connection 47 | is closed. 48 | 49 | ## Deployment 50 | 51 | See azure/README.md for instructions on how to deploy to Azure cloud. You can also easily deploy to 52 | any cloud provider that supports hosting Docker images. 53 | 54 | 55 | ## Configuration 56 | 57 | The default parameters in `config.edn` file are enough for most cases, but it is **highly** recommended 58 | to change at least the admin password. 59 | 60 | Notable configuration options: 61 | 62 | * `:width` and `:height` affect the canvas size (320x200 or 160x100 are good to have something visible, bigger canvas will use more memory also) 63 | * `:command-duration-ms` affects how long the processing of a single command will take at minimum (to prevent flooding with commands) 64 | * `:password` admin UI password 65 | 66 | See `config.edn` for full configuration with suitable sample values. 67 | 68 | ## Endpoints 69 | 70 | HTTP endpoints in the running software: 71 | 72 | * `GET /admin` the admin UI that lets you create/clear canvases and kick players (see above for configuring password) 73 | * `GET /` view a canvas with given name 74 | * `POST /` post a bot command to the given canvas 75 | 76 | The canvas name in URL can only contain letters and the root path is a canvas named "scratch". 77 | A canvas cannot be named "admin". 78 | 79 | ## Logo-like language 80 | 81 | The UI includes a Logo-like language interpreter that can be used through the browser (tested with Chrome). 82 | The evaluation uses the same API as all other bots, via the browser's `fetch`. 83 | 84 | To start the interpreted, view the canvas page and add URL parameter `?client=1`. 85 | For examples on programs see `logo.md` file in this repository. 86 | -------------------------------------------------------------------------------- /bots/typescript/src/Api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, RawAxiosRequestHeaders } from "axios"; 2 | import { Bot } from "./types/Bot"; 3 | import { BotCommand } from "./types/BotCommand"; 4 | import { Color } from "./types/Color"; 5 | import { Pixel } from "./types/Pixel"; 6 | 7 | const API_URL = process.env.PAINTBOTS_URL || "http://localhost:31173/"; 8 | 9 | const config: AxiosRequestConfig = { 10 | headers: { 11 | "content-type": "application/x-www-form-urlencoded", 12 | } as RawAxiosRequestHeaders, 13 | }; 14 | 15 | export const registerBot = async (name: string): Promise => { 16 | try { 17 | const response = await axios.post(API_URL, { register: name }, config); 18 | console.log(`Got bot id from server: ${response.data}`); 19 | 20 | return response.data; 21 | } catch (e) { 22 | if (axios.isAxiosError(e)) { 23 | const errResp = e.response; 24 | const msg = errResp?.data || e.message 25 | 26 | throw Error(`Failed to register bot: ${msg}`); 27 | } else { 28 | throw e; 29 | } 30 | } 31 | }; 32 | 33 | 34 | export const degisterBot = async (bot: Bot) => { 35 | try { 36 | const response = await axios.post(API_URL, {id: bot.id, bye: ''}, config); 37 | 38 | return response.data; 39 | } catch (e) { 40 | if (axios.isAxiosError(e)) { 41 | const errResp = e.response; 42 | const msg = errResp?.data || e.message 43 | 44 | throw Error(`Failed to deregister bot: ${msg}`); 45 | } else { 46 | throw e; 47 | } 48 | } 49 | }; 50 | 51 | 52 | 53 | /** 54 | * Returns an ascii representation of the current canvas. 55 | */ 56 | export const look = async (bot: Bot): Promise => { 57 | try { 58 | const response = await axios.post( 59 | API_URL, 60 | { id: bot.id, look: "" }, 61 | config 62 | ); 63 | return response.data; 64 | } catch (e) { 65 | if (axios.isAxiosError(e)) { 66 | const errResp = e.response; 67 | const msg = errResp?.data || e.message 68 | 69 | throw Error(`Failed to look: ${msg}`); 70 | } else { 71 | throw e; 72 | } 73 | } 74 | }; 75 | 76 | 77 | /** 78 | * Returns (JSON) information about all registered bots 79 | */ 80 | export const bots = async (bot: Bot): Promise => { 81 | try { 82 | const response = await axios.post( 83 | API_URL, 84 | { id: bot.id, bots: "" }, 85 | config 86 | ); 87 | return response.data; 88 | } catch (e) { 89 | if (axios.isAxiosError(e)) { 90 | const errResp = e.response; 91 | const msg = errResp?.data || e.message 92 | 93 | throw Error(`Failed to fetch bots state: ${msg}`); 94 | } else { 95 | throw e; 96 | } 97 | } 98 | }; 99 | 100 | const parsePixelResponse = (response: string): Pixel => { 101 | const params = new URLSearchParams(response); 102 | 103 | let color: Color | undefined = undefined; 104 | let x: number | undefined = undefined; 105 | let y: number | undefined = undefined; 106 | 107 | if (params.get("color")) { 108 | color = params.get("color") as Color; 109 | } 110 | 111 | if (params.get("x")) { 112 | x = parseInt(params.get("x")); 113 | } 114 | 115 | if (params.get("y")) { 116 | y = parseInt(params.get("y")); 117 | } 118 | 119 | if (!color || !x || !y) { 120 | throw Error("Unable to parse pixel response!"); 121 | } 122 | 123 | return { color, position: { x, y } }; 124 | }; 125 | 126 | const apiCommand = async (bot: Bot, command: BotCommand, errorMsg: string) => { 127 | try { 128 | const response = await axios.post(API_URL, command, config); 129 | 130 | return { ...bot, ...parsePixelResponse(response.data) }; 131 | } catch (e) { 132 | if (axios.isAxiosError(e)) { 133 | const errResp = e.response; 134 | const msg = errResp?.data || e.message 135 | 136 | throw Error(`${errorMsg}: ${msg}`); 137 | } else { 138 | throw e; 139 | } 140 | } 141 | }; 142 | 143 | export const moveBot = async (bot: Bot, dir: string): Promise => { 144 | return await apiCommand(bot, { id: bot.id, move: dir }, "Failed to move bot"); 145 | }; 146 | 147 | export const setColor = async (bot: Bot, color: Color): Promise => { 148 | return await apiCommand(bot, { id: bot.id, color }, "Failed to set color"); 149 | }; 150 | 151 | export const paintPixel = async (bot: Bot): Promise => { 152 | return await apiCommand(bot, { id: bot.id, paint: "" }, "Failed to paint"); 153 | }; 154 | 155 | export const clearPixel = async (bot: Bot): Promise => { 156 | return await apiCommand(bot, { id: bot.id, clear: "" }, "Failed to clear a pixel" 157 | ); 158 | }; 159 | 160 | export const say = async (bot: Bot, msg: string): Promise => { 161 | return await apiCommand(bot, { id: bot.id, msg }, "Failed to say a message"); 162 | }; 163 | -------------------------------------------------------------------------------- /bots/clojure/src/paintbots/bot.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.bot 2 | (:require [org.httpkit.client :as http] 3 | [clojure.string :as str] 4 | [bortexz.resocket :as ws] 5 | [clojure.core.async :refer [!!] :as async]) 6 | (:import (java.net URLEncoder URLDecoder))) 7 | 8 | (def url (or (System/getenv "PAINTBOTS_URL") 9 | "ws://localhost:31173")) 10 | 11 | (def integer-fields #{:x :y}) 12 | ;; Bot interface to server 13 | 14 | (def ^:dynamic *retry?* false) 15 | 16 | (defn- form->params [body] 17 | (into {} 18 | (for [field (str/split body #"&") 19 | :let [[k v] (str/split field #"=") 20 | kw (keyword (URLDecoder/decode k)) 21 | v (when v (URLDecoder/decode v))]] 22 | [kw (if (integer-fields kw) 23 | (Integer/parseInt v) 24 | v)]))) 25 | 26 | (defn- params->form [p] 27 | (str/join "&" 28 | (for [[k v] p] 29 | (str (URLEncoder/encode (name k)) "=" (URLEncoder/encode (str v)))))) 30 | 31 | (defn- post [args] 32 | (let [{:keys [status body headers] :as _resp} @(http/post url {:form-params args :as :text})] 33 | (cond 34 | (and (= status 409) *retry?*) 35 | (do (Thread/sleep 1000) 36 | (binding [*retry?* false] 37 | (post args))) 38 | (>= status 400) 39 | (throw (ex-info "Unexpected status code" {:status status :body body})) 40 | 41 | (= (:content-type headers) "application/x-www-form-urlencoded") 42 | (form->params body) 43 | 44 | (contains? args :register) 45 | {:id body} 46 | 47 | :else body))) 48 | 49 | 50 | 51 | (defn send! [{id :id :as bot} msg] 52 | (if-let [ws (::ws bot)] 53 | ;; Use WS connection 54 | (do 55 | (def *ws ws) 56 | ;; id is implicit in the WS connection and not needed 57 | (->> (dissoc msg :id) params->form (>!! (:output ws))) 58 | (->> ws :input params (merge bot))) 59 | 60 | ;; Use HTTP 61 | (merge bot (post (merge msg 62 | (when id 63 | {:id id})))))) 64 | 65 | (defn register [name] 66 | (send! (merge {:name name} 67 | (when (str/starts-with? url "ws") 68 | ;; if url is ws:// or wss:// use websocket connection 69 | {::ws ( bot paint (move dir))) 115 | bot (range c)) 116 | curve colors))))) 117 | 118 | (defn move-to [bot to-x to-y] 119 | (let [dx (- (:x bot) to-x) 120 | dy (- (:y bot) to-y) 121 | 122 | ;; move randomly either x or y (to look cool ;) 123 | r (rand-int 2)] 124 | (cond 125 | (and (zero? dx) (zero? dy)) 126 | bot 127 | 128 | (and (= 0 r) (not= 0 dx)) 129 | (recur (move bot (if (neg? dx) "RIGHT" "LEFT")) to-x to-y) 130 | 131 | (and (= 1 r) (not= 0 dy)) 132 | (recur (move bot (if (neg? dy) "DOWN" "UP")) to-x to-y) 133 | 134 | :else 135 | (recur bot to-x to-y)))) 136 | 137 | (defn -main [& args] 138 | (-> (register (str "dragon" (System/currentTimeMillis))) 139 | (move "LEFT") 140 | (move-to (+ 75 (rand-int 10)) (+ 45 (rand-int 10))) 141 | (draw 9 4))) 142 | 143 | (defn stress-test 144 | "Run n bots to stress test the server. 145 | Returns 0 arity function to stop the stress test." 146 | [n-bots] 147 | (let [done? (atom false)] 148 | (dotimes [i n-bots] 149 | (.start (Thread. #(binding [*retry?* true] 150 | (loop [bot (register (str "stress" i))] 151 | (when-not @done? 152 | (-> bot 153 | (move (rand-nth ["LEFT" "RIGHT" "UP" "DOWN"])) 154 | paint 155 | (color (rand-nth "0123456789abcdef")) 156 | recur))))))) 157 | #(reset! done? true))) 158 | -------------------------------------------------------------------------------- /logo.md: -------------------------------------------------------------------------------- 1 | # Some interesting toy logo programs 2 | 3 | Some are adaptions from the excellent gallery at http://www.mathcats.com/gallery/15wordcontest.html 4 | 5 | ## Tree branches 6 | 7 | This returns to the same position and draws randomly sprawling branches upwards. 8 | ``` 9 | repeat 10 [ 10 | setxy 50 150 11 | setang rnd 220 320 12 | repeat 10 [ fd 2 rt rnd -15 15] 13 | ] 14 | ``` 15 | 16 | ## Circle of starts 17 | 18 | Draw 6 stars in a circle. 19 | ``` 20 | repeat 6 [ 21 | randpen 22 | repeat 5 [ fd 25 rt 144 ] 23 | fd 30 rt 60 24 | ] 25 | ``` 26 | 27 | ## Hypercube 28 | 29 | Adapted from (http://www.mathcats.com/gallery/15wordcontest.html#lissajous) 30 | ``` 31 | repeat 8 [repeat 4 [rt 90 fd 25] bk 25 rt -45] 32 | ``` 33 | 34 | ## Penta spiral 35 | 36 | ``` 37 | setxy 160 100 38 | for [l 0 95 3] [ 39 | repeat 5 [randpen fd :l rt 144] 40 | fd :l 41 | rt 30 42 | ] 43 | ``` 44 | 45 | ## Spiral curve 46 | 47 | Define function to draw curve and use it. 48 | 49 | ``` 50 | setxy 160 100 51 | def curve(n len rot) { 52 | repeat :n [ randpen fd :len rt :rot ] 53 | } 54 | curve(10 10 rnd 10 20) 55 | curve(10 8 rnd 20 30) 56 | curve(10 6 rnd 30 50) 57 | ``` 58 | 59 | Repeat from same starting point: 60 | ```repeat 10 [ 61 | setxy 160 100 62 | setang rnd 0 360 63 | def curve(n len rot) { 64 | repeat :n [ randpen fd :len rt :rot ] 65 | } 66 | curve(10 9 rnd 10 20) 67 | curve(10 8 rnd 20 30) 68 | curve(10 6 rnd 30 50) 69 | ] 70 | ``` 71 | 72 | ## Perspective road 73 | 74 | ``` 75 | def linexyz(sx sy sz ex ey ez) { 76 | setxy 160-((160-:sx)/(:sz*0.1)) 100-((100-:sy)/(:sz*0.1)) 77 | line 160-((160-:ex)/(:ez*0.1)) 100-((100-:ey)/(:ez*0.1)) 78 | } 79 | 80 | def linexyz(sx sy sz ex ey ez) { 81 | setxy 160-((160-:sx)/(:sz*0.1)) 100-((100-:sy)/(:sz*0.1)) 82 | line 160-((160-:ex)/(:ez*0.1)) 100-((100-:ey)/(:ez*0.1)) 83 | } 84 | 85 | def road() { 86 | linexyz(80 200 10 80 200 1000) 87 | linexyz(240 200 10 240 200 1000) 88 | for [z 10 100 8] [ 89 | linexyz(156 200 :z 156 200 :z+4) 90 | linexyz(156 200 :z+4 164 200 :z+4) 91 | linexyz(164 200 :z+4 164 200 :z) 92 | linexyz(156 200 :z 164 200 :z) 93 | ] 94 | } 95 | 96 | pen 7 97 | road() 98 | 99 | def housel(x y z w h d) { 100 | linexyz(:x+:w :y :z :x+:w :y-:h :z) 101 | linexyz(:x+:w :y-:h :z :x :y-:h :z) 102 | linexyz(:x :y-:h :z :x :y :z) 103 | linexyz(:x :y :z :x+:w :y :z) 104 | linexyz(:x+:w :y :z :x+:w :y :z+:d) 105 | linexyz(:x+:w :y :z+:d :x+:w :y-:h :z+:d) 106 | linexyz(:x+:w :y-:h :z+:d :x+:w :y-:h :z) 107 | linexyz(:x :y-:h :z :x :y-:h :z+:d) 108 | linexyz(:x :y-:h :z+:d :x+:w :y-:h :z+:d) 109 | } 110 | 111 | 112 | housel(35 200 10 30 25 4) 113 | 114 | housel(35 200 16 30 45 5) 115 | 116 | def linexyz(sx sy sz ex ey ez) { 117 | setxy 160-((160-:sx)/(:sz*0.1)) 100-((100-:sy)/(:sz*0.1)) 118 | line 160-((160-:ex)/(:ez*0.1)) 100-((100-:ey)/(:ez*0.1)) 119 | } 120 | 121 | /* draw some tree */ 122 | def tree(x y z) { 123 | pen 4 // draw trunk with brown 124 | linexyz(:x :y :z :x :y-rnd 10 15 :z) 125 | pen b // draw leafy branches with green 126 | savexy ax ay 127 | repeat 5 [ 128 | setang rnd 240 290 129 | fd (100-:z)*0.04 130 | setxy :ax :ay 131 | ] 132 | } 133 | 134 | /* draw a lovely random forest */ 135 | for [z 10 80 3] [ 136 | repeat rnd 1 4 [ 137 | tree(245 + rnd 5 20 138 | 180 + rnd 0 20 139 | :z) 140 | ] 141 | ] 142 | 143 | ``` 144 | 145 | ## Draw letters 146 | 147 | All letter drawing functions expect to start at bottom left corner with angle 0. 148 | The turtle is positioned after drawing to the next character (half size forward). 149 | 150 | The functions take a single parameter s (for size, height). 151 | 152 | ``` 153 | 154 | def a(s) { 155 | rt -90 fd :s 156 | rt 90 fd :s/2 157 | rt 90 fd :s/2 158 | rt 90 fd :s/2 bk :s/2 rt -90 fd :s/2 159 | rt -90 } 160 | 161 | def h(s) { 162 | rt -90 fd :s bk :s/2 rt 90 fd :s/2 rt -90 fd :s/2 rt 180 163 | fd :s rt -90 164 | } 165 | 166 | def e(s) { 167 | rt -90 fd :s rt 90 168 | repeat 2 [ fd :s/2 bk :s/2 rt 90 fd :s/2 rt -90 ] 169 | fd :s/2 170 | } 171 | 172 | def l(s) { rt -90 fd :s bk :s rt 90 fd :s/2 } 173 | def o(s) { pu fd :s/2 rt 180 pd fd :s/2 rt 90 fd :s rt 90 fd :s/2 rt 90 fd :s rt -90 } 174 | def t(s) { pu fd :s/2 pd rt -90 fd :s rt -90 pu fd :s/2 rt 180 pd fd :s rt 90 pu fd :s rt -90 pd } 175 | def u(s) { rt -90 fd :s rt 180 pu fd :s rt -90 pd fd :s/2 rt -90 fd :s rt 180 pu fd :s pd rt -90 } 176 | def r(s) { 177 | pu fd :s/2 savexy rx ry bk :s/2 pd 178 | saveang ra 179 | rt -90 fd :s 180 | repeat 3 [ rt 90 fd :s/2 ] 181 | line :rx :ry 182 | setang :ra 183 | } 184 | 185 | 186 | /* underscore for space and spacing */ 187 | def _(s) { pu fd :s/2 pd } 188 | 189 | setxy 10 70 190 | setang 330 191 | for [ c "hello_there" ] [ &c(10) _(10) rt 15 ] 192 | 193 | 194 | ## Simple face 195 | 196 | Let's draw a simple round face with eyes, nose, mouth, and some hair. 197 | 198 | 199 | ``` 200 | setxy 100 20 setang 0 201 | def face() { 202 | savexy tx ty // save top of head position 203 | repeat 36 [fd 10 rt 10] 204 | rt 90 pu fd 50 rt 90 fd 20 rt 180 205 | pd fd 2 // eye1 206 | pu fd 50 207 | pd fd 2 // eye2 208 | pu bk 25 rt 90 fd 10 209 | pd rt 15 fd 20 rt -105 fd 5 // nose 210 | pu fd 25 rt 90 fd 5 211 | pd 212 | rt 40 repeat 50 [ fd 1 rt 2] // mouth 213 | /* random hair */ 214 | repeat 20 [ 215 | setxy :tx :ty 216 | setang rnd 0 180 217 | repeat 10 [ fd 4 rt rnd -15 15 ] 218 | ] 219 | } 220 | 221 | face() 222 | 223 | 224 | 225 | ``` 226 | -------------------------------------------------------------------------------- /bots/java/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /bots/prolog/bot.pl: -------------------------------------------------------------------------------- 1 | :- use_module(library(http/http_client)). 2 | :- use_module(library(dcg/basics)). 3 | :- set_prolog_flag(double_quotes, chars). 4 | 5 | 6 | url(URL) :- getenv("PAINTBOTS_URL", URL), !; URL='http://localhost:31173'. 7 | 8 | post(FormData, Result) :- 9 | url(URL), 10 | http_post(URL, form(FormData), Result, []). 11 | 12 | % Basic DCG state nonterminals 13 | state(S), [S] --> [S]. 14 | state(S0, S), [S] --> [S0]. 15 | 16 | % X,Y position accessor 17 | pos(X,Y) --> state(bot(_,X,Y,_,_)). 18 | 19 | %% Issue bot command, update bot state from response 20 | %% The last part of the state is User data which is passed 21 | %% through unchanged. 22 | cmd(Args) --> 23 | state(bot(Id, _, _, _, User), bot(Id, X, Y, C, User)), 24 | { post([ id=Id | Args ], Res), 25 | member(x=Xs, Res), 26 | member(y=Ys, Res), 27 | member(color=C, Res), 28 | atom_to_term(Xs, X, []), 29 | atom_to_term(Ys, Y, []) }. 30 | 31 | % Calculate points for a line from [X1,Y1] to [X2,Y2] 32 | line([X,Y],[X,Y],[]). 33 | line([X1, Y1], [X2,Y2], Points) :- 34 | Dx is abs(X1-X2), 35 | Dy is abs(Y1-Y2), 36 | (Dx > Dy -> Steps = Dx; Steps = Dy), 37 | XStep is (X2 - X1) / Steps, 38 | YStep is (Y2 - Y1) / Steps, 39 | line_([X1,Y1], [X2,Y2], XStep, YStep, Steps, Points). 40 | 41 | % All steps done, return end point 42 | line_(_, [X,Y], _, _, 0, [[X,Y]]). 43 | 44 | % More steps to do 45 | line_([Xp,Yp], To, XStep, YStep, Steps, [[X,Y]|Rest]) :- 46 | X is round(Xp), 47 | Y is round(Yp), 48 | Xn is Xp + XStep, 49 | Yn is Yp + YStep, 50 | StepsN is Steps - 1, 51 | line_([Xn,Yn], To, XStep, YStep, StepsN, Rest). 52 | 53 | %% Register bot with name, fetches and returns initial state of the bot 54 | register(Name, State) :- register(Name, initial, State). 55 | 56 | register(Name, UserData, State) :- 57 | post([register = Name], Id), 58 | phrase(cmd([info='']), [bot(Id,_,_,_,UserData)], [State]), 59 | writeln(registered(Name,State)). 60 | 61 | %% Bot commands take form of DCG nonterminals with bot state as final args: 62 | %% command(CommandArgs, S0, S1) 63 | move(Dir) --> cmd([move=Dir]). 64 | 65 | paint --> cmd([paint='']). 66 | 67 | draw_line(To) --> 68 | pos(X,Y), 69 | { line([X,Y], To, Points) }, 70 | traverse(Points). 71 | 72 | % Traverse empty points 73 | traverse([]) --> []. 74 | 75 | % Traverse 1 point, just paint it 76 | traverse([_]) --> paint. 77 | 78 | % Two or more points, paint and move 79 | traverse([_,P2|Points]) --> 80 | paint, 81 | move_to(P2), 82 | traverse([P2|Points]). 83 | 84 | % At position, do nothing 85 | move_to([X,Y]) --> pos(X,Y). 86 | 87 | % Not at position, move horizontally or vertically 88 | move_to([Xt,Yt]) --> 89 | pos(Xp,Yp), 90 | { once(dir([Xp,Yp],[Xt,Yt],Dir)) }, 91 | move(Dir), 92 | move_to([Xt,Yt]). 93 | 94 | bye(bot(Id,_,_,_,_)) :- 95 | post([id=Id, bye=''], _Out). 96 | 97 | say(Msg) --> cmd([msg=Msg]). 98 | 99 | color(C) --> cmd([color=C]). 100 | 101 | % Determine which direction to go, based on two points 102 | dir([X1,_],[X2,_], 'RIGHT') :- X1 < X2. 103 | dir([X1,_],[X2,_], 'LEFT') :- X1 > X2. 104 | dir([_,Y1],[_,Y2], 'DOWN') :- Y1 < Y2. 105 | dir([_,Y1],[_,Y2], 'UP') :- Y1 > Y2. 106 | 107 | line_demo(Name) :- 108 | register(Name, S0), 109 | phrase(draw_line([80, 50]), [S0], [S1]), 110 | bye(S1). 111 | 112 | circle_demo(Name) :- 113 | register(Name, S0), 114 | S0 = bot(_, X,Y, _), 115 | draw_circle(S0, [X, Y], 10, 0, 0.1, S1), 116 | bye(S1). 117 | 118 | draw_circle(_, _, Ang, _) --> 119 | { Max is 2*pi, 120 | Ang >= Max }, 121 | []. 122 | 123 | 124 | draw_circle([Xc, Yc], R, Ang, AngStep) --> 125 | { X is round(Xc + R * cos(Ang)), 126 | Y is round(Yc + R * sin(Ang)) }, 127 | move_to([X,Y]), 128 | paint, 129 | { AngN is Ang + AngStep }, 130 | draw_circle([Xc, Yc], R, AngN, AngStep). 131 | 132 | peace_sign --> 133 | say('Peace to the world!'), 134 | move_to([80, 20]), 135 | draw_line([80,50]), 136 | draw_line([57,68]), 137 | move_to([80,50]), 138 | draw_line([103,68]), 139 | move_to([80,80]), 140 | draw_line([80,50]), 141 | draw_circle([80, 50], 30, 0, 0.05). 142 | 143 | %% Draw the universal peace symbol in the center 144 | peace(Name) :- 145 | register(Name, S0), 146 | phrase(peace_sign, [S0], [SFinal]), 147 | bye(SFinal). 148 | 149 | 150 | %% 151 | %% Turtle graphics DCG 152 | %% 153 | 154 | 155 | ws --> [W], { char_type(W, space) }, ws. 156 | ws --> []. 157 | 158 | turtle([]) --> []. 159 | turtle([P|Ps]) --> ws, turtle_command(P), ws, turtle(Ps). 160 | 161 | turtle_command(Cmd) --> fd(Cmd) | bk(Cmd) | rt(Cmd) | 162 | pen(Cmd) | randpen(Cmd) | 163 | repeat(Cmd) | setxy(Cmd) | 164 | for(Cmd) | say_(Cmd). 165 | 166 | 167 | repeat(repeat(Times,Program)) --> "repeat", ws, arg_(Times), ws, "[", turtle(Program), "]". 168 | 169 | say_(say(Msg)) --> "say", ws, "\"", string_without("\"", Codes), "\"", { atom_codes(Msg, Codes) }. 170 | fd(fd(N)) --> "fd", ws, arg_(N). 171 | bk(bk(N)) --> "bk", ws, arg_(N). 172 | rt(rt(N)) --> "rt", ws, arg_(N). 173 | pen(pen(Col)) --> "pen", ws, [Col], { char_type(Col, alnum) }, ws. 174 | randpen(randpen) --> "randpen". 175 | setxy(setxy(X,Y)) --> "setxy", ws, arg_(X), ws, arg_(Y). 176 | for(for(Var, From, To, Step, Program)) --> 177 | "for", ws, "[", ws, var_name(Var), ws, num(From), ws, num(To), ws, num(Step), ws, "]", ws, 178 | "[", turtle(Program), "]". 179 | var_name(Var) --> [Var], { char_type(Var, alpha) }. 180 | num(N) --> "-", integer(I), { N is -I }. 181 | num(N) --> integer(N). 182 | arg_(num(N)) --> num(N). 183 | arg_(var(V)) --> ":", var_name(V). 184 | 185 | % Parse a turtle program: 186 | % set_prolog_flag(double_quote, chars). 187 | % phrase(turtle(Program), "fd 6 rt 90 fd 6"). 188 | 189 | 190 | % Interpreting a turtle program. 191 | % The state is a compound term turtle(BotState, AngleDegree) 192 | 193 | eval_turtle(Name, Program) :- 194 | setup_call_cleanup( 195 | register(Name, ctx{angle: 0}, B0), 196 | phrase(eval_all(Program), [B0], [_]), 197 | bye(B0)). 198 | 199 | eval_all([]) --> []. 200 | eval_all([Cmd|Cmds]) --> 201 | eval(Cmd), 202 | eval_all(Cmds). 203 | 204 | deg_rad(Deg, Rad) :- 205 | Rad is Deg * pi/180. 206 | 207 | user_data(Old, New) --> 208 | state(bot(Id,X,Y,C,Old), bot(Id,X,Y,C,New)). 209 | 210 | user_data(Current) --> 211 | state(bot(_,_,_,_,Current)). 212 | 213 | %% Eval argument against current ctx, var is taken from dictionary 214 | %% numbers are evaluated as is. 215 | argv(var(V), Val) --> 216 | user_data(Ctx), 217 | { Val = Ctx.V }. 218 | 219 | argv(num(V), V) --> []. 220 | 221 | setval(Var, Val) --> 222 | user_data(Ctx0, Ctx1), 223 | { Ctx1 = Ctx0.put(Var, Val) }. 224 | 225 | eval(rt(DegArg)) --> 226 | argv(DegArg, Deg), 227 | user_data(Ctx0, Ctx1), 228 | { Ang0 = Ctx0.angle, Ang1 is Ang0 + Deg, 229 | Ctx1 = Ctx0.put(angle, Ang1) }. 230 | 231 | eval(fd(LenArg)) --> 232 | argv(LenArg, Len), 233 | state(bot(_,X,Y,_,Ctx)), 234 | { deg_rad(Ctx.angle, Rad), 235 | X1 is round(X + Len * cos(Rad)), 236 | Y1 is round(Y + Len * sin(Rad)) }, 237 | draw_line([X1,Y1]). 238 | 239 | eval(bk(LenArg)) --> 240 | argv(LenArg, Len), 241 | { MinusLen is -Len }, 242 | eval(fd(MinusLen)). 243 | 244 | eval(pen(C)) --> color(C). 245 | eval(randpen) --> 246 | { C is random(16), format(atom(Col), '~16r', [C]) }, 247 | color(Col). 248 | 249 | eval(repeat(num(0), _)) --> []. 250 | eval(repeat(NArg, Cmds)) --> 251 | argv(NArg, N), 252 | { N > 0, 253 | N1 is N - 1 }, 254 | eval_all(Cmds), 255 | eval(repeat(num(N1), Cmds)). 256 | 257 | eval(setxy(XArg,YArg)) --> 258 | argv(XArg, X), argv(YArg, Y), 259 | move_to([X,Y]). 260 | 261 | %% Loop done 262 | eval(for(_, From, To, Step, _)) --> 263 | { (Step > 0, From > To); (Step < 0, From < To) }, []. 264 | 265 | eval(for(Var, From, To, Step, Program)) --> 266 | setval(Var, From), 267 | eval_all(Program), 268 | { From1 is From + Step }, 269 | eval(for(Var, From1, To, Step, Program)). 270 | 271 | eval(say(Msg)) --> 272 | say(Msg). 273 | 274 | % phrase(turtle(T), "fd 5 rt 90 fd 5 rt 90 fd 5") 275 | % eval_turtle('Turtles3', [repeat(10,[rt(50),fd(10)])]). 276 | 277 | run(Name, Program) :- 278 | phrase(turtle(P), Program), 279 | eval_turtle(Name, P). 280 | 281 | %% dahlia.logo 282 | %% see http://www.mathcats.com/gallery/15wordcontest.html 283 | dahlia() :- 284 | run('Dahlia', "setxy 100 10 repeat 8 [rt 45 repeat 6 [repeat 90 [fd 2 rt 2] rt 90]]"). 285 | 286 | %% Draw a simple star 287 | star() :- 288 | run('Star', "repeat 5 [ fd 25 rt 144 ]"). 289 | 290 | stars() :- 291 | run('Stars', "repeat 6 [ randpen repeat 5 [ fd 25 rt 144 ] fd 30 rt 60]"). 292 | 293 | logo(Name) :- 294 | url(URL), 295 | format('toy Logo repl, registerin bot "~w" to server at: ~w', [Name, URL]), 296 | register(Name, ctx{angle: 0}, Bot0), 297 | logo_repl(Bot0, BotF), 298 | bye(BotF). 299 | 300 | logo_repl(Bot0, BotF) :- 301 | read_line_to_string(user_input, Str), 302 | string_chars(Str, Cs), 303 | ( (Cs = "bye"; Str = end_of_file) -> 304 | Bot0 = BotF 305 | ; ( phrase(turtle(Program), Cs) -> 306 | phrase(eval_all(Program), [Bot0], [Bot1]), 307 | logo_repl(Bot1, BotF) 308 | ; writeln(syntax_error(Str)), 309 | logo_repl(Bot0, BotF))). 310 | 311 | start_repl :- 312 | format('Paintbots toy Logo REPL!\n environment variables:\n - PAINTBOTS_URL server url\n - PAINTBOTS_NAME bot name\n\n exit with: bye\n'), 313 | getenv('PAINTBOTS_NAME', Name), 314 | logo(Name), 315 | halt. 316 | -------------------------------------------------------------------------------- /src/paintbots/state.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.state 2 | "Keeper of state! 3 | Contains all the game state in a single atom, and multimethods to 4 | process the state. 5 | 6 | A single go-loop process listens to commands that modify the game state 7 | and processes them in order." 8 | (:require [clojure.core.async :refer [go go-loop ! timeout] :as async] 9 | [ripley.live.source :as source]) 10 | (:import (java.awt.image BufferedImage))) 11 | 12 | (defn- hex->rgb [hex] 13 | (let [hex (if (= \# (.charAt hex 0)) (subs hex 1) hex)] 14 | (mapv #(Integer/parseInt % 16) 15 | [(subs hex 0 2) 16 | (subs hex 2 4) 17 | (subs hex 4 6)]))) 18 | 19 | (defn- palette [& cols] 20 | (into {} 21 | (map-indexed 22 | (fn [i col] 23 | [(Integer/toHexString i) (hex->rgb col)])) 24 | cols)) 25 | 26 | ;; pico-8 16 color palette from https://www.pixilart.com/palettes/pico-8-51001 27 | (def colors 28 | (palette "#000000" 29 | "#1D2B53" 30 | "#7E2553" 31 | "#008751" 32 | "#AB5236" 33 | "#5F574F" 34 | "#C2C3C7" 35 | "#FFF1E8" 36 | "#FF004D" 37 | "#FFA300" 38 | "#FFEC27" 39 | "#00E436" 40 | "#29ADFF" 41 | "#83769C" 42 | "#FF77A8" 43 | "#FFCCAA")) 44 | 45 | 46 | #_(def colors {"R" [255 0 0] ; red 47 | "G" [0 255 0] ; green 48 | "B" [0 0 255] ; blue 49 | "Y" [255 255 0] ; yellow 50 | "P" [255 0 255] ; pink 51 | "C" [0 255 255] ; cyan 52 | }) 53 | 54 | (def color-name (into {} (map (juxt val key)) colors)) 55 | 56 | ;; State is a map containing configuration and a canvases 57 | ;; under key :canvas which maps from canvas name to 58 | ;; map containing currently registered bots (map of bot id to info) 59 | ;; and canvas (BufferedImage) 60 | ;; 61 | ;; Admin can create new canvases 62 | ;; 63 | ;; the default canvas (no path) is named just the empty string "" 64 | ;; A new canvas is created with create-canvas function that can be 65 | ;; sent to the agent 66 | (defonce state (atom {:canvas {}})) 67 | 68 | (defmulti process! (fn [_old-state command] (::command command))) 69 | 70 | (defonce state-processor-ch 71 | (let [ch (async/chan 32)] 72 | (go-loop [cmd ( " e) 82 | [old-state {:error (.getMessage e)}]))] 83 | ;; We don't use swap! as we need new-state and result, and 84 | ;; no other code will modify state 85 | (reset! state new-state) 86 | (when reply-ch 87 | (>! reply-ch result) 88 | (async/close! reply-ch)) 89 | (recur (! state-processor-ch (assoc cmd-args ::command command)))) 97 | 98 | (defn cmd! state-processor-ch (assoc cmd-args 104 | ::command command 105 | ::reply ch))) 106 | ch)) 107 | 108 | (defn cmd-sync! 109 | "Issue a command synchronously, returns command result. Do NOT call in go block!" 110 | [command & {:as cmd-args}] 111 | (async/ state 202 | (assoc-in [:canvas canvas :bots id] (assoc new-bot :in-command? false)) 203 | (assoc-in [:canvas canvas :last-command] (System/currentTimeMillis)) 204 | (update-in [:canvas canvas :changed] #(if (aget changed 0) (System/currentTimeMillis) %))) 205 | new-bot])) 206 | 207 | (defn current-state [] @state) 208 | 209 | (defn has-canvas? [state canvas-name] 210 | (and (string? canvas-name) 211 | (contains? (:canvas state) canvas-name))) 212 | 213 | 214 | (defn bot-by-id [state canvas-name bot-id] 215 | (get-in state [:canvas canvas-name :bots bot-id])) 216 | 217 | (defn canvas-image [state canvas-name] 218 | (get-in state [:canvas canvas-name :img])) 219 | 220 | (defn canvas-bots 221 | "Return information on the bots currently registered for the given canvas." 222 | [state canvas-name] 223 | (vals (get-in state [:canvas canvas-name :bots]))) 224 | 225 | (defn source 226 | "Return a ripley live source reflecting the given path. 227 | Path-fns may be keywords or other getter functions." 228 | [& path-fns] 229 | (source/computed (fn [current-state] 230 | (reduce (fn [here f] (f here)) current-state path-fns)) 231 | state)) 232 | 233 | (defn valid-canvas [name] 234 | (when (has-canvas? (current-state) name) 235 | name)) 236 | -------------------------------------------------------------------------------- /bots/java/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /resources/public/client/logo.pl: -------------------------------------------------------------------------------- 1 | :- use_module(library(dcg/basics)). 2 | :- set_prolog_flag(double_quotes, chars). 3 | 4 | log(Pattern, Args) :- 5 | format(string(L), Pattern, Args), 6 | _ := log(L). 7 | 8 | %% Simple form data parsing, the builtin HTTP client would do this for us, but 9 | %% we are using text transfer between JS fetch and Prolog ¯\_(ツ)_/¯ 10 | form_data([Name=Value | More]) --> string_without("=", NameS), "=", string_without("&", ValueS), 11 | { atom_chars(Name, NameS), 12 | ((Name = x; Name = y) -> number_chars(Value, ValueS); Value = ValueS) 13 | }, 14 | more_form_data(More). 15 | more_form_data([]) --> []. 16 | more_form_data(Fd) --> "&", form_data(Fd). 17 | 18 | form_data_or_plain(Out) --> form_data(Out) | (string_without("=", Id), { atom_chars(Out, Id) }). 19 | 20 | post(FormData, ResultOut) :- 21 | P := pb_post(FormData), 22 | await(P, Result), 23 | TP := Result.text(), 24 | await(TP, ResultText), 25 | string_chars(ResultText, Cs), 26 | phrase(form_data_or_plain(ResultOut), Cs). 27 | %writeln(got_post_result(ResultOut)). 28 | 29 | % Basic DCG state nonterminals 30 | state(S), [S] --> [S]. 31 | state(S0, S), [S] --> [S0]. 32 | 33 | % Push and pop environment bindings 34 | push_env, [S, Env] --> [S], { S = bot(_,_,_,_,t(_,Env,_)) }. 35 | pop_env, [bot(Id,X,Y,C,t(Ang,EnvSaved,PenUp))] --> 36 | [bot(Id,X,Y,C,t(Ang,_,PenUp)), EnvSaved]. 37 | 38 | % X,Y position accessor 39 | pos(X,Y) --> state(bot(_,X,Y,_,_)). 40 | 41 | %% Issue bot command, update bot state from response 42 | %% The last part of the state is User data which is passed 43 | %% through unchanged. 44 | cmd(Args) --> 45 | state(bot(Id, _, _, _, User), bot(Id, X, Y, C, User)), 46 | { post([ id=Id | Args ], Res), 47 | member(x=Xs, Res), 48 | member(y=Ys, Res), 49 | member(color=C, Res), 50 | atom_to_term(Xs, X, []), 51 | atom_to_term(Ys, Y, []), 52 | User = t(Ang, _, _), 53 | _ := botinfo(X,Y,C, Ang) 54 | }. 55 | 56 | % Calculate points for a line from [X1,Y1] to [X2,Y2] 57 | line([X,Y],[X,Y],[]). 58 | line([X1, Y1], [X2,Y2], Points) :- 59 | Dx is abs(X1-X2), 60 | Dy is abs(Y1-Y2), 61 | (Dx > Dy -> Steps = Dx; Steps = Dy), 62 | XStep is (X2 - X1) / Steps, 63 | YStep is (Y2 - Y1) / Steps, 64 | line_([X1,Y1], [X2,Y2], XStep, YStep, Steps, Points). 65 | 66 | % All steps done, return end point 67 | line_(_, [X,Y], _, _, 0, [[X,Y]]). 68 | 69 | % More steps to do 70 | line_([Xp,Yp], To, XStep, YStep, Steps, [[X,Y]|Rest]) :- 71 | X is round(Xp), 72 | Y is round(Yp), 73 | Xn is Xp + XStep, 74 | Yn is Yp + YStep, 75 | StepsN is Steps - 1, 76 | line_([Xn,Yn], To, XStep, YStep, StepsN, Rest). 77 | 78 | %% Register bot with name, fetches and returns initial state of the bot 79 | register(Name, State) :- register(Name, initial, State). 80 | 81 | register(Name, UserData, State) :- 82 | post([register = Name], Id), 83 | phrase(cmd([info='']), [bot(Id,_,_,_,UserData)], [State]), 84 | log('Registered ~w', [State]). 85 | 86 | %% Bot commands take form of DCG nonterminals with bot state as final args: 87 | %% command(CommandArgs, S0, S1) 88 | move(Dir) --> cmd([move=Dir]). 89 | 90 | paint --> cmd([paint='']). 91 | 92 | draw_line(To) --> 93 | pos(X,Y), 94 | { line([X,Y], To, Points) }, 95 | traverse(Points). 96 | 97 | % Traverse empty points 98 | traverse([]) --> []. 99 | 100 | % Traverse 1 point, just paint it 101 | traverse([_]) --> paint. 102 | 103 | % Two or more points, paint and move 104 | traverse([_,P2|Points]) --> 105 | paint, 106 | move_to(P2), 107 | traverse([P2|Points]). 108 | 109 | % At position, do nothing 110 | move_to([X,Y]) --> pos(X,Y). 111 | 112 | % Not at position, move horizontally or vertically 113 | move_to([Xt,Yt]) --> 114 | pos(Xp,Yp), 115 | { once(dir([Xp,Yp],[Xt,Yt],Dir)) }, 116 | move(Dir), 117 | move_to([Xt,Yt]). 118 | 119 | bye(bot(Id,_,_,_,_)) :- 120 | post([id=Id, bye=''], _Out). 121 | 122 | say(Msg) --> cmd([msg=Msg]). 123 | 124 | color(C) --> cmd([color=C]). 125 | 126 | % Determine which direction to go, based on two points 127 | dir([X1,_],[X2,_], 'RIGHT') :- X1 < X2. 128 | dir([X1,_],[X2,_], 'LEFT') :- X1 > X2. 129 | dir([_,Y1],[_,Y2], 'DOWN') :- Y1 < Y2. 130 | dir([_,Y1],[_,Y2], 'UP') :- Y1 > Y2. 131 | 132 | %% 133 | %% Turtle graphics DCG 134 | %% 135 | 136 | strip_comments([], []). 137 | strip_comments([C],[C]). 138 | strip_comments([(/),(/)|Rest],Out) :- skip_to("\n", Rest, Out1), strip_comments(Out1, Out). 139 | strip_comments([(/),(*)|Rest],Out) :- skip_to("*/", Rest, Out1), strip_comments(Out1, Out). 140 | strip_comments([C1,C2|Rest], Out) :- 141 | not((C1 = (/), (C2 = (*); C2 = (/)))), 142 | strip_comments([C2|Rest], RestOut), 143 | append([C1], RestOut, Out). 144 | 145 | skip_to(End, Cs, Out) :- 146 | append(End, Out, Cs). 147 | skip_to(End, [C|Cs], Out) :- 148 | not(append(End,Out,[C|Cs])), 149 | skip_to(End, Cs, Out). 150 | 151 | 152 | ws --> [W], { char_type(W, space) }, ws. 153 | ws --> []. 154 | 155 | % At least one whitespace 156 | ws1 --> [W], { char_type(W, space) }, ws. 157 | 158 | parse(Source, Prg) :- 159 | writeln(parsing(Source)), 160 | strip_comments(Source, SourceStripped), 161 | writeln(stripped(SourceStripped)), 162 | phrase(turtle(Prg), SourceStripped). 163 | 164 | turtle([]) --> []. 165 | turtle([P|Ps]) --> ws, turtle_command(P), ws, turtle(Ps). 166 | 167 | turtle_command(Cmd) --> defn(Cmd) | fncall(Cmd) | 168 | fd(Cmd) | bk(Cmd) | rt(Cmd) | 169 | pen(Cmd) | randpen(Cmd) | 170 | repeat(Cmd) | setxy(Cmd) | savexy(Cmd) | 171 | setang(Cmd) | saveang(Cmd) | 172 | for(Cmd) | say_(Cmd) | 173 | pendown(Cmd) | penup(Cmd) | lineto(Cmd). 174 | 175 | defn(defn(FnName, ArgNames, Body)) --> 176 | "def", ws1, ident(FnName), ws, "(", defn_args(ArgNames), ")", ws, "{", turtle(Body), "}". 177 | 178 | fncall(fncall(var(FnName), ArgValues)) --> "&", ident(FnName), ws, "(", fncall_args(ArgValues), ")". 179 | fncall(fncall(ident(FnName), ArgValues)) --> ident(FnName), ws, "(", fncall_args(ArgValues), ")". 180 | fncall_args([]) --> []. 181 | fncall_args([V|Vs]) --> exprt(V), more_fncall_args(Vs). 182 | more_fncall_args([]) --> ws. 183 | more_fncall_args(Vs) --> ws1, fncall_args(Vs). 184 | 185 | ident_([]) --> []. 186 | ident_([I|Is]) --> [I], { char_type(I, csymf) }, ident_(Is). 187 | ident(I) --> ident_(Cs), { atom_chars(I, Cs) }. 188 | 189 | 190 | defn_args([]) --> []. 191 | defn_args([Arg|Args]) --> ws, ident(Arg), more_defn_args(Args). 192 | more_defn_args([]) --> ws. 193 | more_defn_args(Args) --> ws1, defn_args(Args). 194 | 195 | repeat(repeat(Times,Program)) --> "repeat", exprt(Times), "[", turtle(Program), "]". 196 | 197 | say_(say(Msg)) --> "say", ws, "\"", string_without("\"", Codes), "\"", { atom_codes(Msg, Codes) }. 198 | fd(fd(N)) --> "fd", exprt(N). 199 | bk(bk(N)) --> "bk", exprt(N). 200 | rt(rt(N)) --> "rt", exprt(N). 201 | pen(pen(Col)) --> "pen", ws, [Col], { char_type(Col, alnum) }, ws. 202 | randpen(randpen) --> "randpen". 203 | setxy(setxy(X,Y)) --> "setxy", exprt(X), exprt(Y). 204 | savexy(savexy(X,Y)) --> "savexy", ws, ident(X), ws1, ident(Y). 205 | lineto(lineto(X,Y)) --> "line", exprt(X), exprt(Y). 206 | saveang(saveang(A)) --> "saveang", ws, ident(A). 207 | setang(setang(Deg)) --> "setang", exprt(Deg). 208 | setang(setang(X,Y)) --> "angto", exprt(X), exprt(Y). 209 | for(for(Var, From, To, Step, Program)) --> 210 | "for", ws, "[", ws, ident(Var), exprt(From), exprt(To), exprt(Step), "]", ws, 211 | "[", turtle(Program), "]". 212 | for(for(Var,ListExpr,Program)) --> 213 | "for", ws, "[", ws, ident(Var), exprt(ListExpr), "]", ws, "[", turtle(Program), "]". 214 | num(N) --> "-", num_(I), { N is -I }. 215 | num(N) --> num_(N). 216 | num_(N) --> integer(N). 217 | num_(F) --> digits(IP), ".", digits(FP), { append(IP, ['.'|FP], Term), read_from_chars(Term, F) }. 218 | arg_(num(N)) --> num(N). 219 | arg_(var(V)) --> ":", ident(V). 220 | arg_(rnd(Low,High)) --> "rnd", ws, num(Low), ws, num(High). 221 | arg_(list(Items)) --> "\"", string_without("\"", Atoms), "\"", { list_atoms_items(Atoms, Items) }. 222 | arg_(list(Items)) --> "[", list_items(Items), "]". 223 | list_items([]) --> []. 224 | list_items([I|Items]) --> exprt(I), list_items(Items). 225 | list_atoms_items([], []). 226 | list_atoms_items([A|Atoms], [atom(A)|AtomsRest]) :- list_atoms_items(Atoms, AtomsRest). 227 | penup(penup) --> "pu" | "penup". 228 | pendown(pendown) --> "pd" | "pendown". 229 | 230 | % Parse simple math expression tree. There is no priority for multipliation and addition. 231 | % Use parenthesis to change order. 232 | 233 | exprt(E) --> ws, expr(E), ws. % top level, wrap with whitespace 234 | 235 | expr(A) --> arg_(A). 236 | expr(E) --> "(", exprt(E), ")". 237 | expr(op(Left,Op,Right)) --> expr_left(Left), ws, op_(Op), exprt(Right). 238 | 239 | expr_left(E) --> "(", exprt(E), ")". 240 | expr_left(A) --> arg_(A). 241 | 242 | op_(*) --> "*". 243 | op_(/) --> "/". 244 | op_(+) --> "+". 245 | op_(-) --> "-". 246 | 247 | % Parse a turtle program: 248 | % set_prolog_flag(double_quote, chars). 249 | % phrase(turtle(Program), "fd 6 rt 90 fd 6"). 250 | 251 | 252 | % Interpreting a turtle program. 253 | % 254 | % state is a compound term of: 255 | % t(Angle, Env, PenUp) 256 | % where Env is a dict of the current env bindings (functions and arguments) 257 | % and PenUp is true/false atom if pen is up (should not draw when moving) 258 | 259 | eval_turtle(Name, Program) :- 260 | setup_call_cleanup( 261 | register(Name, t(0, env{}, false), B0), 262 | phrase(eval_all(Program), [B0], [_]), 263 | bye(B0)). 264 | 265 | eval_all([]) --> []. 266 | eval_all([Cmd|Cmds]) --> 267 | %% { writeln(eval_cmd(Cmd)) }, 268 | eval(Cmd), 269 | eval_all(Cmds). 270 | 271 | deg_rad(Deg, Rad) :- 272 | Rad is Deg * pi/180. 273 | 274 | rad_deg(Rad, Deg) :- 275 | Deg is Rad / pi * 180. 276 | 277 | user_data(Old, New) --> 278 | state(bot(Id,X,Y,C,Old), bot(Id,X,Y,C,New)). 279 | 280 | user_data(Current) --> 281 | state(bot(_,_,_,_,Current)). 282 | 283 | set_angle(A) --> 284 | user_data(t(_,Env,PenUp), t(A,Env,PenUp)). 285 | 286 | set_pen_up --> 287 | user_data(t(Ang,Env,_), t(Ang,Env,true)). 288 | 289 | set_pen_down --> 290 | user_data(t(Ang,Env,_), t(Ang,Env,false)). 291 | 292 | %% Eval argument against current ctx, var is taken from dictionary 293 | %% numbers are evaluated as is. 294 | %% 295 | %% Argument can be a math expression op as eval. 296 | 297 | argv(var(V), Val) --> 298 | user_data(t(_,Env,_)), 299 | { Val = Env.V }. 300 | 301 | argv(num(V), V) --> []. 302 | argv(atom(A), A) --> []. 303 | 304 | argv(list([]), []) --> []. 305 | argv(list([Item_|Items_]), [Item|Items]) --> 306 | argv(Item_, Item), 307 | argv(list(Items_), Items). 308 | 309 | argv(rnd(Low,High), V) --> { random_between(Low,High,V) }. 310 | 311 | argv(op(Left_,Op,Right_), V) --> 312 | argv(Left_, Left), 313 | argv(Right_, Right), 314 | { eval_op(Left, Op, Right, V) }. 315 | 316 | fn_name(var(Var), FnName) --> 317 | argv(var(Var), FnName). 318 | 319 | fn_name(ident(FnName), FnName) --> []. 320 | 321 | eval_op(L,+,R,V) :- V is L + R. 322 | eval_op(L,-,R,V) :- V is L - R. 323 | eval_op(L,*,R,V) :- V is L * R. 324 | eval_op(L,/,R,V) :- V is L / R. 325 | 326 | setval(Var, Val) --> 327 | user_data(t(Ang,Env0,PenUp), t(Ang,Env1,PenUp)), 328 | { Env1 = Env0.put(Var, Val) }. 329 | 330 | setargs([],[]) --> []. 331 | setargs([K|Ks], []) --> { throw(error(not_enough_arguments, missing_vars([K|Ks]))) }. 332 | setargs([], [V|Vs]) --> { throw(error(too_many_arguments, extra_values([V|Vs]))) }. 333 | setargs([K|Ks], [V|Vs]) --> 334 | argv(V, Val), 335 | setval(K, Val), setargs(Ks,Vs). 336 | 337 | 338 | % Moving with pen up or down 339 | move_forward(true, Pos) --> move_to(Pos). 340 | move_forward(false, Pos) --> draw_line(Pos). 341 | 342 | eval(rt(DegArg)) --> 343 | argv(DegArg, Deg), 344 | user_data(t(Ang0, Env, PenUp), t(Ang1, Env, PenUp)), 345 | { Ang1 is (Ang0 + Deg) mod 360 }. 346 | 347 | eval(fd(LenArg)) --> 348 | argv(LenArg, Len), 349 | state(bot(_,X,Y,_,t(Ang,_,PenUp))), 350 | { deg_rad(Ang, Rad), 351 | X1 is round(X + Len * cos(Rad)), 352 | Y1 is round(Y + Len * sin(Rad)) }, 353 | move_forward(PenUp, [X1, Y1]). 354 | 355 | eval(bk(LenArg)) --> 356 | argv(LenArg, Len), 357 | { MinusLen is -Len }, 358 | eval(fd(num(MinusLen))). 359 | 360 | eval(pen(C)) --> color(C). 361 | eval(randpen) --> 362 | { C is random(16), format(atom(Col), '~16r', [C]) }, 363 | color(Col). 364 | 365 | eval(repeat(num(0), _)) --> []. 366 | eval(repeat(NArg, Cmds)) --> 367 | argv(NArg, N), 368 | { N > 0, 369 | N1 is N - 1 }, 370 | eval_all(Cmds), 371 | eval(repeat(num(N1), Cmds)). 372 | 373 | eval(setxy(XArg,YArg)) --> 374 | argv(XArg, X_), argv(YArg, Y_), 375 | { X is round(X_), Y is round(Y_) }, 376 | move_to([X,Y]). 377 | 378 | eval(savexy(XVar,YVar)) --> 379 | pos(X, Y), 380 | setval(XVar, X), 381 | setval(YVar, Y). 382 | 383 | eval(saveang(AVar)) --> 384 | state(bot(_,_,_,_,t(Ang,_,_))), 385 | setval(AVar, Ang). 386 | 387 | eval(setang(AngArg)) --> 388 | argv(AngArg, Ang), 389 | set_angle(Ang). 390 | 391 | % Set angle towards a target point 392 | eval(setang(TargetX_,TargetY_)) --> 393 | pos(PosX,PosY), 394 | argv(TargetX_, TargetX), 395 | argv(TargetY_, TargetY), 396 | set_angle(Deg), 397 | { Ang is atan2(TargetY-PosY, TargetX-PosX), 398 | rad_deg(Ang, Deg0), 399 | Deg is round(Deg0), 400 | writeln(angle(to(TargetX,TargetY),from(PosX,PosY),rad_deg(Ang,Deg))) 401 | }. 402 | 403 | eval(penup) --> set_pen_up. 404 | eval(pendown) --> set_pen_down. 405 | 406 | %% Loop from lower to upper number 407 | eval(for_(_, From, To, Step, _)) --> 408 | { (Step > 0, From > To); (Step < 0, From < To) }, []. 409 | 410 | eval(for_(Var, From, To, Step, Program)) --> 411 | setval(Var, From), 412 | eval_all(Program), 413 | { From1 is From + Step }, 414 | eval(for_(Var, From1, To, Step, Program)). 415 | 416 | eval(for(Var, From_, To_, Step_, Program)) --> 417 | argv(From_, From), 418 | argv(To_, To), 419 | argv(Step_, Step), 420 | eval(for_(Var, From, To, Step, Program)). 421 | 422 | %% Loop through a list 423 | 424 | eval(for_(_, [], _)) --> []. 425 | eval(for_(Var, [Item|Items], Program)) --> 426 | setval(Var, Item), 427 | eval_all(Program), 428 | eval(for_(Var, Items, Program)). 429 | 430 | eval(for(Var, List_, Program)) --> 431 | argv(List_, List), 432 | eval(for_(Var, List, Program)). 433 | 434 | 435 | eval(say(Msg)) --> 436 | say(Msg). 437 | 438 | eval(defn(FnName, ArgNames, Body)) --> 439 | setval(FnName, fn(ArgNames,Body)). 440 | 441 | eval(fncall(FnName_, ArgValues)) --> 442 | fn_name(FnName_, FnName), 443 | push_env, 444 | user_data(t(_,Env,_)), 445 | { fn(ArgNames,Body) = Env.FnName }, 446 | setargs(ArgNames, ArgValues), 447 | eval_all(Body), 448 | pop_env. 449 | 450 | eval(lineto(X_, Y_)) --> 451 | argv(X_, Xf), 452 | argv(Y_, Yf), 453 | { X is round(Xf), Y is round(Yf) }, 454 | draw_line([X,Y]). 455 | 456 | 457 | run(Name, Program) :- 458 | phrase(turtle(P), Program), 459 | eval_turtle(Name, P). 460 | 461 | logo(Name) :- 462 | log('toy Logo repl, registering bot "~w"\n', [Name]), 463 | register(Name, t(0, env{}, false), Bot0), 464 | logo_repl(Bot0, BotF), 465 | bye(BotF). 466 | 467 | exec(Program, Bot0, BotOut) :- 468 | catch((log('Executing program',[]), 469 | writeln(program(Program)), 470 | call_time(phrase(eval_all(Program), [Bot0], [Bot1]), Time), 471 | log('DONE in ~1f seconds', [Time.wall]), 472 | BotOut = Bot1), 473 | error(Error,ErrCtx), 474 | (log('ERROR: ~w (~w)', [Error, ErrCtx]), 475 | phrase(cmd([info='']), [Bot0], [BotOut]))). 476 | 477 | 478 | logo_repl(Bot0, BotF) :- 479 | InputPromise := get_input(), 480 | await(InputPromise, Str), 481 | string_chars(Str, Cs), 482 | ( (Cs = "bye"; Str = end_of_file) -> 483 | log('Bye!',[]), 484 | Bot0 = BotF 485 | ; ( parse(Cs, Program) -> 486 | exec(Program, Bot0, Bot1), 487 | logo_repl(Bot1, BotF) 488 | ; log('Syntax error in: ~w', [Str]), 489 | logo_repl(Bot0, BotF))). 490 | 491 | start_repl :- 492 | NameStr := get_bot_name(), 493 | atom_string(Name, NameStr), 494 | Name \= '', 495 | logo(Name). 496 | 497 | 498 | slurp_file(F, L) :- 499 | setup_call_cleanup( 500 | open(F, read, In), 501 | slurp(In, L), 502 | close(In) 503 | ). 504 | 505 | slurp(In, L):- 506 | read_line_to_string(In, Line), 507 | ( Line == end_of_file 508 | -> L = [] 509 | ; string_chars(Line, LineCs), 510 | append(LineCs, Lines, L), 511 | slurp(In,Lines) 512 | ). 513 | -------------------------------------------------------------------------------- /src/paintbots/main.clj: -------------------------------------------------------------------------------- 1 | (ns paintbots.main 2 | (:require [org.httpkit.server :as httpkit] 3 | [ring.middleware.params :as params] 4 | [ring.util.io :as ring-io] 5 | [ring.util.codec :as ring-codec] 6 | [clojure.core.async :refer [go req :uri (subs 1))] 27 | (if (= c "") 28 | "scratch" 29 | c)))) 30 | 31 | (defn register [{{name :register} :form-params :as req}] 32 | (let [canvas (canvas-of req) 33 | name (str/trim name) 34 | state (state/current-state)] 35 | (cond 36 | (not (state/has-canvas? state canvas)) 37 | {:status 404 38 | :body "No such art, try again :("} 39 | 40 | (state/bot-registered? state canvas name) 41 | {:status 409 42 | :body "Already registered!"} 43 | 44 | :else 45 | (let [id (state/cmd-sync! :register :canvas canvas :name name)] 46 | {:status 200 :body id})))) 47 | 48 | (defn handle-bot-command [canvas command-duration-ms 49 | command-fn params id 50 | ch] 51 | (go 52 | ( (count msg) 100) 135 | (subs msg 0 100) 136 | msg)] 137 | (assoc bot :msg msg))))) 138 | 139 | (def ->col (memoize (fn [[r g b]] 140 | (.getRGB (Color. ^int r ^int g ^int b))))) 141 | 142 | (defn paint 143 | "Paint the pixel at the current position with the current color." 144 | [req] 145 | (bot-command 146 | req 147 | (fn [_ {:keys [x y color] :as bot} with-img] 148 | (with-img 149 | (fn [^BufferedImage img] 150 | (when (and (< -1 x (.getWidth img)) 151 | (< -1 y (.getHeight img))) 152 | (.setRGB img x y (->col color))))) 153 | bot))) 154 | 155 | (defn info 156 | "No-op command for returning bots own info." 157 | [req] 158 | (bot-command 159 | req 160 | (fn [_ bot _] bot))) 161 | 162 | (let [clear-color 0] 163 | (defn clear [req] 164 | (bot-command 165 | req 166 | (fn [_state {:keys [x y] :as bot} with-img] 167 | (with-img 168 | (fn [^BufferedImage img] 169 | (when (and (< -1 x (.getWidth img)) 170 | (< -1 y (.getHeight img))) 171 | (.setRGB img x y clear-color)))) 172 | bot)))) 173 | 174 | 175 | (defn bye [{{:keys [id]} :form-params :as req}] 176 | (state/cmd-sync! :deregister 177 | :canvas (canvas-of req) 178 | :id id) 179 | {:status 204}) 180 | 181 | (def from-col 182 | (memoize 183 | (fn [rgb] 184 | (let [c (java.awt.Color. rgb)] 185 | [(.getRed c) (.getGreen c) (.getBlue c)])))) 186 | 187 | (defn look [req] 188 | (bot-command 189 | req 190 | (fn [{r ::response} bot with-img] 191 | (with-img 192 | (fn [^BufferedImage img] 193 | ;; A hacky way to pass out a response 194 | (let [w (.getWidth img) 195 | h (.getHeight img)] 196 | (reset! r (with-out-str 197 | (loop [y 0 198 | x 0] 199 | (cond 200 | (= x w) 201 | (do 202 | (print "\n") 203 | (recur (inc y) 0)) 204 | 205 | (= y h) 206 | :done 207 | 208 | :else 209 | (let [c (.getRGB img x y)] 210 | (print (if (zero? c) "." (state/color-name (from-col c)))) 211 | (recur y (inc x)))))))))) 212 | bot))) 213 | 214 | (defn bots [req] 215 | (bot-command 216 | req 217 | (fn [{r ::response} bot _] 218 | (reset! r 219 | (let [c (canvas-of req) 220 | state (state/current-state)] 221 | (if (state/has-canvas? state c) 222 | (state/canvas-bots (state/current-state) c) 223 | []))) 224 | bot))) 225 | 226 | (defn app-bar [req] 227 | (h/html 228 | [:nav.navbar.bg-base-100 229 | [:div.flex-1 230 | [:span.font-semibold "PaintBots"]] 231 | 232 | [:div.navbar-start.ml-4 233 | [::h/live (state/source :canvas keys) 234 | (fn [canvas-opts] 235 | (let [canvas (canvas-of req)] 236 | (h/html 237 | [:select.select {:on-change "window.location.pathname = window.event.target.value;"} 238 | [::h/for [o canvas-opts 239 | :let [selected? (= canvas o)]] 240 | [:option {:value o :selected selected?} o]]])))]] 241 | 242 | [:div.navbar-end 243 | [:button.btn.btn-sm.mx-2 {:on-click "toggleBots()"} "toggle bots"]]])) 244 | 245 | (defn rgb [[r g b]] 246 | (str "rgb(" r "," g "," b ")")) 247 | 248 | (def background-image-css 249 | (str "#canvas::before { content: ''; position: absolute; top: 50; left: 0; width: 100%; height: 100%; " 250 | "opacity: 0.1; z-index: -1; background-image: url('/logo.png'); " 251 | "background-repeat: no-repeat; background-size: cover; }")) 252 | 253 | (defn with-page [head-fn body-fn] 254 | (h/out! "\n") 255 | (h/html 256 | [:html {:data-theme "dracula"} 257 | [:head 258 | [:meta {:charset "UTF-8"}] 259 | [:link {:rel "stylesheet" :href "/paintbots.css"}] 260 | [:link {:rel "icon" :href "/favicon.ico" :type "image/x-icon"}] 261 | (h/live-client-script "/ws") 262 | (head-fn)] 263 | [:body 264 | (body-fn)]])) 265 | 266 | (defn page [{:keys [width height background-logo?] :as _config} req] 267 | (let [canvas-name (canvas-of req) 268 | state-source (poll/poll-source 1000 #(state/current-state)) 269 | canvas-changed (png/png-bytes-source canvas-name) 270 | bots (source/computed #(get-in % [:canvas canvas-name :bots]) state-source) 271 | client? (contains? (:query-params req) "client")] 272 | (with-page 273 | ;; head stuff 274 | #(do 275 | (h/html 276 | [:script {:type "text/javascript"} 277 | "function toggleBots() { let b = document.querySelector('#bot-positions'); b.style.display = b.style.display == '' ? 'none' : ''; }; "]) 278 | (h/html 279 | [:style 280 | "#gfx { image-rendering: pixelated; width: 100%; position: absolute; z-index: 99; } " 281 | "#bot-positions { z-index: 100; } " 282 | (when background-logo? 283 | (h/out! background-image-css))]) 284 | (when client? 285 | (client/client-head))) 286 | 287 | ;; page content 288 | #(do 289 | (app-bar req) 290 | (h/html 291 | [:div.page 292 | [::h/live bots 293 | (fn [bots] 294 | (h/html 295 | [:div.bots.flex.flex-row 296 | "Painters: " 297 | [::h/for [{n :name c :color m :msg} (vals bots) 298 | :let [col-style (str "width: 16px; height: 16px; " 299 | "position: absolute; left: 2px; top: 2px;" 300 | "background-color: " (rgb c) ";")]] 301 | [:div.inline.relative.ml-5.pl-5 n [:div.inline {:style col-style}] 302 | [::h/when m 303 | [:q.italic.mx-4 m]]]]]))] 304 | 305 | [:div#canvas 306 | [::h/live canvas-changed 307 | (fn [b] 308 | (let [b64 (.encodeToString (java.util.Base64/getEncoder) b) 309 | src (str "data:image/png;base64," b64)] 310 | (h/html 311 | [:img {:id "gfx" :src src}])))] 312 | [:svg#bot-positions {:viewBox (str "0 0 " width " " height)} 313 | [::h/live bots 314 | (fn [bots] 315 | (try 316 | (let [bots (vals bots)] 317 | (h/html 318 | [:g.bots 319 | [::h/for [{:keys [x y color name]} bots 320 | :let [c (apply format "#%02x%02x%02x" color)]] 321 | [:g 322 | [:text {:x (- x 2) :y (- y 2.5) :font-size 2 :fill "white"} name] 323 | [:circle {:cx (+ 0.5 x) :cy (+ 0.5 y) :r 2 :stroke c :stroke-width 0.25}]]]])) 324 | (catch Exception e 325 | (println "EX: " e ", BOTS: " (pr-str bots)))))]]] 326 | [::h/when client? 327 | (client/client-ui)]]))))) 328 | 329 | (defn admin-panel [config req] 330 | (h/html 331 | [:div.m-4 332 | 333 | [:h2 "Canvases"] 334 | [::h/live (state/source :canvas #(for [[name {bots :bots}] %] 335 | [name (for [[id {name :name}] bots] 336 | [id name])])) 337 | (fn [canvases] 338 | (h/html 339 | [:div.canvases.flex.flex-wrap 340 | [::h/for [[canvas-name bots] canvases 341 | :let [img (str "/" canvas-name ".png")]] 342 | [:div {:class "card w-96 bg-base-100 shadow-xl m-4 max-w-1/3"} 343 | [:figure [:img {:src img}]] 344 | [:div.card-body 345 | [:h2.card-title canvas-name] 346 | [:a {:href (str "/" canvas-name ".mp4") :target :_blank} "video"] 347 | "Bots:" 348 | [:ul 349 | [::h/for [[id name] bots] 350 | [:li name [:button.btn.btn-xs.ml-2 {:on-click #(state/cmd! :deregister {:canvas canvas-name 351 | :id id})} 352 | "kick"]]]]] 353 | 354 | [:div.card-actions.justify-end 355 | [:button.btn.btn-md.btn-warning 356 | {:on-click (js/js-when "confirm('Really lose this fine art?')" 357 | #(state/cmd! :clear-canvas :name canvas-name))} "Clear canvas"]]]]]))] 358 | 359 | [:div 360 | [:h2 "Add new canvas"] 361 | [:input.input.input-bordered#newcanvas 362 | {:placeholder "name (letters only)"}] 363 | [:button.btn.btn-md.btn-primary 364 | {:on-click (js/js (fn [name-input] 365 | (let [name (reduce str (filter #(Character/isLetter %) name-input))] 366 | (when (and (not (str/blank? name)) 367 | (not= "admin" name)) 368 | (state/cmd! :create-canvas :name name)))) 369 | (js/input-value :newcanvas))} "Create"]]])) 370 | 371 | (defn admin-page [config req] 372 | (let [[activated set-activated!] (source/use-state false) 373 | activate! (fn [password] 374 | (when (= password (get-in config [:admin :password])) 375 | (set-activated! true)))] 376 | (with-page 377 | (constantly nil) 378 | #(h/html 379 | [:div.admin 380 | [::h/live activated 381 | (fn [activated] 382 | (if activated 383 | (admin-panel config req) 384 | (h/html 385 | [:div.form-control.m-4 386 | [:label.label "Yeah. Whatsda passwoid?"] 387 | [:input#adminpw 388 | {:autofocus true 389 | :type :password 390 | :on-keypress (js/js-when js/enter-pressed? 391 | activate! 392 | (js/input-value "adminpw"))}]])))]])))) 393 | 394 | 395 | (def command-handlers 396 | [[:register #'register] 397 | [:info #'info] 398 | [:move #'move] 399 | [:paint #'paint] 400 | [:color #'change-color] 401 | [:msg #'say] 402 | [:clear #'clear] 403 | [:look #'look] 404 | [:bots #'bots] 405 | [:bye #'bye]]) 406 | 407 | (defn- keywordize-params [p] 408 | (into {} 409 | (map (fn [[k v]] 410 | [(keyword k) v])) 411 | p)) 412 | 413 | (defn- params->command [p] 414 | (some (fn [[required-param handler-fn]] 415 | (when (contains? p required-param) 416 | handler-fn)) 417 | command-handlers)) 418 | 419 | (defn handle-post [req] 420 | (let [{p :form-params :as req} 421 | (update req :form-params keywordize-params) 422 | cmd-handler (params->command p)] 423 | 424 | (or (when cmd-handler 425 | (cmd-handler req)) 426 | {:status 404 427 | :body "I don't recognize those parameters, try something else."}))) 428 | 429 | (defn handle-bot-ws [req] 430 | (let [canvas (canvas-of req) 431 | bot-id (atom nil) 432 | deregister! #(when-let [id @bot-id] 433 | (state/cmd! :deregister :canvas canvas :id id)) 434 | close! (fn [ch] 435 | (deregister!) 436 | (httpkit/close ch))] 437 | (httpkit/as-channel 438 | req 439 | {:on-open (fn [ch] 440 | (if-not (httpkit/websocket? ch) 441 | (httpkit/close ch) 442 | (println "WS connected" req))) 443 | :on-close (fn [_ch _status] (deregister!)) 444 | :on-receive (fn [ch msg] 445 | (let [form (some-> msg str/trim (ring-codec/form-decode "UTF-8")) 446 | params (if (map? form) 447 | (keywordize-params form) 448 | ;; single command like "look" without any value 449 | {(keyword form) ""}) 450 | id @bot-id] 451 | (cond 452 | ;; Not registered yet, handle registration 453 | (and (nil? id) (:register params)) 454 | (let [res (state/cmd-sync! :register :canvas canvas :name (:register params))] 455 | (if (string? res) 456 | (do (reset! bot-id res) 457 | (httpkit/send! ch "OK")) 458 | (do (httpkit/send! ch (:error res)) 459 | (close! ch)))) 460 | 461 | ;; Registered, handle a command 462 | id 463 | (let [params (dissoc params :register) 464 | cmd (params->command params)] 465 | (if-not cmd 466 | (httpkit/send! ch "I don't understand that command :(") 467 | (cmd {::ws ch 468 | :params params 469 | :id id 470 | :canvas canvas}))))))}))) 471 | 472 | (def assets 473 | {"/paintbots.css" {:t "text/css"} 474 | "/favicon.ico" {:t "image/x-icon"} 475 | "/logo.png" {:t "image/png"} 476 | 477 | "/client/swipl-bundle.js" {:t "text/javascript" :enc "gzip" :f "/client/swipl-bundle.js.gz"} 478 | "/client/logo.pl" {:t :text/prolog}}) 479 | 480 | (defn asset [{uri :uri :as req}] 481 | (when-let [asset (assets uri)] 482 | (let [file (->> (or (:f asset) uri) (str "public") io/resource)] 483 | {:status 200 484 | :headers (merge {"Content-Type" (:t asset)} 485 | (when-let [enc (:enc asset)] 486 | {"Content-Encoding" enc})) 487 | :body (ring-io/piped-input-stream 488 | (fn [out] 489 | (with-open [in (.openStream file)] 490 | (io/copy in out))))}))) 491 | 492 | (let [ws-handler (context/connection-handler "/ws" :ping-interval 45)] 493 | (defn handler [config {m :request-method uri :uri :as req}] 494 | (if (= uri "/ws") 495 | (ws-handler req) 496 | (or (asset req) 497 | (cond 498 | (= :post m) 499 | (handle-post req) 500 | 501 | ;; Support bots connecting via WS to increase speed! 502 | (contains? (:headers req) "upgrade") 503 | (handle-bot-ws req) 504 | 505 | (= uri "/admin") 506 | (h/render-response (partial #'admin-page config req)) 507 | 508 | ;; Try to download PNG of a canvas 509 | (str/ends-with? uri ".png") 510 | (let [canvas (some-> req canvas-of (str/replace #".png$" "") state/valid-canvas)] 511 | (if-let [png (png/current-png-bytes canvas)] 512 | {:status 200 513 | :headers {"Cache-Control" "no-cache"} 514 | :body png} 515 | {:status 404 516 | :body "No such canvas!"})) 517 | 518 | ;; Try to download MP4 video of canvas snapshots 519 | (str/ends-with? uri ".mp4") 520 | (if-let [canvas (some-> req canvas-of (str/replace #".mp4$" "") state/valid-canvas)] 521 | {:status 200 522 | :headers {"Content-Type" "video/mp4"} 523 | :body (ring-io/piped-input-stream 524 | (fn [out] 525 | (video/generate (:video config) canvas out)))} 526 | {:status 404 527 | :body "No such canvas!"}) 528 | 529 | :else 530 | (if (state/has-canvas? (state/current-state) (canvas-of req)) 531 | (h/render-response (partial #'page config req)) 532 | (do (println "Someone tried: " (pr-str (:uri req))) 533 | {:status 404 534 | :body "Ain't nothing more here for you, go away!"}))))))) 535 | 536 | (defn -main [& [config-file :as _args]] 537 | (let [config-file (or config-file "config.edn") 538 | _ (println "Reading config from: " config-file) 539 | {:keys [ip port width height command-duration-ms] :as config} 540 | (read-string (slurp config-file))] 541 | (println "Config: " (pr-str config)) 542 | (state/cmd-sync! :config 543 | :width width 544 | :height height 545 | :command-duration-ms command-duration-ms) 546 | (state/cmd-sync! :create-canvas :name "scratch") 547 | (png/listen! state/state config) 548 | (alter-var-root #'server 549 | (fn [_] 550 | (httpkit/run-server (params/wrap-params 551 | (partial #'handler config)) 552 | {:ip ip :port port 553 | :thread 32}))))) 554 | --------------------------------------------------------------------------------