Skip to content

util pygame #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@ jobs:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
dependencies:
- pygame pyglet
- pygame
- pyglet
- "null"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Requirements
if: ${{ matrix.dependencies != 'null' }}
run: pip install ${{ matrix.dependencies }}
run: pip install pygame pyglet
- name: Run Tests
run: python -m unittest tests/pytmx/test_pytmx.py
run: python -m unittest discover -s tests/pytmx -p "test_*.py"
169 changes: 105 additions & 64 deletions pytmx/util_pygame.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2012-2024, Leif Theden <[email protected]>
Copyright (C) 2012-2025, Leif Theden <[email protected]>

This file is part of pytmx.

Expand All @@ -19,10 +19,11 @@
"""
import itertools
import logging
from typing import Optional, Union
from collections.abc import Callable
from typing import Any, Optional, Union

import pytmx
from pytmx.pytmx import ColorLike, PointLike
from pytmx.pytmx import ColorLike, TileFlags

logger = logging.getLogger(__name__)

Expand All @@ -37,8 +38,7 @@


def handle_transformation(
tile: pygame.Surface,
flags: pytmx.TileFlags,
tile: pygame.Surface, flags: pytmx.TileFlags
) -> pygame.Surface:
"""
Transform tile according to the flags and return a new one
Expand All @@ -52,6 +52,10 @@ def handle_transformation(

"""
if flags.flipped_diagonally:
if tile.get_width() != tile.get_height():
raise ValueError(
f"Cannot flip tile {tile.get_size()} diagonally if it is not a square"
)
tile = flip(rotate(tile, 270), True, False)
if flags.flipped_horizontally or flags.flipped_vertically:
tile = flip(tile, flags.flipped_horizontally, flags.flipped_vertically)
Expand All @@ -66,7 +70,7 @@ def smart_convert(
"""
Return new pygame Surface with optimal pixel/data format

This method does several interactive_tests on a surface to determine the optimal
This method does several interactive tests on a surface to determine the optimal
flags and pixel format for each tile surface.

Parameters:
Expand All @@ -78,41 +82,59 @@ def smart_convert(
new tile surface

"""
# tiled set a colorkey
# Set a colorkey if provided
if colorkey:
tile = original.convert()
tile.set_colorkey(colorkey, pygame.RLEACCEL)
# TODO: if there is a colorkey, count the colorkey pixels to determine if RLEACCEL should be used

# no colorkey, so use a mask to determine if there are transparent pixels
# Count the colorkey pixels to determine if RLEACCEL should be used
colorkey_pixels = 0
for x in range(tile.get_width()):
for y in range(tile.get_height()):
if tile.get_at((x, y)) == colorkey:
colorkey_pixels += 1

# If most of the pixels are the colorkey, use RLEACCEL
if colorkey_pixels / (tile.get_width() * tile.get_height()) > 0.5:
tile = tile.convert()
tile.set_colorkey(colorkey, pygame.RLEACCEL)
else:
# If not, use a per-pixel alpha surface if preferred
if pixelalpha:
tile = original.convert_alpha()
tile.set_colorkey(colorkey, pygame.RLEACCEL)

# No colorkey, so use a mask to determine if there are transparent pixels
else:
tile_size = original.get_size()
threshold = 254 # the default

try:
# count the number of pixels in the tile that are not transparent
# Count the number of pixels in the tile that are not transparent
px = pygame.mask.from_surface(original, threshold).count()
except:
# pygame_sdl2 will fail because the mask module is not included
# in this case, just convert_alpha and return it
return original.convert_alpha()

# there are no transparent pixels in the image
# There are no transparent pixels in the image
if px == tile_size[0] * tile_size[1]:
tile = original.convert()

# there are transparent pixels, and set for perpixel alpha
# There are transparent pixels, and set for per-pixel alpha
elif pixelalpha:
tile = original.convert_alpha()

# there are transparent pixels, and we won't handle them
# There are transparent pixels, and we won't handle them
else:
tile = original.convert()

return tile


def pygame_image_loader(filename: str, colorkey: Optional[ColorLike], **kwargs):
def pygame_image_loader(
filename: str, colorkey: Optional[ColorLike], **kwargs: Any
) -> Callable[[Optional[pygame.Rect], Optional[TileFlags]], pygame.Surface]:
"""
pytmx image loader for pygame

Expand All @@ -125,12 +147,20 @@ def pygame_image_loader(filename: str, colorkey: Optional[ColorLike], **kwargs):

"""
if colorkey:
colorkey = pygame.Color("#{0}".format(colorkey))
if isinstance(colorkey, str):
colorkey = pygame.Color(colorkey)
elif isinstance(colorkey, tuple) and 3 <= len(colorkey) <= 4:
colorkey = pygame.Color(colorkey)
else:
logger.error("Invalid colorkey")
raise ValueError("Invalid colorkey")

pixelalpha = kwargs.get("pixelalpha", True)
image = pygame.image.load(filename)

def load_image(rect=None, flags=None):
def load_image(
rect: Optional[pygame.Rect] = None, flags: Optional[TileFlags] = None
) -> pygame.Surface:
if rect:
try:
tile = image.subsurface(rect)
Expand All @@ -149,11 +179,7 @@ def load_image(rect=None, flags=None):
return load_image


def load_pygame(
filename: str,
*args,
**kwargs,
) -> pytmx.TiledMap:
def load_pygame(filename: str, *args: Any, **kwargs: Any) -> pytmx.TiledMap:
"""Load a TMX file, images, and return a TiledMap class

PYGAME USERS: Use me.
Expand Down Expand Up @@ -208,57 +234,67 @@ def build_rects(
"""
if isinstance(tileset, int):
try:
tileset = tmxmap.tilesets[tileset]
tileset_obj = tmxmap.tilesets[tileset]
except IndexError:
msg = "Tileset #{0} not found in map {1}."
logger.debug(msg.format(tileset, tmxmap))
raise IndexError
msg = f"Tileset #{tileset} not found in map {tmxmap}."
logger.debug(msg)
raise IndexError(msg)

elif isinstance(tileset, str):
try:
tileset = [t for t in tmxmap.tilesets if t.name == tileset].pop()
except IndexError:
msg = 'Tileset "{0}" not found in map {1}.'
logger.debug(msg.format(tileset, tmxmap))
raise ValueError

elif tileset:
msg = "Tileset must be either a int or string. got: {0}"
logger.debug(msg.format(type(tileset)))
raise TypeError
# Find the tileset with the matching name
tileset_obj = next((t for t in tmxmap.tilesets if t.name == tileset), None)
if tileset_obj is None:
msg = f'Tileset "{tileset}" not found in map {tmxmap}.'
logger.debug(msg)
raise ValueError(msg)
except Exception as e:
msg = f"Error finding tileset: {e}"
logger.debug(msg)
raise ValueError(msg)

gid = None
if real_gid:
try:
gid, flags = tmxmap.map_gid(real_gid)[0]
# Get the map GID and flags
map_gid = tmxmap.map_gid(real_gid)
if map_gid:
gid, flags = map_gid[0]
except IndexError:
msg = "GID #{0} not found"
logger.debug(msg.format(real_gid))
raise ValueError
msg = f"GID #{real_gid} not found"
logger.debug(msg)
raise ValueError(msg)

if isinstance(layer, int):
layer_data = tmxmap.get_layer_data(layer)
elif isinstance(layer, str):
try:
layer = [l for l in tmxmap.layers if l.name == layer].pop()
layer_data = layer.data
except IndexError:
msg = 'Layer "{0}" not found in map {1}.'
logger.debug(msg.format(layer, tmxmap))
raise ValueError

p = itertools.product(range(tmxmap.width), range(tmxmap.height))
if gid:
points = [(x, y) for (x, y) in p if layer_data[y][x] == gid]
else:
points = [(x, y) for (x, y) in p if layer_data[y][x]]
# Find the layer with the matching name
layer_obj = next(
(l for l in tmxmap.layers if l.name and l.name == layer), None
)
if layer_obj is None:
msg = f'Layer "{layer}" not found in map {tmxmap}.'
logger.debug(msg)
raise ValueError(msg)
layer_data = layer_obj.data
except Exception as e:
msg = f"Error finding layer: {e}"
logger.debug(msg)
raise ValueError(msg)

points = [
(x, y)
for x, y in itertools.product(range(tmxmap.width), range(tmxmap.height))
if (not gid and layer_data[y][x]) or (gid and layer_data[y][x] == gid)
]

rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight)
return rects


def simplify(
all_points: list[PointLike],
all_points: list[tuple[int, int]],
tilewidth: int,
tileheight: int,
) -> list[pygame.Rect]:
Expand Down Expand Up @@ -304,23 +340,28 @@ def simplify(
making a list of rects, one for each tile on the map!
"""

def pick_rect(points, rects) -> None:
ox, oy = sorted([(sum(p), p) for p in points])[0][1]
def pick_rect(points: list[tuple[int, int]], rects: list[pygame.Rect]) -> None:
"""
Recursively pick a rect from the points and add it to the rects list.
"""
if not points:
return

ox, oy = min(points, key=lambda p: (p[0], p[1]))
x = ox
y = oy
ex = None

while 1:
while True:
x += 1
if not (x, y) in points:
if (x, y) not in points:
if ex is None:
ex = x - 1

if (ox, y + 1) in points:
if x == ex + 1:
y += 1
x = ox

else:
y -= 1
break
Expand All @@ -339,14 +380,14 @@ def pick_rect(points, rects) -> None:
rects.append(c_rect)

rect = pygame.Rect(ox, oy, ex - ox + 1, y - oy + 1)
kill = [p for p in points if rect.collidepoint(p)]
[points.remove(i) for i in kill]
points[:] = [p for p in points if not rect.collidepoint(p)]

pick_rect(points, rects)

if points:
pick_rect(points, rects)
if not all_points:
return []

rect_list = []
while all_points:
pick_rect(all_points, rect_list)
rect_list: list[pygame.Rect] = []
pick_rect(all_points, rect_list)

return rect_list
Loading