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.pngTip: 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”
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 arrayimport picogame as pgimport picogame_gameimport picogame_clockimport picogame_shapes as shp
W, H = 320, 240TILE = 16
# . grass : path ~ water(solid) # tree(solid) W wall(solid) D door G goal# P player N npc * coin E enemyMAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....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 herofor 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 * TILEscene.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()step 2 — step2_walk.py · chůze, kamera následuje
Sekce “step 2 — step2_walk.py · chůze, kamera následuje”
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 arrayimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shp
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, TILEfor 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 * TILEscene.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()step 3 — step3_walls.py · kolize s dlaždicemi
Sekce “step 3 — step3_walls.py · kolize s dlaždicemi”
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 arrayimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shp
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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 movementDOWN, 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, TILEfor 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 * TILEscene.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()step 4 — step4_anim.py · animace chůze
Sekce “step 4 — step4_anim.py · animace chůze”
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 arrayimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_animimport picogame_shapes as shp
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, 3FACE_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, TILEfor 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 * TILEscene.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 / 30while 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()step 5 — step5_items.py · předměty + pevný HUD
Sekce “step 5 — step5_items.py · předměty + pevný HUD”
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 arrayimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_animimport picogame_shapes as shpimport picogame_ui as ui
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, 3FACE_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, TILEcoin_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 tilefor 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 = DOWNgot = 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 / 30while 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()step 6 — step6_npc.py · mluv s NPC
Sekce “step 6 — step6_npc.py · mluv s NPC”
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 arrayimport boardimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_animimport picogame_shapes as shpimport picogame_ui as ui
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, 3FACE_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, TILEnpc_x, npc_y = TILE, TILEcoin_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 = DOWNgot = 0state = "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 / 30while 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()step 7 — step7_combat.py · nepřátelé + bump combat
Sekce “step 7 — step7_combat.py · nepřátelé + bump combat”
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 arrayimport boardimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_animimport picogame_shapes as shpimport picogame_ui as ui
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, 3DIR = {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, TILEcoin_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 = DOWNgot = 0hp = 6inv = 0state = "over"frame = 0dlg_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 / 30while 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()step 8 — step8_quest.py · quest (capstone)
Sekce “step 8 — step8_quest.py · quest (capstone)”
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 arrayimport boardimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_animimport picogame_shapes as shpimport picogame_ui as ui
W, H = 320, 240TILE = 16SPEED = 2MAP = [ "##############################", "#.....:......................#", "#.....:........*.............#", "#..##.:.............~~~~~~...#", "#..##.:.............~~~~~~...#", "#.....N.............~~~~~~...#", "#.....:.......E.....~~~~~~...#", "#.....:......................#", "#.....:.....*.........*......#", "#.....:......................#", "#:::::::P:::::::::*::::::::::#", "#.....:...................*..#", "#.....:.............E........#", "#.....:...WWWWWWW............#", "#.....:...W.....W......##....#", "#.....:.*.W..G..W......##....#", "#.....:...W.....W........E...#", "#.....:...WWWDWWW............#", "#.....:......................#", "##############################",]MCOLS, MROWS = 30, 20CH2TILE = {".": 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, 3DIR = {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, TILEcoin_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 = DOWNgot = 0hp = 6inv = 0stage = 0 # 0 not started, 1 collecting, 2 door openstate = "over"frame = 0NCOINS = 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 / 30while 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()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.