├── .gitignore ├── demo.ia ├── helloworld.ia ├── iacompiler.py ├── readme.md ├── scratchjr.py └── squares.ia /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ -------------------------------------------------------------------------------- /demo.ia: -------------------------------------------------------------------------------- 1 | cThis statement increments the cell 15 by 10 2 | +15#10 3 | 4 | cThis statement decrements the cell 15 or goes to line 8 5 | -15>10 6 | 7 | cYou can add pipes at the end of statements to goto another line 8 | +1|5 9 | 10 | cYou can reset a cell if you would like to 11 | r1 12 | 13 | cYou can also print out a predefined string 14 | pHello, world! 15 | 16 | cInput is sent directly to the 0 cell and can be accessed like this 17 | pEnter a number! I will print it out! 18 | -0>28 19 | -0>29 20 | -0>30 21 | -0>31 22 | -0>32 23 | -0>33 24 | -0>34 25 | -0>35 26 | -0>36 27 | -0>37 28 | |39p0 29 | |39p1 30 | |39p2 31 | |39p3 32 | |39p4 33 | |39p5 34 | |39p6 35 | |39p7 36 | |39p8 37 | |39p9 38 | 39 | cCode execution ends when there is none left. 40 | pYay! Were all done! 41 | c(I know. There should be an apostrophe. I forgot how to sanitize SQL tho.) -------------------------------------------------------------------------------- /helloworld.ia: -------------------------------------------------------------------------------- 1 | pThis program says "Hello World" however many times you enter. 2 | pEnter first didget: 3 | -0>5 4 | +1#10|3 5 | pEnter second didget: 6 | -0>8 7 | +1|6 8 | -1>10 9 | |8pHello World! 10 | pProgram is finished! 11 | pYAY!!!! -------------------------------------------------------------------------------- /iacompiler.py: -------------------------------------------------------------------------------- 1 | import scratchjr 2 | from scratchjr import Page, Project, Sprite, ScriptElement, get_shape_from_cursor 3 | import sqlite3 4 | import json 5 | 6 | SPEED_CONSTANT = 2 7 | INSTANT_WAIT_TIME = 1 8 | STORAGE_SIZE = 0.025 9 | 10 | 11 | def create_project_beta(code: str, cursor: sqlite3.Cursor): 12 | scale_bundle = lambda scale: { 13 | "scale": scale, 14 | "defaultScale": scale, 15 | "homescale": scale, 16 | } 17 | 18 | shown_bundle = lambda shown: { 19 | "shown": shown, 20 | "homeshown": shown, 21 | } 22 | 23 | my_project = Project() 24 | my_page = Page() 25 | my_project.add_page(my_page) 26 | 27 | instruction = Sprite( 28 | 50, 50, get_shape_from_cursor(cursor, 100, 100), 100, 100, **scale_bundle(1) 29 | ) 30 | 31 | my_page.add_sprite(instruction) 32 | 33 | vars_block = {} 34 | 35 | reaction_sprite = Sprite( 36 | 150, 37 | 50, 38 | get_shape_from_cursor(cursor, 100, 100, (0, 0, 255)), 39 | 100, 40 | 100, 41 | **scale_bundle(1), 42 | ) 43 | 44 | my_page.add_sprite(reaction_sprite) 45 | 46 | reaction_sprite.add_script( 47 | [ScriptElement("ontouch"), ScriptElement("message", "CustomMemoryHalt")] 48 | ) 49 | 50 | commands = code.split("\n") 51 | 52 | commands = [i if i != "" else "c" for i in commands] 53 | 54 | for i, command in enumerate(commands, 1): 55 | 56 | parsed_values = {"type": None, "goto": i + 1, "increment": 1} 57 | 58 | next_val = None 59 | 60 | for character in command: 61 | match character: 62 | case _ if parsed_values["type"] in ("p", "c"): 63 | if next_val not in parsed_values.keys(): 64 | parsed_values[next_val] = "" 65 | parsed_values[next_val] += character 66 | case "+": 67 | parsed_values["type"] = "+" 68 | next_val = "address" 69 | case "-": 70 | parsed_values["type"] = "-" 71 | next_val = "address" 72 | case ">": 73 | next_val = "elsegoto" 74 | case "|": 75 | next_val = "goto" 76 | parsed_values["goto"] = "" 77 | case "#": 78 | next_val = "increment" 79 | parsed_values["increment"] = "" 80 | case "r": 81 | parsed_values["type"] = "r" 82 | next_val = "address" 83 | case "p": 84 | parsed_values["type"] = "p" 85 | next_val = "text" 86 | case "c": 87 | parsed_values["type"] = "c" 88 | next_val = "comment" 89 | case _ if next_val == None: 90 | raise Exception( 91 | f"WHAT ARE YOU DOING IN LINE {i} AKA: {command}!?!?!" 92 | ) 93 | case _: 94 | if next_val not in parsed_values.keys(): 95 | parsed_values[next_val] = "" 96 | parsed_values[next_val] += character 97 | 98 | match parsed_values["type"]: 99 | case "+": 100 | 101 | instruction.add_script( 102 | [ 103 | ScriptElement("onmessage", f"CustomExecute{i}"), 104 | ScriptElement( 105 | "message", 106 | f"CustomIncrement{parsed_values['address']}From{i}", 107 | ), 108 | ScriptElement( 109 | "message", f"CustomExecute{parsed_values['goto']}" 110 | ), 111 | ] 112 | ) 113 | 114 | if parsed_values["address"] not in vars_block.keys(): 115 | vars_block[parsed_values["address"]] = Sprite( 116 | 150, 117 | 150, 118 | get_shape_from_cursor(cursor, 100, 100, (0, 255, 0)), 119 | 100, 120 | 100, 121 | **scale_bundle(1), 122 | ) 123 | 124 | vars_block[parsed_values["address"]].add_script( 125 | [ 126 | ScriptElement( 127 | "onmessage", 128 | f"CustomIncrement{parsed_values['address']}From{i}", 129 | ), 130 | ScriptElement( 131 | "repeat", 132 | int(parsed_values["increment"]), 133 | [ScriptElement("down", STORAGE_SIZE)], 134 | ), 135 | ] 136 | ) 137 | 138 | case "-": 139 | instruction.add_scripts( 140 | [ 141 | [ 142 | ScriptElement("onmessage", f"CustomExecute{i}"), 143 | ScriptElement( 144 | "message", 145 | f"CustomDecrement{parsed_values['address']}From{i}", 146 | ), 147 | ScriptElement("wait", STORAGE_SIZE * SPEED_CONSTANT * 2), 148 | ScriptElement( 149 | "message", 150 | f"CustomMemoryReset{parsed_values['address']}", 151 | ), 152 | ScriptElement( 153 | "message", 154 | f"CustomExecute{parsed_values['elsegoto']}", 155 | ), 156 | ], 157 | [ 158 | ScriptElement("onmessage", f"CustomCallB{i}"), 159 | ScriptElement("stopmine"), 160 | ScriptElement( 161 | "message", f"CustomExecute{parsed_values['goto']}" 162 | ), 163 | ], 164 | ] 165 | ) 166 | 167 | if parsed_values["address"] not in vars_block.keys(): 168 | vars_block[parsed_values["address"]] = Sprite( 169 | 150, 170 | 150, 171 | get_shape_from_cursor(cursor, 100, 100, (0, 255, 0)), 172 | 100, 173 | 100, 174 | **scale_bundle(1), 175 | ) 176 | 177 | vars_block[parsed_values["address"]].add_scripts( 178 | [ 179 | [ 180 | ScriptElement( 181 | "onmessage", 182 | f"CustomDecrement{parsed_values['address']}From{i}", 183 | ), 184 | ScriptElement("up", STORAGE_SIZE), 185 | ScriptElement("wait", INSTANT_WAIT_TIME), 186 | ScriptElement("message", f"CustomCallB{i}"), 187 | ], 188 | [ 189 | ScriptElement("onmessage", "CustomMemoryHalt"), 190 | ScriptElement("stopmine"), 191 | ], 192 | [ 193 | ScriptElement( 194 | "onmessage", 195 | f"CustomMemoryReset{parsed_values['address']}", 196 | ), 197 | ScriptElement("home"), 198 | ], 199 | ] 200 | ) 201 | 202 | case "p": 203 | if "text" not in parsed_values.keys(): 204 | parsed_values["text"] = "" 205 | instruction.add_script( 206 | [ 207 | ScriptElement("onmessage", f"CustomExecute{i}"), 208 | ScriptElement("say", parsed_values["text"]), 209 | ScriptElement( 210 | "message", f"CustomExecute{parsed_values['goto']}" 211 | ), 212 | ] 213 | ) 214 | case "r": 215 | instruction.add_script( 216 | [ 217 | ScriptElement("onmessage", f"CustomExecute{i}"), 218 | ScriptElement( 219 | "message", 220 | f"CustomMemoryReset{parsed_values['address']}", 221 | ), 222 | ScriptElement( 223 | "message", f"CustomExecute{parsed_values['goto']}" 224 | ), 225 | ] 226 | ) 227 | 228 | if parsed_values["address"] not in vars_block.keys(): 229 | vars_block[parsed_values["address"]] = Sprite( 230 | 150, 231 | 150, 232 | get_shape_from_cursor(cursor, 100, 100, (0, 255, 0)), 233 | 100, 234 | 100, 235 | **scale_bundle(1), 236 | ) 237 | 238 | vars_block[parsed_values["address"]].add_script( 239 | [ 240 | ScriptElement( 241 | "onmessage", 242 | f"CustomMemoryReset{parsed_values['address']}", 243 | ), 244 | ScriptElement("home"), 245 | ] 246 | ) 247 | case "c": 248 | instruction.add_script( 249 | [ 250 | ScriptElement("onmessage", f"CustomExecute{i}"), 251 | ScriptElement( 252 | "message", f"CustomExecute{parsed_values['goto']}" 253 | ), 254 | ] 255 | ) 256 | 257 | for i in vars_block.values(): 258 | my_page.add_sprite(i) 259 | 260 | instruction.add_script( 261 | [ScriptElement("onflag"), ScriptElement("message", "CustomExecute1")] 262 | ) 263 | 264 | if "0" not in vars_block.keys(): 265 | vars_block["0"] = Sprite( 266 | 150, 267 | 150, 268 | get_shape_from_cursor(cursor, 100, 100, (0, 255, 0)), 269 | 100, 270 | 100, 271 | **scale_bundle(1), 272 | ) 273 | my_page.add_sprite(vars_block["0"]) 274 | 275 | vars_block["0"].add_scripts( 276 | [ 277 | [ 278 | ScriptElement("onmessage", "CustomInputIncrement"), 279 | ScriptElement("down", STORAGE_SIZE), 280 | ], 281 | [ScriptElement("onmessage", "CustomInputReset"), ScriptElement("home")], 282 | ] 283 | ) 284 | 285 | for i in range(10): 286 | temp = Sprite( 287 | (i % 10) * 10 + 305, 288 | (i // 10) * 10 + 5, 289 | get_shape_from_cursor(cursor, 10, 10, (0, 255, 255), i), 290 | 10, 291 | 10, 292 | **scale_bundle(1), 293 | ) 294 | my_page.add_sprite(temp) 295 | temp.add_script( 296 | [ 297 | ScriptElement("onclick"), 298 | ScriptElement("message", "CustomInputReset"), 299 | ScriptElement( 300 | "repeat", i, [ScriptElement("message", "CustomInputIncrement")] 301 | ) 302 | if i != 0 303 | else ScriptElement("endstack"), 304 | ] 305 | ) 306 | 307 | return my_project 308 | 309 | 310 | def main(): 311 | conn = sqlite3.connect("/Users/rick/Documents/ScratchJR/scratchjr.sqllite") 312 | cursor = conn.cursor() 313 | 314 | ia_path = input("Please input your .ia path: ") 315 | 316 | file = open(ia_path, "r") 317 | 318 | code = file.read() 319 | 320 | name = input("Please input project name: ") 321 | 322 | my_project = create_project_beta(code, cursor) 323 | 324 | scratchjr.set_project_json(cursor, name, my_project) 325 | print(json.dumps(my_project.get_object())) 326 | conn.commit() 327 | conn.close() 328 | 329 | 330 | if __name__ == "__main__": 331 | main() 332 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ScratchJr Python Wrapper 2 | 3 | The ScratchJr Python Wrapper is a wrapper for the underlying JSON structure of a ScratchJr project. It was mostly made to be used as a tool for its creator; however, it definitely has the ability to be used to procedurally create ScratchJr projects with many different purposes. 4 | 5 | ## Classes 6 | 7 | The classes are split up into the few levels of heirarchy that ScratchJr has. 8 | 9 | ### Project 10 | The project class is an incredibly simple class that holds pages. 11 | ### Page 12 | The page class is an incredibly simple class that has a name and holds sprites. 13 | ### Sprite 14 | The sprite is a more complex class with a name, position, size, texture, and a variable number of other customizable options. It also can contain ScriptElement lists. 15 | ### ScriptElement 16 | The ScriptElement class holds all of the information about a single scripting block in ScratchJr. When put in lists, they can be fed to a sprite. 17 | 18 | ## Compilers 19 | 20 | There is currently a compiler for "Extended Infinite Abacus" code (patent pending), which allows for Turing-Complete programming in ScratchJr. The language is an extension of the [Infinite Abacus](https://www.cambridge.org/core/journals/canadian-mathematical-bulletin/article/how-to-program-an-infinite-abacus/A6EB7DD8D57056044CCB128923764BEB) programming language. It includes resetting cells, incrementing by more than one, printing arbitrary strings, inline jumps, and asynchronous input. These features were all added as they are very simple and add no complexity to the compiled ScratchJr project. EIA can be easily compiled to IA, although this is unneccesary for the project. -------------------------------------------------------------------------------- /scratchjr.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Tuple 2 | from PIL import Image 3 | import json 4 | import base64 5 | from io import BytesIO 6 | import sqlite3 7 | import hashlib 8 | 9 | SPEED_CONSTANT = 2 10 | INSTANT_WAIT_TIME = 1 11 | STORAGE_SIZE = 0.025 12 | 13 | scale_bundle = lambda scale: { 14 | "scale": scale, 15 | "defaultScale": scale, 16 | "homescale": scale, 17 | } 18 | 19 | shown_bundle = lambda shown: { 20 | "shown": shown, 21 | "homeshown": shown, 22 | } 23 | 24 | 25 | class Sprite: 26 | curr_id = 0 27 | 28 | def __init__( 29 | self, 30 | xcoor: int, 31 | ycoor: int, 32 | texture_id: str, 33 | width: int = 50, 34 | height: int = 50, 35 | **kwargs, 36 | ): 37 | self.id = f"ID{Sprite.curr_id}" 38 | Sprite.curr_id += 1 39 | self.x = xcoor 40 | self.y = ycoor 41 | self.texture = texture_id 42 | self.width = width 43 | self.height = height 44 | self.scripts = [] 45 | self.object_out = { 46 | "shown": True, 47 | "type": "sprite", 48 | "md5": self.texture, 49 | "id": self.id, 50 | "flip": False, 51 | "name": self.id, 52 | "angle": 0, 53 | "scale": 0.5, 54 | "speed": 2, 55 | "defaultScale": 0.5, 56 | "sounds": ["pop.mp3"], 57 | "xcoor": self.x, 58 | "ycoor": self.y, 59 | "cx": self.width // 2, 60 | "cy": self.height // 2, 61 | "w": self.width, 62 | "h": self.height, 63 | "homex": self.x, 64 | "homey": self.y, 65 | "homescale": 0.5, 66 | "homeshown": True, 67 | "homeflip": False, 68 | "scripts": self.scripts, 69 | } 70 | for key, value in kwargs.items(): 71 | self.object_out[key] = value 72 | 73 | def get_object(self): 74 | return self.object_out 75 | 76 | def add_script(self, script): 77 | new_script = [i.get_list_form() for i in script] 78 | self.scripts.append(new_script) 79 | 80 | def add_scripts(self, scripts): 81 | for i in scripts: 82 | self.add_script(i) 83 | 84 | 85 | class Page: 86 | current_page = 1 87 | 88 | def __init__(self, image=None, name=None): 89 | self.num = Page.current_page 90 | if name == None: 91 | Page.current_page += 1 92 | self.name = f"page {self.num}" 93 | else: 94 | self.name = name 95 | self.sprites = [] 96 | 97 | def add_sprite(self, sprite): 98 | self.sprites.append(sprite) 99 | 100 | def get_object(self): 101 | object_out = { 102 | "sprites": [i.id for i in self.sprites], 103 | "layers": [i.id for i in self.sprites], 104 | "num": self.num, 105 | } 106 | for i in self.sprites: 107 | object_out[i.id] = i.get_object() 108 | return object_out 109 | 110 | 111 | class Project: 112 | def __init__(self): 113 | self.pages = [] 114 | 115 | def add_page(self, page): 116 | self.pages.append(page) 117 | 118 | def get_object(self): 119 | object_out = { 120 | "pages": [i.name for i in self.pages], 121 | "currentPage": self.pages[0].name, 122 | } 123 | for i in self.pages: 124 | object_out[i.name] = i.get_object() 125 | 126 | return object_out 127 | 128 | 129 | class ScriptElement: 130 | """ 131 | Valid Actions Include: 132 | onflag 133 | onclick 134 | ontouch 135 | onmessage string(color?) 136 | message string(color?) 137 | repeat int scriptlist 138 | forward int 139 | back int 140 | up int 141 | down int 142 | right int 143 | left int 144 | hop int 145 | home 146 | stopmine 147 | say string 148 | grow int 149 | shrink int 150 | same 151 | hide 152 | show 153 | playsnd string 154 | wait int 155 | setspeed int 156 | endstack 157 | forever 158 | gotopage int 159 | """ 160 | 161 | def __init__( 162 | self, 163 | action: str, 164 | parameter: str | float = "null", 165 | script_parameter: List[Any] = [], 166 | ): 167 | self.action = action 168 | self.parameter = parameter 169 | self.script_parameter = script_parameter 170 | 171 | def get_list_form(self): 172 | object_out = [self.action, self.parameter, 50, 50] 173 | if self.action == "repeat": 174 | object_out.append([i.get_list_form() for i in self.script_parameter]) 175 | return object_out 176 | 177 | 178 | def get_bitmap_base64(x: int, y: int, color: Tuple[int, int, int] = (0, 0, 0)): 179 | im = Image.new("RGB", (x, y), tuple(color)) 180 | buffered = BytesIO() 181 | im.save(buffered, format="PNG") 182 | img_str = base64.b64encode(buffered.getvalue()) 183 | return str(img_str, "ascii") 184 | 185 | 186 | def get_svg_base64( 187 | x: int, y: int, color: Tuple[int, int, int] = (0, 0, 0), text: str = "" 188 | ): 189 | im = f""" 190 | 191 | 192 | 193 | 194 | {text} 195 | 196 | 197 | """ 198 | img_str = base64.b64encode(im.encode()) 199 | return str(img_str, "ascii") 200 | 201 | 202 | def get_generic_image_base64( 203 | x: int, 204 | y: int, 205 | color: Tuple[int, int, int] = (0, 0, 0), 206 | text: str = "", 207 | svg: bool = True, 208 | ): 209 | if svg: 210 | return get_svg_base64(x, y, color, text) 211 | else: 212 | return get_bitmap_base64(x, y, color) 213 | 214 | 215 | def get_systematic_image_name( 216 | width: int, 217 | height: int, 218 | color: Tuple[int, int, int] = (0, 0, 0), 219 | text: str = "", 220 | svg: bool = True, 221 | ): 222 | object_out = str(("systematic scrach mmlib", width, height, text, tuple(color))) 223 | hash_str = hashlib.md5(object_out.encode()).hexdigest() 224 | return hash_str + (".svg" if svg else ".png") 225 | 226 | 227 | # def register_image_cursor( 228 | # cursor: sqlite3.Cursor, image_data: Any, file_ending: str = "svg" 229 | # ): 230 | # pass 231 | 232 | 233 | def get_shape_from_cursor( 234 | cursor: sqlite3.Cursor, 235 | width: int, 236 | height: int, 237 | color: Tuple[int, int, int] = (0, 0, 0), 238 | text: str = "", 239 | svg=True, 240 | ): 241 | file_name = get_systematic_image_name(width, height, color, text, svg) 242 | if ( 243 | len( 244 | list( 245 | cursor.execute( 246 | f"SELECT MD5 FROM PROJECTFILES WHERE MD5 = '{file_name}'" 247 | ) 248 | ) 249 | ) 250 | == 0 251 | ): 252 | cursor.execute( 253 | f"INSERT INTO PROJECTFILES (MD5, CONTENTS) VALUES ('{file_name}','{get_generic_image_base64(width,height,color, text, svg)}')" 254 | ) 255 | print(f"Created new file '{file_name}'.") 256 | else: 257 | print(f"Found file '{file_name}'.") 258 | return file_name 259 | 260 | 261 | def set_project_json(cursor: sqlite3.Cursor, name: str, project: Project): 262 | """Sets the JSON data of the named project in to that of the passed project through the cursor.""" 263 | sanitized_json = json.dumps(project.get_object()).replace("'", "''") 264 | cursor.execute( 265 | f"UPDATE PROJECTS SET JSON = '{sanitized_json}' WHERE NAME = '{name}'" 266 | ) 267 | 268 | 269 | def get_project_json(cursor: sqlite3.Cursor, name: str) -> str: 270 | try: 271 | return cursor.execute( 272 | f"SELECT JSON FROM PROJECTS WHERE NAME = '{name}'" 273 | ).fetchall()[0] 274 | except IndexError: 275 | raise Exception(f'Project "{name}" does not exist!') 276 | 277 | 278 | def example(): 279 | connection_path = input("Please input the path of your database file: ") 280 | 281 | if connection_path == "": 282 | connection_path = "/Users/rick/Documents/ScratchJR/scratchjr.sqllite" 283 | 284 | conn = sqlite3.connect(connection_path) 285 | cursor = conn.cursor() 286 | 287 | project_name = input("Please input the project's name: ") 288 | 289 | sprite_text = input("Please input the sprite's text: ") 290 | 291 | sprite_say = input("Please input what the sprite will say: ") 292 | 293 | sprite_times = int(input("Please input how many times the sprite will say it: ")) 294 | 295 | my_project = Project() 296 | my_page = Page() 297 | my_sprite = Sprite( 298 | 100, 299 | 100, 300 | get_shape_from_cursor(cursor, 50, 50, (52, 86, 139), sprite_text), 301 | 50, 302 | 50, 303 | ) 304 | 305 | my_project.add_page(my_page) 306 | my_page.add_sprite(my_sprite) 307 | 308 | my_sprite.add_script( 309 | [ 310 | ScriptElement("onflag"), 311 | ScriptElement("repeat", sprite_times, [ScriptElement("say", sprite_say)]), 312 | ScriptElement("endstack"), 313 | ] 314 | ) 315 | 316 | set_project_json(cursor, project_name, my_project) 317 | 318 | print(f"Final project JSON: {json.dumps(my_project.get_object())}") 319 | conn.commit() 320 | conn.close() 321 | 322 | 323 | if __name__ == "__main__": 324 | example() 325 | -------------------------------------------------------------------------------- /squares.ia: -------------------------------------------------------------------------------- 1 | +3#9 2 | +4#9 3 | +5#9 4 | +1 5 | -1>8 6 | +2 7 | -3|5>11 8 | -2>10 9 | +1|8 10 | +1#2|16 11 | +3#9 12 | -4>13|5 13 | +4#9 14 | -5>15|5 15 | +5#9|5 16 | r6 17 | -5>20 18 | +7 19 | +8|17 20 | -8>34 21 | +5|20 22 | +6 23 | -4>26 24 | +7 25 | +8|23 26 | -8>34 27 | +4|26 28 | +6#2 29 | -3>32 30 | +7 31 | +8|29 32 | -8>34 33 | +3|32 34 | -7>44 35 | -7>45 36 | -7>46 37 | -7>47 38 | -7>48 39 | -7>49 40 | -7>50 41 | -7>51 42 | -7>52 43 | -7>53 44 | |54p9 45 | |54p8 46 | |54p7 47 | |54p6 48 | |54p5 49 | |54p4 50 | |54p3 51 | |54p2 52 | |54p1 53 | |54p0 54 | -6>22 55 | -6>28 56 | pN 57 | -6>5 58 | cit is really difficult to explain what is happening here, but it is intense --------------------------------------------------------------------------------