nesbox / TIC-80

TIC-80 is a fantasy computer for making, playing and sharing tiny games.
https://tic80.com
MIT License
5.01k stars 486 forks source link

In Python, "spr(id, x, y)" crashes if position x or y value is a float. #2339

Closed arcade-mntra closed 12 months ago

arcade-mntra commented 12 months ago

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' `

Skeptim commented 12 months ago

Python was not tested for long before release. This should interest @blueloveTH .

blueloveTH commented 12 months ago

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.

blueloveTH commented 12 months ago

I've also checked tic-80's c api of spr.

void tic_api_spr(tic_mem* memory, s32 index, s32 x, s32 y, s32 w, s32 h, u8* trans_colors, u8 trans_count, s32 scale, tic_flip flip, tic_rotate rotate)

It takes s32 a.k.a int as arguments. So python interprets them as int.

blueloveTH commented 12 months ago

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 :/

blueloveTH commented 12 months ago

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))
arcade-mntra commented 12 months ago

This is expected. The old implementation is incorrect.

@blueloveTH, thanks and congrats on developing pocketpy, it's like a dream come true using python in TIC80 :)

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

I've also checked tic-80's c api of spr.

void tic_api_spr(tic_mem* memory, s32 index, s32 x, s32 y, s32 w, s32 h, u8* trans_colors, u8 trans_count, s32 scale, tic_flip flip, tic_rotate rotate)

It takes s32 a.k.a int as arguments. So python interprets them as int.

But Lua implementation permits use of floats in spr() , circ(), rect() etc I ran Lua version of the above code using speed as int and as float, it didn't crash or raise an error.

blueloveTH commented 12 months ago

Lua uses weak-typed numbers. Lua's number doesn't distinguish int and float (even string can be implicitly converted to numbers). Python is strong-typed. They are different.

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

I have little knowledge about this. But pocketpy has box2d module support at here. This is not enabled for tic-80. You can enabled it in cmake files if you need this feature.

Skeptim commented 12 months ago

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.

arcade-mntra commented 12 months ago

Lua uses weak-typed numbers. Lua's number doesn't distinguish int and float (even string can be implicitly converted to numbers). Python is strong-typed. They are different.

Ah! That makes sense, Lua and its laissez faire approach.

But pocketpy has box2d module support at here. This is not enabled for tic-80. You can enabled it in cmake files if you need this feature.

Pocketpy has Box2d!? This is huge. Atm would be beyond my skillset if I run into bugs but am interested to give it a shot.

arcade-mntra commented 12 months ago

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
arcade-mntra commented 12 months ago

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.

blueloveTH commented 12 months ago

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.

arcade-mntra commented 12 months ago

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)
blueloveTH commented 12 months ago

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))
arcade-mntra commented 12 months ago

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

Fantastic. pocketpy keeps delivering. I have now bookmarked the cheat-sheet for quick reference :)

Does pocketpy have SimpleNamespace ?
That would be super convenient to make small data containers with dot access to attributes instead of using a Dict which is relatively more cumbersome to use.

blueloveTH commented 12 months ago

pocketpy supports standard python modules with __init__.py. But you need a filesystem (not necessary to use real filesystem, virtual interfaces are also ok)

I don't think tic-80 have any support of this.

arcade-mntra commented 12 months ago

No problem. I'm enjoying using python in Tic-80. Will upload some simple games in the coming weeks. Thanks for the help guys.