Overview¶
Sonolus.py is a Python library for creating Sonolus engines. This page provides an overview of the key functionality available in the library. For detailed information, see Types, Constructs, and Builtins, or the Reference section for the API listings.
A Project bundles an Engine
with the levels it can play. An engine's behavior is split across up to four modes: play mode, watch mode,
preview mode, and tutorial mode. Play, watch, and preview modes are driven by archetypes, classes that describe a kind
of entity and the callbacks Sonolus invokes for entities of that archetype; tutorial mode instead defines standalone
preprocess, navigate, and update callbacks. See the Project page for how these pieces fit together
in a project's file layout, and Resources & Declarations for how engines declare skins, particles,
options, and other resources.
Language¶
Sonolus.py compiles Python code into Sonolus nodes. It supports a subset of Python including most syntax and a portion of the standard library. Additionally, Sonolus.py provides its own library of types and functions that are designed for use in Sonolus engines. See Constructs for what is and is not supported.
Syntax¶
Most Python syntax is supported, but there are a few limitations. The primary restrictions are listed below; see Key Differences for the complete list:
- Destructuring assignment with the
*operator is unsupported. - Sequence (list and array)
matchpatterns with the*operator are unsupported. - Mapping (dict)
matchpatterns are unsupported. - Within functions,
importstatements are unsupported. - The
globalandnonlocalkeywords are unsupported. - Exception related statements (
try,except,finally,raise) are unsupported.
Compile Time Evaluation¶
Sonolus.py will evaluate some expressions at compile time such as basic arithmetic operations on constants,
boolean logical operations (and, or, not) on constants, and isinstance type checks. See
Compile Time Evaluation for the full list.
In control flow constructs like if and match, Sonolus.py may determine some branches to be unreachable at compile
time and eliminate them without evaluating them. This allows code like the following to compile successfully:
a = 1
if isinstance(a, Vec2):
# This branch is eliminated at compile time.
# If it were not, compilation would fail because `a` has no attribute `x`.
debug_log(a.x)
else:
debug_log(a)
Variables¶
Numeric (int, float, bool) variables are fully supported and can be freely assigned and modified. See
Variables for details.
All other variables have the restriction that if the compiler finds multiple possible values for a variable, it may not be accessed. For example, the following code will not compile:
if random() < 0.5:
a = Vec2(1, 2)
else:
a = Vec2(3, 4)
# This will not compile because `a` could have been defined in either branch.
debug_log(a.x)
Function Returns¶
Similar to variables, functions returning int, float, or bool can have any number of return statements. Functions
returning None may also have any number of return or return None statements.
Functions returning any other type must only ever return the same value 1. Multiple return statements are fine
when they all return that same object, or when the extra ones are eliminated at compile time. See
Return Values for details. For example, the following code will compile successfully:
def fn(a: int | Vec2):
if isinstance(a, Vec2):
return Vec2(a.x, a.y)
else:
return Vec2(a, a)
fn(123)
Types¶
See Types for full details on each of these.
Numbers¶
Sonolus.py supports the int, float, and bool types and most of the standard operations
such as mathematical operations
(+, -, *, /, //, %, **), comparisons (<, <=, >, >=, ==, !=), and boolean operations
(and, or, not).
Record¶
Record is the main way to define custom types in Sonolus.py.
It functions similarly to a data class and provides a way to define a type with named fields:
class MyRecord(Record):
a: int
b: float
record_1 = MyRecord(1, b=2.3)
Records may also be generic:
class MyGenericRecord[T](Record):
value: T
record_1 = MyGenericRecord[int](123)
record_2 = MyGenericRecord(MyRecord(4, 5.6)) # Type arguments are inferred
Record arguments are retained by reference, so modifying the original record will also modify the record stored in the other record:
record_1 = MyRecord(1, 2.3)
record_2 = MyGenericRecord(record_1)
record_2.value.a = 789 # This also affects `record_1` since they're the same object.
assert record_1.a == record_2.value.a == 789
Array¶
Array is a type that represents a fixed-size array of elements of a
specific type:
array_1 = Array[int, 3](1, 2, 3)
array_2 = Array(4, 5, 6) # Type arguments are inferred
When given record or array values as arguments, the array constructor will copy them:
record_1 = MyRecord(1, 2.5)
array_1 = Array(record_1)
array_1[0].a = 789 # This has no effect on `record_1` since it was copied.
assert record_1.a == 1
Operations¶
This section is an overview of the operations available for records and arrays. For full details see Record and Array; for the API listings see the Record and Array reference pages.
Records and arrays come with the == and != operators predefined to compare their values for equality:
assert MyRecord(1, 2.3) == MyRecord(1, 2.3)
assert Array(1, 2, 3) != Array(4, 5, 6)
The unary + operator makes a copy of a record or array, creating a new instance with the same values:
record_2 = +record_1
array_2 = +array_1
Similarly, a new zero initialized value can be created using the unary + operator on a record or array type:
record_1 = +MyRecord
record_2 = +Array[int, 3]
Records and arrays can be mutated in-place using the @= operator:
record_1 @= MyRecord(1, 2.3)
array_1 @= Array(4, 5, 6)
Record fields and array elements of numeric types can be set using the = operator:
record_1.a = 123
array_1[1] = 456
Setting a record field that's a record or array using the = operator will modify the field in-place; see
Instantiation for when a field does and does not alias the value it was constructed from:
record_1 = MyRecord(1, 2.3)
record_2 = MyGenericRecord(record_1)
record_2.value = MyRecord(4, 5.6) # This modifies `record_1` in-place.
assert record_1 == record_2.value == MyRecord(4, 5.6)
Setting an array element that's a record or array using the = operator will also modify the element in-place:
array_1 = Array(MyRecord(1, 2.3))
record_1 = array_1[0]
array_1[0] = MyRecord(4, 5.6) # This modifies `record_1` in-place.
assert record_1 == array_1[0] == MyRecord(4, 5.6)
Other Types¶
Sonolus.py has limited support for other types of values such as strings, tuples, and functions. These have restrictions such as not being valid as Record field types or Array element types.
dict and set are immutable after creation. All dict keys and set members must be compile-time constants. Dicts can
be created from literals, the dict() constructor (from another dict, an iterable of key-value pairs, or keyword
arguments), or by merging with |. Sets can be created from literals or the set() constructor (from another set or
an iterable). Accessing a dict value with a compile-time constant key is always supported; dynamic access using a
runtime key is only supported when the values are numeric, Array, or Record types.
Modules¶
Sonolus.py provides a number of built-in modules that can be used in Sonolus engines. These include:
- Project
- Core Types
- Engine Resources
- Sonolus Runtime
- Python Builtins
- Utilities
- ArrayLike: Mixin for array functionality.
- Containers: Additional container types like
VarArrayandArrayMap. - Debug: Debugging utilities.
- Easing: Easing functions for animations.
- Interval: Mathematical intervals.
- Iterator: Iterators over collections.
- Maybe: Optional function return values.
- Metadata: Localized text and tags for engines and levels.
- Numtools: Additional numeric utilities.
- Printing: Preview mode number printing.
- Quad: Quadrilaterals.
- Transform: Transformations like translation, rotation, and scaling.
- Values: Generic utilities for working with values.
- Vec: The Vec2 type and related functions.
For more details, see the Reference section.
-
The
Maybetype is an exception to this rule. See theMaybedocumentation for details. ↩