This implementation builds a Sokoban level editor using Pyglet, featuring real-time sprite movement synchronized to mouse psoition, grid-based layout, and interactive tile placement.
Mouse-Driven Sprite Movement
A sprite follows the mouse cursor continuously. On mouse press, a tile is placed at the current grid-aligned position. The core logic uses on_mouse_motion too update the sprite’s coordinates relative to the cursor, and on_mouse_press to register tile placement in a dictionary mapping positions to tile IDs.
import pyglet
from pyglet import shapes
WIDTH, HEIGHT = 1080, 720
SKYBLUE = (176, 196, 222, 255)
LIGHTGRAY = (220, 220, 220, 150)
RED = (255, 0, 0, 255)
window = pyglet.window.Window(WIDTH, HEIGHT, caption="Sokoban Level Editor")
pyglet.gl.glClearColor(*[c / 255 for c in SKYBLUE])
# Load tile images (simplified here as solid-color placeholders)
tiles = []
for i in range(8):
pattern = pyglet.image.SolidColorImagePattern(
color=(120 + i * 20, 140 + i * 15, 160 - i * 10, 255)
)
tiles.append(pattern.create_image(60, 60))
current_tile = 0
maps = {}
grid_size = 60
# Draw grid lines
grid_lines = []
for x in range(0, WIDTH + 1, grid_size):
grid_lines.append(shapes.Line(x, 120, x, HEIGHT, color=LIGHTGRAY))
for y in range(120, HEIGHT + 1, grid_size):
grid_lines.append(shapes.Line(0, y, WIDTH, y, color=LIGHTGRAY))
# Cursor indicator
cursor_circle = shapes.Circle(0, 0, 8, color=RED)
# Active sprite
sprite = pyglet.sprite.Sprite(tiles[current_tile], 0, 0)
sprite.visible = False
# UI label
label = pyglet.text.Label(
"Select tile 1–8; current: wall",
font_size=16, x=20, y=16
)
@window.event
def on_draw():
window.clear()
# Draw grid
for line in grid_lines:
line.draw()
# Draw placed tiles
for (x, y), idx in maps.items():
if idx < len(tiles):
tiles[idx].blit(x, y)
# Draw cursor indicator
cursor_circle.x, cursor_circle.y = sprite.x + 30, sprite.y + 30
cursor_circle.draw()
# Draw active sprite preview
sprite.draw()
# Update label
tile_names = ["wall", "floor", "box0", "person0", "target", "box1", "person1", "delete"]
label.text = f"Select tile 1–8; current: {tile_names[current_tile]}; pos: ({int(sprite.x // grid_size + 1)}, {int(sprite.y // grid_size - 1)})"
label.draw()
@window.event
def on_mouse_motion(x, y, dx, dy):
if 20 <= x < WIDTH - 20 and 140 <= y < HEIGHT - 20:
sprite.visible = True
sprite.x = (x // grid_size) * grid_size
sprite.y = (y // grid_size) * grid_size
else:
sprite.visible = False
@window.event
def on_mouse_press(x, y, button, modifiers):
if 20 <= x < WIDTH - 20 and 140 <= y < HEIGHT - 20:
gx, gy = (x // grid_size) * grid_size, (y // grid_size) * grid_size
maps[(gx, gy)] = current_tile
@window.event
def on_key_press(symbol, modifiers):
global current_tile
if symbol in range(pyglet.window.key._1, pyglet.window.key._9):
current_tile = symbol - pyglet.window.key._1
if current_tile < len(tiles):
sprite.image = tiles[current_tile]
pyglet.app.run()
Grid Rendering Optimization
To avoid memory bloat from recreating line objects every frame, all grid lines are precomputed once into a list and reused in on_draw. This ensures consistent performance regardless of frame rate or rendering frequency.
Tile Selection and Placement Logic
Keys 1–8 select tile types. Pressing a key updates both the active sprite image and the visual indicator. Clicking anywhere within the editable area places the selected tile at the nearest grid intersection. Position tracking uses integer division to snap to the grid, ensuring alignment and deterministic behavior.
Data Representation
The map state is stored in a dictionary where keys are (x, y) pixel coordinates (multiples of grid size), and values are tile indices. This avoids sparse array overhead and simplifies serialization. For example:
# Print ASCII representation of the map
for row in reversed(range(10)):
line = ""
for col in range(18):
pos = (col * 60, row * 60 + 120)
tile_id = maps.get(pos, 7) # default to delete tile
line += str(tile_id + 1) + " "
print(line)
The resulting output aligns precisely with the visual editor layout, enabling round-trip editing and validation.