Math, random & collision
The three modules every PicoPad game leans on for movement, spawning and hit detection. They are tiny, allocation-light, and import-only (no engine setup needed). For bare signatures see /reference/; this page is the why and when.
picogame_math
Section titled “picogame_math”What it is: the small numeric helpers you reach for every frame - clamp/lerp/approach/wrap, turn-based trig (angles as 0..1 turns instead of radians), and 2D vector math (folded in from the old picogame_vec). Import it once as m and use it for smoothing, aiming, screen-wrap and distance.
When to use it: any time you would otherwise hand-roll a clamp, a smooth follow, or atan2. Reach for the _t trig when you want angles that wrap cleanly in [0,1) (rotation state, aim) rather than radians.
API:
clamp(v, lo, hi)- returnsvpinned into[lo, hi]. The everyday helper for keeping a position on screen or HP in range.mid(a, b, c)- median of three; behaves like a clamp when the middle arg is the value.lerp(a, b, t)- linear blenda + (b-a)*t. With a smallteach frame it gives a smooth follow/ease.inv_lerp(a, b, v)- inverse oflerp: wherevsits in[a,b]as0..1. Returns0.0ifa == b.remap(v, a, b, c, d)- mapvfrom range[a,b]onto[c,d]. Safe whena == b(maps toc).sgn(x)- sign as-1,0, or1.approach(v, target, step)- movevtowardtargetby at moststep, never overshooting. Great for friction/acceleration toward a value.wrap(v, lo, hi)- wrapvinto the half-open range[lo, hi). Degenerate or inverted ranges returnlo(no division by zero, never out of range).sin_t(turns)/cos_t(turns)- sine/cosine of an angle in TURNS (1.0= full circle). Positive is clockwise on a y-down screen.atan2_t(dy, dx)- angle of vector(dx, dy)in turns, normalized to0..1. Use for aiming.length(dx, dy)- magnitude of a vector.distance(x1, y1, x2, y2)- distance between two points.normalize(dx, dy)- unit vector; returns(0.0, 0.0)for a zero-length input.angle_rad(dx, dy)- angle in RADIANS (rawatan2);from_angle_rad(a, mag=1.0)- vector of lengthmagat radian anglea.TAU- the constant2*pi, used internally by the_ttrig.
Example (rotate a ship and thrust along its nose, as in asteroids):
import picogame_math as m
ang = 0.0 # heading, in turns 0..1TURN = 0.01ang = (ang + TURN) % 1.0 # rotate rightdx, dy = m.sin_t(ang), -m.cos_t(ang) # nose-up directionvx += dx * 0.25vy += dy * 0.25sp = m.length(vx, vy) # current speedx = m.clamp(x, 8, W - 8) # keep on screenGotchas:
- The
_ttrig is clockwise on the y-down screen, and “up” is-cos_t- flip the sign of the y component, like the asteroids ship above, or your sprite turns the wrong way. wrapis half-open:wrap(hi, lo, hi)returnslo, nothi. For an inverted or zero-width range it just returnslorather than raising.- Use turns (
sin_t/cos_t/atan2_t) and radians (angle_rad/from_angle_rad) consistently; do not mix the two for the same angle.
picogame_rand
Section titled “picogame_rand”What it is: a tiny, seedable xorshift32 RNG plus the helpers small games actually need (weighted picks, in-place shuffle, a shuffle “bag”). Use it instead of the stdlib random when you want reproducible runs - replays, ghosts, daily-challenge seeds, or deterministic level layouts.
When to use it: create one Rand per game (seed it for reproducibility, or leave the seed off for a time-seeded run) and draw all randomness from it. Reach for Bag when you want fairness without streaks (spawns, Tetris-style pieces).
Rand(seed=None):
Rand(1234)seeds from an int (reproducible);Rand()seeds from the clock.seed=0is remapped internally (xorshift cannot start at zero).seed(s)- reseed an existing generator.below(n)- integer in0 .. n-1. Returns0ifn <= 0.randint(a, b)- integer ina .. binclusive. RaisesValueErrorifb < a.random()- float in[0.0, 1.0).chance(p)-Truewith probabilityp(wherepis0..1).choice(seq)- one element ofseq. RaisesValueErroron an empty sequence.shuffle(lst)- Fisher-Yates shuffle oflst, in place (returnsNone).weighted(weights)- return an index0..len-1picked proportionally toweights. RaisesValueErrorif the total is<= 0. No streak control (independent draws).
Bag(items, rng):
- A shuffle-bag / “7-bag”: yields every item once per cycle in shuffled order, so no long streaks or droughts. Fairer than independent picks for spawns and pieces.
next()- return the next item, reshuffling automatically at the start of each cycle. RaisesValueErrorat construction ifitemsis empty.
Example (one seeded RNG for the whole game, plus an anti-streak spawn bag):
import picogame_rand
rng = picogame_rand.Rand(0x1234) # seeded -> reproduciblex = rng.randint(40, W - 40) # 40 .. W-40 inclusiveif rng.chance(0.25): spawn_powerup(x)kind = rng.weighted([5, 3, 1]) # index 0 most likelybag = picogame_rand.Bag([0, 1, 2, 3, 4, 5, 6], rng)piece = bag.next() # every value once per cycleGotchas:
shufflemutates the list in place and returnsNone- do not writelst = rng.shuffle(lst).Bagtakes ownership of a copy ofitems;next()reshuffles that internal list, so the order you passed in is not preserved.weightedreturns an index, not the value - index into your own list with it.- Same seed plus same call sequence equals same results: that is the point, but it also means an accidentally shared
rngcouples two systems’ randomness.
picogame_collide
Section titled “picogame_collide”What it is: zero-allocation collision tests that read positions and sizes straight off sprites, so you stop hand-writing abs(a.x - b.x) < w and ... in every entity loop. They touch only the integer getters (sprite.x, sprite.y, bitmap.width, bitmap.height) and never sprite.anchor (whose getter allocates), so they are safe to call many times per frame. See /hardware/ for the per-frame budget this protects.
When to use it: inside your collision loops. Use hit for box overlap, hit_point to test a bare coordinate against a sprite, and is_within for cheap circular range checks (explosions, magnet pickups, proximity).
API:
hit(a, b, hw=None, hh=None)- axis-aligned box (AABB) overlap of two sprites.hw/hhare the half-extents of the combined box; default is half the sum of the two bitmaps’ sizes (a centre-overlap test). Returns a bool.hit_point(a, px, py, hw=None, hh=None)- is point(px, py)within the sprite’s half-extents of its position? Default half-extents are half the sprite’s bitmap size.is_within(a, b, r)- circular test: are the two sprites’ positions within radiusr? Uses squared distance, so nosqrt.
Positions are treated as the sprites’ reference points: as long as both sprites share an anchor (both centre, or both top-left), the anchor offset cancels and these work regardless of which anchor you chose - see /scene-format/ for anchors.
Example (bullets vs rocks circular, player vs enemy boxed - adapted from asteroids and a platformer):
import picogame_collide as collide
for b in bullets: for r in rocks: if b.visible and collide.is_within(b, r, 18): # circular, no sqrt kill(b, r)
if collide.hit(player, enemy, 12, 16): # explicit half-extents take_damage()Gotchas:
- These read live sprite positions. If you track motion in floats (
data["x"]), callsprite.move(int(x), int(y))before testing, or the collision lags a frame behind the visible sprite. hitis a box test, not pixel-perfect - tunehw/hh(often smaller than the art) so corners do not register false hits.- Default extents come from
bitmap.width/height, so a sprite must have abitmapset before a default-extenthit/hit_pointcall; pass explicithw/hhotherwise. - Guard with
if sprite.visible(and any “alive” flag) yourself - the helpers test geometry only, not whether an entity is active. See /memory/ on pooling freed entities.