pystage 0.0.15__tar.gz → 0.1.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pystage-0.0.15/src/pystage.egg-info → pystage-0.1.0.dev0}/PKG-INFO +14 -2
- {pystage-0.0.15 → pystage-0.1.0.dev0}/README.md +9 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/setup.cfg +2 -2
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/convert/code_writer.py +27 -5
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/convert/sb3.py +81 -44
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/convert/sb3_templates.py +16 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_control.py +1 -1
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_looks.py +1 -1
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_motion.py +2 -1
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_operators.py +17 -4
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_sensing.py +35 -13
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/gui.py +5 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/stage.py +36 -4
- {pystage-0.0.15 → pystage-0.1.0.dev0/src/pystage.egg-info}/PKG-INFO +14 -2
- {pystage-0.0.15 → pystage-0.1.0.dev0}/LICENSE +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/pyproject.toml +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/convert/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_base_sprite.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_events.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_pen.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_sound.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/_variables.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/asking.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/assets.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/code_block.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/constants.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/messages.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/monitors.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/core/sprite.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/de/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/de/sprite.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/de/stage.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/en/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/en/sprite.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/en/stage.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/fonts/roboto-bold.ttf +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/fonts/roboto-light.ttf +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/fonts/roboto-regular.ttf +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/images/grid.svg +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/images/pen.png +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/images/say.png +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/images/think.png +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/images/zombie_idle.png +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/l10n/__init__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/l10n/__main__.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/l10n/api.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/l10n/translations.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/sounds/dancehead2.wav +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/sounds/guitar.wav +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage/util.py +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage.egg-info/SOURCES.txt +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage.egg-info/dependency_links.txt +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage.egg-info/requires.txt +0 -0
- {pystage-0.0.15 → pystage-0.1.0.dev0}/src/pystage.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pystage
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
4
|
Summary: Scratch-like Python programming
|
|
5
5
|
Home-page: https://github.com/pystage/pystage
|
|
6
6
|
Author: Kai Eckert
|
|
7
|
-
Author-email: eckert@
|
|
7
|
+
Author-email: k.eckert@hs-mannheim.de
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Bug Tracker, https://github.com/pystage/pystage/issues
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,6 +13,9 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.6
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pygame
|
|
17
|
+
Requires-Dist: jinja2
|
|
18
|
+
Requires-Dist: svglib
|
|
16
19
|
|
|
17
20
|
# PyStage
|
|
18
21
|
|
|
@@ -89,6 +92,15 @@ And here is the result:
|
|
|
89
92
|
# Installation
|
|
90
93
|
```pip install pystage```
|
|
91
94
|
|
|
95
|
+
**if doesn't work, try following commands one by one.**
|
|
96
|
+
```bash
|
|
97
|
+
pip install wheel
|
|
98
|
+
pip install pygame==2.3.0
|
|
99
|
+
pip install svglib==1.1.0
|
|
100
|
+
pip install reportlab==3.5.67
|
|
101
|
+
pip install pystage
|
|
102
|
+
```
|
|
103
|
+
|
|
92
104
|
# Getting started
|
|
93
105
|
|
|
94
106
|
If you are interested in PyStage and want to get in touch, feel free to join us on our [Discord Server](https://discord.gg/MSGrs3UCCz).
|
|
@@ -73,6 +73,15 @@ And here is the result:
|
|
|
73
73
|
# Installation
|
|
74
74
|
```pip install pystage```
|
|
75
75
|
|
|
76
|
+
**if doesn't work, try following commands one by one.**
|
|
77
|
+
```bash
|
|
78
|
+
pip install wheel
|
|
79
|
+
pip install pygame==2.3.0
|
|
80
|
+
pip install svglib==1.1.0
|
|
81
|
+
pip install reportlab==3.5.67
|
|
82
|
+
pip install pystage
|
|
83
|
+
```
|
|
84
|
+
|
|
76
85
|
# Getting started
|
|
77
86
|
|
|
78
87
|
If you are interested in PyStage and want to get in touch, feel free to join us on our [Discord Server](https://discord.gg/MSGrs3UCCz).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = pystage
|
|
3
|
-
version = 0.0.
|
|
3
|
+
version = 0.1.0.dev
|
|
4
4
|
author = Kai Eckert
|
|
5
|
-
author_email = eckert@
|
|
5
|
+
author_email = k.eckert@hs-mannheim.de
|
|
6
6
|
description = Scratch-like Python programming
|
|
7
7
|
long_description = file: README.md
|
|
8
8
|
long_description_content_type = text/markdown
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import io
|
|
2
|
-
import sys
|
|
3
1
|
import textwrap
|
|
4
2
|
import re
|
|
5
3
|
import logging
|
|
6
4
|
import inspect
|
|
7
|
-
import ast
|
|
8
5
|
import importlib
|
|
9
6
|
import dis
|
|
10
|
-
from jinja2 import
|
|
7
|
+
from jinja2 import Environment
|
|
11
8
|
from jinja2.exceptions import UndefinedError
|
|
12
9
|
logger = logging.getLogger(__name__)
|
|
13
10
|
|
|
14
11
|
from pystage.core.sprite import CoreSprite
|
|
15
12
|
from pystage.core.stage import CoreStage
|
|
16
13
|
|
|
14
|
+
to_underscore = re.compile(r"[^A-Za-z0-9_]")
|
|
15
|
+
valid_variable_start = re.compile(r"[a-zA-Z_]")
|
|
16
|
+
multiple_underscores = re.compile(r"_+")
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def quoted(value):
|
|
@@ -63,6 +63,7 @@ class CodeWriter():
|
|
|
63
63
|
self.jinja_environment.filters["global_sound"] = lambda name: self.global_sound(name)
|
|
64
64
|
self.jinja_environment.filters["global_costume"] = lambda name: self.global_costume(name)
|
|
65
65
|
self.jinja_environment.filters["global_backdrop"] = lambda name: self.global_backdrop(name)
|
|
66
|
+
self.jinja_environment.filters["global_sprite"] = lambda name: self.get_sprite_var(unquoted(name))
|
|
66
67
|
|
|
67
68
|
logger.debug("CodeWriter created.")
|
|
68
69
|
|
|
@@ -73,7 +74,15 @@ class CodeWriter():
|
|
|
73
74
|
|
|
74
75
|
def get_sprite_var(self, name=None):
|
|
75
76
|
def to_python(name: str):
|
|
76
|
-
|
|
77
|
+
vname = name.lower().strip()
|
|
78
|
+
# Replace everything that is not allowed in a
|
|
79
|
+
# variable name with underscores
|
|
80
|
+
vname = to_underscore.sub("_", vname)
|
|
81
|
+
vname = multiple_underscores.sub("_", vname)
|
|
82
|
+
# Check of the variable does not start with a number
|
|
83
|
+
if not valid_variable_start.fullmatch(vname[0]):
|
|
84
|
+
vname = "_" + vname
|
|
85
|
+
return vname
|
|
77
86
|
if name is None:
|
|
78
87
|
return to_python(self.current_sprite)
|
|
79
88
|
else:
|
|
@@ -261,6 +270,19 @@ class CodeWriter():
|
|
|
261
270
|
context["CURRENT_SPRITE"] = self.get_sprite_var()
|
|
262
271
|
if "{{ID}}" in text:
|
|
263
272
|
context["ID"] = self.get_id()
|
|
273
|
+
|
|
274
|
+
# if {{NEXT | indent (4)}} is still in text, but there is no
|
|
275
|
+
# NEXT in context, then it means the Scratch “hat” block is empty,
|
|
276
|
+
# so set NEXT to "pass", and the function is essentially empty.
|
|
277
|
+
if "indent(4)" in text:
|
|
278
|
+
if "NEXT" in text and not context.get("NEXT"):
|
|
279
|
+
context["NEXT"] = "pass"
|
|
280
|
+
if "SUBSTACK" in text and not context.get("SUBSTACK"):
|
|
281
|
+
context["SUBSTACK"] = "pass"
|
|
282
|
+
if "SUBSTACK2" in text and not context.get("SUBSTACK2"):
|
|
283
|
+
context["SUBSTACK2"] = "pass"
|
|
284
|
+
if "{{CONDITION}}" in text and not context.get("CONDITION"):
|
|
285
|
+
context["CONDITION"] = "None"
|
|
264
286
|
template = self.jinja_environment.from_string(text)
|
|
265
287
|
try:
|
|
266
288
|
text = template.render(context)
|
|
@@ -149,6 +149,9 @@ def get_block(block, blocks, stage):
|
|
|
149
149
|
res["params"][f] = f'"{block["fields"][f][0]}"'
|
|
150
150
|
for i in block["inputs"]:
|
|
151
151
|
value = block["inputs"][i][1]
|
|
152
|
+
if not value:
|
|
153
|
+
# empty if condition
|
|
154
|
+
continue
|
|
152
155
|
if isinstance(value, list):
|
|
153
156
|
res["params"][i] = get_input_value(value, stage)
|
|
154
157
|
else:
|
|
@@ -179,6 +182,12 @@ def unique_global_name(name, ext):
|
|
|
179
182
|
global_names.add(f"{iname}.{ext}")
|
|
180
183
|
return iname, ext
|
|
181
184
|
|
|
185
|
+
def get_sb3_data(filename):
|
|
186
|
+
archive = zipfile.ZipFile(filename, 'r')
|
|
187
|
+
with archive.open("project.json") as f:
|
|
188
|
+
data = json.loads(f.read())
|
|
189
|
+
return data
|
|
190
|
+
|
|
182
191
|
def get_intermediate(data, name):
|
|
183
192
|
hat_blocks = [
|
|
184
193
|
"event_whenthisspriteclicked",
|
|
@@ -287,7 +296,7 @@ def get_intermediate(data, name):
|
|
|
287
296
|
return project
|
|
288
297
|
|
|
289
298
|
|
|
290
|
-
def get_python(project, language="core"):
|
|
299
|
+
def get_python(project, language="core", project_link=None):
|
|
291
300
|
lang_module = importlib.import_module(f"pystage.{language}")
|
|
292
301
|
sprite_class = lang_module.sprite_class.__name__
|
|
293
302
|
stage_class = lang_module.stage_class.__name__
|
|
@@ -297,7 +306,12 @@ def get_python(project, language="core"):
|
|
|
297
306
|
add_variable = get_translated_function("pystage_makevariable", language)
|
|
298
307
|
create_sprite = get_translated_function("pystage_createsprite", language, stage=True)
|
|
299
308
|
play = get_translated_function("pystage_play", language, stage=True)
|
|
300
|
-
res =
|
|
309
|
+
res = ""
|
|
310
|
+
if project_link:
|
|
311
|
+
res += textwrap.dedent(f'''\
|
|
312
|
+
# Online Converter, URL: {project_link}
|
|
313
|
+
''')
|
|
314
|
+
res += textwrap.dedent(f'''\
|
|
301
315
|
# {project['name']} (pyStage, converted from Scratch 3)
|
|
302
316
|
|
|
303
317
|
from pystage.{language} import {sprite_class}, {stage_class}
|
|
@@ -339,6 +353,12 @@ def get_python(project, language="core"):
|
|
|
339
353
|
sprite_var = writer.get_sprite_var()
|
|
340
354
|
costumes = [(writer.global_costume(c["local_name"], False), c) for c in sprite["costumes"]]
|
|
341
355
|
sounds = [writer.global_sound(s["local_name"], False) for s in sprite["sounds"]]
|
|
356
|
+
|
|
357
|
+
# Generate initialization code for each sprite.
|
|
358
|
+
res += textwrap.dedent(f"""\
|
|
359
|
+
|
|
360
|
+
# Create and initialize sprite '{sprite_var}'
|
|
361
|
+
""")
|
|
342
362
|
res += textwrap.dedent(f'''\
|
|
343
363
|
{sprite_var} = {stage_var}.{create_sprite}(None)
|
|
344
364
|
{sprite_var}.{get_translated_function("pystage_setname", language)}("{sprite["name"]}")
|
|
@@ -399,6 +419,15 @@ def get_python(project, language="core"):
|
|
|
399
419
|
res += textwrap.dedent(f'''\
|
|
400
420
|
{sprite_var}.{get_translated_function("pystage_setmonitorstyle_large", language)}("{monitor["variable"]}")
|
|
401
421
|
''')
|
|
422
|
+
|
|
423
|
+
# Now generate the code for each sprite.
|
|
424
|
+
for sprite in project["sprites"]:
|
|
425
|
+
writer.set_sprite(sprite["name"])
|
|
426
|
+
sprite_var = writer.get_sprite_var()
|
|
427
|
+
res += textwrap.dedent(f"""\
|
|
428
|
+
|
|
429
|
+
# Scratch Blocks for '{sprite_var}'
|
|
430
|
+
""")
|
|
402
431
|
for block in sprite["blocks"]:
|
|
403
432
|
res += writer.process(block)
|
|
404
433
|
|
|
@@ -414,10 +443,36 @@ def print_python(project, language="core"):
|
|
|
414
443
|
print(get_python(project, language))
|
|
415
444
|
|
|
416
445
|
|
|
446
|
+
def create_project(file_name, project_name, project, directory, language="core", project_link=None):
|
|
447
|
+
print(f"Creating project: {project_name}")
|
|
448
|
+
print(f"Exporting to: {directory}")
|
|
449
|
+
archive = zipfile.ZipFile(file_name, 'r')
|
|
450
|
+
dp = Path(directory)
|
|
451
|
+
if dp.exists():
|
|
452
|
+
raise("Directory must not exist at this point, but it does: {}".format(directory))
|
|
453
|
+
os.mkdir(dp)
|
|
454
|
+
os.mkdir(dp / "images")
|
|
455
|
+
os.mkdir(dp / "sounds")
|
|
456
|
+
for key in project["costumes"]:
|
|
457
|
+
c = project["costumes"][key]
|
|
458
|
+
with archive.open(f'{key}.{c["extension"]}') as infile:
|
|
459
|
+
with open(dp / "images" / f'{c["global_name"]}.{c["extension"]}', "wb") as outfile:
|
|
460
|
+
outfile.write(infile.read())
|
|
461
|
+
for key in project["sounds"]:
|
|
462
|
+
s = project["sounds"][key]
|
|
463
|
+
with archive.open(f'{key}.{s["extension"]}') as infile:
|
|
464
|
+
with open(dp / "sounds" / f'{s["global_name"]}.{s["extension"]}', "wb") as outfile:
|
|
465
|
+
outfile.write(infile.read())
|
|
466
|
+
with open(dp / f'{project_name}.py', "w", encoding="utf-8") as pyfile:
|
|
467
|
+
pyfile.write(get_python(project, language=language, project_link=project_link))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
417
471
|
##
|
|
418
472
|
# Main
|
|
419
473
|
#
|
|
420
474
|
|
|
475
|
+
|
|
421
476
|
if __name__ == "__main__":
|
|
422
477
|
|
|
423
478
|
parser = argparse.ArgumentParser(
|
|
@@ -438,47 +493,29 @@ if __name__ == "__main__":
|
|
|
438
493
|
elif args.verbose:
|
|
439
494
|
logging.basicConfig(level=logging.INFO)
|
|
440
495
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
data =
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
project =
|
|
449
|
-
|
|
450
|
-
|
|
496
|
+
project_name = to_filename(Path(args.file).stem)
|
|
497
|
+
data = get_sb3_data(args.file)
|
|
498
|
+
if args.sb3_json:
|
|
499
|
+
print(json.dumps(data, indent=2))
|
|
500
|
+
sys.exit(0)
|
|
501
|
+
project = get_intermediate(data, project_name)
|
|
502
|
+
if args.intermediate:
|
|
503
|
+
print(json.dumps(project, indent=2))
|
|
504
|
+
sys.exit(1)
|
|
505
|
+
elif args.python:
|
|
506
|
+
print_python(project, args.language)
|
|
507
|
+
sys.exit(1)
|
|
508
|
+
else:
|
|
509
|
+
directory = project_name
|
|
510
|
+
if args.directory:
|
|
511
|
+
directory = args.directory
|
|
512
|
+
print(f"Exporting to: {directory}")
|
|
513
|
+
dp = Path(directory)
|
|
514
|
+
if dp.exists() and not dp.is_dir:
|
|
515
|
+
print("Output directory exists, but is not a directory. Use -d to specify another directory.")
|
|
451
516
|
sys.exit(1)
|
|
452
|
-
elif
|
|
453
|
-
|
|
517
|
+
elif dp.exists() and next(dp.iterdir(), False):
|
|
518
|
+
print("Output directory is not empty. Use -d to specify another one.")
|
|
454
519
|
sys.exit(1)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
directory = project_name
|
|
458
|
-
if args.directory:
|
|
459
|
-
directory = args.directory
|
|
460
|
-
print(f"Exporting to: {directory}")
|
|
461
|
-
dp = Path(directory)
|
|
462
|
-
if dp.exists() and not dp.is_dir:
|
|
463
|
-
print("Output directory exists, but is not a directory. Use -d to specify another directory.")
|
|
464
|
-
sys.exit(1)
|
|
465
|
-
elif dp.exists() and next(dp.iterdir(), False):
|
|
466
|
-
print("Output directory is not empty. Use -d to specify another one.")
|
|
467
|
-
sys.exit(1)
|
|
468
|
-
elif not dp.exists():
|
|
469
|
-
print(f"Creating directory: {dp}")
|
|
470
|
-
os.mkdir(dp)
|
|
471
|
-
os.mkdir(dp / "images")
|
|
472
|
-
os.mkdir(dp / "sounds")
|
|
473
|
-
for key in project["costumes"]:
|
|
474
|
-
c = project["costumes"][key]
|
|
475
|
-
with archive.open(f'{key}.{c["extension"]}') as infile:
|
|
476
|
-
with open(dp / "images" / f'{c["global_name"]}.{c["extension"]}', "wb") as outfile:
|
|
477
|
-
outfile.write(infile.read())
|
|
478
|
-
for key in project["sounds"]:
|
|
479
|
-
s = project["sounds"][key]
|
|
480
|
-
with archive.open(f'{key}.{s["extension"]}') as infile:
|
|
481
|
-
with open(dp / "sounds" / f'{s["global_name"]}.{s["extension"]}', "wb") as outfile:
|
|
482
|
-
outfile.write(infile.read())
|
|
483
|
-
with open(dp / f'{project_name}.py', "w", encoding="utf-8") as pyfile:
|
|
484
|
-
pyfile.write(get_python(project, language=args.language))
|
|
520
|
+
elif not dp.exists():
|
|
521
|
+
create_project(args.file, project_name, project, directory, args.language)
|
|
@@ -42,7 +42,21 @@ templates = {
|
|
|
42
42
|
{{CURRENT_SPRITE}}.{{func}}({{func}}_{{ID}})
|
|
43
43
|
|
|
44
44
|
''',
|
|
45
|
+
|
|
46
|
+
"event_whengreaterthan": '''\
|
|
47
|
+
def {{func}}_{{ID}}(self):
|
|
48
|
+
{{NEXT | indent(4)}}
|
|
49
|
+
{{CURRENT_SPRITE}}.{{func}}({{VALUE}}, {{func}}_{{ID}})
|
|
50
|
+
|
|
51
|
+
''',
|
|
52
|
+
|
|
53
|
+
"event_whenbackdropswitchesto": '''\
|
|
54
|
+
def {{func}}_{{ID}}(self):
|
|
55
|
+
{{NEXT | indent(4)}}
|
|
56
|
+
{{CURRENT_SPRITE}}.{{func}}({{BACKDROP | global_backdrop}}, {{func}}_{{ID}})
|
|
45
57
|
|
|
58
|
+
''',
|
|
59
|
+
|
|
46
60
|
"control_start_as_clone": '''\
|
|
47
61
|
|
|
48
62
|
def {{func}}_{{ID}}(self):
|
|
@@ -106,6 +120,8 @@ templates = {
|
|
|
106
120
|
"sound_sounds_menu": "{{SOUND_MENU | global_sound }}",
|
|
107
121
|
|
|
108
122
|
"sensing_keyoptions": "{{KEY_OPTION}}",
|
|
123
|
+
"sensing_distancetomenu": "{{DISTANCETOMENU | global_sprite}}",
|
|
124
|
+
"sensing_touchingobjectmenu": "{{TOUCHINGOBJECTMENU | global_sprite}}",
|
|
109
125
|
|
|
110
126
|
}
|
|
111
127
|
|
|
@@ -17,7 +17,7 @@ class _Control(BaseSprite):
|
|
|
17
17
|
|
|
18
18
|
def control_stop_this(self):
|
|
19
19
|
# This is equivalent to return
|
|
20
|
-
|
|
20
|
+
self.code_manager.current_block.running = False
|
|
21
21
|
control_stop_this.opcode="control_stop"
|
|
22
22
|
control_stop_this.param="STOP_OPTION"
|
|
23
23
|
control_stop_this.value="this script"
|
|
@@ -200,7 +200,7 @@ class _LooksSprite(_Looks):
|
|
|
200
200
|
pass
|
|
201
201
|
|
|
202
202
|
def looks_think(self, text):
|
|
203
|
-
self.bubble_manager.
|
|
203
|
+
self.bubble_manager.think(text)
|
|
204
204
|
|
|
205
205
|
def looks_switchcostumeto(self, costume):
|
|
206
206
|
self.costume_manager.switch_costume(costume)
|
|
@@ -23,6 +23,7 @@ class _Motion(_Sensing):
|
|
|
23
23
|
# Position is stored internally as PyGame coordinates
|
|
24
24
|
# and refers to the center of the costume.
|
|
25
25
|
self._pos = pygame.Vector2(0, 0)
|
|
26
|
+
self.draggable = False
|
|
26
27
|
self.motion_setx(0)
|
|
27
28
|
self.motion_sety(0)
|
|
28
29
|
|
|
@@ -61,7 +62,7 @@ class _Motion(_Sensing):
|
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
def motion_goto_sprite(self, sprite):
|
|
64
|
-
self.motion_gotoxy(sprite.
|
|
65
|
+
self.motion_gotoxy(sprite.x_position(), sprite.y_position())
|
|
65
66
|
|
|
66
67
|
motion_goto_sprite.opcode = "motion_goto"
|
|
67
68
|
|
|
@@ -16,15 +16,28 @@ class _Operators():
|
|
|
16
16
|
'sqrt': math.sqrt,
|
|
17
17
|
'sin': lambda n: math.sin((math.pi * n) / 180),
|
|
18
18
|
'cos': lambda n: math.cos((math.pi * n) / 180),
|
|
19
|
-
'tan': math.tan,
|
|
19
|
+
'tan': lambda n: math.tan((math.pi * n) / 180) if not n == 90 else float("inf"),
|
|
20
20
|
'asin': lambda n: (math.asin(n) * 180) / math.pi,
|
|
21
21
|
'acos': lambda n: (math.acos(n) * 180) / math.pi,
|
|
22
22
|
'atan': lambda n: (math.atan(n) * 180) / math.pi,
|
|
23
|
-
'ln': math.log,
|
|
24
|
-
'log': math.
|
|
23
|
+
'ln': lambda n : math.log(n) if not n == 0 else -float("inf"),
|
|
24
|
+
'log': lambda n: math.log10(n) if not n == 0 else -float("inf"),
|
|
25
25
|
'e ^': math.exp,
|
|
26
26
|
'10 ^': lambda n: math.pow(10, n),
|
|
27
27
|
}
|
|
28
28
|
if operator not in ops:
|
|
29
29
|
raise ValueError(f"Operator {operator} not supported.")
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
value = ops[operator](number)
|
|
33
|
+
if value == float("inf"):
|
|
34
|
+
return "Infinity"
|
|
35
|
+
elif value == -float("inf"):
|
|
36
|
+
return "-Infinity"
|
|
37
|
+
# avoid for situations like sin(30) == 0.49999999999999994
|
|
38
|
+
return round(value, 10)
|
|
39
|
+
except ValueError:
|
|
40
|
+
# print(f"ValueError: math domain error: {operator}({number})")
|
|
41
|
+
return "NaN"
|
|
42
|
+
except OverflowError:
|
|
43
|
+
return "Infinity"
|
|
@@ -2,7 +2,8 @@ import pygame
|
|
|
2
2
|
import pystage
|
|
3
3
|
from pystage.core.constants import KEY_MAPPINGS
|
|
4
4
|
from pystage.core._base_sprite import BaseSprite
|
|
5
|
-
|
|
5
|
+
from math import sqrt
|
|
6
|
+
import datetime
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class _Sensing(BaseSprite):
|
|
@@ -43,14 +44,14 @@ class _Sensing(BaseSprite):
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def sensing_setdragmode_draggable(self):
|
|
46
|
-
|
|
47
|
+
self.draggable = True
|
|
47
48
|
sensing_setdragmode_draggable.opcode="sensing_setdragmode"
|
|
48
49
|
sensing_setdragmode_draggable.param="DRAG_MODE"
|
|
49
50
|
sensing_setdragmode_draggable.value="draggable"
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def sensing_setdragmode_notdraggable(self):
|
|
53
|
-
|
|
54
|
+
self.draggable = False
|
|
54
55
|
sensing_setdragmode_notdraggable.opcode="sensing_setdragmode"
|
|
55
56
|
sensing_setdragmode_notdraggable.param="DRAG_MODE"
|
|
56
57
|
sensing_setdragmode_notdraggable.value="not draggable"
|
|
@@ -130,56 +131,68 @@ class _Sensing(BaseSprite):
|
|
|
130
131
|
sensing_of_backdropname.value="backdrop name"
|
|
131
132
|
|
|
132
133
|
def sensing_current_year(self):
|
|
133
|
-
|
|
134
|
+
return datetime.datetime.now().year
|
|
134
135
|
sensing_current_year.opcode="sensing_current"
|
|
135
136
|
sensing_current_year.param="CURRENTMENU"
|
|
136
137
|
sensing_current_year.value="YEAR"
|
|
137
138
|
|
|
138
139
|
|
|
139
140
|
def sensing_current_month(self):
|
|
140
|
-
|
|
141
|
+
return datetime.datetime.now().month
|
|
141
142
|
sensing_current_month.opcode="sensing_current"
|
|
142
143
|
sensing_current_month.param="CURRENTMENU"
|
|
143
144
|
sensing_current_month.value="MONTH"
|
|
144
145
|
|
|
145
146
|
|
|
146
147
|
def sensing_current_date(self):
|
|
147
|
-
|
|
148
|
+
return datetime.datetime.now().day
|
|
148
149
|
sensing_current_date.opcode="sensing_current"
|
|
149
150
|
sensing_current_date.param="CURRENTMENU"
|
|
150
151
|
sensing_current_date.value="DATE"
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
def sensing_current_dayofweek(self):
|
|
154
|
-
|
|
155
|
+
current_datetime = datetime.datetime.now().date()
|
|
156
|
+
#This leads to a Sunday=1 based counting
|
|
157
|
+
day_of_week = (current_datetime.isoweekday() % 7) + 1
|
|
158
|
+
return day_of_week
|
|
155
159
|
sensing_current_dayofweek.opcode="sensing_current"
|
|
156
160
|
sensing_current_dayofweek.param="CURRENTMENU"
|
|
157
161
|
sensing_current_dayofweek.value="DAYOFWEEK"
|
|
158
162
|
|
|
159
163
|
|
|
160
164
|
def sensing_current_hour(self):
|
|
161
|
-
|
|
165
|
+
return datetime.datetime.now().hour
|
|
162
166
|
sensing_current_hour.opcode="sensing_current"
|
|
163
167
|
sensing_current_hour.param="CURRENTMENU"
|
|
164
168
|
sensing_current_hour.value="HOUR"
|
|
165
169
|
|
|
166
170
|
|
|
167
171
|
def sensing_current_minute(self):
|
|
168
|
-
|
|
172
|
+
return datetime.datetime.now().minute
|
|
169
173
|
sensing_current_minute.opcode="sensing_current"
|
|
170
174
|
sensing_current_minute.param="CURRENTMENU"
|
|
171
175
|
sensing_current_minute.value="MINUTE"
|
|
172
176
|
|
|
173
177
|
|
|
174
178
|
def sensing_current_second(self):
|
|
175
|
-
|
|
179
|
+
return datetime.datetime.now().second
|
|
176
180
|
sensing_current_second.opcode="sensing_current"
|
|
177
181
|
sensing_current_second.param="CURRENTMENU"
|
|
178
182
|
sensing_current_second.value="SECOND"
|
|
179
183
|
|
|
180
184
|
|
|
181
185
|
def sensing_dayssince2000(self):
|
|
182
|
-
|
|
186
|
+
# use datetime to find days since 2000 in timezone
|
|
187
|
+
start_date = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
|
|
188
|
+
current_datetime = datetime.datetime.now(datetime.timezone.utc)
|
|
189
|
+
time_difference = current_datetime - start_date
|
|
190
|
+
days = time_difference.days
|
|
191
|
+
# find fraction of current day like in Scratch
|
|
192
|
+
decimal_in_day_string = (current_datetime.hour * 3600 + current_datetime.minute * 60 + current_datetime.second) / (24 * 3600)
|
|
193
|
+
# put full days and part of day together
|
|
194
|
+
since_2000 = days + decimal_in_day_string
|
|
195
|
+
return since_2000
|
|
183
196
|
|
|
184
197
|
def sensing_username(self):
|
|
185
198
|
# Makes not a lot of sense, maybe for compatibility?
|
|
@@ -203,6 +216,7 @@ class _SensingSprite(BaseSprite):
|
|
|
203
216
|
sensing_touchingobject_edge.value="_edge_"
|
|
204
217
|
|
|
205
218
|
def sensing_touchingobject_sprite(self, sprite):
|
|
219
|
+
sprite = sprite._core
|
|
206
220
|
if sprite.rect.colliderect(self.rect):
|
|
207
221
|
offset = (self.rect.left - sprite.rect.left, self.rect.top - sprite.rect.top)
|
|
208
222
|
return sprite.mask.overlap(self.mask, offset) is not None
|
|
@@ -217,13 +231,21 @@ class _SensingSprite(BaseSprite):
|
|
|
217
231
|
pass
|
|
218
232
|
|
|
219
233
|
def sensing_distanceto_pointer(self):
|
|
220
|
-
|
|
234
|
+
mx, my = pygame.mouse.get_pos()
|
|
235
|
+
# gets position of sprite
|
|
236
|
+
sprite_rect = self.rect
|
|
237
|
+
# Calculate the distance between the sprite and the mouse pointer
|
|
238
|
+
distance = sqrt((sprite_rect.centerx - mx)**2 + (sprite_rect.centery - my)**2)
|
|
239
|
+
return distance
|
|
240
|
+
|
|
221
241
|
sensing_distanceto_pointer.opcode="sensing_distanceto"
|
|
222
242
|
sensing_distanceto_pointer.param="DISTANCETOMENU"
|
|
223
243
|
sensing_distanceto_pointer.value="_mouse_"
|
|
224
244
|
|
|
225
245
|
def sensing_distanceto_sprite(self, sprite):
|
|
226
|
-
|
|
246
|
+
# self is an instance of CoreSprite
|
|
247
|
+
# sprite is an instance of Sprite
|
|
248
|
+
return self._pos.distance_to(sprite._core._pos)
|
|
227
249
|
sensing_distanceto_sprite.opcode="sensing_distanceto"
|
|
228
250
|
|
|
229
251
|
|
|
@@ -183,11 +183,16 @@ class BubbleManager():
|
|
|
183
183
|
|
|
184
184
|
def say(self, text: str, border=Bubble.SAY):
|
|
185
185
|
# print(text)
|
|
186
|
+
if isinstance(text, (int, float)):
|
|
187
|
+
text = str(round(text, 2) if text % 1 > 0 else int(text))
|
|
186
188
|
if self.bubble:
|
|
187
189
|
self.bubble.kill()
|
|
188
190
|
self.bubble = None
|
|
189
191
|
if text is not None and text.strip() != "":
|
|
190
192
|
self.bubble = Bubble(self.sprite, text, border)
|
|
191
193
|
self.sprite.stage.bubbles.add(self.bubble)
|
|
194
|
+
|
|
195
|
+
def think(self, text: str):
|
|
196
|
+
self.say(text, Bubble.THINK)
|
|
192
197
|
|
|
193
198
|
|
|
@@ -50,8 +50,9 @@ class SpriteGroup(pygame.sprite.OrderedUpdates):
|
|
|
50
50
|
new_index = index + value
|
|
51
51
|
if new_index < 0:
|
|
52
52
|
new_index = 0
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
# because the sprite was already removed above, so don't need to minus 1 for the length
|
|
54
|
+
if new_index > len(self._spritelist):
|
|
55
|
+
new_index = len(self._spritelist)
|
|
55
56
|
self._spritelist.insert(new_index, sprite)
|
|
56
57
|
# print(self._spritelist)
|
|
57
58
|
|
|
@@ -141,6 +142,9 @@ class CoreStage(
|
|
|
141
142
|
|
|
142
143
|
def _update(self, dt):
|
|
143
144
|
self.code_manager._update(dt)
|
|
145
|
+
# Update it so that the layering is correct while drawing
|
|
146
|
+
# otherwise the no effect for to_front, to_back, etc.
|
|
147
|
+
self._update_visible()
|
|
144
148
|
|
|
145
149
|
def _draw(self, surface: pygame.Surface):
|
|
146
150
|
surface.fill(self.background_color)
|
|
@@ -155,6 +159,11 @@ class CoreStage(
|
|
|
155
159
|
This runs the game loop
|
|
156
160
|
"""
|
|
157
161
|
dt = 0
|
|
162
|
+
|
|
163
|
+
# maybe we should move this to other place
|
|
164
|
+
left_clicking_and_holding = False
|
|
165
|
+
clicked_sprite = None
|
|
166
|
+
|
|
158
167
|
while self.running:
|
|
159
168
|
for event in pygame.event.get():
|
|
160
169
|
if event.type == pygame.QUIT:
|
|
@@ -166,9 +175,11 @@ class CoreStage(
|
|
|
166
175
|
for sprite in self.sprites:
|
|
167
176
|
assert(isinstance(sprite, CoreSprite))
|
|
168
177
|
sprite.code_manager.process_key_pressed(event.key)
|
|
169
|
-
|
|
178
|
+
# to make it consistent with Scratch, should be BUTTONDOWN
|
|
179
|
+
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
170
180
|
pos = pygame.Vector2(pygame.mouse.get_pos())
|
|
171
|
-
|
|
181
|
+
# [-1:0:-1] will reverse the list, but not include 0
|
|
182
|
+
for sprite in self.visible_sprites.sprites()[-1::-1]:
|
|
172
183
|
assert(isinstance(sprite, CoreSprite))
|
|
173
184
|
if sprite.rect.collidepoint(pos):
|
|
174
185
|
internal_pos = pos - sprite.rect.topleft
|
|
@@ -180,6 +191,27 @@ class CoreStage(
|
|
|
180
191
|
sprite.code_manager.process_click()
|
|
181
192
|
break
|
|
182
193
|
|
|
194
|
+
# with these three conditions, we can detect a click and hold,
|
|
195
|
+
# but not move the mouse while holding
|
|
196
|
+
if event.type == pygame.MOUSEBUTTONDOWN and not left_clicking_and_holding and pygame.mouse.get_pressed()[0]:
|
|
197
|
+
pos = pygame.mouse.get_pos()
|
|
198
|
+
for sprite in filter(lambda s:s.draggable, self.visible_sprites.sprites()[-1::-1]):
|
|
199
|
+
assert(isinstance(sprite, CoreSprite))
|
|
200
|
+
sprite_rect = sprite.image.get_rect()
|
|
201
|
+
sprite_rect.topleft = sprite.rect.topleft
|
|
202
|
+
if sprite_rect.collidepoint(pos):
|
|
203
|
+
clicked_pos = pos[0] - sprite_rect.left, pos[1] - sprite_rect.top
|
|
204
|
+
if sprite.image.get_at(clicked_pos).a != 0:
|
|
205
|
+
left_clicking_and_holding = True
|
|
206
|
+
clicked_sprite = sprite
|
|
207
|
+
|
|
208
|
+
if not pygame.mouse.get_pressed()[0] and left_clicking_and_holding:
|
|
209
|
+
left_clicking_and_holding = False
|
|
210
|
+
|
|
211
|
+
if left_clicking_and_holding:
|
|
212
|
+
assert isinstance(clicked_sprite, CoreSprite)
|
|
213
|
+
clicked_sprite.motion_goto_pointer()
|
|
214
|
+
|
|
183
215
|
# Handle broadcast messages
|
|
184
216
|
for message in self.message_broker.get_messages():
|
|
185
217
|
for sprite in self.sprites:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pystage
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
4
|
Summary: Scratch-like Python programming
|
|
5
5
|
Home-page: https://github.com/pystage/pystage
|
|
6
6
|
Author: Kai Eckert
|
|
7
|
-
Author-email: eckert@
|
|
7
|
+
Author-email: k.eckert@hs-mannheim.de
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Bug Tracker, https://github.com/pystage/pystage/issues
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,6 +13,9 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.6
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pygame
|
|
17
|
+
Requires-Dist: jinja2
|
|
18
|
+
Requires-Dist: svglib
|
|
16
19
|
|
|
17
20
|
# PyStage
|
|
18
21
|
|
|
@@ -89,6 +92,15 @@ And here is the result:
|
|
|
89
92
|
# Installation
|
|
90
93
|
```pip install pystage```
|
|
91
94
|
|
|
95
|
+
**if doesn't work, try following commands one by one.**
|
|
96
|
+
```bash
|
|
97
|
+
pip install wheel
|
|
98
|
+
pip install pygame==2.3.0
|
|
99
|
+
pip install svglib==1.1.0
|
|
100
|
+
pip install reportlab==3.5.67
|
|
101
|
+
pip install pystage
|
|
102
|
+
```
|
|
103
|
+
|
|
92
104
|
# Getting started
|
|
93
105
|
|
|
94
106
|
If you are interested in PyStage and want to get in touch, feel free to join us on our [Discord Server](https://discord.gg/MSGrs3UCCz).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|