# -*- coding: utf-8 -*-
"""
Lode Runner (mini) - ESP32 + MicroPython + Wokwi
Porte do projeto original em Arduino/lcdgfx (NanoEngine16 + ILI9341)
para MicroPython puro, sem depender da lib lcdgfx (que nao existe
para MicroPython/Wokwi).
Este arquivo contem:
1) Um driver ILI9341 minimo via SPI (equivalente a parte "Display"
do lcdgfx)
2) Um "canvas" simples (buffer RGB565 + desenho de bitmaps
monocromaticos coloridos) equivalente a parte "Canvas/NanoEngine"
do lcdgfx
3) Os sprites do jogo original (bgSprites, playerFlyingImage,
coinImage) convertidos para listas Python
4) O mapa (gameField) e as regras de colisao (paredes, escadas,
canos, ouro) portados de game_basic.h/.cpp
5) A logica do player e do "ninja" (NPC perseguidor), portada de
lode_runner_ili9341.ino e ninja.cpp
6) O loop principal (setup/loop) le os 4 botoes, atualiza o jogo,
desenha um frame e envia para o display
Observacao: a "camera" do NanoEngine original praticamente nao se
move nesse jogo (o mapa tem 240x112px e a janela de jogo tem
240x256px, ou seja, o mapa inteiro sempre cabe na tela), entao aqui
o mundo e desenhado direto, sem logica de scroll.
"""
import time
from machine import Pin, SPI
# =====================================================================
# 1) DRIVER ILI9341 (substitui a parte "Display" da lcdgfx)
# =====================================================================
class ILI9341:
_SWRESET = 0x01
_SLPOUT = 0x11
_DISPOFF = 0x28
_DISPON = 0x29
_CASET = 0x2A
_PASET = 0x2B
_RAMWR = 0x2C
_MADCTL = 0x36
_VSCRSADD = 0x37
_PIXFMT = 0x3A
_FRMCTR1 = 0xB1
_DFUNCTR = 0xB6
_PWCTR1 = 0xC0
_PWCTR2 = 0xC1
_VMCTR1 = 0xC5
_VMCTR2 = 0xC7
_GAMMASET = 0x26
_GMCTRP1 = 0xE0
_GMCTRN1 = 0xE1
_PWCTRA = 0xCB
_PWCTRB = 0xCF
_DTCA = 0xE8
_DTCB = 0xEA
_POSC = 0xED
_ENABLE3G = 0xF2
_PUMPRC = 0xF7
def __init__(self, spi, cs, dc, rst, width=240, height=320):
self.spi = spi
self.cs = cs
self.dc = dc
self.rst = rst
self.width = width
self.height = height
self.cs.init(Pin.OUT, value=1)
self.dc.init(Pin.OUT, value=0)
self.rst.init(Pin.OUT, value=1)
self._init_display()
def _write_cmd(self, cmd):
self.dc.value(0)
self.spi.write(bytearray([cmd]))
def _write_data(self, data):
self.dc.value(1)
self.spi.write(data)
def _cmd(self, cmd, data=None):
self.cs.value(0)
self._write_cmd(cmd)
if data is not None:
self._write_data(data)
self.cs.value(1)
def _init_display(self):
# reset de hardware (equivalente ao reset_mpy() da lib rdagger/micropython-ili9341)
self.rst.value(0)
time.sleep_ms(50)
self.rst.value(1)
time.sleep_ms(50)
self._cmd(self._SWRESET)
time.sleep_ms(100)
# sequencia completa de inicializacao (power control / timing / gamma) --
# sem isso o simulador do Wokwi pode nao acender o painel de verdade
self._cmd(self._PWCTRB, b"\x00\xC1\x30")
self._cmd(self._POSC, b"\x64\x03\x12\x81")
self._cmd(self._DTCA, b"\x85\x00\x78")
self._cmd(self._PWCTRA, b"\x39\x2C\x00\x34\x02")
self._cmd(self._PUMPRC, b"\x20")
self._cmd(self._DTCB, b"\x00\x00")
self._cmd(self._PWCTR1, b"\x23")
self._cmd(self._PWCTR2, b"\x10")
self._cmd(self._VMCTR1, b"\x3E\x28")
self._cmd(self._VMCTR2, b"\x86")
self._cmd(self._MADCTL, b"\x48") # MX=1, BGR=1 (retrato 240x320, corrige imagem de cabeca p/ baixo)
self._cmd(self._VSCRSADD, b"\x00")
self._cmd(self._PIXFMT, b"\x55") # 16 bits/pixel (RGB565)
self._cmd(self._FRMCTR1, b"\x00\x18")
self._cmd(self._DFUNCTR, b"\x08\x82\x27")
self._cmd(self._ENABLE3G, b"\x00")
self._cmd(self._GAMMASET, b"\x01")
self._cmd(self._GMCTRP1, bytes([0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08,
0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03,
0x0E, 0x09, 0x00]))
self._cmd(self._GMCTRN1, bytes([0x00, 0x0E, 0x14, 0x03, 0x11, 0x07,
0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C,
0x31, 0x36, 0x0F]))
self._cmd(self._SLPOUT)
time.sleep_ms(100)
self._cmd(self._DISPON)
time.sleep_ms(100)
def blit_buffer(self, buf, x0, y0, x1, y1):
"""Envia um buffer RGB565 (big-endian, 2 bytes/pixel) para a
janela (x0,y0)-(x1,y1) do display. Mantem o CS baixo durante
toda a sequencia (endereco + dados) numa transacao continua,
que e' o que o simulador do Wokwi espera."""
self.cs.value(0)
self._write_cmd(self._CASET)
self._write_data(bytearray([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]))
self._write_cmd(self._PASET)
self._write_data(bytearray([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]))
self._write_cmd(self._RAMWR)
self.dc.value(1)
mv = memoryview(buf)
chunk = 4096
for i in range(0, len(buf), chunk):
self.spi.write(mv[i:i + chunk])
self.cs.value(1)
# =====================================================================
# 2) CANVAS (substitui Canvas/NanoEngine16 da lcdgfx)
# Buffer RGB565 "big-endian" (2 bytes por pixel, MSB primeiro),
# do jeito que o ILI9341 espera.
# =====================================================================
def rgb565(r, g, b):
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
def put_pixel(buf, buf_w, x, y, color):
if 0 <= x < buf_w:
idx = (y * buf_w + x) * 2
if 0 <= idx < len(buf) - 1:
buf[idx] = color >> 8
buf[idx + 1] = color & 0xFF
def fill_rect(buf, buf_w, x, y, w, h, color):
hi = color >> 8
lo = color & 0xFF
row = bytearray((hi, lo)) * w
buf_h = len(buf) // (buf_w * 2)
for yy in range(y, y + h):
if 0 <= yy < buf_h:
idx = (yy * buf_w + x) * 2
buf[idx:idx + len(row)] = row
def draw_bitmap(buf, buf_w, x, y, bitmap, color, ncols=8, nrows=8):
"""bitmap: lista de `ncols` bytes, cada byte = 1 coluna de
`nrows` pixels (bit 0 = topo), igual ao formato usado pela
lcdgfx (NanoEngine) para os sprites 8x8 originais."""
for col in range(ncols):
byte = bitmap[col]
for row in range(nrows):
if byte & (1 << row):
put_pixel(buf, buf_w, x + col, y + row, color)
# =====================================================================
# 3) SPRITES (convertidos de sprites.h / sprites.cpp)
# =====================================================================
bgSprites = [
[0b01110111, 0b01110111, 0b01110000, 0b01110111,
0b01110111, 0b00000111, 0b01110111, 0b01110111],
[0b00010001, 0b11111111, 0b00010001, 0b00010001,
0b00010001, 0b11111111, 0b00010001, 0b00000000],
[0b00000010, 0b00000010, 0b00000010, 0b00000010,
0b00000010, 0b00000010, 0b00000010, 0b00000010],
[0b00000000, 0b11000000, 0b11100000, 0b10110000,
0b11010000, 0b10100000, 0b11000000, 0b00000000],
[0b01111111, 0b01111111, 0b01111111, 0b01110111,
0b01111111, 0b01111111, 0b01111111, 0b00000000],
]
MAN_ANIM_FLYING = 0
MAN_ANIM_UP = 1
MAN_ANIM_DOWN = 1
MAN_ANIM_LEFT = 2
MAN_ANIM_RIGHT = 3
MAN_ANIM_RIGHT_PIPE = 4
MAN_ANIM_LEFT_PIPE = 5
playerFlyingImage = [
[ # FLYING
[0x00, 0x04, 0x88, 0x4B, 0x3F, 0x48, 0x88, 0x04],
[0x00, 0x10, 0x08, 0xCB, 0x3F, 0xC8, 0x08, 0x10],
],
[ # UP + DOWN
[0x00, 0x00, 0x90, 0x4B, 0x3F, 0x28, 0x66, 0x00],
[0x00, 0x00, 0x66, 0x2B, 0x3F, 0x48, 0x90, 0x00],
],
[ # LEFT
[0x00, 0x10, 0x10, 0xCB, 0x3F, 0x48, 0x90, 0x00],
[0x00, 0x00, 0x20, 0x1B, 0xFF, 0xD8, 0x00, 0x00],
],
[ # RIGHT
[0x00, 0x90, 0x48, 0x3F, 0xCB, 0x10, 0x10, 0x00],
[0x00, 0x00, 0xD8, 0xFF, 0x1B, 0x20, 0x00, 0x00],
],
[ # RIGHT PIPE
[0b00000110, 0b00001000, 0b00111110, 0b00110000,
0b00111000, 0b00110110, 0b00111000, 0b00111000],
[0b00000000, 0b00000110, 0b00111000, 0b00110000,
0b00111110, 0b00110000, 0b00111000, 0b00111000],
],
[ # LEFT PIPE
[0b00111000, 0b00111000, 0b00110110, 0b00111000,
0b00110000, 0b00111110, 0b00001000, 0b00000110],
[0b00111000, 0b00111000, 0b00110000, 0b00111110,
0b00110000, 0b00111000, 0b00000110, 0b00000000],
],
]
coinImage = [
0b00000000, 0b00000000, 0b00111100, 0b01100110,
0b00111100, 0b00000000, 0b00000000, 0b00000000,
]
# Fonte digital 5x7 (recriada; nao e' byte-a-byte igual a
# digital_font5x7_AB original, mas serve para mostrar o placar).
# Cada digito = 5 colunas de 7 bits (bit 0 = topo).
FONT5x7_DIGITS = [
[0x3E, 0x51, 0x49, 0x45, 0x3E], # 0
[0x00, 0x42, 0x7F, 0x40, 0x00], # 1
[0x42, 0x61, 0x51, 0x49, 0x46], # 2
[0x21, 0x41, 0x45, 0x4B, 0x31], # 3
[0x18, 0x14, 0x12, 0x7F, 0x10], # 4
[0x27, 0x45, 0x45, 0x45, 0x39], # 5
[0x3C, 0x4A, 0x49, 0x49, 0x30], # 6
[0x01, 0x71, 0x09, 0x05, 0x03], # 7
[0x36, 0x49, 0x49, 0x49, 0x36], # 8
[0x06, 0x49, 0x49, 0x29, 0x1E], # 9
]
blockColors = [
rgb565(255, 96, 0),
rgb565(255, 255, 192),
rgb565(255, 255, 255),
rgb565(255, 255, 64),
rgb565(128, 128, 128),
]
COLOR_PLAYER = rgb565(64, 255, 255)
COLOR_NINJA = rgb565(64, 64, 255)
COLOR_COIN_FG = rgb565(255, 255, 0)
COLOR_COIN_BG = rgb565(0, 0, 0)
COLOR_TEXT = rgb565(255, 255, 255)
COLOR_TEXT_BG = rgb565(0, 0, 0)
# =====================================================================
# 4) MAPA / COLISAO (portado de game_basic.h / gamebasic.cpp)
# =====================================================================
B_WIDTH = 30
B_HEIGHT = 14
gameField = [
5, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 5,
5, 0, 2, 0, 0, 4, 0, 2, 1, 1, 2, 0, 0, 0, 0, 0, 4, 0, 0, 2, 1, 1, 2, 5, 0, 0, 0, 0, 0, 5,
5, 0, 2, 0, 0, 1, 0, 2, 0, 0, 1, 1, 5, 2, 0, 0, 1, 1, 0, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 5,
5, 0, 2, 1, 0, 0, 0, 2, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 4, 0, 5, 0, 0, 0, 0, 0, 5,
5, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 5, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 0, 2, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 5, 2, 0, 0, 0, 0, 4, 0, 0, 5, 0, 0, 0, 0, 0, 5,
1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 0, 0, 0, 0, 3, 3, 2, 0, 0, 0, 5, 5, 1, 1, 1, 0, 3, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 5,
5, 0, 2, 0, 4, 0, 0, 2, 1, 1, 2, 0, 0, 0, 0, 0, 4, 0, 0, 2, 1, 1, 2, 5, 0, 0, 0, 0, 0, 5,
5, 0, 2, 0, 1, 1, 0, 2, 0, 0, 1, 1, 5, 2, 0, 0, 1, 1, 0, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 5,
5, 0, 2, 0, 0, 0, 0, 2, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 4, 0, 5, 0, 0, 0, 0, 0, 5,
5, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 0, 2, 0, 0, 0, 0, 0, 4, 0, 0, 5, 5, 0, 2, 0, 0, 0, 0, 0, 4, 0, 0, 5, 0, 0, 0, 0, 0, 5,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
]
def isWalkable(t):
return t == 0 or t == 2 or t == 3 or t == 4
def isSolid(t):
return t == 1 or t == 2 or t == 5
def isPipe(t):
return t == 3
def isGold(t):
return t == 4
def isStair(t):
return t == 2
def block_index(bx, by):
return bx + by * B_WIDTH
def block_value(bx, by):
idx = block_index(bx, by)
if idx < 0 or idx >= B_WIDTH * B_HEIGHT:
idx = 0
return gameField[idx]
def block_at(px, py):
return block_value(px // 8, py // 8)
def set_block_at(px, py, v):
idx = block_index(px // 8, py // 8)
if idx < 0 or idx >= B_WIDTH * B_HEIGHT:
idx = 0
gameField[idx] = v
# =====================================================================
# 5) BOTOES / BUZZER
# =====================================================================
BUTTON_NONE = 0
BUTTON_UP = 1
BUTTON_RIGHT = 2
BUTTON_DOWN = 3
BUTTON_LEFT = 4
pin_up = Pin(32, Pin.IN, Pin.PULL_UP)
pin_down = Pin(33, Pin.IN, Pin.PULL_UP)
pin_left = Pin(25, Pin.IN, Pin.PULL_UP)
pin_right = Pin(26, Pin.IN, Pin.PULL_UP)
buzzer = Pin(27, Pin.OUT, value=0)
def read_button():
if pin_up.value() == 0:
return BUTTON_UP
if pin_down.value() == 0:
return BUTTON_DOWN
if pin_left.value() == 0:
return BUTTON_LEFT
if pin_right.value() == 0:
return BUTTON_RIGHT
return BUTTON_NONE
def beep(count, delay_us):
for _ in range(count * 2 + 1):
buzzer.value(buzzer.value() ^ 1)
time.sleep_us(delay_us)
buzzer.value(0)
# =====================================================================
# 6) PLAYER / NINJA (portado do .ino e de ninja.cpp)
# =====================================================================
class Actor:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
self.bitmap = playerFlyingImage[MAN_ANIM_FLYING][0]
self.anim = 0
self.anim_ts = time.ticks_ms()
# pontos de colisao (equivalentes a top()/bottom()/left()/right()/center())
def center(self):
return (self.x + 4, self.y + 4)
def top(self):
return (self.x + 4, self.y)
def bottom(self):
return (self.x + 4, self.y + 7)
def left(self):
return (self.x - 1, self.y + 4)
def right(self):
return (self.x + 8, self.y + 4)
def set_bitmap(self, bmp):
self.bitmap = bmp
def move_to(self, x, y):
self.x = x
self.y = y
def move_by(self, dx, dy):
self.x += dx
self.y += dy
def tick_animation(self):
now = time.ticks_ms()
if time.ticks_diff(now, self.anim_ts) > 150:
self.anim_ts = now
self.anim = 0 if self.anim else 1
player = Actor(8, 8, COLOR_PLAYER)
ninja = Actor(72, 8, COLOR_NINJA)
gold_collection = 0
def move_player(direction):
global gold_collection
bx, by = player.bottom()
bottom_block = block_at(bx, by)
feet_block = block_at(bx, by + 1)
tx, ty = player.top()
hand_block = block_at(tx, ty)
cx, cy = player.center()
center_block = block_at(cx, cy)
rx, ry = player.right()
right_block = block_at(rx, ry)
lx, ly = player.left()
left_block = block_at(lx, ly)
animated = False
if (not isSolid(feet_block)) and (not isPipe(hand_block) or not isPipe(bottom_block)):
player.move_to(cx & ~0x07, player.y + 1)
player.set_bitmap(playerFlyingImage[MAN_ANIM_FLYING][player.anim])
animated = True
else:
if direction == BUTTON_RIGHT:
if isWalkable(right_block):
player.move_by(1, 0)
if isPipe(center_block):
player.set_bitmap(playerFlyingImage[MAN_ANIM_RIGHT_PIPE][player.anim])
else:
player.set_bitmap(playerFlyingImage[MAN_ANIM_RIGHT][player.anim])
animated = True
elif direction == BUTTON_LEFT:
if isWalkable(left_block):
player.move_by(-1, 0)
if isPipe(center_block):
player.set_bitmap(playerFlyingImage[MAN_ANIM_LEFT_PIPE][player.anim])
else:
player.set_bitmap(playerFlyingImage[MAN_ANIM_LEFT][player.anim])
animated = True
elif direction == BUTTON_UP:
if isStair(bottom_block) or isStair(center_block):
ttx, tty = player.top()
player.move_to(ttx & ~0x07, tty - 1)
player.set_bitmap(playerFlyingImage[MAN_ANIM_UP][player.anim])
animated = True
elif direction == BUTTON_DOWN:
if isStair(feet_block) or (not isSolid(feet_block) and isPipe(hand_block)):
ttx, tty = player.top()
player.move_to(ttx & ~0x07, tty + 1)
player.set_bitmap(playerFlyingImage[MAN_ANIM_DOWN][player.anim])
animated = True
if animated and time.ticks_diff(time.ticks_ms(), player.anim_ts) > 150:
player.anim_ts = time.ticks_ms()
player.anim = 0 if player.anim else 1
beep(10, 20)
cx, cy = player.center()
center_block = block_at(cx, cy)
if isGold(center_block):
set_block_at(cx, cy, 0)
update_background_tile(cx // 8, cy // 8)
gold_collection += 1
beep(20, 40)
beep(20, 80)
beep(20, 120)
beep(20, 80)
beep(20, 40)
def ninja_move(target):
tx_target, ty_target = target
bx, by = ninja.bottom()
bottom_block = block_at(bx, by)
feet_block = block_at(bx, by + 1)
hx, hy = ninja.top()
hand_block = block_at(hx, hy)
cx, cy = ninja.center()
center_block = block_at(cx, cy)
rx, ry = ninja.right()
right_block = block_at(rx, ry)
lx, ly = ninja.left()
left_block = block_at(lx, ly)
direction = BUTTON_NONE
animated = False
if (not isSolid(feet_block)) and (not isPipe(hand_block) or not isPipe(bottom_block)):
ninja.move_to(cx & ~0x07, ninja.y + 1)
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_FLYING][ninja.anim])
animated = True
else:
if ty_target < ninja.y - 1:
right_ok = True
left_ok = True
i = 0
while i < 80:
if right_ok:
bxr = block_at(cx + i, cy)
if not isWalkable(bxr):
right_ok = False
if isStair(bxr):
direction = BUTTON_RIGHT
break
if left_ok:
bxl = block_at(cx - i, cy)
if not isWalkable(bxl):
left_ok = False
if isStair(bxl):
direction = BUTTON_LEFT
break
i += 8
if isStair(center_block) or isStair(bottom_block):
direction = BUTTON_UP
elif ty_target > ninja.y + 1:
if isPipe(hand_block):
direction = BUTTON_DOWN
else:
right_ok = True
left_ok = True
i = 0
while i < 80:
if right_ok:
bxr = block_at(cx + i, cy)
if not isWalkable(bxr):
right_ok = False
else:
b2 = block_at(bx + i, by + 1)
if isWalkable(b2):
direction = BUTTON_RIGHT
break
if left_ok:
bxl = block_at(cx - i, cy)
if not isWalkable(bxl):
left_ok = False
else:
b2 = block_at(bx - i, by + 1)
if isWalkable(b2):
direction = BUTTON_LEFT
break
i += 8
if isWalkable(feet_block):
direction = BUTTON_DOWN
elif tx_target > ninja.x:
if isWalkable(right_block):
direction = BUTTON_RIGHT
elif tx_target < ninja.x:
if isWalkable(left_block):
direction = BUTTON_LEFT
if direction == BUTTON_RIGHT:
ninja.move_by(1, 0)
if isPipe(center_block):
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_RIGHT_PIPE][ninja.anim])
else:
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_RIGHT][ninja.anim])
animated = True
elif direction == BUTTON_LEFT:
ninja.move_by(-1, 0)
if isPipe(center_block):
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_LEFT_PIPE][ninja.anim])
else:
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_LEFT][ninja.anim])
animated = True
elif direction == BUTTON_UP:
ttx, tty = ninja.top()
ninja.move_to(ttx & ~0x07, tty - 1)
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_UP][ninja.anim])
animated = True
elif direction == BUTTON_DOWN:
ttx, tty = ninja.top()
ninja.move_to(ttx & ~0x07, tty + 1)
ninja.set_bitmap(playerFlyingImage[MAN_ANIM_DOWN][ninja.anim])
animated = True
if animated and time.ticks_diff(time.ticks_ms(), ninja.anim_ts) > 150:
ninja.anim_ts = time.ticks_ms()
ninja.anim = 0 if ninja.anim else 1
# =====================================================================
# 7) RENDERIZACAO
# SEM buffer de tela inteira (isso e' o que estourava a RAM do
# ESP32). Desenha direto no display, tile por tile (8x8), usando
# dois bufferzinhos pequenos e reaproveitados:
# - tile_local: 8x8 pixels (128 bytes) -> tiles de fundo e sprites
# - header_local: 32x8 pixels (512 bytes) -> moeda + placar
# Cada "frame" so redesenha o que realmente mudou: o(s) tile(s)
# onde o player/ninja estavam antes de mover, os sprites na
# posicao nova, e o cabecalho.
# =====================================================================
import gc
SCREEN_W = 240
WORLD_Y_OFFSET = 64 # onde a area de jogo comeca na tela
CANVAS_H = WORLD_Y_OFFSET + B_HEIGHT * 8 # 64 + 112 = 176 (so' usado p/ calculo)
TILE_W = 8
tile_local = bytearray(TILE_W * 8 * 2) # 128 bytes, reaproveitado
HEADER_W = 32
header_local = bytearray(HEADER_W * 8 * 2) # 512 bytes, reaproveitado
def _clear(buf):
for i in range(len(buf)):
buf[i] = 0
def draw_bg_tile_to_display(col, row):
"""Desenha (direto no display) o tile de fundo da posicao
(col,row) do gameField."""
block_type = gameField[block_index(col, row)]
x = col * 8
y = WORLD_Y_OFFSET + row * 8
_clear(tile_local)
if block_type != 0:
draw_bitmap(tile_local, TILE_W, 0, 0, bgSprites[block_type - 1],
blockColors[block_type - 1])
display.blit_buffer(tile_local, x, y, x + 7, y + 7)
def build_background():
"""Desenha o mapa inteiro uma vez (equivalente ao onDraw() do
engine original), tile por tile, direto no display."""
for row in range(B_HEIGHT):
for col in range(B_WIDTH):
draw_bg_tile_to_display(col, row)
def update_background_tile(col, row):
"""Redesenha so um tile do mapa (usado quando uma moeda some)."""
draw_bg_tile_to_display(col, row)
def restore_bg_rect(sx, sy, w, h):
"""Redesenha, a partir do gameField, os tiles de fundo que cobrem
o retangulo (sx,sy,w,h) em coordenadas de TELA. Usado para
'apagar' a posicao antiga de um sprite antes dele se mover."""
wy0 = sy - WORLD_Y_OFFSET
wy1 = sy + h - 1 - WORLD_Y_OFFSET
col_lo = max(0, sx // 8)
col_hi = min(B_WIDTH - 1, (sx + w - 1) // 8)
row_lo = max(0, wy0 // 8)
row_hi = min(B_HEIGHT - 1, wy1 // 8)
for row in range(row_lo, row_hi + 1):
for col in range(col_lo, col_hi + 1):
draw_bg_tile_to_display(col, row)
def draw_sprite_to_display(x, y, bitmap, color):
_clear(tile_local)
draw_bitmap(tile_local, TILE_W, 0, 0, bitmap, color)
display.blit_buffer(tile_local, x, y, x + 7, y + 7)
def draw_text_number(buf, buf_w, x, y, value, color):
tens = (value // 10) % 10
units = value % 10
draw_bitmap(buf, buf_w, x, y, FONT5x7_DIGITS[tens], color, ncols=5, nrows=7)
draw_bitmap(buf, buf_w, x + 6, y, FONT5x7_DIGITS[units], color, ncols=5, nrows=7)
def draw_header_to_display():
_clear(header_local)
draw_bitmap(header_local, HEADER_W, 0, 0, coinImage, COLOR_COIN_FG)
draw_text_number(header_local, HEADER_W, 9, 0, gold_collection, COLOR_TEXT)
display.blit_buffer(header_local, 0, 0, HEADER_W - 1, 7)
# posicoes onde player/ninja foram desenhados no frame anterior
_player_drawn_at = (player.x, player.y)
_ninja_drawn_at = (ninja.x, ninja.y)
def render_frame():
global _player_drawn_at, _ninja_drawn_at
# apaga (restaura o fundo) nas posicoes antigas
restore_bg_rect(_ninja_drawn_at[0], _ninja_drawn_at[1] + WORLD_Y_OFFSET, 8, 8)
restore_bg_rect(_player_drawn_at[0], _player_drawn_at[1] + WORLD_Y_OFFSET, 8, 8)
# desenha nas posicoes novas (player primeiro, ninja por cima,
# igual a ordem original engine.insert(player) / engine.insert(ninja))
draw_sprite_to_display(player.x, player.y + WORLD_Y_OFFSET, player.bitmap, player.color)
draw_sprite_to_display(ninja.x, ninja.y + WORLD_Y_OFFSET, ninja.bitmap, ninja.color)
draw_header_to_display()
_player_drawn_at = (player.x, player.y)
_ninja_drawn_at = (ninja.x, ninja.y)
# =====================================================================
# 8) SETUP / LOOP
# =====================================================================
spi = SPI(2, baudrate=40000000, polarity=0, phase=0,
sck=Pin(18), mosi=Pin(23), miso=Pin(19))
display = ILI9341(spi, cs=Pin(5), dc=Pin(2), rst=Pin(4))
print("memoria livre antes do jogo:", gc.mem_free())
build_background()
render_frame()
FRAME_MS = 33 # ~30 fps
while True:
frame_start = time.ticks_ms()
direction = read_button()
move_player(direction)
ninja_move((player.x, player.y))
render_frame()
elapsed = time.ticks_diff(time.ticks_ms(), frame_start)
if elapsed < FRAME_MS:
time.sleep_ms(FRAME_MS - elapsed)