├── examples
├── out
│ └── .gitkeep
├── __main__.py
├── helloworld.py
├── music.py
├── basics.py
├── branflakes.py
├── assets
│ ├── scratchcat.svg
│ └── beer.b
└── all_supported_blocks.py
├── __init__.py
├── boiga
├── __init__.py
├── utils.py
├── expressions.py
├── ast.py
├── statements.py
├── codegen.py
└── ast_core.py
├── LICENSE
├── .gitignore
├── README.md
└── tools
└── run_scratch.js
/examples/out/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .boiga import *
2 |
--------------------------------------------------------------------------------
/boiga/__init__.py:
--------------------------------------------------------------------------------
1 | from .codegen import Project
2 | from .ast import *
3 |
--------------------------------------------------------------------------------
/examples/__main__.py:
--------------------------------------------------------------------------------
1 | from . import helloworld
2 | from . import basics
3 | from . import branflakes
4 | from . import music
5 | from . import all_supported_blocks
6 |
--------------------------------------------------------------------------------
/examples/helloworld.py:
--------------------------------------------------------------------------------
1 | from boiga import *
2 |
3 | project = Project()
4 |
5 | cat = project.new_sprite("Scratch Cat")
6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50))
7 |
8 | cat.on_flag([
9 | Say("Hello, world!"),
10 | ])
11 |
12 | project.save("examples/out/Boiga Examples: Hello World.sb3")
13 |
--------------------------------------------------------------------------------
/examples/music.py:
--------------------------------------------------------------------------------
1 | from boiga import *
2 |
3 | project = Project()
4 |
5 | cat = project.new_sprite("Scratch Cat")
6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50))
7 |
8 | cat.on_flag([
9 | SetTempo(50),
10 | ChangeTempoBy(10),
11 | SetInstrument(Instruments.ElectricPiano),
12 | PlayNote(60, 0.25),
13 | RestFor(0.5),
14 | PlayDrum(Drums.HandClap, 1),
15 | Say(GetTempo()),
16 | ])
17 |
18 | project.save("examples/out/Boiga Examples: Music.sb3")
19 |
--------------------------------------------------------------------------------
/boiga/utils.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 |
3 | BLANK_SVG = b""""""
6 |
7 |
8 | _SOUP = '!#%()*+,-./:;=?@[]^_`{|}~' + \
9 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
10 |
11 | def gen_uid(seed):
12 | n = int.from_bytes(hashlib.sha256(repr(seed).encode()).digest(), "little")
13 | uid = ""
14 | for _ in range(20):
15 | uid += _SOUP[n % len(_SOUP)]
16 | n //= len(_SOUP)
17 | #return "".join(random.choices(_SOUP, k=20))
18 | return uid
19 |
20 |
21 | # https://stackoverflow.com/a/12472564
22 | def flatten(S):
23 | if S == []:
24 | return S
25 | if isinstance(S[0], list):
26 | return flatten(S[0]) + flatten(S[1:])
27 | return S[:1] + flatten(S[1:])
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 David Buchanan
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 |
--------------------------------------------------------------------------------
/examples/basics.py:
--------------------------------------------------------------------------------
1 | from boiga import *
2 |
3 | project = Project()
4 |
5 | cat = project.new_sprite("Scratch Cat")
6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50))
7 |
8 | my_variable = project.stage.new_var("my variable")
9 | var_foo = cat.new_var("foo", 123)
10 | stdout = project.stage.new_list("stdout", [], monitor=[0, 0, 480-2, 140])
11 |
12 |
13 | hex_out = cat.new_list("hex_out")
14 | i = cat.new_var("i")
15 | tmp = cat.new_var("tmp")
16 |
17 | @cat.proc_def()
18 | def hex_decode(locals, hex_in): return [
19 | hex_out.delete_all(),
20 | locals.i[:hex_in.len():2] >> [
21 | hex_out.append( Literal("0x").join(hex_in[locals.i]).join(hex_in[locals.i+1]) + 0 )
22 | ]
23 | ]
24 |
25 | @cat.proc_def("multiply [number a] with [number b]")
26 | def multiply_proc(locals, number_a, number_b): return [
27 | locals.result <= number_a * number_b
28 | ]
29 |
30 | cat.on_flag([
31 | stdout.delete_all(),
32 | stdout.append("Hello, world!"),
33 | multiply_proc(7, 9),
34 | stdout.append(multiply_proc.result),
35 | hex_decode("deadbeefcafebabe"),
36 | tmp <= "",
37 | i[:hex_out.len()] >> [
38 | tmp <= tmp.join(hex_out[i]),
39 | If (i != hex_out.len() - 1) [
40 | tmp <= tmp.join(", ")
41 | ]
42 | ],
43 | stdout.append(tmp),
44 | stdout.append(hex_out)
45 | ])
46 |
47 | project.save("examples/out/Boiga Examples: Basics.sb3")
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | #lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | *.sb3
132 | DEBUG.json
133 |
134 | # npm noise
135 | node_modules
136 | package-lock.json
137 | package.json
138 |
--------------------------------------------------------------------------------
/examples/branflakes.py:
--------------------------------------------------------------------------------
1 | from boiga import *
2 |
3 | ASCII_LUT = Literal("�"*0x1f + bytes(range(0x20, 0x7f)).decode())
4 |
5 | project = Project()
6 |
7 | stdout = project.stage.new_list("stdout", [], monitor=[0, 0, 480-2, 292])
8 |
9 | sprite = project.new_sprite("Branflakes")
10 | bfmem = sprite.new_list("bfmem")
11 | memptr = sprite.new_var("memptr")
12 |
13 | @sprite.proc_def()
14 | def putchar(locals, codepoint): return [
15 | If (codepoint == 0x0a) [
16 | stdout.append(locals.line_buffer),
17 | locals.line_buffer <= ""
18 | ].Else [
19 | If (codepoint == 0x0d) [
20 | locals.line_buffer <= ""
21 | ].Else [
22 | locals.line_buffer <= locals.line_buffer.join(ASCII_LUT[codepoint-1])
23 | ]
24 | ]
25 | ]
26 |
27 | def compile_bf(prog, is_subblock=False):
28 | compiled = []
29 | i = 0
30 | ptrshift = 0
31 | while i < len(prog):
32 | if prog[i] in "+-":
33 | delta = 0
34 | while i < len(prog) and prog[i] in "+-":
35 | delta += 1 if prog[i] == "+" else -1
36 | i += 1
37 | compiled.append(
38 | bfmem[memptr+ptrshift] <= (bfmem[memptr+ptrshift] + delta) & 0xff
39 | )
40 | elif prog[i] in "><":
41 | delta = 0
42 | while i < len(prog) and prog[i] in "><":
43 | delta += 1 if prog[i] == ">" else -1
44 | i += 1
45 | ptrshift += delta
46 | elif prog[i] == "[":
47 | depth = 0
48 | i += 1
49 | body = ""
50 | while i < len(prog):
51 | if prog[i] == "[":
52 | depth += 1
53 | elif prog[i] == "]":
54 | if depth:
55 | depth -= 1
56 | else:
57 | i += 1
58 | break
59 | body += prog[i]
60 | i += 1
61 | else:
62 | raise Exception("Unexpected EOF")
63 |
64 | compiled += [
65 | memptr.changeby(ptrshift) if ptrshift else [],
66 | RepeatUntil (bfmem[memptr] == 0) [
67 | compile_bf(body, is_subblock=True)
68 | ]
69 | ]
70 | ptrshift = 0
71 |
72 | elif prog[i] == "]":
73 | raise Exception("Unexpected ]")
74 | elif prog[i] == ",":
75 | raise Exception("Not implemented: ,")
76 | elif prog[i] == ".":
77 | compiled.append(
78 | putchar(bfmem[memptr+ptrshift])
79 | )
80 | i += 1
81 | else:
82 | i += 1
83 |
84 | if is_subblock and ptrshift:
85 | compiled.append(
86 | memptr.changeby(ptrshift)
87 | )
88 |
89 | return compiled
90 |
91 | @sprite.proc_def(turbo=False)
92 | def bf_main(locals):
93 | return compile_bf(open("examples/assets/beer.b").read())
94 |
95 | sprite.on_flag([
96 | stdout.delete_all(),
97 | bfmem.delete_all(),
98 | Repeat (30000) [
99 | bfmem.append(0)
100 | ],
101 | memptr <= 0,
102 | bf_main()
103 | ])
104 |
105 | project.save("examples/out/Boiga Examples: Branflakes.sb3")
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # boiga
2 |
3 | Boiga is a Python library enabling ergonomic Scratch 3.0 code generation.
4 |
5 | Have you ever attempted to author advanced or accelerated algorithms in Scratch? It's not much fun - you might end up making miles of mouse movements for even the most miniscule of modifications.
6 |
7 | Boiga (ab)uses Python's operator overloading, to write Scratch expressions with intuitive(ish) Python syntax. We expose a simple AST representation, making it easy to write custom code generators and macros.
8 |
9 | See the examples directory for... examples. Further usage examples can be seen in my [Scratch Cryptography Library](https://github.com/DavidBuchanan314/scratch-cryptography-library).
10 |
11 | See also: [scratch-vscode](https://github.com/DavidBuchanan314/scratch-vscode) - A vscode extension that lets you preview Scratch projects right in your editor, with live reload.
12 |
13 | ## Project Status:
14 |
15 | It's a working prototype. Not all blocks are supported, but you can use this to write non-trivial scratch projects.
16 | If you plan to actually use this for something, you might want to make a copy of the code - I'll probably be making lots of breaking changes as I implement more features.
17 |
18 | ## Features:
19 | - Scratch .sb3 code generation.
20 | - Very basic AST optimisation (e.g. constant folding)
21 | - Optional inlining of "custom blocks".
22 | - Some bitwise operators.
23 | - 0-indexed lists.
24 |
25 | ## TODO
26 | - Documentation (sorry lol - for now, just look at the examples)
27 | - Static allocation of "sub-lists" within lists
28 | - Dynamic allocation of space within lists (i.e. malloc)?
29 | - For both of the above, we can pass indices around like pointers
30 |
31 | ## Building Examples:
32 |
33 | ```sh
34 | python3 -m examples
35 | ```
36 |
37 | NOTE: Requires Python 3.10 or above!
38 |
39 | [`examples/helloworld.py`](https://github.com/DavidBuchanan314/boiga/blob/main/examples/helloworld.py) looks like this:
40 |
41 | ```python
42 | from boiga import *
43 |
44 | project = Project()
45 |
46 | cat = project.new_sprite("Scratch Cat")
47 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50))
48 |
49 | cat.on_flag([
50 | Say("Hello, world!"),
51 | ])
52 |
53 | project.save("examples/out/Boiga Examples: Hello World.sb3")
54 | ```
55 |
56 | Which compiles to the following scratch project:
57 |
58 |
59 |
60 | Obviously, it's probably easier to write programs like that using the drag-and-drop interface. Conversely, [`examples/branflakes.py`](https://github.com/DavidBuchanan314/boiga/blob/main/examples/branflakes.py) implements a brainf\*ck to Scratch compiler, which compiles a "99 bottles of beer" program into the following:
61 |
62 |
63 |
64 | This is just a preview, the whole script doesn't even come close to fitting in a single screenshot! You can see the full scratch code [here](https://scratch.mit.edu/projects/677776603/).
65 |
--------------------------------------------------------------------------------
/tools/run_scratch.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // Script taken from https://github.com/shinh/elvm/blob/master/tools/run_scratch.js
4 |
5 | // This script executes a Scratch 3.0 program that the Scratch backend generates.
6 | // To run this script, first install npm package 'scratch-vm' under 'tools' directory (here)
7 | // by the following command:
8 | // $ ls run_scratch.js # make sure you are in the same directory
9 | // run_scratch.js
10 | // $ npm install scratch-vm # this generates 'node_modules' on this directory.
11 |
12 | const fs = require('fs');
13 |
14 | const readline = require('readline');
15 | const VirtualMachine = require('scratch-vm');
16 | const ScratchStorage = require('scratch-storage');
17 | const storage = new ScratchStorage();
18 | const vm = new VirtualMachine();
19 | vm.attachStorage(storage);
20 | vm.start();
21 | vm.clear();
22 | vm.setCompatibilityMode(false);
23 | vm.setTurboMode(true);
24 |
25 | // Ignore warning / error messages
26 | require('minilog').disable();
27 |
28 | // Replace special characters with '\dXXX'
29 | const special_chars = /([^ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~])/g;
30 | //const inputStr = fs.readFileSync(0).toString().
31 | // replace(special_chars, (_match, g, _offset, _str) => '\d' + g.charCodeAt(0).toString().padStart(3, '0'));
32 |
33 | // Input bytes from STDIN as a response to Scratch's ask block.
34 | // If the program requires more input, put EOF ('\0').
35 |
36 | const rl = readline.createInterface({
37 | input: process.stdin,
38 | output: process.stdout,
39 | });
40 |
41 | vm.runtime.addListener('QUESTION', (q, r) => {
42 | if (q === null)
43 | return;
44 | rl.question("> ", ans => {
45 | //rl.close();
46 | vm.runtime.emit('ANSWER', ans);
47 | })
48 | });
49 |
50 | // Get output bytes that the program wrote as contents of Scratch's list block,
51 | // then replace special characters with corresponding ones.
52 | // ('\dXXX' (decimal XXX) → a character with code point XXX)
53 | function printResult() {
54 | if (0) {// I patched scratch3_data.js to print on append to stdout
55 | const stdout_result = Object.values(vm.runtime.targets[0].variables).filter(v => v.name == "stdout")[0].value;
56 | const stdout_str =
57 | stdout_result.join('\n').replace(/\(\d{1,3})/g,
58 | (_match, g, _offset, _str) => String.fromCharCode(g));
59 | process.stdout.write(stdout_str + "\n");
60 | }
61 | process.nextTick(process.exit);
62 | };
63 |
64 | // Count active threads and detect finish of execution.
65 | function whenThreadsComplete() {
66 | return new Promise((resolve, reject) => {
67 | setInterval(() => {
68 | let active = 0;
69 | const threads = vm.runtime.threads;
70 | for (let i = 0; i < threads.length; i++) {
71 | if (!threads[i].updateMonitor) {
72 | active += 1;
73 | }
74 | }
75 | if (active === 0) {
76 | resolve();
77 | }
78 | }, 100);
79 | })
80 | }
81 |
82 | const filename = process.argv[2];
83 | const project = new Buffer.from(fs.readFileSync(filename));
84 |
85 | vm.loadProject(project)
86 | .then(() => vm.greenFlag())
87 | .then(() => whenThreadsComplete())
88 | .then(printResult);
89 |
--------------------------------------------------------------------------------
/examples/assets/scratchcat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/all_supported_blocks.py:
--------------------------------------------------------------------------------
1 | from math import floor, ceil
2 | from boiga import *
3 |
4 | project = Project()
5 |
6 | cat = project.new_sprite("Scratch Cat")
7 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50))
8 |
9 | a = cat.new_var("a")
10 | b = cat.new_var("b")
11 | c = cat.new_var("c")
12 |
13 | mylist = cat.new_list("mylist")
14 |
15 | cat.on_receive("hello", [
16 | Say("ok"),
17 | ])
18 |
19 | @cat.proc_def()
20 | def my_custom_block(locals, a, b, c: bool): return [
21 | If (c) [
22 | locals.result <= a + b
23 | ]
24 | ]
25 |
26 | cat.on_flag([
27 | # ================= MOTION ====================
28 |
29 | #MoveSteps(10),
30 | #TurnDegreesCW(15),
31 | #TurnDegreesCCW(15),
32 |
33 | #GoTo(Position.Random),
34 | #GoTo(Position.Mouse),
35 | SetXYPos(0, 0),
36 | #GlideTo(),
37 | #GlideToXYPos(),
38 |
39 | #PointInDirection(),
40 | #PointTowards(),
41 |
42 | ChangeXPos(10),
43 | SetXPos(0),
44 | ChangeYPos(10),
45 | SetYPos(0),
46 |
47 | #IfOnEdgeBounce(),
48 |
49 | #SetRotationStyle(),
50 |
51 | Say(GetXPos()),
52 | Say(GetYPos()),
53 | Say(GetDirection()),
54 |
55 |
56 | # ================= LOOKS ====================
57 |
58 | #SayForDuration("Hello", 1),
59 | Say("Hello!"),
60 | #ThinkForDuration("Hmm...", 1),
61 | #Think("Hmm..."),
62 |
63 | SetCostume("scratchcat"),
64 | #NextCostume(),
65 | #SetBackdrop(),
66 | #NextBackdrop(),
67 |
68 | #ChangeSizeBy(),
69 | SetSize(100),
70 |
71 | ChangeEffect(Effects.Color, 25),
72 | SetEffect(Effects.Color, 0),
73 | #ClearEffects(),
74 |
75 | Hide(),
76 | Show(),
77 |
78 | #GoToLayer(),
79 | #GoForwardsLayers(),
80 | #GoBackwardsLayers(),
81 |
82 | Say(CostumeNumber()),
83 | #CostumeName(),
84 | #BackdropNumber(),
85 | #BackdropName(),
86 |
87 | #GetSize(),
88 |
89 |
90 | # ================= SOUNDS ====================
91 |
92 | #PlaySoundUntilDone(),
93 | #StartSound(),
94 | #StopAllSounds(),
95 |
96 | #ChangeSoundEffect(),
97 | #SetSoundEffect(),
98 | #ClearSoundEffects(),
99 |
100 | #ChangeVolumeBy(),
101 | #SetVolumeTo(),
102 |
103 | #GetVolume(),
104 |
105 |
106 | # ================= EVENTS ====================
107 |
108 | BroadcastAndWait("hello"),
109 | #Broadcast("hello"),
110 |
111 |
112 | # ================= CONTROL ====================
113 |
114 | Wait(1),
115 |
116 | Repeat (10) [
117 | Say("foo"), # TODO: allow empty body?
118 | ],
119 |
120 | If (Literal(1) == 2) [
121 | Forever [
122 | Say("foo"), # TODO: allow empty body?
123 | ],
124 | ],
125 |
126 | If (Literal(1) == 1) [
127 | Say("foo"), # TODO: allow empty body?
128 | ].Else [
129 | Say("bar"), # TODO: allow empty body?
130 | ],
131 |
132 | #WaitUntil(Literal(1) == 1),
133 |
134 | RepeatUntil (Literal(1) == 1) [
135 | Say("foo"), # TODO: allow empty body?
136 | ],
137 |
138 | If (Literal(1) == 2) [
139 | StopAll(),
140 | ],
141 | If (Literal(1) == 2) [
142 | StopThisScript(),
143 | ],
144 | StopOtherScriptsInSprite(),
145 |
146 | #CreateClone(),
147 | #DeleteThisClone(),
148 |
149 |
150 | # ================= SENSING ====================
151 |
152 | Say(Touching("edge")),
153 | Say(TouchingColour(LiteralColour("#FF0000"))),
154 | #Say(ColourTouchingColour(LiteralColour("#0000FF"), LiteralColour("#FF0000")))
155 | #Say(DistanceTo()),
156 |
157 | AskAndWait("What's the answer?"),
158 | Say(Answer()),
159 |
160 | #Say(KeyPressed("a")),
161 | Say(MouseDown()),
162 | Say(MouseX()),
163 | Say(MouseY()),
164 |
165 | #SetDragMode(DragMode.DRAGGABLE),
166 |
167 | #Say(Loudness()),
168 |
169 | #Say(Timer()),
170 | #ResetTimer(),
171 |
172 | #GetProperty("backdrop #", "Stage");
173 |
174 | #GetCurrent("year"),
175 |
176 | Say(DaysSince2k()),
177 |
178 | #Say(Username()),
179 |
180 |
181 | # ================= OPERATORS ====================
182 |
183 | Say(a + b),
184 | Say(a - b),
185 | Say(a * b),
186 | Say(a / b),
187 |
188 | # reflected operators
189 | Say(5 + b),
190 | Say(5 - b),
191 | Say(5 * b),
192 | Say(5 / b),
193 |
194 | Say(pickrandom(1, 10)),
195 |
196 | Say(a > b),
197 | Say(a < b),
198 | Say(a == b),
199 | Say((a == a).AND(b == b)),
200 | Say((a == a).OR(b == b)),
201 | Say((a == a).NOT()),
202 |
203 | Say(Literal("apple").join("banana")),
204 | Say(Literal("apple")[0]),
205 | Say(Literal("apple").item(1)), # 1-index alternative to the above
206 | Say(Literal("apple").len()),
207 | #Say(Literal("apple").contains("a")),
208 |
209 | Say(a % b),
210 | Say(round(a)),
211 |
212 | Say(abs(a)),
213 | Say(floor(a)),
214 | Say(ceil(a)),
215 | Say(a.sqrt()),
216 | Say(a.sin()),
217 | Say(a.cos()),
218 | #Say(a.tan()),
219 | #Say(a.asin()),
220 | #Say(a.acos()),
221 | Say(a.atan()),
222 | Say(a.log()),
223 | Say(a.log10()),
224 | Say(a ** b),
225 |
226 |
227 | # ================= VARIABLES ====================
228 |
229 | a <= 1,
230 | a.changeby(1),
231 | #a.show(),
232 | #a.hide(),
233 |
234 | mylist.append("thing"),
235 |
236 | mylist.delete_at(0),
237 | mylist.delete_at1(1), # 1-index alternative to the above
238 |
239 | mylist.delete_all(),
240 |
241 | #mylist.insert_at(0, "thing"),
242 | #mylist.insert_at1(1, "thing"), # 1-index alternative to the above
243 |
244 | mylist[0] <= "thing",
245 | mylist.item(1) <= "thing", # 1-index alternative to the above
246 |
247 | Say(mylist[0]),
248 | Say(mylist.item(1)), # 1-index alternative to the above
249 |
250 | Say(mylist.index("thing")),
251 | Say(mylist.index1("thing")), # 1-index alternative to the above
252 |
253 | Say(mylist.contains("thing")),
254 |
255 | #mylist.show(),
256 | #mylist.hide(),
257 |
258 |
259 | # ================= CUSTOM BLOCKS ====================
260 |
261 | my_custom_block(a, b, a == a),
262 | my_custom_block(a, b, a == a).inline(),
263 |
264 |
265 | # ================= MUSIC ====================
266 |
267 | PlayDrum(Drums.SnareDrum, 0.25),
268 | RestFor(0.25),
269 | PlayNote(60, 0.25),
270 | SetInstrument(Instruments.Piano),
271 | SetTempo(60),
272 | ChangeTempoBy(10),
273 | Say(GetTempo()),
274 |
275 |
276 | # ================= PEN ====================
277 |
278 | EraseAll(),
279 | Stamp(),
280 | PenDown(),
281 | PenUp(),
282 | SetPenColour(LiteralColour("#ff0000")),
283 | #ChangePenEffect("color", 10),
284 | SetPenParam("color", 10),
285 | #ChangePenSizeBy(1),
286 | SetPenSize(10),
287 | ])
288 |
289 |
290 | cat.on_press("a", [
291 | Say("a"),
292 | ])
293 |
294 | project.save("examples/out/Boiga Examples: All Blocks.sb3")
295 |
--------------------------------------------------------------------------------
/boiga/expressions.py:
--------------------------------------------------------------------------------
1 | from . import ast
2 |
3 | def serialise_expression(sprite, expression, parent, shadow=False):
4 | if not issubclass(type(expression), ast.core.Expression):
5 | raise Exception(f"Cannot serialise {expression!r} as a expression")
6 |
7 | blocks_json = sprite.blocks_json
8 |
9 | uid = sprite.gen_uid()
10 | blocks_json[uid] = {
11 | "next": None,
12 | "parent": parent,
13 | "inputs": {},
14 | "fields": {},
15 | "shadow": shadow,
16 | "topLevel": False,
17 | }
18 |
19 | UNARYMATHOPS = ["abs", "floor", "ceiling", "sqrt", "sin", "cos", "tan",
20 | "asin", "acos", "atan", "ln", "log", "e ^", "10 ^"]
21 |
22 | if type(expression) is ast.core.BinaryOp:
23 | opmap = {
24 | "+": ("operator_add", "NUM"),
25 | "-": ("operator_subtract", "NUM"),
26 | "*": ("operator_multiply", "NUM"),
27 | "/": ("operator_divide", "NUM"),
28 | "<": ("operator_lt", "OPERAND"),
29 | ">": ("operator_gt", "OPERAND"),
30 | "==": ("operator_equals", "OPERAND"),
31 | "&&": ("operator_and", "OPERAND"),
32 | "||": ("operator_or", "OPERAND"),
33 | "join": ("operator_join", "STRING"),
34 | "%": ("operator_mod", "NUM"),
35 | "[]": ("operator_letter_of", "LETTER"),
36 | "random": ("operator_random", "FROM")
37 | }
38 | if expression.op in opmap:
39 | opcode, argname = opmap[expression.op]
40 | serialiser = sprite.serialise_bool if expression.op in ["&&", "||"] else sprite.serialise_arg
41 |
42 | an1 = argname+"1"
43 | an2 = argname+"2"
44 |
45 | # TODO: encode this nicely in the table above...
46 | if expression.op == "[]":
47 | an1 = "LETTER"
48 | an2 = "STRING"
49 | elif expression.op == "random":
50 | an1 = "FROM"
51 | an2 = "TO"
52 |
53 | out = {
54 | "opcode": opcode,
55 | "inputs": {
56 | an1: serialiser(expression.lval, uid),
57 | an2: serialiser(expression.rval, uid),
58 | },
59 | }
60 | else:
61 | raise Exception(f"Unable to serialise expression {expression!r}")
62 |
63 | elif type(expression) is ast.core.ListIndex:
64 | out = {
65 | "opcode": "data_itemoflist",
66 | "inputs": {
67 | "INDEX": sprite.serialise_arg(expression.index, uid)
68 | },
69 | "fields": {
70 | "LIST": [
71 | expression.list.name,
72 | expression.list.uid
73 | ]
74 | },
75 | }
76 |
77 | elif type(expression) is ast.core.ListItemNum:
78 | out = {
79 | "opcode": "data_itemnumoflist",
80 | "inputs": {
81 | "ITEM": sprite.serialise_arg(expression.item, uid)
82 | },
83 | "fields": {
84 | "LIST": [
85 | expression.list.name,
86 | expression.list.uid
87 | ]
88 | },
89 | }
90 |
91 | elif type(expression) is ast.core.ListContains:
92 | out = {
93 | "opcode": "data_listcontainsitem",
94 | "inputs": {
95 | "ITEM": sprite.serialise_arg(expression.thing, uid)
96 | },
97 | "fields": {
98 | "LIST": [
99 | expression.list.name,
100 | expression.list.uid
101 | ]
102 | },
103 | }
104 |
105 | elif type(expression) is ast.core.UnaryOp:
106 | if expression.op == "!":
107 | out = {
108 | "opcode": "operator_not",
109 | "inputs": {
110 | "OPERAND": sprite.serialise_bool(expression.value, uid),
111 | },
112 | }
113 |
114 | elif expression.op == "len":
115 | out = {
116 | "opcode": "operator_length",
117 | "inputs": {
118 | "STRING": sprite.serialise_arg(expression.value, uid),
119 | },
120 | }
121 |
122 | elif expression.op == "round":
123 | out = {
124 | "opcode": "operator_round",
125 | "inputs": {
126 | "NUM": sprite.serialise_arg(expression.value, uid),
127 | },
128 | }
129 |
130 | elif expression.op in UNARYMATHOPS:
131 | out = {
132 | "opcode": "operator_mathop",
133 | "inputs": {
134 | "NUM": sprite.serialise_arg(expression.value, uid),
135 | },
136 | "fields": {
137 | "OPERATOR": [expression.op, None]
138 | },
139 | }
140 |
141 | elif expression.op == "listlen":
142 | out = {
143 | "opcode": "data_lengthoflist",
144 | "fields": {
145 | "LIST": [expression.value.name, expression.value.uid],
146 | },
147 | }
148 |
149 | else:
150 | raise Exception(f"Unable to serialise expression {expression!r}")
151 |
152 | elif type(expression) is ast.core.ProcVar:
153 | out = {
154 | "opcode": "argument_reporter_string_number",
155 | "fields": {
156 | "VALUE": [expression.name, None]
157 | },
158 | }
159 |
160 | elif type(expression) is ast.core.ProcVarBool:
161 | out = {
162 | "opcode": "argument_reporter_boolean",
163 | "fields": {
164 | "VALUE": [expression.name, None]
165 | },
166 | }
167 |
168 | elif type(expression) is ast.DaysSince2k:
169 | out = {"opcode": "sensing_dayssince2000"}
170 |
171 | elif type(expression) is ast.Answer:
172 | out = {"opcode": "sensing_answer"}
173 |
174 | elif type(expression) is ast.MouseDown:
175 | out = {"opcode": "sensing_mousedown"}
176 |
177 | elif type(expression) is ast.MouseX:
178 | out = {"opcode": "sensing_mousex"}
179 |
180 | elif type(expression) is ast.MouseY:
181 | out = {"opcode": "sensing_mousey"}
182 |
183 | elif type(expression) is ast.CostumeNumber:
184 | out = {
185 | "opcode": "looks_costumenumbername",
186 | "fields": {
187 | "NUMBER_NAME": ["number", None]
188 | },
189 | }
190 |
191 | elif type(expression) is ast.Touching:
192 | out = {
193 | "opcode": "sensing_touchingobject",
194 | "inputs": {
195 | "TOUCHINGOBJECTMENU": sprite.serialise_arg(expression.thing, uid)
196 | },
197 | }
198 |
199 | elif type(expression) is ast.TouchingColour:
200 | out = {
201 | "opcode": "sensing_touchingcolor",
202 | "inputs": {
203 | "COLOR": sprite.serialise_arg(expression.colour, uid, alternative=[9, "#FF0000"])
204 | },
205 | }
206 |
207 | elif type(expression) is ast.core.PenParamMenu:
208 | out = {
209 | "opcode": "pen_menu_colorParam",
210 | "fields": {
211 | "colorParam": [expression.param, None],
212 | }
213 | }
214 |
215 | elif type(expression) is ast.core.TouchingObjectMenu:
216 | out = {
217 | "opcode": "sensing_touchingobjectmenu",
218 | "fields": {
219 | "TOUCHINGOBJECTMENU": [expression.object, None],
220 | }
221 | }
222 |
223 | elif type(expression) is ast.core.Costume:
224 | out = {
225 | "opcode": "looks_costume",
226 | "fields": {
227 | "COSTUME": [expression.costumename, None],
228 | }
229 | }
230 |
231 | elif type(expression) is ast.core.Instrument:
232 | out = {
233 | "opcode": expression.op,
234 | "fields": {
235 | "INSTRUMENT": [expression.instrument, None],
236 | }
237 | }
238 |
239 | elif type(expression) is ast.core.Drum:
240 | out = {
241 | "opcode": expression.op,
242 | "fields": {
243 | "DRUM": [expression.drum, None],
244 | }
245 | }
246 |
247 | elif type(expression) is ast.GetTempo:
248 | out = {
249 | "opcode": "music_getTempo",
250 | }
251 |
252 | elif type(expression) is ast.GetXPos:
253 | out = {
254 | "opcode": "motion_xposition",
255 | }
256 |
257 | elif type(expression) is ast.GetYPos:
258 | out = {
259 | "opcode": "motion_yposition",
260 | }
261 |
262 | elif type(expression) is ast.GetDirection:
263 | out = {
264 | "opcode": "motion_direction",
265 | }
266 |
267 | else:
268 | raise Exception(f"Unable to serialise expression {expression!r}")
269 |
270 | blocks_json[uid].update(out)
271 | return uid
272 |
--------------------------------------------------------------------------------
/examples/assets/beer.b:
--------------------------------------------------------------------------------
1 | ##########################
2 | ###
3 | ### Severely updated version!
4 | ### (now says "1 bottle" and
5 | ### contains no extra "0" verse)
6 | ###
7 | ##########################
8 | ### 99 Bottles of Beer ###
9 | ### coded in Brainf**k ###
10 | ### with explanations ###
11 | ##########################
12 | #
13 | # This Bottles of Beer program
14 | # was written by Andrew Paczkowski
15 | # Coder Alias: thepacz
16 | # three_halves_plus_one@yahoo.com
17 | #####
18 |
19 | > 0 in the zeroth cell
20 | +++++++>++++++++++[<+++++>-] 57 in the first cell or "9"
21 | +++++++>++++++++++[<+++++>-] 57 in second cell or "9"
22 | ++++++++++ 10 in third cell
23 | >+++++++++ 9 in fourth cell
24 |
25 | ##########################################
26 | ### create ASCII chars in higher cells ###
27 | ##########################################
28 |
29 | >>++++++++[<++++>-] " "
30 | >++++++++++++++[<+++++++>-] b
31 | +>+++++++++++[<++++++++++>-] o
32 | ++>+++++++++++++++++++[<++++++>-] t
33 | ++>+++++++++++++++++++[<++++++>-] t
34 | >++++++++++++[<+++++++++>-] l
35 | +>++++++++++[<++++++++++>-] e
36 | +>+++++++++++++++++++[<++++++>-] s
37 | >++++++++[<++++>-] " "
38 | +>+++++++++++[<++++++++++>-] o
39 | ++>++++++++++[<++++++++++>-] f
40 | >++++++++[<++++>-] " "
41 | >++++++++++++++[<+++++++>-] b
42 | +>++++++++++[<++++++++++>-] e
43 | +>++++++++++[<++++++++++>-] e
44 | >+++++++++++++++++++[<++++++>-] r
45 | >++++++++[<++++>-] " "
46 | +>+++++++++++[<++++++++++>-] o
47 | >+++++++++++[<++++++++++>-] n
48 | >++++++++[<++++>-] " "
49 | ++>+++++++++++++++++++[<++++++>-] t
50 | ++++>++++++++++[<++++++++++>-] h
51 | +>++++++++++[<++++++++++>-] e
52 | >++++++++[<++++>-] " "
53 | ++>+++++++++++++[<+++++++++>-] w
54 | +>++++++++++++[<++++++++>-] a
55 | >++++++++++++[<+++++++++>-] l
56 | >++++++++++++[<+++++++++>-] l
57 | >+++++[<++>-] LF
58 | ++>+++++++++++++++++++[<++++++>-] t
59 | +>++++++++++++[<++++++++>-] a
60 | +++>+++++++++++++[<++++++++>-] k
61 | +>++++++++++[<++++++++++>-] e
62 | >++++++++[<++++>-] " "
63 | +>+++++++++++[<++++++++++>-] o
64 | >+++++++++++[<++++++++++>-] n
65 | +>++++++++++[<++++++++++>-] e
66 | >++++++++[<++++>-] " "
67 | >++++++++++[<++++++++++>-] d
68 | +>+++++++++++[<++++++++++>-] o
69 | ++>+++++++++++++[<+++++++++>-] w
70 | >+++++++++++[<++++++++++>-] n
71 | >++++++++[<++++>-] " "
72 | +>++++++++++++[<++++++++>-] a
73 | >+++++++++++[<++++++++++>-] n
74 | >++++++++++[<++++++++++>-] d
75 | >++++++++[<++++>-] " "
76 | ++>+++++++++++[<++++++++++>-] p
77 | +>++++++++++++[<++++++++>-] a
78 | +>+++++++++++++++++++[<++++++>-] s
79 | +>+++++++++++++++++++[<++++++>-] s
80 | >++++++++[<++++>-] " "
81 | +>+++++++++++++[<++++++++>-] i
82 | ++>+++++++++++++++++++[<++++++>-] t
83 | >++++++++[<++++>-] " "
84 | +>++++++++++++[<++++++++>-] a
85 | >+++++++++++++++++++[<++++++>-] r
86 | +>+++++++++++[<++++++++++>-] o
87 | >+++++++++++++[<+++++++++>-] u
88 | >+++++++++++[<++++++++++>-] n
89 | >++++++++++[<++++++++++>-] d
90 | >+++++[<++>-] LF
91 | +++++++++++++ CR
92 |
93 | [<]>>>> go back to fourth cell
94 |
95 | #################################
96 | ### initiate the display loop ###
97 | #################################
98 |
99 | [ loop
100 | < back to cell 3
101 | [ loop
102 | [>]<< go to last cell and back to LF
103 | .. output 2 newlines
104 | [<]> go to first cell
105 |
106 | ###################################
107 | #### begin display of characters###
108 | ###################################
109 | #
110 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
111 | #X X b o t t l e s o f b e e r
112 | #.>.>.>.>.>.>.>.>.>.>.>.
113 | #o n t h e w a l l N
114 | #[<]> go to first cell
115 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.>
116 | #X X b o t t l e s o f b e e r N
117 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
118 | #t a k e o n e d o w n a n d p a s s
119 | #.>.>.>.>.>.>.>.>.>.
120 | #i t a r o u n d N
121 | #####
122 |
123 | [<]>> go to cell 2
124 | - subtract 1 from cell 2
125 | < go to cell 1
126 |
127 | ########################
128 | ### display last line ##
129 | ########################
130 | #
131 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
132 | #X X b o t t l e s o f b e e r
133 | #.>.>.>.>.>.>.>.>.>.>.
134 | #o n t h e w a l l
135 | #####
136 |
137 | [<]>>>- go to cell 3/subtract 1
138 | ] end loop when cell 3 is 0
139 | ++++++++++ add 10 to cell 3
140 | <++++++++++ back to cell 2/add 10
141 | <- back to cell 1/subtract 1
142 | [>]<. go to last line/carriage return
143 | [<]> go to first line
144 |
145 | ########################
146 | ### correct last line ##
147 | ########################
148 | #
149 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
150 | #X X b o t t l e s o f b e e r
151 | #.>.>.>.>.>.>.>.>.>.>.
152 | #o n t h e w a l l
153 | #####
154 |
155 | [<]>>>>- go to cell 4/subtract 1
156 | ] end loop when cell 4 is 0
157 |
158 | ##############################################################
159 | ### By this point verses 99\10 are displayed but to work ###
160 | ### with the lower numbered verses in a more readable way ###
161 | ### we initiate a new loop for verses 9{CODE} that will not ###
162 | ### use the fourth cell at all ###
163 | ##############################################################
164 |
165 | + add 1 to cell four (to keep it non\zero)
166 | <-- back to cell 3/subtract 2
167 |
168 | [ loop
169 | [>]<< go to last cell and back to LF
170 | .. output 2 newlines
171 | [<]> go to first cell
172 |
173 | ###################################
174 | #### begin display of characters###
175 | ###################################
176 | #
177 | #>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
178 | # X b o t t l e s o f b e e r
179 | #.>.>.>.>.>.>.>.>.>.>.>.
180 | #o n t h e w a l l N
181 | #[<]> go to first cell
182 | #>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.>
183 | # X b o t t l e s o f b e e r N
184 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
185 | #t a k e o n e d o w n a n d p a s s
186 | #.>.>.>.>.>.>.>.>.>.
187 | #i t a r o u n d N
188 | #####
189 |
190 | [<]>> go to cell 2
191 | - subtract 1 from cell 2
192 |
193 | ########################
194 | ### display last line ##
195 | ########################
196 | #
197 | #.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
198 | #X b o t t l e s o f b e e r
199 | #.>.>.>.>.>.>.>.>.>.>.
200 | #o n t h e w a l l
201 | #####
202 |
203 | [<]>>>- go to cell 3/subtract 1
204 | ] end loop when cell 3 is 0
205 | + add 1 to cell 3 to keep it non\zero
206 |
207 | [>]<. go to last line/carriage return
208 | [<]> go to first line
209 |
210 | ########################
211 | ### correct last line ##
212 | ########################
213 | #
214 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>.>
215 | # X b o t t l e o f b e e r
216 | #.>.>.>.>.>.>.>.>.>.>.<<<<.
217 | #o n t h e w a l l
218 | #####
219 |
220 | [>]<< go to last cell and back to LF
221 | .. output 2 newlines
222 | [<]> go to first line
223 |
224 | #########################
225 | ### the final verse ##
226 | #########################
227 | #
228 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>.>
229 | # X b o t t l e o f b e e r
230 | #.>.>.>.>.>.>.>.>.>.>.>.
231 | #o n t h e w a l l N
232 | #[<]> go to first cell
233 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.>
234 | # X b o t t l e o f b e e r N
235 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
236 | #t a k e o n e d o w n a n d p a s s
237 | #.>.>.>.>.>.>.>.>.>.
238 | #i t a r o u n d N
239 | #[>]< go to last line
240 | #<<<.<<.<<<.
241 | # n o
242 | #[<]>>>> go to fourth cell
243 | #>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>
244 | # b o t t l e s o f b e e r
245 | #.>.>.>.>.>.>.>.>.>.>.>.
246 | #o n t h e w a l l N
247 | #####fin##
248 |
--------------------------------------------------------------------------------
/boiga/ast.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | from . import ast_core as core
4 | from .ast_core import ensure_expression, Literal, LiteralColour
5 |
6 | # MOTION
7 |
8 | class SetXYPos(core.Statement):
9 | def __init__(self, x, y):
10 | super().__init__("motion_gotoxy",
11 | X=ensure_expression(x),
12 | Y=ensure_expression(y))
13 |
14 | class ChangeXPos(core.Statement):
15 | def __init__(self, x):
16 | super().__init__("motion_changexby",
17 | DX=ensure_expression(x))
18 |
19 | class SetXPos(core.Statement):
20 | def __init__(self, x):
21 | super().__init__("motion_setx",
22 | X=ensure_expression(x))
23 |
24 | class ChangeYPos(core.Statement):
25 | def __init__(self, y):
26 | super().__init__("motion_changeyby",
27 | DY=ensure_expression(y))
28 |
29 | class SetYPos(core.Statement):
30 | def __init__(self, y):
31 | super().__init__("motion_sety",
32 | Y=ensure_expression(y))
33 |
34 | class GetXPos(core.Expression):
35 | def __init__(self):
36 | pass
37 |
38 | class GetYPos(core.Expression):
39 | def __init__(self):
40 | pass
41 |
42 | class GetDirection(core.Expression):
43 | def __init__(self):
44 | pass
45 |
46 | # END MOTION
47 |
48 | # LOOKS
49 |
50 | class SetCostume(core.Statement):
51 | def __init__(self, costume):
52 | if type(costume) is str:
53 | costume = core.Costume(costume)
54 | super().__init__("looks_switchcostumeto",
55 | COSTUME=costume)
56 |
57 | class Say(core.Statement):
58 | def __init__(self, msg):
59 | super().__init__("looks_say",
60 | MESSAGE=ensure_expression(msg))
61 |
62 | class SetEffect(core.Statement):
63 | def __init__(self, effect, value):
64 | super().__init__("looks_seteffectto",
65 | EFFECT=str(effect), # TODO: assert is str?
66 | VALUE=ensure_expression(value))
67 |
68 | class Effects:
69 | Color = "color"
70 | Fisheye = "fisheye"
71 | Whirl = "whirl"
72 | Pixelate = "pixelate"
73 | Mosaic = "mosaic"
74 | Brightness = "brightness"
75 | Ghost = "ghost"
76 |
77 | class ChangeEffect(core.Statement):
78 | def __init__(self, effect, change):
79 | super().__init__("looks_changeeffectby",
80 | EFFECT=str(effect), # TODO: assert is str?
81 | CHANGE=ensure_expression(change))
82 |
83 | class SetSize(core.Statement):
84 | def __init__(self, size):
85 | super().__init__("looks_setsizeto",
86 | SIZE=ensure_expression(size))
87 |
88 | class Show(core.Statement):
89 | def __init__(self):
90 | self.op = "looks_show"
91 |
92 | class Hide(core.Statement):
93 | def __init__(self):
94 | self.op = "looks_hide"
95 |
96 | # END LOOKS
97 |
98 | # BEGIN SOUND
99 |
100 | # END SOUND
101 |
102 | # EVENTS
103 |
104 | class BroadcastAndWait(core.Statement):
105 | def __init__(self, event):
106 | super().__init__("event_broadcastandwait", BROADCAST_INPUT=str(event))
107 |
108 | # END EVENTS
109 |
110 | # BEGIN CONTROL
111 |
112 | class Wait(core.Statement):
113 | def __init__(self, duration):
114 | super().__init__("control_wait", DURATION=ensure_expression(duration))
115 |
116 | Repeat = core.repeatn
117 |
118 | class ForeverHack():
119 | def __getitem__(self, do):
120 | if type(do) != tuple:
121 | do = [do]
122 | return core.Statement("control_forever", SUBSTACK=list(do))
123 | Forever = ForeverHack()
124 |
125 | def If(condition, then=None):
126 | if then is None:
127 | return core.getitem_hack(If, condition)
128 | return core.IfStatement(condition, then)
129 |
130 | RepeatUntil = core.repeatuntil
131 |
132 | def StopAll():
133 | return core.Statement("control_stop", STOP_OPTION="all")
134 |
135 | def StopThisScript():
136 | return core.Statement("control_stop", STOP_OPTION="this script")
137 |
138 | def StopOtherScriptsInSprite():
139 | return core.Statement("control_stop", STOP_OPTION="other scripts in sprite")
140 |
141 | # END CONTROL
142 |
143 | # BEGIN SENSING
144 |
145 | class AskAndWait(core.Statement):
146 | def __init__(self, prompt=""):
147 | self.op = "sensing_askandwait"
148 | self.prompt = ensure_expression(prompt)
149 |
150 | class Answer(core.Expression):
151 | def __init__(self):
152 | pass
153 |
154 | class MouseDown(core.Expression):
155 | type = "bool"
156 | def __init__(self):
157 | pass
158 |
159 | class Touching(core.Expression):
160 | type = "bool"
161 | def __init__(self, object):
162 | self.thing = core.TouchingObjectMenu(object)
163 |
164 | class TouchingColour(core.Expression):
165 | type = "bool"
166 | def __init__(self, colour):
167 | self.colour = colour
168 |
169 | class MouseX(core.Expression):
170 | def __init__(self):
171 | pass
172 |
173 | class MouseY(core.Expression):
174 | def __init__(self):
175 | pass
176 |
177 | class CostumeNumber(core.Expression):
178 | def __init__(self):
179 | pass
180 |
181 | class DaysSince2k(core.Expression):
182 | def __init__(self):
183 | pass
184 |
185 | # END SENSING
186 |
187 | # BEGIN OPERATORS
188 |
189 | def pickrandom(a, b):
190 | return core.BinaryOp("random", a, b)
191 |
192 | # END OPERATORS
193 |
194 | # BEGIN PEN
195 |
196 | class EraseAll(core.Statement):
197 | def __init__(self):
198 | self.op = "pen_clear"
199 |
200 | class Stamp(core.Statement):
201 | def __init__(self):
202 | self.op = "pen_stamp"
203 |
204 | class PenDown(core.Statement):
205 | def __init__(self):
206 | self.op = "pen_penDown"
207 |
208 | class PenUp(core.Statement):
209 | def __init__(self):
210 | self.op = "pen_penUp"
211 |
212 | # correct spellings only!
213 | class SetPenColour(core.Statement):
214 | def __init__(self, colour):
215 | if type(colour) is int and colour < 0x1000000:
216 | colour = LiteralColour(f"#{colour:06x}")
217 | elif type(colour) is not LiteralColour:
218 | colour = ensure_expression(colour)+0 # TODO: only add zero if it's not an expression to begin with
219 | super().__init__("pen_setPenColorToColor", COLOR=colour)
220 |
221 |
222 | class SetPenParam(core.Statement):
223 | def __init__(self, param, value):
224 | super().__init__("pen_setPenColorParamTo", COLOR_PARAM=core.PenParamMenu(param), VALUE=ensure_expression(value))
225 |
226 | def RGBA(r, g, b, a):
227 | return (ensure_expression(r) << 16) + \
228 | (ensure_expression(g) << 8) + \
229 | (ensure_expression(b)) + \
230 | (ensure_expression(a) << 24)
231 |
232 | def RGB(r, g, b):
233 | return RGBA(r, g, b, 0)
234 |
235 | class SetPenSize(core.Statement):
236 | def __init__(self, size):
237 | super().__init__("pen_setPenSizeTo", SIZE=ensure_expression(size))
238 |
239 | # END PEN
240 |
241 | # BEGIN MUSIC
242 |
243 | class Instruments:
244 | Piano = core.Instrument(1)
245 | ElectricPiano = core.Instrument(2)
246 | Organ = core.Instrument(3)
247 | Guitar = core.Instrument(4)
248 | ElectricGuitar = core.Instrument(5)
249 | Bass = core.Instrument(6)
250 | Pizzicato = core.Instrument(7)
251 | Cello = core.Instrument(8)
252 | Trombone = core.Instrument(9)
253 | Clarinet = core.Instrument(10)
254 | Saxophone = core.Instrument(11)
255 | Flute = core.Instrument(12)
256 | WoodenFlute = core.Instrument(13)
257 | Bassoon = core.Instrument(14)
258 | Choir = core.Instrument(15)
259 | Vibraphone = core.Instrument(16)
260 | MusicBox = core.Instrument(17)
261 | SteelDrum = core.Instrument(18)
262 | Marimba = core.Instrument(19)
263 | SynthLead = core.Instrument(20)
264 | SynthPad = core.Instrument(21)
265 |
266 | class Drums:
267 | SnareDrum = core.Drum(1)
268 | BassDrum = core.Drum(2)
269 | SideStick = core.Drum(3)
270 | CrashCymbal = core.Drum(4)
271 | OpenHiHat = core.Drum(5)
272 | ClosedHiHat = core.Drum(6)
273 | Tambourine = core.Drum(7)
274 | HandClap = core.Drum(8)
275 | Claves = core.Drum(9)
276 | WoodBlock = core.Drum(10)
277 | Cowbell = core.Drum(11)
278 | Triangle = core.Drum(12)
279 | Bongo = core.Drum(13)
280 | Conga = core.Drum(14)
281 | Cabasa = core.Drum(15)
282 | Guiro = core.Drum(16)
283 | Vibraslap = core.Drum(17)
284 | Cuica = core.Drum(18)
285 |
286 | class SetInstrument(core.Statement):
287 | def __init__(self, instrument):
288 | if not core.is_expression(instrument):
289 | instrument = ensure_expression(instrument).join("")
290 | super().__init__("music_setInstrument", INSTRUMENT=instrument)
291 |
292 | class SetTempo(core.Statement):
293 | def __init__(self, tempo):
294 | super().__init__("music_setTempo", TEMPO=ensure_expression(tempo))
295 |
296 | class ChangeTempoBy(core.Statement):
297 | def __init__(self, tempo):
298 | super().__init__("music_changeTempo", TEMPO=ensure_expression(tempo))
299 |
300 | class GetTempo(core.Expression):
301 | def __init__(self):
302 | pass
303 |
304 | class PlayNote(core.Statement):
305 | def __init__(self, note, beats):
306 | super().__init__("music_playNoteForBeats", NOTE=ensure_expression(note), BEATS=ensure_expression(beats))
307 |
308 | class PlayDrum(core.Statement):
309 | def __init__(self, drum, beats):
310 | if not core.is_expression(drum):
311 | drum = ensure_expression(drum).join("")
312 | super().__init__("music_playDrumForBeats", DRUM=drum, BEATS=ensure_expression(beats))
313 |
314 | class RestFor(core.Statement):
315 | def __init__(self, beats):
316 | super().__init__("music_restForBeats", BEATS=ensure_expression(beats))
317 |
318 | # END MUSIC
319 |
320 | # misc helpers
321 |
322 | def sumchain(arr):
323 | result = arr[0]
324 | for i in arr[1:]:
325 | result += i
326 | return result
327 |
328 | millis_now = DaysSince2k() * 86400000
329 |
330 | def nop(*args):
331 | return []
332 |
--------------------------------------------------------------------------------
/boiga/statements.py:
--------------------------------------------------------------------------------
1 | from email.policy import default
2 | import json
3 |
4 | from . import ast_core
5 |
6 | def serialise_statement(sprite, statement):
7 | if not issubclass(type(statement), ast_core.Statement):
8 | raise Exception(f"Cannot serialise {statement!r} as a statement")
9 |
10 | blocks_json = sprite.blocks_json
11 |
12 | uid = sprite.gen_uid()
13 | blocks_json[uid] = {
14 | "inputs": {},
15 | "fields": {},
16 | "shadow": False,
17 | "topLevel": False,
18 | }
19 |
20 | # ===== EVENTS =======
21 | if statement.op == "event_whenflagclicked":
22 | out = {
23 | "opcode": "event_whenflagclicked"
24 | }
25 |
26 | elif statement.op == "event_whenbroadcastreceived":
27 | out = {
28 | "opcode": "event_whenbroadcastreceived",
29 | "fields": {
30 | "BROADCAST_OPTION": statement.args["BROADCAST_OPTION"],
31 | }
32 | }
33 |
34 | elif statement.op == "event_whenkeypressed":
35 | out = {
36 | "opcode": "event_whenkeypressed",
37 | "fields": {
38 | "KEY_OPTION": [statement.args["KEY_OPTION"], None],
39 | }
40 | }
41 |
42 | elif statement.op == "event_broadcastandwait":
43 | out = {
44 | "opcode": "event_broadcastandwait",
45 | "inputs": {
46 | "BROADCAST_INPUT": [1, [11, statement.args["BROADCAST_INPUT"], sprite.project.broadcasts[statement.args["BROADCAST_INPUT"]]]],
47 | }
48 | }
49 |
50 | # ===== CONTROL =======
51 | elif statement.op == "control_repeat":
52 | out = {
53 | "opcode": "control_repeat",
54 | "inputs": {
55 | "TIMES": sprite.serialise_arg(statement.args["TIMES"], uid),
56 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid)
57 | }
58 | }
59 | elif statement.op == "control_repeat_until":
60 | out = {
61 | "opcode": "control_repeat_until",
62 | "inputs": {
63 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid),
64 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid)
65 | }
66 | }
67 | elif statement.op == "control_forever":
68 | out = {
69 | "opcode": "control_forever",
70 | "inputs": {
71 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid)
72 | }
73 | }
74 | elif statement.op == "control_if":
75 | out = {
76 | "opcode": "control_if",
77 | "inputs": {
78 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid),
79 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid)
80 | }
81 | }
82 | elif statement.op == "control_if_else":
83 | out = {
84 | "opcode": "control_if_else",
85 | "inputs": {
86 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid),
87 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid),
88 | "SUBSTACK2": sprite.serialise_script(statement.args["SUBSTACK2"], uid)
89 | }
90 | }
91 | elif statement.op == "control_wait":
92 | out = {
93 | "opcode": "control_wait",
94 | "inputs": {
95 | "DURATION": sprite.serialise_arg(statement.args["DURATION"], uid)
96 | }
97 | }
98 | elif statement.op == "control_stop":
99 | out = {
100 | "opcode": "control_stop",
101 | "fields": {
102 | "STOP_OPTION": [statement.args["STOP_OPTION"], None]
103 | },
104 | "mutation": {
105 | "tagName": "mutation",
106 | "children": [],
107 | "hasnext": "true" if statement.args["STOP_OPTION"] == "other scripts in sprite" else "false"
108 | },
109 | }
110 |
111 | # ===== DATA =======
112 | elif statement.op == "data_setvariableto":
113 | out = {
114 | "opcode": "data_setvariableto",
115 | "inputs": {
116 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid)
117 | },
118 | "fields": {
119 | "VARIABLE": [
120 | statement.args["VARIABLE"].name,
121 | statement.args["VARIABLE"].uid
122 | ]
123 | }
124 | }
125 | elif statement.op == "data_changevariableby":
126 | out = {
127 | "opcode": "data_changevariableby",
128 | "inputs": {
129 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid)
130 | },
131 | "fields": {
132 | "VARIABLE": [
133 | statement.args["VARIABLE"].name,
134 | statement.args["VARIABLE"].uid
135 | ]
136 | }
137 | }
138 | elif statement.op == "data_replaceitemoflist":
139 | out = {
140 | "opcode": "data_replaceitemoflist",
141 | "inputs": {
142 | "INDEX": sprite.serialise_arg(statement.args["INDEX"], uid),
143 | "ITEM": sprite.serialise_arg(statement.args["ITEM"], uid)
144 | },
145 | "fields": {
146 | "LIST": [
147 | statement.args["LIST"].name,
148 | statement.args["LIST"].uid
149 | ]
150 | }
151 | }
152 | elif statement.op == "data_addtolist":
153 | out = {
154 | "opcode": "data_addtolist",
155 | "inputs": {
156 | "ITEM": sprite.serialise_arg(statement.args["ITEM"], uid)
157 | },
158 | "fields": {
159 | "LIST": [
160 | statement.args["LIST"].name,
161 | statement.args["LIST"].uid
162 | ]
163 | }
164 | }
165 | elif statement.op == "data_deletealloflist":
166 | out = {
167 | "opcode": "data_deletealloflist",
168 | "fields": {
169 | "LIST": [
170 | statement.args["LIST"].name,
171 | statement.args["LIST"].uid
172 | ]
173 | }
174 | }
175 | elif statement.op == "data_deleteoflist":
176 | out = {
177 | "opcode": "data_deleteoflist",
178 | "inputs": {
179 | "INDEX": sprite.serialise_arg(statement.args["INDEX"], uid)
180 | },
181 | "fields": {
182 | "LIST": [
183 | statement.args["LIST"].name,
184 | statement.args["LIST"].uid
185 | ]
186 | }
187 | }
188 |
189 | # ======= custom blocks =======
190 |
191 | elif statement.op == "procedures_definition":
192 | out = {
193 | "opcode": "procedures_definition",
194 | "inputs": {
195 | "custom_block": sprite.serialise_procproto(statement.proto, uid)
196 | }
197 | }
198 |
199 | elif statement.op == "procedures_call":
200 | inputs = {}
201 | for arg, var in zip(statement.args["ARGS"], statement.proc.vars):
202 | inputs[var.uid2] = sprite.serialise_arg(arg, uid)
203 | out = {
204 | "opcode": "procedures_call",
205 | "inputs": inputs,
206 | "mutation": {
207 | "tagName": "mutation",
208 | "children": [],
209 | "proccode": statement.proc.proccode,
210 | "argumentids": json.dumps(list(inputs.keys())),
211 | "warp": "true" if statement.proc.turbo else "false"
212 | }
213 | }
214 |
215 | elif statement.op == "sensing_askandwait":
216 | out = {
217 | "opcode": "sensing_askandwait",
218 | "inputs": {
219 | "QUESTION": sprite.serialise_arg(statement.prompt, uid)
220 | }
221 | }
222 |
223 | # ======= motion =======
224 |
225 | elif statement.op == "motion_gotoxy":
226 | out = {
227 | "opcode": statement.op,
228 | "inputs": {
229 | "X": sprite.serialise_arg(statement.args["X"], uid),
230 | "Y": sprite.serialise_arg(statement.args["Y"], uid)
231 | }
232 | }
233 |
234 | elif statement.op == "motion_changexby":
235 | out = {
236 | "opcode": statement.op,
237 | "inputs": {
238 | "DX": sprite.serialise_arg(statement.args["DX"], uid)
239 | }
240 | }
241 |
242 | elif statement.op == "motion_setx":
243 | out = {
244 | "opcode": statement.op,
245 | "inputs": {
246 | "X": sprite.serialise_arg(statement.args["X"], uid)
247 | }
248 | }
249 |
250 | elif statement.op == "motion_changeyby":
251 | out = {
252 | "opcode": statement.op,
253 | "inputs": {
254 | "DY": sprite.serialise_arg(statement.args["DY"], uid)
255 | }
256 | }
257 |
258 | elif statement.op == "motion_sety":
259 | out = {
260 | "opcode": statement.op,
261 | "inputs": {
262 | "Y": sprite.serialise_arg(statement.args["Y"], uid)
263 | }
264 | }
265 |
266 | # ======= looks =======
267 |
268 | elif statement.op in ["looks_show", "looks_hide"]:
269 | out = {"opcode": statement.op}
270 |
271 | elif statement.op == "looks_switchcostumeto":
272 | out = {
273 | "opcode": statement.op,
274 | "inputs": { # TODO: insert correct sub-block
275 | "COSTUME": sprite.serialise_arg(statement.args["COSTUME"], uid, alternative=sprite.serialise_expression(ast_core.Costume(next(iter(sprite.costumes.keys()))), uid, shadow=True))
276 | }
277 | }
278 |
279 | elif statement.op == "looks_say":
280 | out = {
281 | "opcode": statement.op,
282 | "inputs": {
283 | "MESSAGE": sprite.serialise_arg(statement.args["MESSAGE"], uid)
284 | }
285 | }
286 |
287 | elif statement.op == "looks_seteffectto":
288 | out = {
289 | "opcode": statement.op,
290 | "fields": {
291 | "EFFECT": [statement.args["EFFECT"], None],
292 | },
293 | "inputs": { # TODO: insert correct sub-block
294 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid)
295 | }
296 | }
297 |
298 | elif statement.op == "looks_changeeffectby":
299 | out = {
300 | "opcode": statement.op,
301 | "fields": {
302 | "EFFECT": [statement.args["EFFECT"], None],
303 | },
304 | "inputs": { # TODO: insert correct sub-block
305 | "CHANGE": sprite.serialise_arg(statement.args["CHANGE"], uid)
306 | }
307 | }
308 |
309 | elif statement.op == "looks_setsizeto":
310 | out = {
311 | "opcode": statement.op,
312 | "inputs": {
313 | "SIZE": sprite.serialise_arg(statement.args["SIZE"], uid)
314 | }
315 | }
316 |
317 | # ======= pen =======
318 |
319 | elif statement.op in ["pen_clear", "pen_stamp", "pen_penDown", "pen_penUp"]:
320 | out = {"opcode": statement.op}
321 |
322 | elif statement.op == "pen_setPenColorToColor":
323 | out = {
324 | "opcode": "pen_setPenColorToColor",
325 | "inputs": {
326 | "COLOR": sprite.serialise_arg(statement.args["COLOR"], uid, alternative=[9, "#FF0000"])
327 | }
328 | }
329 |
330 | elif statement.op == "pen_setPenSizeTo":
331 | out = {
332 | "opcode": "pen_setPenSizeTo",
333 | "inputs": {
334 | "SIZE": sprite.serialise_arg(statement.args["SIZE"], uid)
335 | }
336 | }
337 |
338 | elif statement.op == "pen_setPenColorParamTo":
339 | out = {
340 | "opcode": "pen_setPenColorParamTo",
341 | "inputs": {
342 | "COLOR_PARAM": sprite.serialise_arg(statement.args["COLOR_PARAM"], uid),
343 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid)
344 | }
345 | }
346 |
347 | #elif statement.op == "pen_menu_colorParam":
348 | # out = {
349 | # "opcode": "pen_menu_colorParam",
350 | # "fields": {
351 | # "colorParam": [statement.args["colorParam"], None],
352 | # }
353 | # }
354 |
355 | elif statement.op == "music_setInstrument":
356 | out = {
357 | "opcode": "music_setInstrument",
358 | "inputs": {
359 | "INSTRUMENT": sprite.serialise_arg(statement.args["INSTRUMENT"], uid, alternative=sprite.serialise_expression(ast_core.Instrument(1), uid, shadow=True)),
360 | }
361 | }
362 |
363 | elif statement.op == "music_setTempo":
364 | out = {
365 | "opcode": "music_setTempo",
366 | "inputs": {
367 | "TEMPO": sprite.serialise_arg(statement.args["TEMPO"], uid),
368 | }
369 | }
370 |
371 | elif statement.op == "music_changeTempo":
372 | out = {
373 | "opcode": "music_changeTempo",
374 | "inputs": {
375 | "TEMPO": sprite.serialise_arg(statement.args["TEMPO"], uid),
376 | }
377 | }
378 |
379 | elif statement.op == "music_restForBeats":
380 | out = {
381 | "opcode": "music_restForBeats",
382 | "inputs": {
383 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid),
384 | }
385 | }
386 |
387 | elif statement.op == "music_playNoteForBeats":
388 | out = {
389 | "opcode": "music_playNoteForBeats",
390 | "inputs": {
391 | "NOTE": sprite.serialise_arg(statement.args["NOTE"], uid),
392 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid),
393 | }
394 | }
395 |
396 | elif statement.op == "music_playDrumForBeats":
397 | out = {
398 | "opcode": "music_playDrumForBeats",
399 | "inputs": {
400 | "DRUM": sprite.serialise_arg(statement.args["DRUM"], uid, alternative=sprite.serialise_expression(ast_core.Instrument(1), uid, shadow=True)),
401 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid),
402 | }
403 | }
404 |
405 | else:
406 | raise Exception(f"I don't know how to serialise this op: {statement.op!r}")
407 |
408 | blocks_json[uid].update(out)
409 | return uid
410 |
--------------------------------------------------------------------------------
/boiga/codegen.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import io
3 |
4 | from .utils import flatten, gen_uid, BLANK_SVG
5 | from zipfile import ZipFile
6 | import json
7 | import sys
8 | import subprocess
9 | import os
10 |
11 | from . import ast
12 | from . import ast_core
13 | from .expressions import serialise_expression
14 | from .statements import serialise_statement
15 |
16 | class Project():
17 | def __init__(self):
18 | self.asset_data = {} # maps file name (md5.ext) to file contents
19 | self.sprites = []
20 | self.monitors = []
21 | self.broadcasts = {} # maps broadcast names to uids
22 | self.stage = self.new_sprite("Stage", is_stage=True)
23 |
24 | def new_sprite(self, name, is_stage=False):
25 | sprite = Sprite(self, name, is_stage=is_stage)
26 | self.sprites.append(sprite)
27 | return sprite
28 |
29 | def save(self, filename, stealthy=False, execute=False, capture=False):
30 | self.asset_data = {}
31 | self.used_layers = set() # used during serialisation
32 |
33 | print(f"[*] Creating sb3 project file: {filename!r}")
34 |
35 | zip_buf = io.BytesIO()
36 | with ZipFile(zip_buf, "w") as zf:
37 | project = {
38 | "targets": [s.serialise(self.used_layers) for s in self.sprites],
39 | "monitors": self.monitors,
40 | "extensions": ["pen", "music"],
41 | "meta": {
42 | "semver": "3.0.0",
43 | "vm": "0.2.0-prerelease.20210706190652",
44 | # plausible useragent string (dunno what the best long-term value is...)
45 | "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36"
46 | } if stealthy else {
47 | "semver": "3.0.0",
48 | "vm": "0.0.1-com.github.davidbuchanan314.boiga",
49 | "agent": "Python " + sys.version.replace("\n", "")
50 | }
51 | }
52 |
53 | block_count = 0
54 | for serialised, sprite in zip(project["targets"], self.sprites):
55 | block_count += len(serialised["blocks"]) + sprite.block_count
56 | print(f"[*] Serialised {block_count} blocks")
57 |
58 | # TODO: put this behind a debug flag
59 | debug_json = json.dumps(project, indent=4)
60 | #print(debug_json)
61 | open("DEBUG.json", "w").write(debug_json + "\n")
62 |
63 | with zf.open("project.json", "w") as projfile:
64 | json_blob = json.dumps(project).encode()
65 | print(f"[*] project.json size: {len(json_blob)}")
66 | projfile.write(json_blob)
67 |
68 | for asset_name, data in self.asset_data.items():
69 | with zf.open(asset_name, "w") as f:
70 | f.write(data)
71 |
72 | # we buffer the zip in memory, and write it to disk in a single write,
73 | # to avoid tripping up programs like inotify/fswatch
74 | # TODO: do this for .sprite3 exports too
75 | zip_bytes = zip_buf.getvalue()
76 | with open(filename, "wb") as zf:
77 | zf.write(zip_bytes)
78 |
79 | print(f"[*] Done writing {filename!r} ({len(zip_bytes)} bytes)")
80 |
81 | if execute:
82 | return subprocess.run([os.path.dirname(__file__) + "/../tools/run_scratch.js", filename], check=True, capture_output=capture)
83 |
84 |
85 | class Sprite():
86 | def __init__(self, project, name, is_stage=False):
87 | self.project = project
88 | self.name = name
89 | self.is_stage = is_stage
90 | self.variable_uids = {} # name to uid
91 | self.variable_values = {} # uid to value
92 | self.list_uids = {} # name to uid
93 | self.list_values = {} # uid to value
94 | self.scripts = []
95 | self.costumes = {} # indexed by name
96 |
97 | self.current_costume = 0 # some way to adjust this?
98 | self.volume = 100
99 |
100 | def new_var(self, name, value="", monitor=None):
101 | if not type(name) is str:
102 | raise Exception("Variable name must be a string")
103 |
104 | if name in self.variable_uids:
105 | uid = self.variable_uids[name]
106 | else:
107 | uid = self.gen_uid(["var", name])
108 |
109 | self.variable_uids[name] = uid
110 | self.variable_values[uid] = value
111 |
112 | if monitor:
113 | self.project.monitors.append({
114 | "id": uid,
115 | "mode": "default",
116 | "opcode": "data_variable",
117 | "params": {
118 | "VARIABLE": name
119 | },
120 | "spriteName": None if self.name == "Stage" else self.name,
121 | "value": [],
122 | "width": 0,
123 | "height": 0,
124 | "x": monitor[0],
125 | "y": monitor[1],
126 | "visible": True,
127 | "sliderMin": 0,
128 | "sliderMax": 100,
129 | "isDiscrete": True
130 | })
131 |
132 | return ast_core.Var(self, name, uid)
133 |
134 | def new_list(self, name, value=[], monitor=None):
135 | if not type(name) is str:
136 | raise Exception("List name must be a string")
137 |
138 | uid = self.list_uids.get(name, self.gen_uid(["list", name]))
139 | self.list_uids[name] = uid
140 | self.list_values[uid] = value
141 |
142 | if monitor:
143 | self.project.monitors.append({
144 | "id": uid,
145 | "mode": "list",
146 | "opcode": "data_listcontents",
147 | "params": {
148 | "LIST": name
149 | },
150 | "spriteName": None if self.name == "Stage" else self.name,
151 | "value": [],
152 | "width": monitor[2],
153 | "height": monitor[3],
154 | "x": monitor[0],
155 | "y": monitor[1],
156 | "visible": True
157 | })
158 |
159 | return ast_core.List(self, name, uid)
160 |
161 | def add_script(self, stack):
162 | self.scripts.append(stack)
163 |
164 | def add_costume(self, name, data_or_path, extension=None, center=(0, 0)):
165 | if type(data_or_path) is str:
166 | path = data_or_path
167 | data = open(path, "rb").read()
168 | if extension is None:
169 | extension = path.split(".")[-1]
170 | else:
171 | data = data_or_path
172 | if not extension:
173 | raise Exception("Unspecified file extension")
174 |
175 | self.costumes[name] = (data, extension, center)
176 |
177 | def on_flag(self, stack):
178 | self.add_script(ast_core.on_flag(stack))
179 |
180 | def on_receive(self, event, stack):
181 | if event not in self.project.broadcasts:
182 | self.project.broadcasts[event] = gen_uid(["broadcast", event])
183 |
184 | self.add_script(ast_core.on_receive(event, self.project.broadcasts[event], stack))
185 |
186 | def on_press(self, key, stack):
187 | self.add_script(ast_core.on_press(key, stack))
188 |
189 | def proc_def(self, fmt=None, generator=None, locals_prefix=None, inline_only=False, turbo=True):
190 | if generator is None: # function decorator hackery
191 | return lambda generator: self.proc_def(fmt, generator, locals_prefix, inline_only, turbo)
192 |
193 | if fmt is None:
194 | arg_names = generator.__code__.co_varnames[:generator.__code__.co_argcount]
195 | fmt = generator.__name__
196 | for arg in arg_names[1:]: # skip locals
197 | if generator.__annotations__.get(arg) is bool:
198 | fmt += f" <{arg}>"
199 | else:
200 | fmt += f" [{arg}]"
201 |
202 | uid = gen_uid(["procproto", fmt])
203 | proc_proto = ast_core.ProcProto(self, fmt, uid, locals_prefix, turbo)
204 |
205 | for varname, vartype in zip(proc_proto.argnames, proc_proto.argtypes):
206 | varinit = ast_core.ProcVarBool if vartype == "bool" else ast_core.ProcVar
207 | proc_proto.vars.append(
208 | varinit(
209 | self, proc_proto, varname,
210 | gen_uid(["procvar1", fmt, varname]),
211 | gen_uid(["procvar2", fmt, varname])
212 | )
213 | )
214 |
215 | procdef = ast_core.ProcDef(proc_proto, generator)
216 |
217 | if not inline_only:
218 | self.add_script([procdef] + generator(procdef, *proc_proto.vars))
219 |
220 | return procdef
221 |
222 | def save(self, filename):
223 | self.project.asset_data = {} # XXX these are both written to by sprite.serialize() - they should probably be returned by it instead
224 |
225 | print(f"[*] Creating sprite3 file: {filename!r}")
226 | with ZipFile(filename, "w") as zf:
227 | with zf.open("sprite.json", "w") as spritefile:
228 | serialised = self.serialise()
229 | print(f"[*] Serialised {len(serialised['blocks']) + self.block_count} blocks")
230 | json_blob = json.dumps(serialised).encode()
231 | print(f"[*] sprite.json size: {len(json_blob)}")
232 | spritefile.write(json_blob)
233 |
234 |
235 | for asset_name, data in self.project.asset_data.items():
236 | with zf.open(asset_name, "w") as f:
237 | f.write(data)
238 |
239 | print(f"[*] Done writing {filename!r} ({os.path.getsize(filename)} bytes)")
240 |
241 | def serialise(self, used_layers=None):
242 | self.block_count = 0
243 | self.blocks_json = {}
244 | self.hat_count = 0
245 | self.uid_ctr = 0
246 |
247 | if not self.costumes:
248 | self.add_costume("costume", BLANK_SVG, "svg")
249 |
250 | sprite = {
251 | "isStage": self.is_stage,
252 | "name": self.name,
253 | "variables": {
254 | uid: [name, self.variable_values[uid]]
255 | for name, uid
256 | in self.variable_uids.items()
257 | },
258 | "lists": {
259 | uid: [name, self.list_values[uid]]
260 | for name, uid
261 | in self.list_uids.items()
262 | },
263 | "broadcasts": {},
264 | "blocks": self.blocks_json,
265 | "comments": {},
266 | "currentCostume": self.current_costume,
267 | "costumes": [],
268 | "sounds": [],
269 | "volume": self.volume
270 | }
271 |
272 | # we don't care about this if we're serialising for a .sprite3
273 | if used_layers is not None:
274 | # find the next unused layer
275 | for i in range(99999999):
276 | if i not in used_layers:
277 | sprite["layerOrder"] = i
278 | used_layers.add(i)
279 | break
280 | else:
281 | raise Exception("Too many layers?!??!")
282 |
283 | if self.is_stage:
284 | sprite.update({
285 | "tempo": 60,
286 | "videoTransparency": 50,
287 | "videoState": "on",
288 | "textToSpeechLanguage": None,
289 | })
290 | else:
291 | sprite.update({
292 | "visible": True,
293 | "x": 0,
294 | "y": 0,
295 | "size": 100,
296 | "direction": 90,
297 | "draggable": False,
298 | "rotationStyle": "all around"
299 | })
300 |
301 | for costume_name, (data, extension, center) in self.costumes.items():
302 | md5 = hashlib.md5(data).hexdigest()
303 | md5ext = f"{md5}.{extension}"
304 | sprite["costumes"].append({
305 | "assetId": md5,
306 | "name": costume_name,
307 | "md5ext": md5ext,
308 | "dataFormat": extension,
309 | "rotationCenterX": center[0],
310 | "rotationCenterY": center[1]
311 | })
312 | self.project.asset_data[md5ext] = data
313 |
314 | for script in self.scripts:
315 | self.serialise_script(script)
316 |
317 | return sprite
318 |
319 | def serialise_script(self, script, parent=None):
320 | top_uid = None
321 | script = flatten(script)
322 | for statement in script:
323 | uid = self.serialise_statement(statement)
324 | top_uid = top_uid or uid
325 | self.blocks_json[uid]["next"] = None
326 | if parent:
327 | self.blocks_json[uid]["parent"] = parent
328 | self.blocks_json[parent]["next"] = uid
329 | else:
330 | self.blocks_json[uid].update({
331 | "parent": None,
332 | "topLevel": True,
333 | "x": self.hat_count * 500, # naively arrange in columns
334 | "y": 0
335 | })
336 | self.hat_count += 1
337 | parent = uid
338 |
339 | return [2, top_uid]
340 |
341 | def serialise_arg(self, expression, parent, alternative=[10, ""]):
342 | #expression = expression.simplified() # experimental!
343 |
344 | # primitive expressions https://github.com/LLK/scratch-vm/blob/80e25f7b2a47ec2f3d8bb05fb62c7ceb8a1c99f0/src/serialization/sb3.js#L63
345 | if type(expression) is ast_core.Literal:
346 | return [1, [10 if type(expression.value) is str else 4, str(expression.value)]]
347 | if type(expression) is ast_core.LiteralColour:
348 | return [1, [9, expression.value]]
349 | if type(expression) is ast_core.Var:
350 | self.block_count += 1
351 | return [3, [12, expression.name, expression.uid], alternative]
352 | if type(expression) is ast_core.List:
353 | self.block_count += 1
354 | return [3, [13, expression.name, expression.uid], alternative]
355 | if issubclass(type(expression), ast_core.MenuExpression):
356 | #print("MenuExpression detected!")
357 | # todo: how does this affect block count?
358 | return [1, self.serialise_expression(expression, parent, shadow=True)]
359 |
360 | # compound expressions
361 | return [3, self.serialise_expression(expression, parent), alternative]
362 |
363 | def serialise_bool(self, expression, parent):
364 | if expression.type != "bool":
365 | raise Exception("Cannot serialise non-bool expression as bool: " + repr(expression))
366 | return [2, self.serialise_expression(expression, parent)]
367 |
368 | def serialise_procproto(self, proto, parent):
369 | inputs = {}
370 | self.block_count -= 1
371 |
372 | for var in proto.vars:
373 | self.block_count -= 1
374 | inputs[var.uid2] = [1, var.uid]
375 | self.serialise_expression(var, proto.uid, shadow=True)
376 |
377 | self.blocks_json[proto.uid] = {
378 | "opcode": "procedures_prototype",
379 | "next": None,
380 | "parent": parent,
381 | "inputs": inputs,
382 | "fields": {},
383 | "shadow": True,
384 | "topLevel": False,
385 | "mutation": {
386 | "tagName": "mutation",
387 | "children": [],
388 | "proccode": proto.proccode,
389 | "argumentids": json.dumps(list(inputs.keys())),
390 | "argumentnames": json.dumps(proto.argnames),
391 | "argumentdefaults": json.dumps(["false" if x == "bool" else "" for x in proto.argtypes]),
392 | "warp": "true" if proto.turbo else "false"
393 | }
394 | }
395 | return [1, proto.uid]
396 |
397 | def serialise_expression(self, expression, parent, shadow=False):
398 | return serialise_expression(self, expression, parent, shadow)
399 |
400 | def serialise_statement(self, statement):
401 | return serialise_statement(self, statement)
402 |
403 | def gen_uid(self, seed=None):
404 | if seed is None:
405 | seed = [self.uid_ctr]
406 | self.uid_ctr += 1
407 | seed = [self.name] + seed
408 | return gen_uid(seed)
409 |
--------------------------------------------------------------------------------
/boiga/ast_core.py:
--------------------------------------------------------------------------------
1 | import math
2 | import operator
3 |
4 |
5 | def is_expression(value):
6 | return issubclass(type(value), Expression)
7 |
8 |
9 | def ensure_expression(value):
10 | if is_expression(value):
11 | return value
12 | if type(value) in [str, int, float]:
13 | return Literal(value)
14 | if type(value) is ProcCall:
15 | raise Exception("Scratch procedure calls cannot be used as expressions!")
16 | raise Exception(f"Can't interpret {value!r} as Expression")
17 |
18 |
19 | class Expression():
20 | type = "generic"
21 |
22 | def __init__(self):
23 | raise Exception("Expression can't be instantiated directly (Maybe you want a Literal?)")
24 |
25 | def __add__(self, other):
26 | return BinaryOp("+", self, other)
27 |
28 | def __radd__(self, other):
29 | return BinaryOp("+", other, self)
30 |
31 | def __sub__(self, other):
32 | return BinaryOp("-", self, other)
33 |
34 | def __rsub__(self, other):
35 | return BinaryOp("-", other, self)
36 |
37 | def __neg__(self):
38 | return Literal(0) - self
39 |
40 | def __mul__(self, other):
41 | return BinaryOp("*", self, other)
42 |
43 | def __rmul__(self, other):
44 | return BinaryOp("*", other, self)
45 |
46 | def __mod__(self, other):
47 | return BinaryOp("%", self, other)
48 |
49 | def __rmod__(self, other):
50 | return BinaryOp("%", other, self)
51 |
52 | def __truediv__(self, other):
53 | return BinaryOp("/", self, other)
54 |
55 | def __rtruediv__(self, other):
56 | return BinaryOp("/", other, self)
57 |
58 | def __floordiv__(self, other):
59 | return (self / other).__floor__()
60 |
61 | def __rfloordiv__(self, other):
62 | return (other / self).__floor__()
63 |
64 | def __eq__(self, other):
65 | return BinaryOp("==", self, other)
66 |
67 | def __ne__(self, other):
68 | return BinaryOp("==", self, other).NOT()
69 |
70 | def __gt__(self, other):
71 | return BinaryOp(">", self, other)
72 |
73 | def __lt__(self, other):
74 | return BinaryOp("<", self, other)
75 |
76 | def __floor__(self):
77 | return UnaryOp("floor", self)
78 |
79 | def __ceil__(self):
80 | return UnaryOp("ceiling", self)
81 |
82 | def __round__(self):
83 | return UnaryOp("round", self)
84 |
85 | def round(self):
86 | return self.__round__()
87 |
88 | def __abs__(self):
89 | return UnaryOp("abs", self)
90 |
91 | def sqrt(self):
92 | return UnaryOp("sqrt", self)
93 |
94 | def sin(self):
95 | return UnaryOp("sin", self)
96 |
97 | def cos(self):
98 | return UnaryOp("cos", self)
99 |
100 | def atan(self):
101 | return UnaryOp("atan", self)
102 |
103 | def log(self):
104 | return UnaryOp("ln", self)
105 |
106 | def log10(self):
107 | return UnaryOp("log", self)
108 |
109 | def __pow__(self, other):
110 | return UnaryOp("e ^", (self.log() * other))
111 |
112 | #def __rpow__(self, other):
113 | # TODO
114 | # return UnaryOp("e ^", (other.log() * self))
115 |
116 | def root(self, other):
117 | return UnaryOp("e ^", (self.log() / other))
118 |
119 | def __rshift__(self, other):
120 | if type(other) is int:
121 | return self // (1 << other)
122 | return self // round(Literal(2) ** other) # exponentiation relies on log, so results need rounding
123 |
124 | def __lshift__(self, other):
125 | if type(other) is int:
126 | return self * (1 << other)
127 | return self * round(Literal(2) ** other) # exponentiation relies on log, so results need rounding
128 |
129 | def __and__(self, other):
130 | if not type(other) is int:
131 | raise Exception("Can only AND with constant (for now)")
132 | is_low_mask = (other & (other + 1) == 0)
133 | if not is_low_mask:
134 | raise Exception("AND can only be used to mask off low bits, for now")
135 | return self % (other + 1)
136 |
137 | def __rand__(self, other):
138 | return self.__and__(other)
139 |
140 | def __getitem__(self, other):
141 | return BinaryOp("[]", ensure_expression(other+1).simplified(), self)
142 |
143 | def item(self, other):
144 | return BinaryOp("[]", ensure_expression(other), self)
145 |
146 | def len(self):
147 | return UnaryOp("len", self)
148 |
149 | def join(self, other):
150 | return BinaryOp("join", self, other)
151 |
152 | def simplified(self):
153 | return self
154 |
155 | def OR(self, other):
156 | return BinaryOp("||", self, other)
157 |
158 | def AND(self, other):
159 | return BinaryOp("&&", self, other)
160 |
161 | def NOT(self):
162 | return UnaryOp("!", self)
163 |
164 |
165 | class Literal(Expression):
166 | def __init__(self, value):
167 | assert(type(value) in [str, int, float])
168 | self.value = value
169 |
170 | def __repr__(self):
171 | #return f"Literal({self.value!r})"
172 | return repr(self.value)
173 |
174 |
175 | class LiteralColour(Expression):
176 | def __init__(self, value):
177 | self.value = value
178 |
179 | def __repr__(self):
180 | return f"LiteralColour({self.value!r})"
181 |
182 |
183 | class BinaryOp(Expression):
184 | def __init__(self, op, lval, rval):
185 | self.op = op
186 | self.lval = ensure_expression(lval)
187 | self.rval = ensure_expression(rval)
188 |
189 | if op in [">", "<", "==", "&&", "||"]:
190 | self.type = "bool"
191 |
192 | def __repr__(self):
193 | return f"BinaryOpExpression({self.lval!r} {self.op} {self.rval!r})"
194 |
195 | def simplified(self):
196 | # todo: don't modify in-place...
197 | simpler = BinaryOp(self.op, self.lval.simplified(), self.rval.simplified())
198 | #print(simpler)
199 |
200 | #print("simplifying:", self)
201 | match simpler:
202 | case BinaryOp(
203 | lval=Literal(value=int()|float()),
204 | op="+"|"-"|"%"|"*"|"/",
205 | rval=Literal(value=int()|float())
206 | ):
207 | opmap = {
208 | "+": operator.add,
209 | "-": operator.sub,
210 | "%": operator.mod,
211 | "*": operator.mul,
212 | "/": operator.truediv,
213 | }
214 |
215 | # convert the inputs to floats, to emulate scratch's arithmetic
216 | value = opmap[simpler.op](float(simpler.lval.value), float(simpler.rval.value))
217 | if value == math.floor(value):
218 | value = int(value) # if we can, convert back to int, to avoid redundant ".0"s in the output
219 | return Literal(value)
220 |
221 | # foo * 1 => foo
222 | case BinaryOp(op=("*"|"/"), rval=Literal(value=1)):
223 | return simpler.lval
224 |
225 | # 1 * foo => foo
226 | case BinaryOp(lval=Literal(value=1), op="*"):
227 | return simpler.rval
228 |
229 | # foo + 0 => foo
230 | case BinaryOp(op=("+"|"-"), rval=Literal(value=0)):
231 | return simpler.lval
232 |
233 | # 0 + foo => foo
234 | case BinaryOp(lval=Literal(value=0), op="+"):
235 | return simpler.rval
236 |
237 | # ((foo + a) + b) => (foo + (a+b))
238 | case BinaryOp(
239 | lval=BinaryOp(
240 | lval=subexpr,
241 | op=("+"|"-"),
242 | rval=Literal()
243 | ),
244 | op=("+"|"-"),
245 | rval=Literal()
246 | ):
247 | a = simpler.rval.value if simpler.op == "+" else -simpler.rval.value
248 | b = simpler.lval.rval.value if simpler.lval.op == "+" else -simpler.lval.rval.value
249 | val = a + b
250 | if val == 0:
251 | return subexpr
252 | return BinaryOp("+", subexpr, val)
253 |
254 | return simpler
255 |
256 |
257 | class UnaryOp(Expression):
258 | def __init__(self, op, value):
259 | self.op = op
260 | self.value = ensure_expression(value)
261 |
262 | if op == "!":
263 | self.type = "bool"
264 |
265 | #def simplified(self):
266 | # return UnaryOp(self.op, self.value.simplified())
267 |
268 | def __repr__(self):
269 | return f"UnaryOpExpression({self.op}({self.value!r}))"
270 |
271 |
272 | class Var(Expression):
273 | def __init__(self, sprite, name, uid):
274 | self.sprite = sprite
275 | self.name = name
276 | self.uid = uid
277 |
278 | def __le__(self, other):
279 | other = ensure_expression(other)
280 |
281 | # If other is (self+x) or (x+self), optimise to "change by"
282 | if type(other) is BinaryOp and other.op == "+":
283 | #print(repr(other))
284 | if other.lval is self:
285 | return Statement("data_changevariableby", VARIABLE=self, VALUE=other.rval)
286 | elif other.rval is self:
287 | return Statement("data_changevariableby", VARIABLE=self, VALUE=other.lval)
288 |
289 | return Statement("data_setvariableto", VARIABLE=self, VALUE=other)
290 |
291 | def __getitem__(self, _slice):
292 | if type(_slice) is not slice:
293 | #raise Exception("You can't index a non-list variable")
294 | return super().__getitem__(_slice)
295 |
296 | return VarRangeIterationHack(self, _slice.start, _slice.stop, _slice.step)
297 |
298 | def changeby(self, other):
299 | return Statement("data_changevariableby", VARIABLE=self, VALUE=ensure_expression(other))
300 |
301 | def __repr__(self):
302 | return f"Var({self.sprite.name}: {self.name})"
303 |
304 | class VarRangeIterationHack():
305 | def __init__(self, var, start, stop, step):
306 | self.var = var
307 | self.start = 0 if start is None else start
308 | self.stop = stop
309 | self.step = 1 if step is None else step
310 |
311 | def __rshift__(self, values):
312 | if type(self.start) is int and type(self.stop) is int and type(self.step) is int:
313 | return varloop(self.var, range(self.start, self.stop, self.step), values)
314 | else:
315 | return condvarloop(self.var, self.start, self.stop, self.step, values)
316 |
317 | class List(Expression):
318 | def __init__(self, sprite, name, uid):
319 | self.sprite = sprite
320 | self.name = name
321 | self.uid = uid
322 |
323 | def append(self, other):
324 | return Statement("data_addtolist", LIST=self, ITEM=ensure_expression(other))
325 |
326 | def delete_all(self):
327 | return Statement("data_deletealloflist", LIST=self)
328 |
329 | def delete_at(self, other):
330 | return Statement("data_deleteoflist", LIST=self, INDEX=(ensure_expression(other)+1).simplified())
331 |
332 | def delete_at1(self, other):
333 | return Statement("data_deleteoflist", LIST=self, INDEX=ensure_expression(other))
334 |
335 | def len(self):
336 | return UnaryOp("listlen", self)
337 |
338 | def index(self, other):
339 | return ListItemNum(self, other) - 1
340 |
341 | def index1(self, other):
342 | return ListItemNum(self, other)
343 |
344 | def __repr__(self):
345 | return f"ListVar({self.sprite.name}: {self.name})"
346 |
347 | def __getitem__(self, index):
348 | return ListIndex(self, (ensure_expression(index)+1).simplified())
349 |
350 | def item(self, one_index):
351 | return ListIndex(self, one_index)
352 |
353 | def contains(self, thing):
354 | return ListContains(self, thing)
355 |
356 | class ListItemNum(Expression):
357 | def __init__(self, list_, item):
358 | self.list = list_
359 | self.item = ensure_expression(item)
360 |
361 | class ListIndex(Expression):
362 | def __init__(self, list_, index):
363 | self.list = list_
364 | self.index = ensure_expression(index)
365 |
366 | def __le__(self, other):
367 | other = ensure_expression(other)
368 | return Statement("data_replaceitemoflist", LIST=self.list, INDEX=self.index, ITEM=other)
369 |
370 | def __repr__(self):
371 | return f"{self.list!r}[{self.index!r}]"
372 |
373 | class ListContains(Expression):
374 | type = "bool"
375 | def __init__(self, list_, thing):
376 | self.list = list_
377 | self.thing = ensure_expression(thing)
378 |
379 | class Statement():
380 | def __init__(self, op, **args):
381 | self.op = op
382 | self.args = args
383 |
384 | def __repr__(self):
385 | return f"Statement({self.op}, {self.args!r})"
386 |
387 | class ElseHack():
388 | def __init__(self, condition, then):
389 | self.condition = condition
390 | self.then = then
391 |
392 | def __getitem__(self, do):
393 | if type(do) != tuple:
394 | do = [do]
395 | return IfElseStatement(self.condition, self.then, list(do))
396 |
397 | class IfStatement(Statement):
398 | def __init__(self, condition, then):
399 | super().__init__("control_if", CONDITION=ensure_expression(condition), SUBSTACK=then)
400 | self.Else = ElseHack(condition, then)
401 |
402 | class IfElseStatement(Statement):
403 | def __init__(self, condition, then, elsedo):
404 | super().__init__("control_if_else", CONDITION=ensure_expression(condition), SUBSTACK=then, SUBSTACK2=elsedo)
405 |
406 |
407 |
408 |
409 |
410 |
411 | class ProcDef(Statement):
412 | def __init__(self, proto, generator):
413 | self.op = "procedures_definition"
414 | self.proto = proto
415 | self.generator = generator # todo: maybe store generator inside proto?
416 |
417 | def __call__(self, *args):
418 | if len(args) != len(self.proto.vars):
419 | raise Exception(f"{self!r} expects {len(self.proto.vars)} args, {len(args)} given")
420 |
421 | return ProcCall(self, args, self.generator)
422 |
423 | def __getattr__(self, attr):
424 | varname = self.proto.locals_prefix + attr
425 | return self.proto.sprite.new_var(varname)
426 |
427 | def __repr__(self):
428 | return f"ProcDef({self.proto!r})"
429 |
430 | class ProcProto(Statement):
431 | def __init__(self, sprite, fmt, uid, locals_prefix, turbo=True):
432 | self.op = "procedures_prototype"
433 | self.sprite = sprite
434 | self.uid = uid
435 | self.fmt = fmt
436 | self.locals_prefix = fmt + ":" if locals_prefix is None else locals_prefix
437 | self.turbo = turbo
438 |
439 | # quick and dirty parser state machine
440 | # [square] brackets denote numeric/string args, brackets denote bool args
441 | # (does anyone ever actually use bool args?)
442 | self.argtypes = []
443 | self.proccode = ""
444 | self.argnames = []
445 | mode = "label"
446 | for char in fmt:
447 | if mode == "label":
448 | if char == "[":
449 | self.argtypes.append("generic")
450 | self.argnames.append("")
451 | self.proccode += "%s"
452 | mode = "strarg"
453 | elif char == "<":
454 | self.argtypes.append("bool")
455 | self.argnames.append("")
456 | self.proccode += "%b"
457 | mode = "boolarg"
458 | else:
459 | self.proccode += char
460 | elif mode == "strarg":
461 | if char == "]":
462 | mode = "label"
463 | else:
464 | self.argnames[-1] += char
465 | elif mode == "boolarg":
466 | if char == ">":
467 | mode = "label"
468 | else:
469 | self.argnames[-1] += char
470 | else:
471 | raise Exception("Invalid parser state")
472 |
473 | #print(self.argtypes, self.proccode, self.argnames)
474 |
475 | # NOTE: codegen will initialise self.vars
476 | self.vars = []
477 |
478 | def __repr__(self):
479 | return f"ProcProto({self.fmt!r})"
480 |
481 |
482 | class ProcCall(Statement):
483 | def __init__(self, proc, args, generator, turbo=True):
484 | self.procdef = proc # todo: fix these field names lol
485 | self.proc = proc.proto
486 | self.argv = list(map(ensure_expression, args))
487 | self.generator = generator
488 |
489 | for arg, argtype in zip(self.argv, proc.proto.argtypes):
490 | if argtype == "bool" and arg.type != "bool":
491 | raise Exception("Cannot pass non-boolean expression to boolean proc arg")
492 |
493 | super().__init__("procedures_call", PROC=proc.proto.uid, ARGS=self.argv)
494 |
495 | def inline(self):
496 | return self.generator(self.procdef, *self.argv) # todo use better variable namespacing
497 |
498 |
499 | class ProcVar(Expression):
500 | def __init__(self, sprite, procproto, name, uid, uid2):
501 | self.sprite = sprite
502 | self.procproto = procproto
503 | self.name = name
504 | self.uid = uid
505 | self.uid2 = uid2
506 |
507 | def __repr__(self):
508 | return f"ProcVar({self.procproto.fmt!r}: {self.name})"
509 |
510 | class ProcVarBool(Expression):
511 | def __init__(self, sprite, procproto, name, uid, uid2):
512 | self.type = "bool"
513 | self.sprite = sprite
514 | self.procproto = procproto
515 | self.name = name
516 | self.uid = uid
517 | self.uid2 = uid2
518 |
519 | def __repr__(self):
520 | return f"ProcVarBool({self.procproto.fmt!r}: {self.name})"
521 |
522 |
523 | def getitem_hack(fn, *args):
524 | class GetitemHack():
525 | def __init__(self, *args):
526 | self.args = args
527 |
528 | def __getitem__(self, then):
529 | if type(then) != tuple:
530 | then = [then]
531 | return fn(*self.args, list(then))
532 |
533 | return GetitemHack(*args)
534 |
535 | def varloop(var, _range, body): return [
536 | var <= _range.start,
537 | repeatn(len(_range),
538 | body +
539 | [ var <= var + _range.step ]
540 | )
541 | ]
542 |
543 | # when number of iterations not known at compile-time
544 | def condvarloop(var, start, stop, step, body): return [
545 | var <= start,
546 | repeatuntil((var + 1).simplified() > stop,
547 | body +
548 | [var.changeby(step)]
549 | )
550 | ]
551 |
552 | def repeatn(times, body=None):
553 | if body is None:
554 | return getitem_hack(repeatn, times)
555 | return Statement("control_repeat", TIMES=ensure_expression(times), SUBSTACK=body)
556 |
557 | def repeatuntil(cond, body=None):
558 | if body is None:
559 | return getitem_hack(repeatuntil, cond)
560 | return Statement("control_repeat_until", CONDITION=ensure_expression(cond), SUBSTACK=body)
561 |
562 | def on_flag(substack=None):
563 | return [Statement("event_whenflagclicked")] + substack
564 |
565 | def on_receive(event, event_uid, substack=None):
566 | return [Statement("event_whenbroadcastreceived", BROADCAST_OPTION=[str(event), event_uid])] + substack
567 |
568 | def on_press(key, substack=None):
569 | # TODO: use enum for key, check the argument is actually an enum field
570 | return [Statement("event_whenkeypressed", KEY_OPTION=str(key))] + substack
571 |
572 | class MenuExpression(Expression):
573 | pass
574 |
575 | class PenParamMenu(MenuExpression):
576 | def __init__(self, param):
577 | self.op = "pen_menu_colorParam"
578 | self.param = param
579 |
580 | # todo: probably need one of these for costume selection
581 | class TouchingObjectMenu(MenuExpression):
582 | def __init__(self, object):
583 | self.op = "sensing_touchingobjectmenu"
584 | self.object = object
585 |
586 | class Costume(MenuExpression):
587 | def __init__(self, costume):
588 | self.op = "looks_costume"
589 | self.costumename = costume
590 |
591 | class Instrument(MenuExpression):
592 | def __init__(self, instrument):
593 | if type(instrument) is not int:
594 | raise TypeError()
595 | self.op = "music_menu_INSTRUMENT"
596 | self.instrument = instrument
597 |
598 | class Drum(MenuExpression):
599 | def __init__(self, drum):
600 | if type(drum) is not int:
601 | raise TypeError()
602 | self.op = "music_menu_DRUM"
603 | self.drum = drum
604 |
605 | if __name__ == "__main__":
606 | class Sprite():
607 | name = "Sprite"
608 | s = Sprite()
609 | print(math.floor((Var(s, "foo", None) + 7 + 3) * 5) == Literal(3) / 5)
610 | print(List(s, "bar", None)[5])
611 | print(Literal(3) + 4)
612 | print(Literal(123) >> 2)
613 | print(Literal(1234123) & 0xFF)
614 | print(Literal(5) > 7)
615 | #List("foo")[5] = 123
616 |
--------------------------------------------------------------------------------