Math, random & collision
This page covers numeric helpers, a seedable random generator, and the collision methods on Sprite. Neither helper module requires engine setup. See /reference/ for the signatures.
picogame_math
Section titled “picogame_math”picogame_math provides clamp, lerp, approach, wrap, 2D vector functions, and trigonometry expressed in turns. Functions ending in _t use the interval [0,1) for one full rotation, which is convenient for stored headings and aiming. The module also keeps the vector helpers formerly provided by picogame_vec.
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 screenpicogame_rand
Section titled “picogame_rand”This seedable xorshift32 generator supports weighted choices, in-place shuffling, and a shuffle bag. Use a fixed seed for reproducible replays, ghost data, tests, or level layouts. Rand() without an argument seeds itself from the clock. Bag emits each item once per shuffled cycle, which prevents long streaks caused by independent choices.
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 cycleSprite collision
Section titled “Sprite collision”Every Sprite has box and radius collision methods. They use the drawn bounds after anchor, scale, and rotation without allocating a result object. Use overlaps() for sprite or rectangular intersections and near() for a circular distance check.
API (methods on any Sprite):
a.overlaps(b, inset=0)- inclusive AABB box overlap (they collide the moment they touch).bmay be anotherSprite, a point(x, y), or a rect(x1, y1, x2, y2)- e.g. a trigger zone, or(0, 0, W, H)to test whether the sprite is still on screen.insetshrinks THIS sprite’s box by N px on each side, for a hitbox smaller than the art. Returns a bool.a.near(b, r)- circular test: is this sprite’s centre withinrpx ofb’s centre? Uses squared distance, so nosqrt.bmay be aSpriteor a point(x, y).
Boxes and centres come from the sprite’s drawn rectangle, so both methods are anchor/scale/rotation aware - correct for any anchor (unlike a hand-rolled x-based test). See /scene-format/ for anchors.
For arbitrary boxes with no sprite (two computed regions), drop to the raw primitive pg.collide(x1, y1, x2, y2, ax1, ay1[, ax2, ay2]). For tile-grid walls/terrain, probe picogame_tiles flags rather than overlapping every tile.
Example (bullets vs rocks circular, player vs enemy boxed - from asteroids and a platformer):
for b in bullets: for r in rocks: if b.visible and b.near(r, 18): # circular, no sqrt kill(b, r)
if player.overlaps(enemy): # box overlap, anchor-correct take_damage()
if not ship.overlaps((0, 0, W, H)): # off-screen? despawn it pool.free(ship)