├── 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 /