TIC-80: In Python, "spr(id, x, y)" crashes if position x or y value is a float.

A python project made with previous version crashed when run using current version with an error expecting int values in spr(id, x, y). Some objects like projectiles had advance movement with acceleration as a float or enemies with variable speed in increments of 0.5.

That project is too big to recreate here. The issue can be recreated by moving a sprite, incrementing its x or y position with a float value.

Similar error noticed for other builtin draw shape functions like pix(), circ(), rect() etc.

Version : 1.1.2837 (be42d6f) OS : Windows 10

Reproduction steps

  1. new python
  2. esc to enter code editor
  3. go to sprite editor, sprites section and bucket fill any color other than black on sprite number 256 or draw rectangle using rect(x,y,w,h,color).
  4. In the following code, project runs fine if speed = 1 and crashes if speed = 1.0

`x = 120 y = 24 w = 8 h = 8 id = 256 speed = 1.0

def TIC():

global x,y,speed
cls(0)

if y > 140: 
    y = 0
	
y += speed

#works only if variable y is an int
spr(id, x, y)
#pix(x, y, 2)
#rect(x, y, w, h, 4)`

`

error while running TIC Traceback (most recent call last): File “main.py”, line 28, in TIC spr(id, x, y) TypeError: expected ‘int’, got ‘float’ `

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Reactions: 1
  • Comments: 17 (8 by maintainers)

Most upvoted comments

pocketpy integrated box2d a long time ago. box2d module is stable. And it is used for my custom engine.

To enable it, just add a line into cmakelists.txt,

option(PK_USE_BOX2D "" ON)

then you can import box2d.

tic-80’s pocketpy does not enable box2d by default. Because I think tic-80 aims to restrict users, making them not too powerful.

You can override the builtin functions to simply your work if you don’t want to replace them all.

For example,

_spr = spr
spr = lambda id,x,y: _spr(id, int(x), int(y))

This is really impressing!

Some tips:

print("x: "+str(object.x),int(object.x-4),int(object.y-8))

You can use f-string to format floats, which avoids 0.9999999...

print(f"x: {object.x:.2f}",int(object.x-4),int(object.y-8))

Following up on the simple movement vs smooth movement situation,

Converting float values to nearest whole number before using the spr() function results in janky movement as seen in the gif below.

Using, spr(id, round(x), round(y)) works but @blueloveTH has cautioned not to implicitly convert float to int inside the spr() function for potential future compatibility reasons.

Using his following solution of custom spr implementation solves the problem for achieving smooth/gradual movement.

_spr = spr spr = lambda id,x,y: _spr(id, int(x), int(y))

Tic80_python_movement_compare2

Python code to comapre simple movement vs smooth movement.

## -- HELPER FUNCTION -- ##
_spr = spr
spr = lambda id,x,y: _spr(id, int(x), int(y))

class Box:
	def __init__(self,x,y,id):
		self.x = x
		self.y = y
		self.x_vel = 0
		self.x_dir = 1
		self.max_speed = 3
		self.acc = 0.25
		self.friction = 0.95
		self.id = id
				
box_1 = Box(120,24,256)
box_2 = Box(120,64,257)
box_3 = Box(120,104,258)

def move(object, mode):
	# simple movment, instant start and stop
	if mode == 1:
		# L-R movement
		if btn(2): object.x -= object.max_speed
		if btn(3): object.x += object.max_speed
		
	# advance movement with acc and friction
	if mode > 1:
		# apply friction to velocity every frame
		object.x_vel *= object.friction
		
		# L-R movement, apply acc to velocity when btn pressed
		if btn(2): object.x_vel -= object.acc
		if btn(3): object.x_vel += object.acc
		
		# apply velocity to x position every frame
		object.x += object.x_vel
		
		# rounding float to nearest whole number
		# gives smooth start but janky stop
		if mode == 2: object.x = round(object.x)
		
		# rounding float to nearst decimal place
		# gives smooth start and fairly smooth stop
		if mode == 3: object.x = round(object.x,2)
		
	# draw box
	#spr(object.id, round(object.x), round(object.y))  #this works but can cause future compatibility issue
	spr(object.id, object.x, object.y)  #using custom implementation of spr
	print("x: "+str(object.x),int(object.x-4),int(object.y-8))

def TIC():
	cls(0)
	# mode = 1: simple movement, no acc, no friction
	# mode = 2: movement with acc, friction and round(box.x)
	# mode = 3: movement with acc, friction and round(box.x,1)
	move(box_1,1)
	move(box_2,2)
	move(box_3,3)
	
	# DISPLAY INFO
	print("STOPPAGE", 2,2,12)
	print("Instant:", 2,24,3)
	print("Janky:", 2,64,5)
	print("Gradual:", 2,104,10)

emm…box2d is much more lightweight than pocketpy.

I’m currently working on a making course using game engines like Godot, Defold and beginner python gamedev course using PygameZero(pygame minus the boilerplate). So crushed for time.

Just last month discovered TIC80 has python and performs way better than pygame.

It’s a pain to export to web a game made in pygame, which is where TIC80 shines with ease of export to web, desktop etc.

TIC80 has integrated sprite editor, tilemap editor, sfx editor and code editor that works on mobile phones also. This is huge for teaching programming in developing countries where kids dont have laptops but have access to mobile phones.

If TIC80 can ship with box2d enabled it has potential to become the default tool to teach python in schools and make HTML5 games instead of the boilerplate roadblocks the Pygame puts you through.

Is there a way to implement gradual acceleration, friction or angle rotation using only int values without everything running way too fast?

At the end the pixels positions are integers so you can still use floats but they will be converted to int at some point (but I may misunderstand what you mean). If you need to use floats (especially concerning angle rotation) you could use ttri instead.

When using Lua, I can directly increment or decrement velocity by acceleration = 0.25 per frame to apply the ramp-up effect. Or multiply velocity by friction = 0.95 i.e. 5% reduction per frame instead of stopping instantly to make the floor feel slippery.

I will try to implement it in python today. Meanwhile, sample Lua code to compare the two movements:

player={
    x=120,
    y=68,
    x_vel=0,
    max_speed=3,
    acc=0.25,
    img = 256
}

friction=0.95

function TIC()
  cls()
	
  --simple_movement()
	
  advance_movement()

  -- draw player 
  spr(player.img, player.x, player.y)
  print(player.x)

end


function simple_movement()
  if btn(2) then 
  	player.x = player.x - player.max_speed --LEFT
  elseif btn(3) then 
  	player.x = player.x + player.max_speed --RIGHT
  end
end


function advance_movement()
  -- apply friction when btn released
  player.x_vel = player.x_vel * friction

  -- apply acc when moving Left or Right btn pressed
  if btn(2) then 
  	player.x_vel = player.x_vel - player.acc --LEFT
  elseif btn(3) then 
  	player.x_vel = player.x_vel + player.acc --RIGHT
  end

  --limit left/right max speed
  if player.x_vel < -player.max_speed then 
  	player.x_vel = -player.max_speed 
  end
  if player.x_vel > player.max_speed then 
	player.x_vel = player.max_speed 
  end

  --apply x_vel to player position
  player.x = player.x + player.x_vel
end

Is there a way to implement gradual acceleration, friction or angle rotation using only int values without everything running way too fast?

At the end the pixels positions are integers so you can still use floats but they will be converted to int at some point (but I may misunderstand what you mean). If you need to use floats (especially concerning angle rotation) you could use ttri instead.

That project is too big to recreate here. The issue can be recreated by moving a sprite, incrementing its x or y position with a float value.

I know that you are relying on the previous float conversion and need to change a lot of things. For example,

spr(id, int(x), int(y))

or,

spr(id, round(x), round(y))

However, I believe not to do float to int implicit conversion is the right implementation and this is better for future 😕

This is expected. The old implementation is incorrect. Here is the stub file for the lastest tic-80.

def btn(id: int) -> bool: ...
def btnp(id: int, hold=-1, period=-1) -> bool: ...
def circ(x: int, y: int, radius: int, color: int): ...
def circb(x: int, y: int, radius: int, color: int): ...
def clip(x: int, y: int, width: int, height: int): ...
def cls(color=0): ...
def elli(x: int, y: int, a: int, b: int, color: int): ...
def ellib(x: int, y: int, a: int, b: int, color: int): ...
def exit(): ...
def fget(sprite_id: int, flag: int) -> bool: ...
def fset(sprite_id: int, flag: int, b: bool): ...
def font(text: str, x: int, y: int, chromakey: int, char_width=8, char_height=8, fixed=False, scale=1, alt=False) -> int: ...
def key(code=-1) -> bool: ...
def keyp(code=-1, hold=-1, period=-17) -> int: ...
def line(x0: int, y0: int, x1: int, y1: int, color: int): ...
def map(x=0, y=0, w=30, h=17, sx=0, sy=0, colorkey=-1, scale=1, remap=None): ...
def memcpy(dest: int, source: int, size: int): ...
def memset(dest: int, value: int, size: int): ...
def mget(x: int, y: int) -> int: ...
def mset(x: int, y: int, tile_id: int): ...
def mouse() -> tuple[int, int, bool, bool, bool, int, int]: ...
def music(track=-1, frame=-1, row=-1, loop=True, sustain=False, tempo=-1, speed=-1): ...
def peek(addr: int, bits=8) -> int: ...
def peek1(addr: int) -> int: ...
def peek2(addr: int) -> int: ...
def peek4(addr: int) -> int: ...
def pix(x: int, y: int, color: int=None) -> int | None: ...
def pmem(index: int, value: int=None) -> int: ...
def poke(addr: int, value: int, bits=8): ...
def poke1(addr: int, value: int): ...
def poke2(addr: int, value: int): ...
def poke4(addr: int, value: int): ...
def print(text, x=0, y=0, color=15, fixed=False, scale=1, alt=False): ...
def rect(x: int, y: int, w: int, h: int, color: int): ...
def rectb(x: int, y: int, w: int, h: int, color: int): ...
def reset(): ...
def sfx(id: int, note=-1, duration=-1, channel=0, volume=15, speed=0): ...
def spr(id: int, x: int, y: int, colorkey=-1, scale=1, flip=0, rotate=0, w=1, h=1): ...
def sync(mask=0, bank=0, tocart=False): ...
def ttri(x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, u1: float, v1: float, u2: float, v2: float, u3: float, v3: float, texsrc=0, chromakey=-1, z1=0.0, z2=0.0, z3=0.0): ...
def time() -> int: ...
def trace(message, color=15): ...
def tri(x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, color: int): ...
def trib(x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, color: int): ...
def tstamp() -> int: ...
def vbank(bank: int=None) -> int: ...

Python allows implicit conversion from int to float but not float to int.