Přeskočit na obsah

Tutoriál 3 — Quest

Zeldovský overworld: chodíš po scrollovací mapě, sbíráš mince, mluvíš s NPC, bojuješ se slimy a plníš quest. Předpokládá, že máš za sebou 01-bounce a 02-starship. Tady přichází na řadu engine kamera a tilemapa jako svět.

Celý zdrojový kód najdeš na GitHubu.

Spuštění libovolného kroku:

python3 sim/run.py tutorials/03-quest/stepN_name.py --hold DOWN --shot /tmp/out.png

Tip: kroky s dialogem a questem (6–8) si nejlíp prohlédneš živě přes --backend pygame — bezhlavý --shot může chytit dialogové okno uprostřed kreslení (overlay se maluje na několik průchodů), takže box může vyjít na screenshotu bez textu, i když na zařízení a v živém okně se vykreslí správně.


step 1 — step1_world.py · svět větší než obrazovka

Sekce “step 1 — step1_world.py · svět větší než obrazovka”

Quest – krok 1 ASCII mapa se promění na Tilemap (30×20 dlaždic = 480×320 px, větší než obrazovka 320×240). shp.tileset_colors postaví tileset (tráva/cesta/voda/strom/zeď/dveře/cíl). scene.set_view(ox, oy) vybere viditelné okno — to je kamera. Hrdina žije ve světových souřadnicích; offset view rozhodne, kam dopadne na obrazovce. Uvidíš: kus světa s hrdinou uprostřed. Zkus: uprav stringy v MAP.

15 collapsed lines
# Quest -- step 1: a world bigger than the screen.
#
# This third tutorial builds a top-down RPG. It teaches what Bounce and Starship
# couldn't: a scrolling world larger than the display, a camera that follows the
# hero, tile-based wall collision, a walk animation, items, an NPC you talk to, and
# light combat. Do 01-bounce and 02-starship first.
#
# What you learn here: a Tilemap can be a whole WORLD (30x20 tiles = 480x320 px,
# bigger than the 320x240 screen). scene.set_view(ox, oy) chooses which part of the
# world is on screen -- that's the camera. We centre it on the hero. The hero lives
# in WORLD coordinates; the view offset decides where that lands on screen.
#
# New: a large Tilemap from an ASCII map, shp.tileset_colors, scene.set_view.
#
# Run: python3 sim/run.py tutorials/03-quest/step1_world.py --shot /tmp/q1.png
import array
import picogame as pg
import picogame_game
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
TILE = 16
# . grass : path ~ water(solid) # tree(solid) W wall(solid) D door G goal
# P player N npc * coin E enemy
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
# map char -> tile value (entities sit on grass; the tile under them is grass/path)
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
# tile colours for values 1..7 (value 0 is unused)
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(0, 0, 0))
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE # world pixel position of the hero
for ty in range(MROWS):
row = MAP[ty]
for tx in range(MCOLS):
ch = row[tx] if tx < len(row) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
scene.add(world)
def hero_bitmap():
"""A simple hero: a 16x16 body with a lighter 'face' nub on the facing edge.
frame 0=down, 1=up, 2=left, 3=right."""
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60), pg.rgb565(255, 225, 170)])
stride = TILE * 4
data = bytearray(stride * TILE)
for f in range(4):
for y in range(TILE):
for x in range(TILE):
face = ((f == 0 and y >= TILE - 4) or (f == 1 and y < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + f * TILE + x] = 2 if face else 1
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=4, stride=stride)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=0)
scene.add(hero)
def follow():
# centre the camera on the hero, clamped so we never show past the world edges
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
while True:
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 2 — step2_walk.py · chůze, kamera následuje

Sekce “step 2 — step2_walk.py · chůze, kamera následuje”

Quest – krok 2 Pohyb do 4 směrů; po každém pohybu follow() znovu vycentruje kameru, clampnutou ke světu, takže nikdy nekoukneš za jeho okraj (blízko okraje hrdina kráčí k okraji obrazovky místo toho). Hrdina se otáčí ve směru pohybu (sprite.frame). Zatím žádná kolize se zdmi — můžeš chodit po vodě. Zkus: změň SPEED.

52 collapsed lines
# Quest -- step 2: walk around, camera follows.
#
# What you learn: world vs screen coordinates. The hero moves in WORLD space; after
# each move we call follow() to re-aim the camera. Near the world edges the clamp in
# follow() stops the camera and the hero walks toward the screen edge instead -- the
# classic top-down feel. The hero also FACES the way it moves (frame = facing).
# There's no wall collision yet, so you can walk over water and trees (step 3 fixes
# that).
#
# New vs step 1: 4-direction input, facing (sprite.frame), calling follow() on move.
#
# Run: python3 sim/run.py tutorials/03-quest/step2_walk.py --hold DOWN --shot /tmp/q2.png
import array
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3 # facing -> frame
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(0, 0, 0))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
scene.add(world)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60), pg.rgb565(255, 225, 170)])
stride = TILE * 4
data = bytearray(stride * TILE)
for f in range(4):
for y in range(TILE):
for x in range(TILE):
face = ((f == 0 and y >= TILE - 4) or (f == 1 and y < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + f * TILE + x] = 2 if face else 1
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=4, stride=stride)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=DOWN)
scene.add(hero)
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
hero.frame = RIGHT if dx > 0 else LEFT # face horizontally
elif dy:
hero.frame = DOWN if dy > 0 else UP # face vertically
if dx or dy:
hero.move(hero.x + dx * SPEED, hero.y + dy * SPEED)
follow()
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 3 — step3_walls.py · kolize s dlaždicemi

Sekce “step 3 — step3_walls.py · kolize s dlaždicemi”

Quest – krok 3 solid_at(px, py) namapuje světový pixel na dlaždici a zkontroluje množinu SOLID; can_walk() osahá hrdinovy čtyři rohy. Pohyb v X a Y testujeme odděleně, takže místo zaseknutí podél zdi sklouzneš, když do ní tlačíš diagonálně. Uvidíš: voda, stromy a zdi tě teď blokují. Zkus: přidej hodnotu dlaždice do SOLID (nebo nějakou odeber).

52 collapsed lines
# Quest -- step 3: walls (tile-based collision).
#
# What you learn: stop the hero walking through water/trees/walls. Convert a world
# pixel to a tile (tx = px // TILE), look the tile up, and treat some values as
# SOLID. can_walk() probes the hero's body (its four corners, inset a little) so it
# can't clip into a wall. We test the X and Y moves SEPARATELY, so the hero slides
# along a wall instead of sticking when you push diagonally into it.
#
# New vs step 2: solid_at()/can_walk() pixel->tile collision, per-axis movement.
#
# Run: python3 sim/run.py tutorials/03-quest/step3_walls.py --hold RIGHT --shot /tmp/q3.png
import array
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6) # water, tree, wall, door block movement
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(0, 0, 0))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
scene.add(world)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60), pg.rgb565(255, 225, 170)])
stride = TILE * 4
data = bytearray(stride * TILE)
for f in range(4):
for y in range(TILE):
for x in range(TILE):
face = ((f == 0 and y >= TILE - 4) or (f == 1 and y < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + f * TILE + x] = 2 if face else 1
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=4, stride=stride)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=DOWN)
scene.add(hero)
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
# probe the hero's body corners (inset 2px) -- all must be free
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
hero.frame = RIGHT if dx > 0 else LEFT
elif dy:
hero.frame = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y): # test X alone
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED): # then Y alone -> slide
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow()
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 4 — step4_anim.py · animace chůze

Sekce “step 4 — step4_anim.py · animace chůze”

Quest – krok 4 Hrdina se stane 8snímkovým sheetem (2 kroky × 4 směry), řízeným přes picogame_anim.AnimatedSprite: play(name) vybere animaci daného směru, tick(dt) ji posune pomocí reálného dt z clock.tick() (takže rychlost chůze nezávisí na snímkovací frekvenci). Když stojíš, ukáže se klidový snímek. Zkus: změň fps animace.

56 collapsed lines
# Quest -- step 4: a walking animation.
#
# What you learn: time-based animation. We upgrade the hero to an 8-frame sheet --
# two frames per facing (a small step "bob") -- and drive it with
# picogame_anim.AnimatedSprite: named animations, advanced by tick(dt). While the
# hero moves we play() the animation for the current facing and tick it with the
# frame's real dt (from clock.tick()); when standing still we show the still frame.
# Using dt (not a frame counter) keeps the walk speed the same at any frame rate.
#
# New vs step 3: an 8-frame hero, picogame_anim.AnimatedSprite, play()/tick(dt),
# the dt returned by clock.tick().
#
# Run: python3 sim/run.py tutorials/03-quest/step4_anim.py --hold RIGHT --shot /tmp/q4.png
import array
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_anim
import picogame_shapes as shp
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6)
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
FACE_NAME = ("down", "up", "left", "right")
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(0, 0, 0))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
scene.add(world)
def hero_bitmap():
"""8 frames = 4 facings x 2 walk steps. Step 1 bobs the body down 1px so the
walk reads as motion. frame index = facing*2 + step."""
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60),
pg.rgb565(255, 225, 170), pg.rgb565(120, 40, 30)])
stride = TILE * 8
data = bytearray(stride * TILE)
for f in range(4):
for s in range(2):
fr = f * 2 + s
bob = s # bob down 1px on the second step
for y in range(bob, TILE):
yy = y - bob
for x in range(TILE):
face = ((f == 0 and yy >= TILE - 4) or (f == 1 and yy < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + fr * TILE + x] = 2 if face else 1
# two feet at the bottom, swapping sides per step (extra motion cue)
lx = 4 if s == 0 else 6
for x in (lx, TILE - 1 - lx):
data[(TILE - 1) * stride + fr * TILE + x] = 3
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=8,
stride=stride, transparent=0)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=0)
walk = picogame_anim.AnimatedSprite(hero, {
"down": ([0, 1], 8, True), "up": ([2, 3], 8, True),
"left": ([4, 5], 8, True), "right": ([6, 7], 8, True)})
scene.add(hero)
facing = DOWN
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
dt = 1 / 30
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
facing = RIGHT if dx > 0 else LEFT
elif dy:
facing = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y):
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED):
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow()
walk.play(FACE_NAME[facing]) # animate the walk while moving
walk.tick(dt)
else:
hero.frame = facing * 2 # still frame for the current facing
scene.refresh()
dt = clock.tick()
▶ Vyzkoušet v prohlížeči

step 5 — step5_items.py · předměty + pevný HUD

Sekce “step 5 — step5_items.py · předměty + pevný HUD”

Quest – krok 5 Mince jsou sprity umístěné z mapy; jako běžné položky scény se scrollují se světem. Sebereme jednu, když je hrdina blízko, schováme ji a počítáme. picogame_ui.SceneLabel je pevná vrstva — NEscrolluje se, takže počítadlo zůstává připnuté v rohu. Zkus: přidej do mapy další mince *.

59 collapsed lines
# Quest -- step 5: collectible items + a HUD over the scrolling world.
#
# What you learn: world-space pickups and a camera-fixed HUD. Coins are sprites
# placed at map positions; because they're normal scene items they scroll with the
# world. We collect one when the hero is close enough (a simple distance test), hide
# it, and bump a counter. picogame_ui.SceneLabel is a FIXED scene layer -- it does NOT
# scroll, so the coin counter stays pinned to the corner while the world moves under
# it.
#
# New vs step 4: item sprites placed from the map, distance-based pickup, a fixed
# SceneLabel counter.
#
# Run: python3 sim/run.py tutorials/03-quest/step5_items.py --hold RIGHT --shot /tmp/q5.png
import array
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_anim
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6)
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
FACE_NAME = ("down", "up", "left", "right")
BG = pg.rgb565(0, 0, 0)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE
coin_spots = []
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
elif ch == "*":
coin_spots.append((tx * TILE, ty * TILE))
scene.add(world)
coin_bm = shp.circle(8, pg.rgb565(245, 215, 60))
coins = [pg.Sprite(coin_bm, x + 4, y + 4) for (x, y) in coin_spots] # +4 to centre in tile
for c in coins:
scene.add(c)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60),
pg.rgb565(255, 225, 170), pg.rgb565(120, 40, 30)])
stride = TILE * 8
data = bytearray(stride * TILE)
for f in range(4):
for s in range(2):
fr = f * 2 + s
for y in range(s, TILE):
yy = y - s
for x in range(TILE):
face = ((f == 0 and yy >= TILE - 4) or (f == 1 and yy < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + fr * TILE + x] = 2 if face else 1
lx = 4 if s == 0 else 6
for x in (lx, TILE - 1 - lx):
data[(TILE - 1) * stride + fr * TILE + x] = 3
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=8,
stride=stride, transparent=0)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=0)
walk = picogame_anim.AnimatedSprite(hero, {
"down": ([0, 1], 8, True), "up": ([2, 3], 8, True),
"left": ([4, 5], 8, True), "right": ([6, 7], 8, True)})
scene.add(hero)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BG)
facing = DOWN
got = 0
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
dt = 1 / 30
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
facing = RIGHT if dx > 0 else LEFT
elif dy:
facing = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y):
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED):
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow()
walk.play(FACE_NAME[facing]); walk.tick(dt)
else:
hero.frame = facing * 2
# pick up any coin we're standing on
for c in coins:
if c.visible and abs(hero.x - c.x) < 12 and abs(hero.y - c.y) < 12:
c.visible = False
got += 1
hud.set("COINS %d/%d" % (got, len(coins)))
scene.refresh()
dt = clock.tick()
▶ Vyzkoušet v prohlížeči

step 6 — step6_npc.py · mluv s NPC

Sekce “step 6 — step6_npc.py · mluv s NPC”

Quest – krok 6 Postav se k NPC a stiskni A pro vstup do stavu dialog: svět se pod tím dál vykresluje, zatímco picogame_ui.TextBox překryje zprávu; pohyb je zmrazený, dokud nestiskneš tlačítko. Uvidíš: prompt „PRESS A“ poblíž NPC, pak dialogové okno. Zkus: změň LINES dialogu.

62 collapsed lines
# Quest -- step 6: an NPC you can talk to.
#
# What you learn: interaction + a simple state machine for dialog. An NPC is a
# sprite; when the hero stands next to it and presses A we switch to a "dialog"
# state: the world keeps drawing underneath, and picogame_ui.TextBox draws a
# multi-line message box on top (a screen-space overlay, drawn after scene.refresh).
# While in dialog we DON'T process movement -- the game is paused on the box until
# you press a button to dismiss it.
#
# New vs step 5: an NPC sprite, an over/dialog state, picogame_ui.TextBox, freezing
# the world during dialog, an adjacency "PRESS A" prompt.
#
# Run: python3 sim/run.py tutorials/03-quest/step6_npc.py --shot /tmp/q6.png
import array
import board
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_anim
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6)
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
FACE_NAME = ("down", "up", "left", "right")
BG = pg.rgb565(0, 0, 0)
WHITE = pg.rgb565(255, 255, 255)
NAVY = pg.rgb565(10, 10, 40)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
hero_x, hero_y = TILE, TILE
npc_x, npc_y = TILE, TILE
coin_spots = []
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
hero_x, hero_y = tx * TILE, ty * TILE
elif ch == "N":
npc_x, npc_y = tx * TILE, ty * TILE
elif ch == "*":
coin_spots.append((tx * TILE, ty * TILE))
scene.add(world)
coin_bm = shp.circle(8, pg.rgb565(245, 215, 60))
coins = [pg.Sprite(coin_bm, x + 4, y + 4) for (x, y) in coin_spots]
for c in coins:
scene.add(c)
npc = pg.Sprite(shp.rect(TILE, TILE, pg.rgb565(230, 200, 60)), npc_x, npc_y)
scene.add(npc)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60),
pg.rgb565(255, 225, 170), pg.rgb565(120, 40, 30)])
stride = TILE * 8
data = bytearray(stride * TILE)
for f in range(4):
for s in range(2):
fr = f * 2 + s
for y in range(s, TILE):
yy = y - s
for x in range(TILE):
face = ((f == 0 and yy >= TILE - 4) or (f == 1 and yy < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + fr * TILE + x] = 2 if face else 1
lx = 4 if s == 0 else 6
for x in (lx, TILE - 1 - lx):
data[(TILE - 1) * stride + fr * TILE + x] = 3
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=8,
stride=stride, transparent=0)
hero = pg.Sprite(hero_bitmap(), hero_x, hero_y, frame=0)
walk = picogame_anim.AnimatedSprite(hero, {
"down": ([0, 1], 8, True), "up": ([2, 3], 8, True),
"left": ([4, 5], 8, True), "right": ([6, 7], 8, True)})
scene.add(hero)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, WHITE, BG)
dlg = ui.TextBox(pg, terminalio.FONT, 8, H - 60, W - 16, 54, WHITE, NAVY, maxlines=4)
LINES = ["Villager:", "Beware the slimes in the", "tall grass, traveller.", "(press A)"]
facing = DOWN
got = 0
state = "over"
dlg_shown = False # draw the modal once, not every frame
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def near_npc():
return abs(hero.x - npc.x) <= TILE and abs(hero.y - npc.y) <= TILE
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
dt = 1 / 30
while True:
btn.poll()
if state == "dialog":
if not dlg_shown: # draw ONCE -> no per-frame flicker
scene.refresh() # world frozen under the box
dlg.draw(board.DISPLAY, bufA, LINES)
dlg_shown = True
if btn.just_pressed(btn.A) or btn.just_pressed(btn.B):
state = "over"
scene.invalidate() # repaint over the box next frame
clock.tick()
continue
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
facing = RIGHT if dx > 0 else LEFT
elif dy:
facing = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y):
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED):
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow()
walk.play(FACE_NAME[facing]); walk.tick(dt)
else:
hero.frame = facing * 2
for c in coins:
if c.visible and abs(hero.x - c.x) < 12 and abs(hero.y - c.y) < 12:
c.visible = False
got += 1
if near_npc():
hud.set("COINS %d/%d A: TALK" % (got, len(coins)))
if btn.just_pressed(btn.A):
state = "dialog"; dlg_shown = False
else:
hud.set("COINS %d/%d" % (got, len(coins)))
scene.refresh()
dt = clock.tick()
▶ Vyzkoušet v prohlížeči

step 7 — step7_combat.py · nepřátelé + bump combat

Sekce “step 7 — step7_combat.py · nepřátelé + bump combat”

Quest – krok 7 Slimy honí hrdinu (pomaleji než ty, respektují zdi). Dotyk jednoho stojí HP a dá ti krátké i-framy; stiskni B pro sek na dlaždici, na kterou koukáš, a slima poraz. HP se ukazuje v HUDu; při dosažení 0 tě to vrátí na start. (A pořád mluví.) Zkus: změň rychlost slimů nebo své startovní HP.

62 collapsed lines
# Quest -- step 7: enemies and bump combat.
#
# What you learn: simple chasing AI, taking damage, and attacking. Slimes step
# toward the hero (slower than you, and they respect walls via can_walk). Touching
# one costs HP and grants brief invulnerability (i-frames) so one touch doesn't
# drain you instantly. Press B to swing: we defeat any slime in the tile just ahead
# of the way you're facing. HP shows in the HUD; reaching 0 sends you back to start.
#
# New vs step 6: enemy sprites with chase AI, player HP + i-frames + knock-back,
# a B attack in the facing direction. (A still talks to the NPC.)
#
# Run: python3 sim/run.py tutorials/03-quest/step7_combat.py --hold B --shot /tmp/q7.png
import array
import board
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_anim
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6)
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
DIR = {DOWN: (0, 1), UP: (0, -1), LEFT: (-1, 0), RIGHT: (1, 0)}
FACE_NAME = ("down", "up", "left", "right")
BG = pg.rgb565(0, 0, 0)
WHITE = pg.rgb565(255, 255, 255)
NAVY = pg.rgb565(10, 10, 40)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
START = (TILE, TILE)
npc_x, npc_y = TILE, TILE
coin_spots, enemy_spots = [], []
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
START = (tx * TILE, ty * TILE)
elif ch == "N":
npc_x, npc_y = tx * TILE, ty * TILE
elif ch == "*":
coin_spots.append((tx * TILE, ty * TILE))
elif ch == "E":
enemy_spots.append((tx * TILE, ty * TILE))
scene.add(world)
coin_bm = shp.circle(8, pg.rgb565(245, 215, 60))
coins = [pg.Sprite(coin_bm, x + 4, y + 4) for (x, y) in coin_spots]
for c in coins:
scene.add(c)
slime_bm = shp.circle(14, pg.rgb565(120, 200, 80))
enemies = [pg.Sprite(slime_bm, x + 1, y + 1) for (x, y) in enemy_spots]
for e in enemies:
scene.add(e)
npc = pg.Sprite(shp.rect(TILE, TILE, pg.rgb565(230, 200, 60)), npc_x, npc_y)
scene.add(npc)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60),
pg.rgb565(255, 225, 170), pg.rgb565(120, 40, 30)])
stride = TILE * 8
data = bytearray(stride * TILE)
for f in range(4):
for s in range(2):
fr = f * 2 + s
for y in range(s, TILE):
yy = y - s
for x in range(TILE):
face = ((f == 0 and yy >= TILE - 4) or (f == 1 and yy < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + fr * TILE + x] = 2 if face else 1
lx = 4 if s == 0 else 6
for x in (lx, TILE - 1 - lx):
data[(TILE - 1) * stride + fr * TILE + x] = 3
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=8,
stride=stride, transparent=0)
hero = pg.Sprite(hero_bitmap(), START[0], START[1], frame=0)
walk = picogame_anim.AnimatedSprite(hero, {
"down": ([0, 1], 8, True), "up": ([2, 3], 8, True),
"left": ([4, 5], 8, True), "right": ([6, 7], 8, True)})
scene.add(hero)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, WHITE, BG)
dlg = ui.TextBox(pg, terminalio.FONT, 8, H - 60, W - 16, 54, WHITE, NAVY, maxlines=4)
LINES = ["Villager:", "Slimes ahead! Press B to", "swing at them.", "(press A)"]
facing = DOWN
got = 0
hp = 6
inv = 0
state = "over"
frame = 0
dlg_shown = False # draw the modal once, not every frame
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def near(a, bx, by, d=TILE):
return abs(a.x - bx) < d and abs(a.y - by) < d
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
follow()
dt = 1 / 30
while True:
btn.poll()
frame += 1
if state == "dialog":
if not dlg_shown: # draw ONCE -> no per-frame flicker
scene.refresh()
dlg.draw(board.DISPLAY, bufA, LINES)
dlg_shown = True
if btn.just_pressed(btn.A) or btn.just_pressed(btn.B):
state = "over"; scene.invalidate()
clock.tick()
continue
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
facing = RIGHT if dx > 0 else LEFT
elif dy:
facing = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y):
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED):
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow(); walk.play(FACE_NAME[facing]); walk.tick(dt)
else:
hero.frame = facing * 2
# attack: defeat a slime in the tile ahead of the facing
if btn.just_pressed(btn.B):
ddx, ddy = DIR[facing]
ax, ay = hero.x + ddx * TILE, hero.y + ddy * TILE
for e in enemies:
if e.visible and abs(e.x - ax) < TILE and abs(e.y - ay) < TILE:
e.visible = False
# slimes chase (slower: move every other frame) and respect walls
if frame % 2 == 0:
for e in enemies:
if not e.visible:
continue
sx = (hero.x > e.x) - (hero.x < e.x)
sy = (hero.y > e.y) - (hero.y < e.y)
if sx and can_walk(e.x + sx, e.y):
e.move(e.x + sx, e.y)
if sy and can_walk(e.x, e.y + sy):
e.move(e.x, e.y + sy)
# take damage on contact (unless in i-frames)
if inv > 0:
inv -= 1
else:
for e in enemies:
if e.visible and near(e, hero.x, hero.y, 13):
hp -= 1
inv = 40
if hp <= 0: # down -> back to start, full HP
hp = 6
hero.move(START[0], START[1])
follow()
break
for c in coins:
if c.visible and abs(hero.x - c.x) < 12 and abs(hero.y - c.y) < 12:
c.visible = False; got += 1
if near(hero, npc.x, npc.y):
hud.set("HP %d COINS %d/%d A:TALK B:SWING" % (hp, got, len(coins)))
if btn.just_pressed(btn.A):
state = "dialog"; dlg_shown = False
else:
hud.set("HP %d COINS %d/%d" % (hp, got, len(coins)))
scene.refresh()
dt = clock.tick()
▶ Vyzkoušet v prohlížeči

step 8 — step8_quest.py · quest (capstone)

Sekce “step 8 — step8_quest.py · quest (capstone)”

Quest – krok 8 Všechno se stane cílem: NPC chce všechny mince; jakmile je máš, další rozhovor otevře dveře (přepíšeme tyhle dlaždice z „door“ na „path“, aby tě kolize pustila skrz); šlápnutí na dlaždici svatyně vyhrává. Malá proměnná stage řídí dialog i dveře. Uvidíš: mluv → seber → odemkni → dojdi ke svatyni → „QUEST COMPLETE“. Zkus: vyžaduj, aby ses před otevřením dveří navíc musel zbavit všech slimů.

69 collapsed lines
# Quest -- step 8: a quest, a goal, and a win state (capstone).
#
# What you learn: tying the systems into an actual game. The NPC gives an objective
# (collect every coin); once you have them all, talking again OPENS the door (we
# rewrite those tiles from "door" to "path", so collision lets you through); stepping
# on the shrine tile wins. A small quest-stage variable drives the dialog text and
# the door. This is the whole loop: talk -> collect -> unlock -> reach the goal.
#
# New vs step 7: a quest stage, objective-driven dialog, opening the door by editing
# tiles, a goal tile + win state.
#
# BIG PICTURE: you've now hand-built an RPG -- map, camera, collision, animation,
# items, NPC, combat, quest. You DON'T have to keep hand-coding maps like this: the
# editor (editor/) lets you paint the map, place the hero/NPC/coins, and FLAG tiles
# (solid/coin/goal) visually, then the picogame_scene loader builds the scene for
# you -- the exact things this file does by hand become data. See tutorials/README.md
# and examples/picogame_platformer_scene.py for a game whose level is loaded that way.
#
# Run: python3 sim/run.py tutorials/03-quest/step8_quest.py --shot /tmp/q8.png
import array
import board
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_anim
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
TILE = 16
SPEED = 2
MAP = [
"##############################",
"#.....:......................#",
"#.....:........*.............#",
"#..##.:.............~~~~~~...#",
"#..##.:.............~~~~~~...#",
"#.....N.............~~~~~~...#",
"#.....:.......E.....~~~~~~...#",
"#.....:......................#",
"#.....:.....*.........*......#",
"#.....:......................#",
"#:::::::P:::::::::*::::::::::#",
"#.....:...................*..#",
"#.....:.............E........#",
"#.....:...WWWWWWW............#",
"#.....:...W.....W......##....#",
"#.....:.*.W..G..W......##....#",
"#.....:...W.....W........E...#",
"#.....:...WWWDWWW............#",
"#.....:......................#",
"##############################",
]
MCOLS, MROWS = 30, 20
CH2TILE = {".": 1, "P": 1, "N": 1, "*": 1, "E": 1, ":": 2, "~": 3, "#": 4,
"W": 5, "D": 6, "G": 7}
TILE_RGB = [(40, 120, 50), (180, 160, 110), (40, 90, 200), (20, 80, 30),
(120, 120, 130), (150, 90, 40), (240, 210, 60)]
SOLID = (3, 4, 5, 6)
DOWN, UP, LEFT, RIGHT = 0, 1, 2, 3
DIR = {DOWN: (0, 1), UP: (0, -1), LEFT: (-1, 0), RIGHT: (1, 0)}
FACE_NAME = ("down", "up", "left", "right")
BG = pg.rgb565(0, 0, 0)
WHITE = pg.rgb565(255, 255, 255)
NAVY = pg.rgb565(10, 10, 40)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
tileset = shp.tileset_colors(TILE, TILE, [pg.rgb565(*c) for c in TILE_RGB])
world = pg.Tilemap(tileset, MCOLS, MROWS)
START = (TILE, TILE)
npc_x, npc_y = TILE, TILE
coin_spots, enemy_spots, door_tiles = [], [], []
for ty in range(MROWS):
for tx in range(MCOLS):
ch = MAP[ty][tx] if tx < len(MAP[ty]) else "."
world.tile(tx, ty, CH2TILE.get(ch, 1))
if ch == "P":
START = (tx * TILE, ty * TILE)
elif ch == "N":
npc_x, npc_y = tx * TILE, ty * TILE
elif ch == "*":
coin_spots.append((tx * TILE, ty * TILE))
elif ch == "E":
enemy_spots.append((tx * TILE, ty * TILE))
elif ch == "D":
door_tiles.append((tx, ty))
scene.add(world)
coin_bm = shp.circle(8, pg.rgb565(245, 215, 60))
coins = [pg.Sprite(coin_bm, x + 4, y + 4) for (x, y) in coin_spots]
for c in coins:
scene.add(c)
slime_bm = shp.circle(14, pg.rgb565(120, 200, 80))
enemies = [pg.Sprite(slime_bm, x + 1, y + 1) for (x, y) in enemy_spots]
for e in enemies:
scene.add(e)
npc = pg.Sprite(shp.rect(TILE, TILE, pg.rgb565(230, 200, 60)), npc_x, npc_y)
scene.add(npc)
def hero_bitmap():
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(210, 80, 60),
pg.rgb565(255, 225, 170), pg.rgb565(120, 40, 30)])
stride = TILE * 8
data = bytearray(stride * TILE)
for f in range(4):
for s in range(2):
fr = f * 2 + s
for y in range(s, TILE):
yy = y - s
for x in range(TILE):
face = ((f == 0 and yy >= TILE - 4) or (f == 1 and yy < 4) or
(f == 2 and x < 4) or (f == 3 and x >= TILE - 4))
data[y * stride + fr * TILE + x] = 2 if face else 1
lx = 4 if s == 0 else 6
for x in (lx, TILE - 1 - lx):
data[(TILE - 1) * stride + fr * TILE + x] = 3
return pg.Bitmap(data, TILE, TILE, format=pg.PAL8, palette=pal, frames=8,
stride=stride, transparent=0)
hero = pg.Sprite(hero_bitmap(), START[0], START[1], frame=0)
walk = picogame_anim.AnimatedSprite(hero, {
"down": ([0, 1], 8, True), "up": ([2, 3], 8, True),
"left": ([4, 5], 8, True), "right": ([6, 7], 8, True)})
scene.add(hero)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, WHITE, BG)
dlg = ui.TextBox(pg, terminalio.FONT, 8, H - 64, W - 16, 58, WHITE, NAVY, maxlines=4)
facing = DOWN
got = 0
hp = 6
inv = 0
stage = 0 # 0 not started, 1 collecting, 2 door open
state = "over"
frame = 0
NCOINS = len(coins)
overlay_shown = False # draw dialog/win modal once, not every frame
def solid_at(px, py):
tx, ty = px // TILE, py // TILE
if tx < 0 or tx >= MCOLS or ty < 0 or ty >= MROWS:
return True
return world.tile(tx, ty) in SOLID
def can_walk(px, py):
return not (solid_at(px + 2, py + 2) or solid_at(px + TILE - 3, py + 2) or
solid_at(px + 2, py + TILE - 3) or solid_at(px + TILE - 3, py + TILE - 3))
def near(a, bx, by, d=TILE):
return abs(a.x - bx) < d and abs(a.y - by) < d
def follow():
ox = max(W - MCOLS * TILE, min(0, W // 2 - (hero.x + TILE // 2)))
oy = max(H - MROWS * TILE, min(0, H // 2 - (hero.y + TILE // 2)))
scene.set_view(int(ox), int(oy))
def dialog_lines():
if stage == 0:
return ["Villager:", "Bring me all %d coins and" % NCOINS,
"I'll open the shrine door.", "(press A)"]
if stage == 1 and got < NCOINS:
return ["Villager:", "You have %d of %d coins." % (got, NCOINS),
"Keep looking!", "(press A)"]
return ["Villager:", "The door is open.",
"Seek the shrine within.", "(press A)"]
def open_door():
for (tx, ty) in door_tiles:
world.tile(tx, ty, 2) # door -> path (no longer SOLID)
scene.invalidate()
follow()
dt = 1 / 30
while True:
btn.poll()
frame += 1
if state == "win":
if not overlay_shown: # draw ONCE -> no per-frame flicker
scene.refresh()
dlg.draw(board.DISPLAY, bufA, ["You reached the shrine!", "", "QUEST COMPLETE", "(press A)"])
overlay_shown = True
if btn.just_pressed(btn.A):
state = "over"; scene.invalidate()
clock.tick()
continue
if state == "dialog":
if not overlay_shown: # draw ONCE -> no per-frame flicker
scene.refresh()
dlg.draw(board.DISPLAY, bufA, dialog_lines())
overlay_shown = True
if btn.just_pressed(btn.A) or btn.just_pressed(btn.B):
if stage == 0:
stage = 1
elif stage == 1 and got >= NCOINS:
stage = 2
open_door()
state = "over"; scene.invalidate()
clock.tick()
continue
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
dy = btn.is_pressed(btn.DOWN) - btn.is_pressed(btn.UP)
if dx:
facing = RIGHT if dx > 0 else LEFT
elif dy:
facing = DOWN if dy > 0 else UP
moved = False
if dx and can_walk(hero.x + dx * SPEED, hero.y):
hero.move(hero.x + dx * SPEED, hero.y); moved = True
if dy and can_walk(hero.x, hero.y + dy * SPEED):
hero.move(hero.x, hero.y + dy * SPEED); moved = True
if moved:
follow(); walk.play(FACE_NAME[facing]); walk.tick(dt)
else:
hero.frame = facing * 2
if btn.just_pressed(btn.B):
ddx, ddy = DIR[facing]
ax, ay = hero.x + ddx * TILE, hero.y + ddy * TILE
for e in enemies:
if e.visible and abs(e.x - ax) < TILE and abs(e.y - ay) < TILE:
e.visible = False
if frame % 2 == 0:
for e in enemies:
if not e.visible:
continue
sx = (hero.x > e.x) - (hero.x < e.x)
sy = (hero.y > e.y) - (hero.y < e.y)
if sx and can_walk(e.x + sx, e.y):
e.move(e.x + sx, e.y)
if sy and can_walk(e.x, e.y + sy):
e.move(e.x, e.y + sy)
if inv > 0:
inv -= 1
else:
for e in enemies:
if e.visible and near(e, hero.x, hero.y, 13):
hp -= 1
inv = 40
if hp <= 0:
hp = 6
hero.move(START[0], START[1]); follow()
break
for c in coins:
if c.visible and abs(hero.x - c.x) < 12 and abs(hero.y - c.y) < 12:
c.visible = False; got += 1
# reach the shrine (goal tile) once the door is open
if stage >= 2:
ctx = (hero.x + TILE // 2) // TILE
cty = (hero.y + TILE // 2) // TILE
if world.tile(ctx, cty) == 7:
state = "win"; overlay_shown = False
if near(hero, npc.x, npc.y):
hud.set("HP %d COINS %d/%d A:TALK" % (hp, got, NCOINS))
if btn.just_pressed(btn.A):
state = "dialog"; overlay_shown = False
else:
obj = "FIND THE COINS" if stage < 1 or got < NCOINS else ("DOOR OPEN!" if stage >= 2 else "RETURN TO NPC")
hud.set("HP %d COINS %d/%d %s" % (hp, got, NCOINS, obj))
scene.refresh()
dt = clock.tick()
▶ Vyzkoušet v prohlížeči

Výplata: ručně jsi postavil celé RPG. Mapy ale nemusíš pořád psát ručně — webový editor ti dovolí tuhle mapu namalovat, umístit hrdinu/NPC/mince a vizuálně označit dlaždice (solid/coin/goal), a picogame_scene ji načte. Všechno, co step8 dělá ručně, se stane daty. Mrkni na formát scény a na příklad examples/picogame_platformer_scene.py.