sonolus.py 0.1.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 (75) hide show
  1. sonolus/__init__.py +0 -0
  2. sonolus/backend/__init__.py +0 -0
  3. sonolus/backend/allocate.py +51 -0
  4. sonolus/backend/blocks.py +756 -0
  5. sonolus/backend/excepthook.py +37 -0
  6. sonolus/backend/finalize.py +69 -0
  7. sonolus/backend/flow.py +92 -0
  8. sonolus/backend/interpret.py +333 -0
  9. sonolus/backend/ir.py +89 -0
  10. sonolus/backend/mode.py +24 -0
  11. sonolus/backend/node.py +40 -0
  12. sonolus/backend/ops.py +197 -0
  13. sonolus/backend/optimize.py +9 -0
  14. sonolus/backend/passes.py +6 -0
  15. sonolus/backend/place.py +90 -0
  16. sonolus/backend/simplify.py +30 -0
  17. sonolus/backend/utils.py +48 -0
  18. sonolus/backend/visitor.py +880 -0
  19. sonolus/build/__init__.py +0 -0
  20. sonolus/build/cli.py +170 -0
  21. sonolus/build/collection.py +293 -0
  22. sonolus/build/compile.py +90 -0
  23. sonolus/build/defaults.py +32 -0
  24. sonolus/build/engine.py +149 -0
  25. sonolus/build/level.py +23 -0
  26. sonolus/build/node.py +43 -0
  27. sonolus/build/project.py +94 -0
  28. sonolus/py.typed +0 -0
  29. sonolus/script/__init__.py +0 -0
  30. sonolus/script/archetype.py +651 -0
  31. sonolus/script/array.py +241 -0
  32. sonolus/script/bucket.py +192 -0
  33. sonolus/script/callbacks.py +105 -0
  34. sonolus/script/comptime.py +146 -0
  35. sonolus/script/containers.py +247 -0
  36. sonolus/script/debug.py +70 -0
  37. sonolus/script/effect.py +132 -0
  38. sonolus/script/engine.py +101 -0
  39. sonolus/script/globals.py +234 -0
  40. sonolus/script/graphics.py +141 -0
  41. sonolus/script/icon.py +73 -0
  42. sonolus/script/internal/__init__.py +5 -0
  43. sonolus/script/internal/builtin_impls.py +144 -0
  44. sonolus/script/internal/context.py +365 -0
  45. sonolus/script/internal/descriptor.py +17 -0
  46. sonolus/script/internal/error.py +15 -0
  47. sonolus/script/internal/generic.py +197 -0
  48. sonolus/script/internal/impl.py +69 -0
  49. sonolus/script/internal/introspection.py +14 -0
  50. sonolus/script/internal/native.py +38 -0
  51. sonolus/script/internal/value.py +144 -0
  52. sonolus/script/interval.py +98 -0
  53. sonolus/script/iterator.py +211 -0
  54. sonolus/script/level.py +52 -0
  55. sonolus/script/math.py +92 -0
  56. sonolus/script/num.py +382 -0
  57. sonolus/script/options.py +194 -0
  58. sonolus/script/particle.py +158 -0
  59. sonolus/script/pointer.py +30 -0
  60. sonolus/script/project.py +17 -0
  61. sonolus/script/range.py +58 -0
  62. sonolus/script/record.py +293 -0
  63. sonolus/script/runtime.py +526 -0
  64. sonolus/script/sprite.py +332 -0
  65. sonolus/script/text.py +404 -0
  66. sonolus/script/timing.py +42 -0
  67. sonolus/script/transform.py +118 -0
  68. sonolus/script/ui.py +160 -0
  69. sonolus/script/values.py +43 -0
  70. sonolus/script/vec.py +48 -0
  71. sonolus_py-0.1.0.dist-info/METADATA +10 -0
  72. sonolus_py-0.1.0.dist-info/RECORD +75 -0
  73. sonolus_py-0.1.0.dist-info/WHEEL +4 -0
  74. sonolus_py-0.1.0.dist-info/entry_points.txt +2 -0
  75. sonolus_py-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,247 @@
1
+ from __future__ import annotations
2
+
3
+ from sonolus.script.array import Array
4
+ from sonolus.script.debug import error
5
+ from sonolus.script.iterator import ArrayLike, SonolusIterator
6
+ from sonolus.script.range import Range
7
+ from sonolus.script.record import Record
8
+ from sonolus.script.values import alloc, copy
9
+
10
+
11
+ class Pair[T, U](Record):
12
+ first: T
13
+ second: U
14
+
15
+
16
+ class VarArray[T, Capacity](Record, ArrayLike[T]):
17
+ _size: int
18
+ _array: Array[T, Capacity]
19
+
20
+ @classmethod
21
+ def new(cls):
22
+ element_type = cls._get_type_arg_(T)
23
+ capacity = cls._get_type_arg_(Capacity)
24
+ return cls(0, alloc(Array[element_type, capacity]))
25
+
26
+ def size(self) -> int:
27
+ return self._size
28
+
29
+ @classmethod
30
+ def capacity(cls) -> int:
31
+ return cls._get_type_arg_(Capacity)
32
+
33
+ def is_full(self) -> bool:
34
+ return self._size == self.capacity()
35
+
36
+ def __getitem__(self, item) -> T:
37
+ return self._array[item]
38
+
39
+ def __setitem__(self, key: int, value: T):
40
+ self._array[key] = value
41
+
42
+ def append(self, value: T):
43
+ """Appends a copy of the given value to the end of the array."""
44
+ assert self._size < self._array.size()
45
+ self._array[self._size] = value
46
+ self._size += 1
47
+
48
+ def extend(self, values: ArrayLike[T]):
49
+ """Appends copies of the values in the given array to the end of the array."""
50
+ for value in values:
51
+ self.append(value)
52
+
53
+ def pop(self, index: int | None = None) -> T:
54
+ """Removes and returns a copy of the value at the given index.
55
+
56
+ Preserves the relative order of the elements.
57
+ """
58
+ if index is None:
59
+ index = self._size - 1
60
+ assert 0 <= index < self._size
61
+ value = copy(self._array[index])
62
+ self._size -= 1
63
+ if index < self._size:
64
+ for i in Range(index, self._size):
65
+ self._array[i] = self._array[i + 1]
66
+ return value
67
+
68
+ def insert(self, index: int, value: T):
69
+ """Inserts a copy of the given value at the given index.
70
+
71
+ Preserves the relative order of the elements.
72
+ """
73
+ assert 0 <= index <= self._size
74
+ assert self._size < self._array.size()
75
+ self._size += 1
76
+ for i in Range(self._size - 1, index, -1):
77
+ self._array[i] = self._array[i - 1]
78
+ self._array[index] = value
79
+
80
+ def remove(self, value: T) -> bool:
81
+ """Removes the first occurrence of the given value, returning whether the value was removed.
82
+
83
+ Preserves the relative order of the elements.
84
+ """
85
+ index = self.index_of(value)
86
+ if index < 0:
87
+ return False
88
+ self.pop(index)
89
+ return True
90
+
91
+ def clear(self):
92
+ """Sets size to zero."""
93
+ self._size = 0
94
+
95
+ def set_add(self, value: T) -> bool:
96
+ """Adds a copy of the given value if it is not already present, returning whether the value was added.
97
+
98
+ If the value is already present, the array is not modified.
99
+ If the array is full, the value is not added.
100
+ """
101
+ if self._size >= self._array.size():
102
+ return False
103
+ if value in self:
104
+ return False
105
+ self.append(value)
106
+ return True
107
+
108
+ def set_remove(self, value: T) -> bool:
109
+ """Removes the first occurrence of the given value, returning whether the value was removed.
110
+
111
+ Does not preserve the relative order of the elements.
112
+ """
113
+ index = self.index_of(value)
114
+ if index < 0:
115
+ return False
116
+ if index < self._size - 1:
117
+ self._array[index] = self._array[self._size - 1]
118
+ self._size -= 1
119
+ return True
120
+
121
+ def __eq__(self, other):
122
+ if self.size() != other.size():
123
+ return False
124
+ i = 0
125
+ while i < self.size():
126
+ if self[i] != other[i]:
127
+ return False
128
+ i += 1
129
+ return True
130
+
131
+ def __ne__(self, other):
132
+ return not self == other
133
+
134
+ def __hash__(self):
135
+ raise TypeError("unhashable type: 'VarArray'")
136
+
137
+
138
+ class ArrayMapEntry[K, V](Record):
139
+ key: K
140
+ value: V
141
+
142
+
143
+ class ArrayMap[K, V, Capacity](Record):
144
+ _size: int
145
+ _array: Array[ArrayMapEntry[K, V], Capacity]
146
+
147
+ @classmethod
148
+ def new(cls):
149
+ key_type = cls._get_type_arg_(K)
150
+ value_type = cls._get_type_arg_(V)
151
+ capacity = cls._get_type_arg_(Capacity)
152
+ return cls(0, alloc(Array[ArrayMapEntry[key_type, value_type], capacity]))
153
+
154
+ def size(self) -> int:
155
+ return self._size
156
+
157
+ @classmethod
158
+ def capacity(cls) -> int:
159
+ return cls._get_type_arg_(Capacity)
160
+
161
+ def is_full(self) -> bool:
162
+ return self._size == self.capacity()
163
+
164
+ def keys(self) -> SonolusIterator[K]:
165
+ return _ArrayMapKeyIterator(self, 0)
166
+
167
+ def values(self) -> SonolusIterator[V]:
168
+ return ArrayMapValueIterator(self, 0)
169
+
170
+ def items(self) -> SonolusIterator[tuple[K, V]]:
171
+ return ArrayMapEntryIterator(self, 0)
172
+
173
+ def __getitem__(self, key: K) -> V:
174
+ for i in Range(self._size):
175
+ entry = self._array[i]
176
+ if entry.key == key:
177
+ return entry.value
178
+ error()
179
+
180
+ def __setitem__(self, key: K, value: V):
181
+ for i in Range(self._size):
182
+ entry = self._array[i]
183
+ if entry.key == key:
184
+ entry.value = value
185
+ return
186
+ # assert self._size < self.capacity()
187
+ self._array[self._size] = ArrayMapEntry(key, value)
188
+ self._size += 1
189
+
190
+ def __contains__(self, key: K) -> bool:
191
+ for i in Range(self._size): # noqa: SIM110
192
+ if self._array[i].key == key:
193
+ return True
194
+ return False
195
+
196
+ def pop(self, key: K) -> V:
197
+ for i in Range(self._size):
198
+ entry = self._array[i]
199
+ if entry.key == key:
200
+ value = copy(entry.value)
201
+ self._size -= 1
202
+ if i < self._size:
203
+ self._array[i] = self._array[self._size]
204
+ return value
205
+ error()
206
+
207
+ def clear(self):
208
+ self._size = 0
209
+
210
+
211
+ class _ArrayMapKeyIterator[K, V, Capacity](Record, SonolusIterator):
212
+ _map: ArrayMap[K, V, Capacity]
213
+ _index: int
214
+
215
+ def has_next(self) -> bool:
216
+ return self._index < self._map.size()
217
+
218
+ def next(self) -> K:
219
+ key = self._map._array[self._index].key
220
+ self._index += 1
221
+ return key
222
+
223
+
224
+ class ArrayMapValueIterator[K, V, Capacity](Record, SonolusIterator):
225
+ _map: ArrayMap[K, V, Capacity]
226
+ _index: int
227
+
228
+ def has_next(self) -> bool:
229
+ return self._index < self._map.size()
230
+
231
+ def next(self) -> V:
232
+ value = self._map._array[self._index].value
233
+ self._index += 1
234
+ return value
235
+
236
+
237
+ class ArrayMapEntryIterator[K, V, Capacity](Record, SonolusIterator):
238
+ _map: ArrayMap[K, V, Capacity]
239
+ _index: int
240
+
241
+ def has_next(self) -> bool:
242
+ return self._index < self._map.size()
243
+
244
+ def next(self) -> tuple[K, V]:
245
+ entry = self._map._array[self._index]
246
+ self._index += 1
247
+ return entry.key, entry.value
@@ -0,0 +1,70 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, Never
3
+
4
+ from sonolus.backend.flow import cfg_to_mermaid
5
+ from sonolus.backend.mode import Mode
6
+ from sonolus.backend.ops import Op
7
+ from sonolus.backend.simplify import CoalesceFlow
8
+ from sonolus.script.comptime import Comptime
9
+ from sonolus.script.internal.context import GlobalContextState, ctx, set_ctx
10
+ from sonolus.script.internal.impl import meta_fn, validate_value
11
+ from sonolus.script.internal.native import native_function
12
+ from sonolus.script.num import Num
13
+ from sonolus.script.values import with_default
14
+
15
+
16
+ @meta_fn
17
+ def error(message: str | None = None) -> None:
18
+ message = Comptime._accept_(message)._as_py_() if message is not None else "Error"
19
+ if not isinstance(message, str):
20
+ raise ValueError("Expected a string")
21
+ if ctx():
22
+ debug_log(ctx().map_constant(message))
23
+ debug_pause()
24
+ terminate()
25
+ else:
26
+ raise RuntimeError(message)
27
+
28
+
29
+ @native_function(Op.DebugLog)
30
+ def debug_log(value: Num):
31
+ print(f"[DEBUG] {value}")
32
+
33
+
34
+ @native_function(Op.DebugPause)
35
+ def debug_pause():
36
+ input("[DEBUG] Paused")
37
+
38
+
39
+ def assert_true(value: Num, message: str | None = None):
40
+ message = with_default(message, "Assertion failed")
41
+ if not value:
42
+ error(message)
43
+
44
+
45
+ def assert_false(value: Num, message: str | None = None):
46
+ message = with_default(message, "Assertion failed")
47
+ if value:
48
+ error(message)
49
+
50
+
51
+ @meta_fn
52
+ def assert_unreachable(message: str | None = None) -> Never:
53
+ message = validate_value(message)._as_py_() or "Unreachable code reached"
54
+ raise RuntimeError(message)
55
+
56
+
57
+ @meta_fn
58
+ def terminate():
59
+ if ctx():
60
+ set_ctx(ctx().into_dead())
61
+ else:
62
+ raise RuntimeError("Terminated")
63
+
64
+
65
+ def visualize_cfg(fn: Callable[[], Any]) -> str:
66
+ from sonolus.build.compile import callback_to_cfg
67
+
68
+ cfg = callback_to_cfg(GlobalContextState(Mode.Play), fn, "")
69
+ cfg = CoalesceFlow().run(cfg)
70
+ return cfg_to_mermaid(cfg)
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Annotated, Any, Protocol, dataclass_transform, get_origin
5
+
6
+ from sonolus.backend.ops import Op
7
+ from sonolus.script.internal.introspection import get_field_specifiers
8
+ from sonolus.script.internal.native import native_function
9
+ from sonolus.script.record import Record
10
+
11
+
12
+ class Effect(Record):
13
+ id: int
14
+
15
+ def is_available(self) -> bool:
16
+ return _has_effect_clip(self.id)
17
+
18
+ def play(self, distance: float) -> None:
19
+ _play(self.id, distance)
20
+
21
+ def schedule(self, time: float, distance: float) -> None:
22
+ _play_scheduled(self.id, time, distance)
23
+
24
+ def loop(self) -> LoopedEffectHandle:
25
+ return LoopedEffectHandle(_play_looped(self.id))
26
+
27
+ def schedule_loop(self, start_time: float) -> ScheduledLoopedEffectHandle:
28
+ return ScheduledLoopedEffectHandle(_play_looped_scheduled(self.id, start_time))
29
+
30
+
31
+ class LoopedEffectHandle(Record):
32
+ id: int
33
+
34
+ def stop(self) -> None:
35
+ _stop_looped(self.id)
36
+
37
+
38
+ class ScheduledLoopedEffectHandle(Record):
39
+ id: int
40
+
41
+ def stop(self, end_time: float) -> None:
42
+ _stop_looped_scheduled(self.id, end_time)
43
+
44
+
45
+ @native_function(Op.HasEffectClip)
46
+ def _has_effect_clip(effect_id: int) -> bool:
47
+ raise NotImplementedError
48
+
49
+
50
+ @native_function(Op.Play)
51
+ def _play(effect_id: int, distance: float) -> None:
52
+ raise NotImplementedError
53
+
54
+
55
+ @native_function(Op.PlayLooped)
56
+ def _play_looped(effect_id: int) -> int:
57
+ raise NotImplementedError
58
+
59
+
60
+ @native_function(Op.PlayLoopedScheduled)
61
+ def _play_looped_scheduled(effect_id: int, start_time: float) -> int:
62
+ raise NotImplementedError
63
+
64
+
65
+ @native_function(Op.PlayScheduled)
66
+ def _play_scheduled(effect_id: int, time: float, distance: float) -> None:
67
+ raise NotImplementedError
68
+
69
+
70
+ @native_function(Op.StopLooped)
71
+ def _stop_looped(handle: int) -> None:
72
+ raise NotImplementedError
73
+
74
+
75
+ @native_function(Op.StopLoopedScheduled)
76
+ def _stop_looped_scheduled(handle: int, end_time: float) -> None:
77
+ raise NotImplementedError
78
+
79
+
80
+ @dataclass
81
+ class EffectInfo:
82
+ name: str
83
+
84
+
85
+ def effect(name: str) -> Any:
86
+ return EffectInfo(name)
87
+
88
+
89
+ class Effects(Protocol):
90
+ _effects_: list[str]
91
+
92
+
93
+ @dataclass_transform()
94
+ def effects[T](cls: type[T]) -> T | Effects:
95
+ if len(cls.__bases__) != 1:
96
+ raise ValueError("Effects class must not inherit from any class (except object)")
97
+ instance = cls()
98
+ names = []
99
+ for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
100
+ if get_origin(annotation) is not Annotated:
101
+ raise TypeError(f"Invalid annotation for effects: {annotation}")
102
+ annotation_type = annotation.__args__[0]
103
+ annotation_values = annotation.__metadata__
104
+ if annotation_type is not Effect:
105
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected annotation of type Effect")
106
+ if len(annotation_values) != 1 or not isinstance(annotation_values[0], EffectInfo):
107
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected a single string annotation value")
108
+ effect_name = annotation_values[0].name
109
+ names.append(effect_name)
110
+ setattr(instance, name, Effect(i))
111
+ instance._effects_ = names
112
+ instance._is_comptime_value_ = True
113
+ return instance
114
+
115
+
116
+ class StandardEffect:
117
+ Miss = Annotated[Effect, effect("#MISS")]
118
+ Perfect = Annotated[Effect, effect("#PERFECT")]
119
+ Great = Annotated[Effect, effect("#GREAT")]
120
+ Good = Annotated[Effect, effect("#GOOD")]
121
+ Hold = Annotated[Effect, effect("#HOLD")]
122
+ MissAlternative = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
123
+ PerfectAlternative = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
124
+ GreatAlternative = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
125
+ GoodAlternative = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
126
+ HoldAlternative = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
127
+ Stage = Annotated[Effect, effect("#STAGE")]
128
+
129
+
130
+ @effects
131
+ class EmptyEffects:
132
+ pass
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+
5
+ from sonolus.build.collection import Asset
6
+ from sonolus.script.archetype import BaseArchetype
7
+ from sonolus.script.bucket import Buckets, EmptyBuckets
8
+ from sonolus.script.effect import Effects, EmptyEffects
9
+ from sonolus.script.options import EmptyOptions, Options
10
+ from sonolus.script.particle import EmptyParticles, Particles
11
+ from sonolus.script.sprite import EmptySkin, Skin
12
+ from sonolus.script.ui import UiConfig
13
+
14
+
15
+ class Engine:
16
+ version = 12
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ name: str,
22
+ title: str | None = None,
23
+ subtitle: str = "Sonolus.py Engine",
24
+ author: str = "Unknown",
25
+ skin: str | None = None,
26
+ background: str | None = None,
27
+ effect: str | None = None,
28
+ particle: str | None = None,
29
+ thumbnail: Asset | None = None,
30
+ data: EngineData,
31
+ ) -> None:
32
+ self.name = name
33
+ self.title = title or name
34
+ self.subtitle = subtitle
35
+ self.author = author
36
+ self.skin = skin
37
+ self.background = background
38
+ self.effect = effect
39
+ self.particle = particle
40
+ self.thumbnail = thumbnail
41
+ self.data = data
42
+
43
+
44
+ def default_callback() -> float:
45
+ return 0.0
46
+
47
+
48
+ class PlayMode:
49
+ def __init__(
50
+ self,
51
+ *,
52
+ archetypes: list[type[BaseArchetype]] | None = None,
53
+ skin: Skin = EmptySkin,
54
+ effects: Effects = EmptyEffects,
55
+ particles: Particles = EmptyParticles,
56
+ buckets: Buckets = EmptyBuckets,
57
+ ) -> None:
58
+ self.archetypes = archetypes or []
59
+ self.skin = skin
60
+ self.effects = effects
61
+ self.particles = particles
62
+ self.buckets = buckets
63
+
64
+
65
+ class WatchMode:
66
+ def __init__(
67
+ self,
68
+ *,
69
+ archetypes: list[type[BaseArchetype]] | None = None,
70
+ skin: Skin = EmptySkin,
71
+ effects: Effects = EmptyEffects,
72
+ particles: Particles = EmptyParticles,
73
+ buckets: Buckets = EmptyBuckets,
74
+ update_spawn: Callable[[], float],
75
+ ) -> None:
76
+ self.archetypes = archetypes or []
77
+ self.skin = skin
78
+ self.effects = effects
79
+ self.particles = particles
80
+ self.buckets = buckets
81
+ self.update_spawn = update_spawn
82
+
83
+
84
+ class EngineData:
85
+ ui: UiConfig
86
+ options: Options
87
+ play: PlayMode
88
+ watch: WatchMode
89
+
90
+ def __init__(
91
+ self,
92
+ *,
93
+ ui: UiConfig | None = None,
94
+ options: Options = EmptyOptions,
95
+ play: PlayMode | None = None,
96
+ watch: WatchMode | None = None,
97
+ ) -> None:
98
+ self.ui = ui or UiConfig()
99
+ self.options = options
100
+ self.play = play or PlayMode()
101
+ self.watch = watch or WatchMode(update_spawn=default_callback)