sonolus.py 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (42) hide show
  1. sonolus/backend/optimize/constant_evaluation.py +2 -2
  2. sonolus/backend/optimize/optimize.py +12 -4
  3. sonolus/backend/optimize/passes.py +2 -1
  4. sonolus/backend/place.py +95 -14
  5. sonolus/backend/visitor.py +51 -3
  6. sonolus/build/cli.py +60 -9
  7. sonolus/build/collection.py +68 -26
  8. sonolus/build/compile.py +85 -27
  9. sonolus/build/engine.py +166 -40
  10. sonolus/build/node.py +8 -1
  11. sonolus/build/project.py +30 -11
  12. sonolus/script/archetype.py +110 -26
  13. sonolus/script/array.py +11 -0
  14. sonolus/script/debug.py +2 -2
  15. sonolus/script/effect.py +2 -2
  16. sonolus/script/engine.py +123 -15
  17. sonolus/script/internal/builtin_impls.py +21 -2
  18. sonolus/script/internal/constant.py +5 -1
  19. sonolus/script/internal/context.py +30 -25
  20. sonolus/script/internal/math_impls.py +2 -1
  21. sonolus/script/internal/transient.py +4 -0
  22. sonolus/script/internal/value.py +6 -0
  23. sonolus/script/interval.py +16 -0
  24. sonolus/script/iterator.py +17 -0
  25. sonolus/script/level.py +113 -10
  26. sonolus/script/metadata.py +32 -0
  27. sonolus/script/num.py +9 -0
  28. sonolus/script/options.py +5 -3
  29. sonolus/script/pointer.py +2 -0
  30. sonolus/script/project.py +41 -5
  31. sonolus/script/record.py +7 -2
  32. sonolus/script/runtime.py +61 -10
  33. sonolus/script/sprite.py +18 -1
  34. sonolus/script/ui.py +7 -3
  35. sonolus/script/values.py +8 -5
  36. sonolus/script/vec.py +28 -0
  37. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
  38. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/RECORD +42 -41
  39. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
  40. /sonolus/script/{print.py → printing.py} +0 -0
  41. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
  42. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ from abc import abstractmethod
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
7
  from enum import Enum, StrEnum
@@ -177,6 +178,23 @@ def imported(*, name: str | None = None) -> Any:
177
178
  return _ArchetypeFieldInfo(name, _StorageType.IMPORTED)
178
179
 
179
180
 
181
+ def entity_data() -> Any:
182
+ """Declare a field as entity data.
183
+
184
+ Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
185
+ and is read-only in other callbacks.
186
+
187
+ It functions like `imported`, except that it is not loaded from the level data.
188
+
189
+ Usage:
190
+ ```
191
+ class MyArchetype(PlayArchetype):
192
+ field: int = entity_data()
193
+ ```
194
+ """
195
+ return _ArchetypeFieldInfo(None, _StorageType.IMPORTED)
196
+
197
+
180
198
  def exported(*, name: str | None = None) -> Any:
181
199
  """Declare a field as exported.
182
200
 
@@ -418,7 +436,11 @@ class _BaseArchetype:
418
436
  bound.apply_defaults()
419
437
  data = []
420
438
  for field in cls._memory_fields_.values():
421
- data.extend(field.type._accept_(bound.arguments[field.name] or zeros(field.type))._to_list_())
439
+ data.extend(
440
+ field.type._accept_(
441
+ bound.arguments[field.name] if field.name in bound.arguments else zeros(field.type)
442
+ )._to_list_()
443
+ )
422
444
  native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
423
445
 
424
446
  @classmethod
@@ -446,9 +468,7 @@ class _BaseArchetype:
446
468
  raise TypeError("Cannot directly subclass Archetype, use the Archetype subclass for your mode")
447
469
  cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
448
470
  return
449
- if getattr(cls, "_callbacks_", None) is not None:
450
- raise TypeError("Cannot subclass Archetypes")
451
- if cls.name is None:
471
+ if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
452
472
  cls.name = cls.__name__
453
473
  cls._callbacks_ = []
454
474
  for name in cls._supported_callbacks_:
@@ -463,17 +483,32 @@ class _BaseArchetype:
463
483
  if cls._field_init_done:
464
484
  return
465
485
  cls._field_init_done = True
486
+ for mro_entry in cls.mro()[1:]:
487
+ if hasattr(mro_entry, "_field_init_done"):
488
+ mro_entry._init_fields()
466
489
  field_specifiers = get_field_specifiers(
467
490
  cls, skip={"name", "is_scored", "_callbacks_", "_field_init_done"}
468
491
  ).items()
469
- cls._imported_fields_ = {}
470
- cls._exported_fields_ = {}
471
- cls._memory_fields_ = {}
472
- cls._shared_memory_fields_ = {}
473
- imported_offset = 0
474
- exported_offset = 0
475
- memory_offset = 0
476
- shared_memory_offset = 0
492
+ if not hasattr(cls, "_imported_fields_"):
493
+ cls._imported_fields_ = {}
494
+ else:
495
+ cls._imported_fields_ = {**cls._imported_fields_}
496
+ if not hasattr(cls, "_exported_fields_"):
497
+ cls._exported_fields_ = {}
498
+ else:
499
+ cls._exported_fields_ = {**cls._exported_fields_}
500
+ if not hasattr(cls, "_memory_fields_"):
501
+ cls._memory_fields_ = {}
502
+ else:
503
+ cls._memory_fields_ = {**cls._memory_fields_}
504
+ if not hasattr(cls, "_shared_memory_fields_"):
505
+ cls._shared_memory_fields_ = {}
506
+ else:
507
+ cls._shared_memory_fields_ = {**cls._shared_memory_fields_}
508
+ imported_offset = sum(field.type._size_() for field in cls._imported_fields_.values())
509
+ exported_offset = sum(field.type._size_() for field in cls._exported_fields_.values())
510
+ memory_offset = sum(field.type._size_() for field in cls._memory_fields_.values())
511
+ shared_memory_offset = sum(field.type._size_() for field in cls._shared_memory_fields_.values())
477
512
  for name, value in field_specifiers:
478
513
  if value is ClassVar or get_origin(value) is ClassVar:
479
514
  continue
@@ -504,24 +539,32 @@ class _BaseArchetype:
504
539
  name, field_info.name or name, field_info.storage, imported_offset, field_type
505
540
  )
506
541
  imported_offset += field_type._size_()
542
+ if imported_offset > _ENTITY_DATA_SIZE:
543
+ raise ValueError("Imported fields exceed entity data size")
507
544
  setattr(cls, name, cls._imported_fields_[name])
508
545
  case _StorageType.EXPORTED:
509
546
  cls._exported_fields_[name] = _ArchetypeField(
510
547
  name, field_info.name or name, field_info.storage, exported_offset, field_type
511
548
  )
512
549
  exported_offset += field_type._size_()
550
+ if exported_offset > _ENTITY_DATA_SIZE:
551
+ raise ValueError("Exported fields exceed entity data size")
513
552
  setattr(cls, name, cls._exported_fields_[name])
514
553
  case _StorageType.MEMORY:
515
554
  cls._memory_fields_[name] = _ArchetypeField(
516
555
  name, field_info.name or name, field_info.storage, memory_offset, field_type
517
556
  )
518
557
  memory_offset += field_type._size_()
558
+ if memory_offset > _ENTITY_MEMORY_SIZE:
559
+ raise ValueError("Memory fields exceed entity memory size")
519
560
  setattr(cls, name, cls._memory_fields_[name])
520
561
  case _StorageType.SHARED:
521
562
  cls._shared_memory_fields_[name] = _ArchetypeField(
522
563
  name, field_info.name or name, field_info.storage, shared_memory_offset, field_type
523
564
  )
524
565
  shared_memory_offset += field_type._size_()
566
+ if shared_memory_offset > _ENTITY_SHARED_MEMORY_SIZE:
567
+ raise ValueError("Shared memory fields exceed entity shared memory size")
525
568
  setattr(cls, name, cls._shared_memory_fields_[name])
526
569
  cls._imported_keys_ = {
527
570
  name: i
@@ -542,6 +585,30 @@ class _BaseArchetype:
542
585
  [inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
543
586
  )
544
587
 
588
+ @property
589
+ @abstractmethod
590
+ def index(self) -> int:
591
+ """The index of this entity."""
592
+ raise NotImplementedError
593
+
594
+ @meta_fn
595
+ def ref(self) -> EntityRef[Self]:
596
+ """Get a reference to this entity.
597
+
598
+ Valid both in level data and in callbacks.
599
+ """
600
+ match self._data_:
601
+ case _ArchetypeSelfData():
602
+ return EntityRef[type(self)](index=self.index)
603
+ case _ArchetypeReferenceData(index=index):
604
+ return EntityRef[type(self)](index=index)
605
+ case _ArchetypeLevelData():
606
+ result = EntityRef[type(self)](index=-1)
607
+ result._ref_ = self
608
+ return result
609
+ case _:
610
+ raise RuntimeError("Invalid entity data")
611
+
545
612
 
546
613
  class PlayArchetype(_BaseArchetype):
547
614
  """Base class for play mode archetypes.
@@ -682,6 +749,7 @@ class PlayArchetype(_BaseArchetype):
682
749
  return self._info.state == 2
683
750
 
684
751
  @property
752
+ @meta_fn
685
753
  def life(self) -> ArchetypeLife:
686
754
  """How this entity contributes to life."""
687
755
  if not ctx():
@@ -693,6 +761,7 @@ class PlayArchetype(_BaseArchetype):
693
761
  raise RuntimeError("Life is not available in level data")
694
762
 
695
763
  @property
764
+ @meta_fn
696
765
  def result(self) -> PlayEntityInput:
697
766
  """The result of this entity.
698
767
 
@@ -706,17 +775,6 @@ class PlayArchetype(_BaseArchetype):
706
775
  case _:
707
776
  raise RuntimeError("Result is only accessible from the entity itself")
708
777
 
709
- def ref(self):
710
- """Get a reference to this entity for creating level data.
711
-
712
- Not valid elsewhere.
713
- """
714
- if not isinstance(self._data_, _ArchetypeLevelData):
715
- raise RuntimeError("Entity is not level data")
716
- result = EntityRef[type(self)](index=-1)
717
- result._ref_ = self
718
- return result
719
-
720
778
 
721
779
  class WatchArchetype(_BaseArchetype):
722
780
  """Base class for watch mode archetypes.
@@ -801,6 +859,7 @@ class WatchArchetype(_BaseArchetype):
801
859
  return self._info.state == 1
802
860
 
803
861
  @property
862
+ @meta_fn
804
863
  def life(self) -> ArchetypeLife:
805
864
  """How this entity contributes to life."""
806
865
  if not ctx():
@@ -812,6 +871,7 @@ class WatchArchetype(_BaseArchetype):
812
871
  raise RuntimeError("Life is not available in level data")
813
872
 
814
873
  @property
874
+ @meta_fn
815
875
  def result(self) -> WatchEntityInput:
816
876
  """The result of this entity.
817
877
 
@@ -857,6 +917,7 @@ class PreviewArchetype(_BaseArchetype):
857
917
  """
858
918
 
859
919
  @property
920
+ @meta_fn
860
921
  def _info(self) -> PreviewEntityInfo:
861
922
  if not ctx():
862
923
  raise RuntimeError("Calling info is only allowed within a callback")
@@ -974,21 +1035,35 @@ class WatchEntityInput(Record):
974
1035
 
975
1036
 
976
1037
  class EntityRef[A: _BaseArchetype](Record):
977
- """Reference to another entity."""
1038
+ """Reference to another entity.
1039
+
1040
+ May be used with `Any` to reference an unknown archetype.
1041
+
1042
+ Usage:
1043
+ ```
1044
+ class MyArchetype(PlayArchetype):
1045
+ ref_1: EntityRef[OtherArchetype] = imported()
1046
+ ref_2: EntityRef[Any] = imported()
1047
+ ```
1048
+ """
978
1049
 
979
1050
  index: int
980
1051
 
981
1052
  @classmethod
982
1053
  def archetype(cls) -> type[A]:
1054
+ """Get the archetype type."""
983
1055
  return cls.type_var_value(A)
984
1056
 
985
1057
  def with_archetype(self, archetype: type[A]) -> EntityRef[A]:
1058
+ """Return a new reference with the given archetype type."""
986
1059
  return EntityRef[archetype](index=self.index)
987
1060
 
988
1061
  def get(self) -> A:
1062
+ """Get the entity."""
989
1063
  return self.archetype().at(self.index)
990
1064
 
991
- def is_valid(self) -> bool:
1065
+ def archetype_matches(self) -> bool:
1066
+ """Check if entity at the index is precisely of the archetype."""
992
1067
  return self.index >= 0 and self.archetype().is_at(self.index)
993
1068
 
994
1069
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
@@ -1000,9 +1075,18 @@ class EntityRef[A: _BaseArchetype](Record):
1000
1075
  raise KeyError("Reference to entity not in level data")
1001
1076
  return [level_refs[ref]]
1002
1077
 
1078
+ def _copy_from_(self, value: Self):
1079
+ super()._copy_from_(value)
1080
+ if hasattr(value, "_ref_"):
1081
+ self._ref_ = value._ref_
1082
+
1003
1083
  @classmethod
1004
1084
  def _accepts_(cls, value: Any) -> bool:
1005
- return super()._accepts_(value) or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
1085
+ return (
1086
+ super()._accepts_(value)
1087
+ or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
1088
+ or (issubclass(type(value), EntityRef) and issubclass(value.archetype(), cls.archetype()))
1089
+ )
1006
1090
 
1007
1091
  @classmethod
1008
1092
  def _accept_(cls, value: Any) -> Self:
sonolus/script/array.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  from collections.abc import Iterable
5
5
  from typing import Any, Self, final
6
6
 
7
+ from sonolus.backend.ir import IRConst, IRSet
7
8
  from sonolus.backend.place import BlockPlace
8
9
  from sonolus.script.array_like import ArrayLike
9
10
  from sonolus.script.debug import assert_unreachable
@@ -159,6 +160,16 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
159
160
  else:
160
161
  return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
161
162
 
163
+ @classmethod
164
+ def _zero_(cls) -> Self:
165
+ if ctx():
166
+ place = ctx().alloc(size=cls._size_())
167
+ result: Self = cls._from_place_(place)
168
+ ctx().add_statements(*[IRSet(place.add_offset(i), IRConst(0)) for i in range(cls._size_())])
169
+ return result
170
+ else:
171
+ return cls._with_value([cls.element_type()._zero_() for _ in range(cls.size())])
172
+
162
173
  def __len__(self):
163
174
  return self.size()
164
175
 
sonolus/script/debug.py CHANGED
@@ -1,4 +1,4 @@
1
- from collections.abc import Callable
1
+ from collections.abc import Callable, Sequence
2
2
  from contextvars import ContextVar
3
3
  from typing import Any, Never
4
4
 
@@ -75,7 +75,7 @@ def terminate():
75
75
  raise RuntimeError("Terminated")
76
76
 
77
77
 
78
- def visualize_cfg(fn: Callable[[], Any], passes: list[CompilerPass] | None = None) -> str:
78
+ def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
79
79
  from sonolus.build.compile import callback_to_cfg
80
80
 
81
81
  if passes is None:
sonolus/script/effect.py CHANGED
@@ -25,7 +25,7 @@ class Effect(Record):
25
25
  """Return whether the effect clip is available."""
26
26
  return _has_effect_clip(self.id)
27
27
 
28
- def play(self, distance: float) -> None:
28
+ def play(self, distance: float = 0) -> None:
29
29
  """Play the effect clip.
30
30
 
31
31
  If the clip was already played within the specified distance, it will be skipped.
@@ -35,7 +35,7 @@ class Effect(Record):
35
35
  """
36
36
  _play(self.id, distance)
37
37
 
38
- def schedule(self, time: float, distance: float) -> None:
38
+ def schedule(self, time: float, distance: float = 0) -> None:
39
39
  """Schedule the effect clip to play at a specific time.
40
40
 
41
41
  This is not suitable for real-time effects such as responses to user input. Use `play` instead.
sonolus/script/engine.py CHANGED
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from collections.abc import Callable
5
+ from os import PathLike
6
+ from pathlib import Path
4
7
  from typing import Any
5
8
 
6
- from sonolus.build.collection import Asset
9
+ from sonolus.build.collection import Asset, load_asset
7
10
  from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
8
11
  from sonolus.script.bucket import Buckets, EmptyBuckets
9
12
  from sonolus.script.effect import Effects, EmptyEffects
@@ -13,12 +16,52 @@ from sonolus.script.instruction import (
13
16
  TutorialInstructionIcons,
14
17
  TutorialInstructions,
15
18
  )
19
+ from sonolus.script.metadata import AnyText, Tag, as_localization_text
16
20
  from sonolus.script.options import EmptyOptions, Options
17
21
  from sonolus.script.particle import EmptyParticles, Particles
18
22
  from sonolus.script.sprite import EmptySkin, Skin
19
23
  from sonolus.script.ui import UiConfig
20
24
 
21
25
 
26
+ class ExportedEngine:
27
+ """An exported Sonolus.py engine."""
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ item: dict,
33
+ thumbnail: bytes,
34
+ play_data: bytes,
35
+ watch_data: bytes,
36
+ preview_data: bytes,
37
+ tutorial_data: bytes,
38
+ rom: bytes | None = None,
39
+ configuration: bytes,
40
+ ) -> None:
41
+ self.item = item
42
+ self.thumbnail = thumbnail
43
+ self.play_data = play_data
44
+ self.watch_data = watch_data
45
+ self.preview_data = preview_data
46
+ self.tutorial_data = tutorial_data
47
+ self.rom = rom
48
+ self.configuration = configuration
49
+
50
+ def write_to_dir(self, path: PathLike):
51
+ """Write the exported engine to a directory."""
52
+ path = Path(path)
53
+ path.mkdir(parents=True, exist_ok=True)
54
+ (path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
55
+ (path / "thumbnail").write_bytes(self.thumbnail)
56
+ (path / "playData").write_bytes(self.play_data)
57
+ (path / "watchData").write_bytes(self.watch_data)
58
+ (path / "previewData").write_bytes(self.preview_data)
59
+ (path / "tutorialData").write_bytes(self.tutorial_data)
60
+ if self.rom is not None:
61
+ (path / "rom").write_bytes(self.rom)
62
+ (path / "configuration").write_bytes(self.configuration)
63
+
64
+
22
65
  class Engine:
23
66
  """A Sonolus.py engine.
24
67
 
@@ -33,6 +76,9 @@ class Engine:
33
76
  particle: The default particle for the engine.
34
77
  thumbnail: The thumbnail for the engine.
35
78
  data: The engine's modes and configurations.
79
+ tags: The tags of the engine.
80
+ description: The description of the engine.
81
+ meta: Additional metadata of the engine.
36
82
  """
37
83
 
38
84
  version = 12
@@ -41,26 +87,68 @@ class Engine:
41
87
  self,
42
88
  *,
43
89
  name: str,
44
- title: str | None = None,
45
- subtitle: str = "Sonolus.py Engine",
46
- author: str = "Unknown",
90
+ title: AnyText | None = None,
91
+ subtitle: AnyText = "Sonolus.py Engine",
92
+ author: AnyText = "Unknown",
47
93
  skin: str | None = None,
48
94
  background: str | None = None,
49
95
  effect: str | None = None,
50
96
  particle: str | None = None,
51
97
  thumbnail: Asset | None = None,
52
98
  data: EngineData,
99
+ tags: list[Tag] | None = None,
100
+ description: AnyText | None = None,
101
+ meta: Any = None,
53
102
  ) -> None:
54
103
  self.name = name
55
- self.title = title or name
56
- self.subtitle = subtitle
57
- self.author = author
104
+ self.title = as_localization_text(title or name)
105
+ self.subtitle = as_localization_text(subtitle)
106
+ self.author = as_localization_text(author)
58
107
  self.skin = skin
59
108
  self.background = background
60
109
  self.effect = effect
61
110
  self.particle = particle
62
111
  self.thumbnail = thumbnail
63
112
  self.data = data
113
+ self.tags = tags or []
114
+ self.description = as_localization_text(description) if description is not None else None
115
+ self.meta = meta
116
+
117
+ def export(self) -> ExportedEngine:
118
+ """Export the engine in a sonolus-pack compatible format.
119
+
120
+ Returns:
121
+ An exported engine.
122
+ """
123
+ from sonolus.build.engine import package_engine
124
+ from sonolus.build.project import BLANK_PNG
125
+
126
+ item = {
127
+ "version": self.version,
128
+ "title": self.title,
129
+ "subtitle": self.subtitle,
130
+ "author": self.author,
131
+ "tags": [tag.as_dict() for tag in self.tags],
132
+ "skin": self.skin,
133
+ "background": self.background,
134
+ "effect": self.effect,
135
+ "particle": self.particle,
136
+ }
137
+ packaged = package_engine(self.data)
138
+ if self.description is not None:
139
+ item["description"] = self.description
140
+ if self.meta is not None:
141
+ item["meta"] = self.meta
142
+ return ExportedEngine(
143
+ item=item,
144
+ thumbnail=load_asset(self.thumbnail) if self.thumbnail is not None else BLANK_PNG,
145
+ play_data=packaged.play_data,
146
+ watch_data=packaged.watch_data,
147
+ preview_data=packaged.preview_data,
148
+ tutorial_data=packaged.tutorial_data,
149
+ rom=packaged.rom,
150
+ configuration=packaged.configuration,
151
+ )
64
152
 
65
153
 
66
154
  def default_callback() -> Any:
@@ -190,6 +278,30 @@ class TutorialMode:
190
278
  self.update = update
191
279
 
192
280
 
281
+ def empty_play_mode() -> PlayMode:
282
+ """Create an empty play mode."""
283
+ return PlayMode()
284
+
285
+
286
+ def empty_watch_mode() -> WatchMode:
287
+ """Create an empty watch mode."""
288
+ return WatchMode(update_spawn=default_callback)
289
+
290
+
291
+ def empty_preview_mode() -> PreviewMode:
292
+ """Create an empty preview mode."""
293
+ return PreviewMode()
294
+
295
+
296
+ def empty_tutorial_mode() -> TutorialMode:
297
+ """Create an empty tutorial mode."""
298
+ return TutorialMode(
299
+ preprocess=default_callback,
300
+ navigate=default_callback,
301
+ update=default_callback,
302
+ )
303
+
304
+
193
305
  class EngineData:
194
306
  """A Sonolus.py engine's modes and configurations.
195
307
 
@@ -214,11 +326,7 @@ class EngineData:
214
326
  ) -> None:
215
327
  self.ui = ui or UiConfig()
216
328
  self.options = options
217
- self.play = play or PlayMode()
218
- self.watch = watch or WatchMode(update_spawn=default_callback)
219
- self.preview = preview or PreviewMode()
220
- self.tutorial = tutorial or TutorialMode(
221
- preprocess=default_callback,
222
- navigate=default_callback,
223
- update=default_callback,
224
- )
329
+ self.play = play or empty_play_mode()
330
+ self.watch = watch or empty_watch_mode()
331
+ self.preview = preview or empty_preview_mode()
332
+ self.tutorial = tutorial or empty_tutorial_mode()
@@ -52,9 +52,11 @@ def _enumerate(iterable, start=0):
52
52
  from sonolus.backend.visitor import compile_and_call
53
53
 
54
54
  iterable = validate_value(iterable)
55
- if not hasattr(iterable, "__iter__"):
55
+ if isinstance(iterable, TupleImpl):
56
+ return TupleImpl._accept_(tuple((start + i, value) for i, value in enumerate(iterable._as_py_(), start=start)))
57
+ elif not hasattr(iterable, "__iter__"):
56
58
  raise TypeError(f"'{type(iterable).__name__}' object is not iterable")
57
- if isinstance(iterable, ArrayLike):
59
+ elif isinstance(iterable, ArrayLike):
58
60
  return compile_and_call(iterable._enumerate_, start)
59
61
  else:
60
62
  iterator = compile_and_call(iterable.__iter__)
@@ -232,10 +234,27 @@ _int._type_mapping_ = Num
232
234
  _float._type_mapping_ = Num
233
235
  _bool._type_mapping_ = Num
234
236
 
237
+
238
+ def _any(iterable):
239
+ for value in iterable: # noqa: SIM110
240
+ if value:
241
+ return True
242
+ return False
243
+
244
+
245
+ def _all(iterable):
246
+ for value in iterable: # noqa: SIM110
247
+ if not value:
248
+ return False
249
+ return True
250
+
251
+
235
252
  # classmethod, property, staticmethod are supported as decorators, but not within functions
236
253
 
237
254
  BUILTIN_IMPLS = {
238
255
  id(abs): _abs,
256
+ id(all): _all,
257
+ id(any): _any,
239
258
  id(bool): _bool,
240
259
  id(callable): _callable,
241
260
  id(enumerate): _enumerate,
@@ -107,7 +107,7 @@ class ConstantValue(Value):
107
107
  def _get_(self) -> Self:
108
108
  return self
109
109
 
110
- def _set_(self, value: Any) -> Self:
110
+ def _set_(self, value: Any):
111
111
  if value is not self:
112
112
  raise ValueError(f"{type(self).__name__} is immutable")
113
113
 
@@ -122,6 +122,10 @@ class ConstantValue(Value):
122
122
  def _alloc_(cls) -> Self:
123
123
  return cls()
124
124
 
125
+ @classmethod
126
+ def _zero_(cls) -> Self:
127
+ return cls()
128
+
125
129
  @meta_fn
126
130
  def __eq__(self, other):
127
131
  return self is other