Skip to main content

2D Game Engine using pyglet (OpenGL) for rendering

Project description

PyVerse2D

A batteries-included 2D game engine for Python, built on top of pyglet.

PyVerse2D gives you a complete ECS-based runtime - physics, lighting, particles, tilemaps, GUI, audio, video - behind a clean and minimal API.

PyPI version Python License


Features

  • ECS architecture - entities, components, and systems with dependency/conflict validation
  • Physics engine - rigid bodies, gravity, collision detection and resolution (circles, capsules, polygons, ellipses, rounded rects)
  • Rendering pipeline - layered scenes, cameras with letterboxing, viewport transforms, z-ordering
  • Lighting system - ambient, point lights, cone lights, bloom, vignette, tint
  • Particle system - line, circle, cone and point emitters with modifiers (wind, drag, gravity, attractor)
  • Tilemap support - Tiled TMX loader, automatic collision injection, parallax cameras
  • GUI system - widgets, tweens, behaviors (click, hover, focus, select), toggle buttons, scrollbars, labels
  • Asset management - images, animations, fonts, sounds, music, playlists, video
  • Input system - keyboard, mouse, combo listeners, repeat and condition support
  • Post-processing - shader-based effects (blur, chromatic aberration, color grading, glitch, distortion, scanlines…) applied per zone with spatial masking
  • Built-in profiler - frame-accurate profiling with export

Installation

pip install pyverse2d

Or install the latest dev version directly from GitHub:

pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip

Requirements: Python 3.11+, pyglet 2.x


Hello World

The minimal setup: open a window and draw a bouncing ball.

import pyverse2d as pv
from pyverse2d import Window, LogicalScreen, Camera, Viewport
from pyverse2d import world, scene

# --- Window ---
screen = LogicalScreen(1920, 1080)
window = Window(screen=screen, caption="Hello World", vsync=True)
pv.set_window(window)

# --- Scene ---
camera = Camera(anchor=(0.5, 0.5), view_height=30)
viewport = Viewport(width=1920, height=1080, origin=(0.5, 0.5))
main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene)

main_world = world.World()
main_scene.add_layer(scene.WorldLayer(main_world), z=0)

# --- Systems ---
main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem(slop=0.01, restitution_threshold=0.05))

# --- Ground ---
ground_shape = pv.shape.Rect(30.0, 1.0)
ground = world.Entity(
    world.Transform(position=(0.0, -8.0), anchor=(0.5, 0.5)),
    world.ShapeRenderer(shape=ground_shape, filling_color=(80, 80, 80)),
    world.Collider(shape=ground_shape),
    world.RigidBody(restitution=0.5, friction=0.4),
)
main_world.add_entity(ground)

# --- Ball ---
ball_shape = pv.shape.Circle(1)
ball = world.Entity(
    world.Transform(position=(0.0, 8.0)),
    world.ShapeRenderer(shape=ball_shape, filling_color=(80, 180, 255)),
    world.Collider(shape=ball_shape),
    world.RigidBody(mass=1.0, restitution=0.8),
)
main_world.add_entity(ball)

# --- Run ---
pv.preload()
pv.run()

Run Configuration

Engine API

These are the top-level functions exposed directly on the pyverse2d module.

# 1 - Bind the OS window to the engine (must be called before anything else)
pv.set_window(window)
 
# 2 - Upload scene assets to the GPU before the first frame
pv.preload()           # preloads all pushed scenes
pv.preload(my_scene)   # preloads a specific scene only
 
# 3 - Start the main loop
def on_update(dt: float):
    pass   # game logic, called every frame
 
def on_draw():
    pass   # extra draw hook
 
pv.run(on_update=on_update, on_draw=on_draw)
 
# 4 - Stop cleanly (closes the window and exits the pyglet loop)
pv.stop()

Profiling

pv.preload()
pv.profile(duration=10.0, on_update=on_update, export_path="profile_report.txt")

The profiler runs the main loop for duration seconds, then writes a frame-accurate report to export_path (pass None to print to console instead).

Traceback

By default PyVerse2D enriches Python tracebacks with engine context. You can control this:

pv.enable_traceback()    # on by default
pv.disable_traceback()
pv.set_traceback(True)   # equivalent to enable

Internal objects

Assets

Assets are immutable descriptors - they hold configuration but no runtime state. They are cheap to create and safe to share.

# Image - width or height scales proportionally, scale_factor is a multiplier
img = pv.asset.Image("player.png", height=2.5)
img = pv.asset.Image("tile.png", scale_factor=2.0)
 
# Animation - built from an Image sequence
anim = pv.asset.Animation.from_folder("assets/", prefix="run_", framerate=12, height=2.5)
anim.duration   # total duration in seconds
anim.frames     # tuple of Images
 
# Font - system name, file path, or default
font = pv.asset.Font("Arial", size=24)
font = pv.asset.Font("fonts/custom.ttf", size=18)
font = pv.asset.Font()                   # built-in FreeSans fallback
font.text_width("Hello")                 # pixel width of a string
font.clip_text("Long string", max_width=200, suffix="…")
 
# Text - immutable content + font pair
text = pv.asset.Text("Score: 0", font=pv.asset.Font(size=20))
text.with_text("Score: 100")             # returns a new Text
text.with_font(pv.asset.Font(size=32))
 
# Color - accepts int [0, 255] or float [0.0, 1.0] per channel, with optional alpha
red   = pv.asset.Color(255, 0, 0)
white = pv.asset.Color(1.0, 1.0, 1.0)
semi  = pv.asset.Color(0.2, 0.6, 1.0, 0.8)
semi.rgb    # (r, g, b) as floats
semi.rgba8  # (r, g, b, a) as ints [0, 255]
semi.with_alpha(0.5)
 
# Sound - SFX, loaded entirely in memory
sound = pv.asset.Sound("hit.wav", volume=0.8, cooldown=0.1)
sound.play()
sound.play(loop=True, limit=3)
sound.pause()
sound.stop()
 
# Sound with random variations (picks one file at random each play)
footstep = pv.asset.Sound.from_variations("assets/audio/", prefix="step_", cooldown=0.4)
footstep.add_variation("assets/audio/step_extra.wav")
 
# Music - streamed from disk
music = pv.asset.Music("theme.ogg", volume=0.9)
music.play(loop=True, fade_s=2.0)
music.stop(fade_s=1.0)
 
# Video
video = pv.asset.Video("intro.mp4", volume=0.8)
 
# Playlist
playlist = pv.asset.Playlist(
    [music_a, music_b, music_c],
    shuffle=True, loop=True,
    cross_fade=2.0, fade_in=1.0,
)
playlist.play()
playlist.add(music_d)

Bundles

A Bundle loads a whole folder of assets at once, keyed by filename (minus extension and optional prefix). Results are cached - calling get() twice for the same key returns the same instance.

# ImageBundle
images = pv.asset.ImageBundle.from_folder("assets/ui/", height=64)
btn_img = images.get("button")           # Image
images.random()                          # random Image from the bundle
 
# AnimationBundle - not a built-in type, use Animation.from_folder directly
anim = pv.asset.Animation.from_folder("assets/run/", framerate=12, height=2.5)
 
# FontBundle
fonts = pv.asset.FontBundle.from_folder("assets/fonts/", size=24)
title_font = fonts.get("bold", size=48)  # override size per call
 
# SoundBundle
sounds = pv.audio.load_sounds("assets/audio/sfx/", extensions=[".wav"], volume=1.0, cooldown=0.3)
sounds["footstep"].play()
sounds.random().play()
 
# MusicBundle - preloads each music in a background thread
musics = pv.audio.load_musics("assets/audio/music/", extensions=[".ogg"], volume=0.8)
musics.preload()
pv.audio.play_music(musics["theme"])
pv.audio.switch_music(musics.random(), fade_s=2.0)
 
# Common Bundle API
bundle.keys()         # all keys
bundle.values_list()  # all loaded assets
bundle.has("key")
len(bundle)
"key" in bundle

Rendering Pipeline

Window & screen

LogicalScreen defines the virtual resolution your game is designed for. Window wraps the OS window and handles letterboxing automatically, your game scales cleanly to any physical window size.

screen = LogicalScreen(1920, 1080)   # virtual canvas
window = Window(screen=screen, resizable=True, vsync=True)
pv.set_window(window)

Viewport

A Viewport defines which region of the LogicalScreen a scene renders into, and how its local axes are oriented. By default a viewport covers the full screen with a standard basis, but you can split the screen, mirror axes, or skew the coordinate space.

# Full screen, centered origin (default setup)
viewport = Viewport(width=1920, height=1080, origin=(0.5, 0.5))
 
# Bottom-left quarter of the screen
viewport = Viewport(position=(0.0, 0.0), width=960, height=540, origin=(0.0, 0.0))
 
# Mirrored horizontal axis (flip x)
viewport = Viewport(
    width=1920, height=1080,
    origin=(0.5, 0.5),
    x_direction=(-1.0, 0.0),
    y_direction=(0.0, 1.0),
)

x_direction and y_direction form the local basis of the viewport - they must not be collinear. Changing them lets you rotate or mirror the rendered output independently of the camera.

Camera

Cameras define the point of view. They support smooth following, animated transitions, and parallax attachment.

# Follow a target with smooth lag
camera.follow(player.transform, offset=(0.0, 1.0), smoothing=0.05)

# Animated transition to a position
camera.goto((10.0, 0.0), duration=1.5, easing=pv.math.easing.ease_in_out_quad)

# Parallax-derived camera (e.g. for a background layer)
background_camera = Camera.derived_from(camera, parallax_x=0.3)

Core concepts

Scene & layers

A Scene is a self-contained game state. Layers stack inside a scene at a given z-index. WorldLayer, TileLayer, GuiLayer, LightLayer and ParticleLayer each handle a specific rendering domain.

main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene)                                     # push onto the scene stack

main_scene.add_layer(scene.WorldLayer(main_world), z=0)    # ECS world
main_scene.add_layer(scene.GuiLayer(), z=100)              # UI on top

Entities & components

An Entity is a container of components. Components are plain data objects, logic lives in systems.

player = world.Entity(
    world.Transform(position=(0.0, 5.0), anchor=(0.5, 0.0)),
    world.ShapeRenderer(shape=pv.shape.Capsule(0.4, 2.0), filling_color=(255, 120, 60)),
    world.Collider(shape=pv.shape.Capsule(0.4, 2.0)),
    world.RigidBody(mass=60.0, friction=0.4),
    world.GroundSensor(),
)
main_world.add_entity(player)

Available components: Transform, ShapeRenderer, SpriteRenderer, TextRenderer, Animator, TrailRenderer, Collider, RigidBody, GroundSensor, Follow, SoundEmitter, VideoPlayer.

Systems

Systems process entities every frame. Add them to a World and they run in order:

main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem())
main_world.add_system(world.AnimationSystem())
main_world.add_system(world.SteeringSystem())
main_world.add_system(world.SoundSystem(origin=camera))

Tilemaps

# Load a Tiled .tmx file
stage = pv.tile.MapLoader.from_tiled_tmx("map/stage_0.tmx", tile_width=1.5, tile_height=1.5)

# Add layers to the scene
ground = stage["ground"]
main_scene.add_layer(pv.scene.TileLayer(ground), z=4)

# Inject tile colliders automatically into the world
pv.tile.CollisionMapper(ground).inject(main_world)

Lighting

light_layer = pv.scene.LightLayer(
    pv.fx.Ambient(intensity=0.2, color=(0, 0, 30)),
    gamma=1.0,
    exposure=3.0,
)
main_scene.add_layer(light_layer, z=3)

# Point light attached to the player
point = pv.fx.PointLight(radius=10, intensity=1.0)
point.attach_to(player.transform, offset=(0.0, 1.0))
light_layer.add_source(point)

# Cone light (e.g. spotlight from above)
cone = pv.fx.ConeLight(position=(0.0, 30.0), direction=(0.0, -1.0), angle=20, intensity=1.2)
light_layer.add_source(cone)

Particles

particle_layer = pv.scene.ParticleLayer(additive=True)
main_scene.add_layer(particle_layer, z=-1)

particle = pv.fx.Particle(
    lifetime=(4, 8), speed=(5, 10),
    size=(0.3, 0.8), size_end=0.1,
    color_start=(0, 100, 255), color_end=(255, 255, 255),
)
emitter = pv.fx.LineEmitter(p1=(-50, 30), p2=(50, 30), normal=True, particle=particle, rate=80)
emitter.add_modifier(pv.fx.Wind(direction=-45, strength=1.5, turbulence=8))
particle_layer.add_emitter(emitter)

Post-processing

Post-processing effects are applied via a PostFxLayer. Each layer holds one or more PostFxZone objects, a zone can cover the entire screen (unbounded) or be restricted to a Circle or Rect shape with an optional soft blend edge. Effects chain in order and are GPU-accelerated (GLSL shaders).

postfx_layer = pv.scene.PostFxLayer()
main_scene.add_layer(postfx_layer, z=50)

# Unbounded zone: covers the full screen
zone = pv.fx.PostFxZone()
zone.add_effect(pv.fx.ColorGrade(brightness=0.05, contrast=1.1, saturation=0.9))
zone.add_effect(pv.fx.Vignette(strength=0.6, radius=80, softness=40))
postfx_layer.add_zone(zone)

# Bounded zone: circle around a point, with edge blending
local_zone = pv.fx.PostFxZone(shape=pv.shape.Circle(10), position=(0.0, 0.0), blend=3.0)
local_zone.add_effect(pv.fx.Blur(radius=4.0, passes=2))
postfx_layer.add_zone(local_zone)

# Attach a zone to a moving object
local_zone.attach_to(player.transform, offset=(0.0, 1.0), smoothing=0.05)

Available effects and their key parameters:

Effect Description Key params
Blur Gaussian blur radius, passes
MotionBlur Dual frames motion blur strength
Chromatic Chromatic aberration strength, angle
ColorGrade Brightness / contrast / saturation / tint brightness, contrast, saturation, tint
EdgeDetect Sobel edge detection threshold, strength, edge_color
Flicker Luminosity flicker (unstable light source) amplitude, speed
Glitch Digital corruption with band shifting strength, density, speed
Pixelate Pixelation block_size
Posterize Color level reduction levels
Scanlines CRT scanline overlay spacing, strength, softness
Vignette Edge darkening strength, radius, softness, color
Wave Sinusoidal distortion amplitude_x, amplitude_y, frequency_x, frequency_y, speed
DistortSwirl Vortex rotation angle, falloff
DistortSqueeze Asymmetric directional stretch strength_x, strength_y, falloff
DistortRipple Radial concentric wave amplitude, frequency, speed, falloff
Fog FBM dynamic fog angle, velocity, density, softness, scale, octaves, lacunarity, gain, color

Effects can be added, removed, replaced and reordered at runtime:

zone.replace_effect(pv.fx.ColorGrade(saturation=0.0))   # swap params live
zone.move_effect(pv.fx.Blur, index=0)                   # push blur to first pass
zone.remove_effect(pv.fx.Vignette)
zone.disable()                                          # pause the zone without removing it

Managers

KeyManager

pv.key tracks keyboard state. Key constants are exposed directly on the manager (K_A, K_SPACE, K_F11…).

# State queries (use these inside on_update)
pv.key.is_pressed(pv.key.K_D)        # held down
pv.key.just_pressed(pv.key.K_SPACE)  # pressed this frame
pv.key.just_released(pv.key.K_LSHIFT)
 
# Human-readable label
pv.key.name(pv.key.K_ENTER)          # "Enter"

MouseManager

pv.mouse tracks cursor position, movement, scroll, and button state. Position is in LogicalScreen coordinates.

pv.mouse.position          # (x, y) in logical space
pv.mouse.scroll_y          # wheel delta this frame
pv.mouse.is_pressed(pv.mouse.B_LEFT)
pv.mouse.just_pressed(pv.mouse.B_RIGHT)
 
# World-space position under the cursor
wx, wy = pv.mouse.get_world_position(viewport=viewport, camera=camera)
 
# Cursor appearance
pv.mouse.set_appearance(pv.mouse.SystemMouseCursor(pv.mouse.CURSOR_HAND))
pv.mouse.set_appearance(pv.mouse.ImageMouseCursor(img, anchor_x=0.0, anchor_y=1.0))
 
# Exclusive mode (FPS-style grab)
pv.mouse.set_exclusive(True)

InputsManager

pv.inputs maps keys and mouse buttons to callbacks. All listeners return a Listener object that can be enabled, disabled, or invalidated at any time.

# Single key, fires on press
pv.inputs.add_listener(pv.key.K_SPACE, player.jump)
 
# Held key - fires every frame while pressed
pv.inputs.add_listener(pv.key.K_D, player.move_right, repeat=True)
 
# Release
pv.inputs.add_listener(pv.key.K_LSHIFT, on_sprint_end, up=True)
 
# Conditional - only fires when the lambda returns True
pv.inputs.add_listener(pv.key.K_RIGHT, cam_right, repeat=True, condition=lambda: not pv.key.is_pressed(pv.key.K_LEFT))
 
# Any of a set of keys
pv.inputs.when_any_of([pv.key.K_A, pv.key.K_LEFT], player.move_left, repeat=True)
 
# Combo - all keys held simultaneously
pv.inputs.when_all_of([pv.key.K_LEFT, pv.key.K_DOWN], move_downleft, repeat=True)
 
# Mouse button
pv.inputs.add_listener(pv.mouse.B_LEFT, on_click)
 
# Fire once then auto-remove
pv.inputs.add_listener(pv.key.K_ENTER, on_confirm, once=True)
 
# Manage a listener
listener = pv.inputs.add_listener(pv.key.K_P, on_pause)
listener.disable()   # temporarily mute
listener.enable()
listener.invalidate() # remove permanently

EventManager

pv.event gives direct access to raw pyglet events via subscribable slots. Each slot supports a priority and a consume flag to stop propagation.

# Subscribe
pv.event.on_key_press.subscribe(my_handler, priority=10)
pv.event.on_mouse_scroll.subscribe(on_scroll)
pv.event.on_resize.subscribe(on_resize)
 
# Unsubscribe
pv.event.on_key_press.unsubscribe(my_handler)
 
# Stop propagation from inside a handler
def my_handler(symbol):
    if symbol == pv.key.K_ESCAPE:
        return pv.event.ConsumeFlag

Available slots: on_key_press, on_key_release, on_text, on_text_motion, on_text_motion_select, on_mouse_press, on_mouse_release, on_mouse_motion, on_mouse_drag, on_mouse_scroll, on_mouse_enter, on_mouse_leave, on_resize, on_close, on_activate, on_deactivate, on_show, on_hide, on_move.

TimeManager

pv.time manages the game clock, delta-time, FPS cap, time scaling, and deferred callbacks.

pv.time.clock          # total elapsed time (seconds)
pv.time.frame          # total frame count
pv.time.dt             # capped delta-time
pv.time.fps            # current FPS
pv.time.smooth_fps     # averaged over last 50 frames
 
pv.time.target_fps = 120          # cap framerate
pv.time.time_scale = 0.5          # slow motion
 
# Deferred call
pv.time.after(2.0, spawn_enemy)            # once after 2 s
pv.time.every(10.0, lambda: switch_music)  # repeat every 10 s

AudioManager

pv.audio handles sound effects and music, including crossfades, playlists, and sound groups.

# Sound effects
sound = pv.asset.Sound("hit.wav", volume=0.8, cooldown=0.1)
pv.audio.play_sound(sound)
pv.audio.play_sound(sound, loop=True, limit=3)
pv.audio.pause_sound(sound)
pv.audio.stop_sound(sound)
 
# Volume groups
sfx_group = pv.audio.SoundGroup(volume=0.6, pool_max=8)
sound = pv.asset.Sound("step.wav", group=sfx_group)
sfx_group.pause_all()
 
# Music
music = pv.asset.Music("theme.ogg")
pv.audio.play_music(music, loop=True, fade_s=2.0)
pv.audio.switch_music(other_music, fade_s=1.5)   # crossfade
pv.audio.pause_music()
pv.audio.stop_music(fade_s=1.0)
 
# Playlist
playlist = pv.asset.Playlist(musics.values_list(), shuffle=True, loop=True, cross_fade=2.0)
pv.audio.play_playlist(playlist)
 
# Master volumes
pv.audio.master_volume = 0.8
pv.audio.music_volume  = 0.5

CoordinatesManager

pv.coordinates converts positions between the four coordinate spaces: World, Framebuffer (logical pixels), and Window (physical pixels).

# World → screen
fx, fy = pv.coordinates.world_to_framebuffer(entity.x, entity.y)
 
# Window (OS pixels) → world
wx, wy = pv.coordinates.window_to_world(raw_x, raw_y)
 
# Frustum corners and AABB (useful for culling)
corners = pv.coordinates.get_frustum_corners()
x_min, y_min, x_max, y_max = pv.coordinates.get_frustum_aabb()
 
# Generic converter
x, y = pv.coordinates.convert(x, y, from_space="world", to_space="framebuffer")

A context must be applied before converting (done automatically each frame inside layers). Use temporary_context to convert with a specific camera or viewport:

with pv.coordinates.temporary_context(viewport=my_viewport, camera=my_camera):
    wx, wy = pv.coordinates.framebuffer_to_world(fx, fy)

UIManager

pv.ui arbitrates widget focus and hover state. It is mostly used internally by the GUI system but can be queried directly.

pv.ui.hovered   # currently hovered widget, or None
pv.ui.focused   # currently focused widget, or None
pv.ui.unhover()
pv.ui.unfocus()

Project structure

pyverse2d/
├── abc/           # Abstract base classes (Space, Component, System, Asset…)
├── asset/         # Image, Animation, Font, Sound, Music, Video, Text, Playlist
├── fx/
│   ├── light/     # Ambient, PointLight, ConeLight, Bloom, Vignette, Tint
│   ├── particle/  # Emitters (Line, Circle, Cone, Point) + Modifiers
│   └── postfx/    # PostFxZone, PostFxLayer + 14 shader effects
├── gui/           # Widgets, Tweens, Behaviors, SelectionGroup
├── math/          # Point, Vector, Line, easing functions, vertex helpers
├── scene/         # Scene, WorldLayer, TileLayer, GuiLayer, LightLayer, ParticleLayer, PostFxLayer
├── shape/         # Circle, Rect, RoundedRect, Ellipse, Capsule, Polygon, RegularPolygon
├── tile/          # Tiled TMX loader, CollisionMapper, TileMap
├── typing/        # Type aliases
└── world/
    ├── _component/ # Transform, Collider, RigidBody, Renderers, Animator, Follow…
    └── _system/    # Physics, Gravity, Collision, Render, Animation, Steering, Sound, Video

License

MIT - © WhiteWolf45380

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyverse2d-1.6.31.tar.gz (289.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyverse2d-1.6.31-py3-none-any.whl (406.1 kB view details)

Uploaded Python 3

File details

Details for the file pyverse2d-1.6.31.tar.gz.

File metadata

  • Download URL: pyverse2d-1.6.31.tar.gz
  • Upload date:
  • Size: 289.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyverse2d-1.6.31.tar.gz
Algorithm Hash digest
SHA256 034d3eeea5f70be4e2a79e1c195761fdbece84cdf2dfe613f32d62cd85184124
MD5 08479a6f8bf23184730a234dadc34fed
BLAKE2b-256 cb8fedbec329baa9b9238a51d5b4340cb1fd042d5ffada86ce90c8b71ea8d98c

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyverse2d-1.6.31.tar.gz:

Publisher: publish.yml on WhiteWolf45380/PyVerse2D

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyverse2d-1.6.31-py3-none-any.whl.

File metadata

  • Download URL: pyverse2d-1.6.31-py3-none-any.whl
  • Upload date:
  • Size: 406.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyverse2d-1.6.31-py3-none-any.whl
Algorithm Hash digest
SHA256 9a2ccd8d02aac8dc3bab1b46cfd291ab63ae3c6ddfc8a15799c9343044ca92dd
MD5 7c20cbb252623ea6962e297e5c2c6e08
BLAKE2b-256 edbaac7d8ea4e63d705a5af3d7a5803f860719f13ab2e9b6f604a3199456a8a4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyverse2d-1.6.31-py3-none-any.whl:

Publisher: publish.yml on WhiteWolf45380/PyVerse2D

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page