# -*- coding: utf-8 -*-
"""
Bomberman (mini) - ESP32 + MicroPython + Wokwi
Diagrama: wokwi-esp32-devkit-v1 + wokwi-ili9341 (rotate 90 = paisagem
320x240) + wokwi-analog-joystick + buzzer.
Reaproveita o mesmo driver ILI9341 "na unha" e a mesma estrategia de
desenho direto no display (sem framebuffer gigante) do projeto
anterior (Lode Runner), que ja' foi validada e nao estoura a RAM do
ESP32.
Pinos (conforme diagram.json):
LCD: SCK=D18 MOSI=D23 MISO=D19 CS=D5 DC=D2 RST=D4 VCC/LED=3V3
Buzzer: D27
Joystick: VERT=D25 (ADC) HORZ=D33 (ADC) SEL=D32 (digital, pull-up)
"""
import time
import random
from machine import Pin, SPI, ADC
# =====================================================================
# 1) DRIVER ILI9341 (identico ao projeto anterior, so mudando para
# orientacao PAISAGEM: 320 largura x 240 altura)
# =====================================================================
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=320, height=240):
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):
self.rst.value(0)
time.sleep_ms(50)
self.rst.value(1)
time.sleep_ms(50)
self._cmd(self._SWRESET)
time.sleep_ms(100)
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"\x28") # MV=1,BGR=1 -> paisagem 320x240
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) para a janela
(x0,y0)-(x1,y1). CS fica baixo durante toda a transacao."""
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: funcoes basicas de desenho num buffer RGB565 pequeno
# (nunca um buffer do tamanho da tela inteira - isso foi o que deu
# MemoryError no projeto anterior)
# =====================================================================
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 = bytes((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 fill_circle(buf, buf_w, cx, cy, r, color):
r2 = r * r
for yy in range(cy - r, cy + r + 1):
dy = yy - cy
for xx in range(cx - r, cx + r + 1):
dx = xx - cx
if dx * dx + dy * dy <= r2:
put_pixel(buf, buf_w, xx, yy, color)
def draw_bitmap(buf, buf_w, x, y, bitmap, color, ncols=8, nrows=8):
"""bitmap: lista de `ncols` bytes, cada byte = 1 coluna (bit0=topo)."""
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)
def clear_buf(buf):
for i in range(len(buf)):
buf[i] = 0
# fonte digital 5x7 (mesma do projeto anterior) para o placar
FONT5x7_DIGITS = [
[0x3E, 0x51, 0x49, 0x45, 0x3E], [0x00, 0x42, 0x7F, 0x40, 0x00],
[0x42, 0x61, 0x51, 0x49, 0x46], [0x21, 0x41, 0x45, 0x4B, 0x31],
[0x18, 0x14, 0x12, 0x7F, 0x10], [0x27, 0x45, 0x45, 0x45, 0x39],
[0x3C, 0x4A, 0x49, 0x49, 0x30], [0x01, 0x71, 0x09, 0x05, 0x03],
[0x36, 0x49, 0x49, 0x49, 0x36], [0x06, 0x49, 0x49, 0x29, 0x1E],
]
def draw_number(buf, buf_w, x, y, value, color, digits=2):
s = str(value)
while len(s) < digits:
s = "0" + s
for i, ch in enumerate(s[-digits:]):
d = ord(ch) - 48
if 0 <= d <= 9:
draw_bitmap(buf, buf_w, x + i * 6, y, FONT5x7_DIGITS[d], color, ncols=5, nrows=7)
# =====================================================================
# 3) CORES
# =====================================================================
COLOR_FLOOR = rgb565(15, 15, 22)
COLOR_WALL = rgb565(120, 120, 132)
COLOR_WALL_DARK = rgb565(80, 80, 92)
COLOR_SOFT = rgb565(175, 110, 60)
COLOR_SOFT_DARK = rgb565(120, 75, 40)
COLOR_BOMB = rgb565(15, 15, 15)
COLOR_BOMB_HI = rgb565(255, 255, 255)
COLOR_FUSE = rgb565(210, 160, 60)
COLOR_FIRE_OUT = rgb565(255, 130, 0)
COLOR_FIRE_IN = rgb565(255, 235, 70)
COLOR_PLAYER = rgb565(60, 200, 255)
COLOR_PLAYER_EYE = rgb565(10, 10, 20)
COLOR_ENEMY = rgb565(255, 60, 120)
COLOR_ENEMY_EYE = rgb565(10, 10, 20)
COLOR_HEADER_BG = rgb565(10, 10, 16)
COLOR_LIFE = rgb565(255, 70, 90)
COLOR_TEXT = rgb565(255, 255, 255)
COLOR_BOMB_READY = rgb565(60, 220, 90)
COLOR_BOMB_COOLDOWN = rgb565(90, 90, 90)
COLOR_FLASH = rgb565(220, 20, 20)
# =====================================================================
# 4) MAPA / GRADE
# =====================================================================
TILE = 16
GRID_COLS = 20
GRID_ROWS = 14
HEADER_H = 16
SCREEN_W = 320
SCREEN_H = 240
FLOOR, WALL, SOFT, BOMB_T = 0, 1, 2, 3
SPAWN_COL, SPAWN_ROW = 1, 1
ENEMY_SPAWN_COL, ENEMY_SPAWN_ROW = GRID_COLS - 3, GRID_ROWS - 2 # (17,12): nunca cai num pilar (nao sao os dois pares)
DIRS4 = ((1, 0), (-1, 0), (0, 1), (0, -1))
field = bytearray(GRID_COLS * GRID_ROWS)
def cell_idx(col, row):
return row * GRID_COLS + col
def generate_map():
for row in range(GRID_ROWS):
for col in range(GRID_COLS):
near_player = col <= 2 and row <= 2
near_enemy = col >= GRID_COLS - 3 and row >= GRID_ROWS - 3
if row == 0 or row == GRID_ROWS - 1 or col == 0 or col == GRID_COLS - 1:
t = WALL
elif near_player or near_enemy:
# areas de spawn tem prioridade: nunca viram parede-pilar,
# mesmo que caiam numa posicao (col par, row par)
t = FLOOR
elif col % 2 == 0 and row % 2 == 0:
t = WALL
else:
t = SOFT if random.getrandbits(8) < 150 else FLOOR
field[cell_idx(col, row)] = t
def is_walkable(col, row):
if col < 0 or col >= GRID_COLS or row < 0 or row >= GRID_ROWS:
return False
return field[cell_idx(col, row)] == FLOOR
# =====================================================================
# 5) ENTIDADES / ESTADO DO JOGO
# =====================================================================
player_col, player_row = SPAWN_COL, SPAWN_ROW
player_invuln_until = 0
enemy_col, enemy_row = ENEMY_SPAWN_COL, ENEMY_SPAWN_ROW
enemy_alive = True
enemy_dir = 0
enemy_last_move_ts = 0
enemy_respawn_at = 0
bomb_state = None # {'col':c,'row':r,'t0':ticks} ou None
fire_cells = {} # {(col,row): ticks_de_expiracao}
lives = 3
score = 0
BOMB_FUSE_MS = 2000
FIRE_MS = 400
BLAST_RADIUS = 2
ENEMY_MOVE_MS = 500
INVULN_MS = 1500
MOVE_REPEAT_MS = 160
# =====================================================================
# 6) BOTAO / BUZZER
# =====================================================================
buzzer = Pin(27, Pin.OUT, value=0)
def beep(count, delay_us):
for _ in range(count * 2 + 1):
buzzer.value(buzzer.value() ^ 1)
time.sleep_us(delay_us)
buzzer.value(0)
# =====================================================================
# 7) DESENHO (direto no display, sem framebuffer da tela inteira)
# =====================================================================
tile_local = bytearray(TILE * TILE * 2) # 512 bytes, reaproveitado
header_local = bytearray(SCREEN_W * HEADER_H * 2) # 10240 bytes, reaproveitado
def draw_fire(buf):
fill_rect(buf, TILE, 0, 0, TILE, TILE, COLOR_FLOOR)
fill_circle(buf, TILE, TILE // 2, TILE // 2, 7, COLOR_FIRE_OUT)
fill_circle(buf, TILE, TILE // 2, TILE // 2, 4, COLOR_FIRE_IN)
def draw_bomb_gfx(buf):
fill_rect(buf, TILE, 0, 0, TILE, TILE, COLOR_FLOOR)
fill_circle(buf, TILE, TILE // 2, TILE // 2 + 1, 6, COLOR_BOMB)
put_pixel(buf, TILE, TILE // 2 - 2, TILE // 2 - 2, COLOR_BOMB_HI)
fill_rect(buf, TILE, TILE // 2, 2, 2, 3, COLOR_FUSE)
def draw_player_gfx(buf):
fill_circle(buf, TILE, TILE // 2, TILE // 2, 6, COLOR_PLAYER)
put_pixel(buf, TILE, TILE // 2 - 2, TILE // 2 - 2, COLOR_PLAYER_EYE)
put_pixel(buf, TILE, TILE // 2 + 2, TILE // 2 - 2, COLOR_PLAYER_EYE)
def draw_enemy_gfx(buf):
fill_circle(buf, TILE, TILE // 2, TILE // 2, 6, COLOR_ENEMY)
put_pixel(buf, TILE, TILE // 2 - 2, TILE // 2 - 2, COLOR_ENEMY_EYE)
put_pixel(buf, TILE, TILE // 2 + 2, TILE // 2 - 2, COLOR_ENEMY_EYE)
def redraw_cell(col, row):
x = col * TILE
y = HEADER_H + row * TILE
clear_buf(tile_local)
if (col, row) in fire_cells:
draw_fire(tile_local)
else:
t = field[cell_idx(col, row)]
if t == WALL:
fill_rect(tile_local, TILE, 0, 0, TILE, TILE, COLOR_WALL)
fill_rect(tile_local, TILE, 2, 2, TILE - 4, TILE - 4, COLOR_WALL_DARK)
elif t == SOFT:
fill_rect(tile_local, TILE, 0, 0, TILE, TILE, COLOR_SOFT)
fill_rect(tile_local, TILE, 2, 2, TILE - 4, TILE - 4, COLOR_SOFT_DARK)
elif t == BOMB_T:
draw_bomb_gfx(tile_local)
else:
fill_rect(tile_local, TILE, 0, 0, TILE, TILE, COLOR_FLOOR)
if enemy_alive and enemy_col == col and enemy_row == row:
draw_enemy_gfx(tile_local)
if player_col == col and player_row == row:
draw_player_gfx(tile_local)
display.blit_buffer(tile_local, x, y, x + TILE - 1, y + TILE - 1)
def redraw_all_cells():
for row in range(GRID_ROWS):
for col in range(GRID_COLS):
redraw_cell(col, row)
def redraw_header():
clear_buf(header_local)
fill_rect(header_local, SCREEN_W, 0, 0, SCREEN_W, HEADER_H, COLOR_HEADER_BG)
for i in range(lives):
fill_rect(header_local, SCREEN_W, 4 + i * 12, 4, 8, 8, COLOR_LIFE)
draw_number(header_local, SCREEN_W, SCREEN_W - 22, 4, score, COLOR_TEXT, digits=3)
bomb_color = COLOR_BOMB_READY if bomb_state is None else COLOR_BOMB_COOLDOWN
fill_rect(header_local, SCREEN_W, SCREEN_W // 2 - 4, 4, 8, 8, bomb_color)
display.blit_buffer(header_local, 0, 0, SCREEN_W - 1, HEADER_H - 1)
def flash_screen(color, times):
for _ in range(times):
clear_buf(header_local)
fill_rect(header_local, SCREEN_W, 0, 0, SCREEN_W, HEADER_H, color)
for y0 in range(0, SCREEN_H, HEADER_H):
display.blit_buffer(header_local, 0, y0, SCREEN_W - 1, y0 + HEADER_H - 1)
time.sleep_ms(150)
clear_buf(header_local)
for y0 in range(0, SCREEN_H, HEADER_H):
display.blit_buffer(header_local, 0, y0, SCREEN_W - 1, y0 + HEADER_H - 1)
time.sleep_ms(150)
# =====================================================================
# 8) LOGICA DO JOGO
# =====================================================================
def check_fire_collision_player():
global player_invuln_until
if (player_col, player_row) in fire_cells:
now = time.ticks_ms()
if time.ticks_diff(now, player_invuln_until) >= 0:
player_hit()
def check_fire_collision_enemy():
if enemy_alive and (enemy_col, enemy_row) in fire_cells:
enemy_hit()
def check_player_enemy_contact():
if enemy_alive and player_col == enemy_col and player_row == enemy_row:
now = time.ticks_ms()
if time.ticks_diff(now, player_invuln_until) >= 0:
player_hit()
def player_hit():
global lives, player_invuln_until, player_col, player_row
lives -= 1
beep(15, 40)
player_invuln_until = time.ticks_ms() + INVULN_MS
if lives <= 0:
game_over()
else:
old = (player_col, player_row)
player_col, player_row = SPAWN_COL, SPAWN_ROW
redraw_cell(old[0], old[1])
redraw_cell(player_col, player_row)
redraw_header()
def enemy_hit():
global enemy_alive, enemy_respawn_at, score
old = (enemy_col, enemy_row)
enemy_alive = False
enemy_respawn_at = time.ticks_ms() + 3000
score += 5
redraw_cell(old[0], old[1])
redraw_header()
def game_over():
flash_screen(COLOR_FLASH, 4)
time.sleep_ms(300)
reset_game()
def reset_game():
global lives, score, bomb_state, player_col, player_row
global enemy_col, enemy_row, enemy_alive
lives = 3
score = 0
bomb_state = None
fire_cells.clear()
generate_map()
player_col, player_row = SPAWN_COL, SPAWN_ROW
enemy_col, enemy_row = ENEMY_SPAWN_COL, ENEMY_SPAWN_ROW
enemy_alive = True
redraw_all_cells()
redraw_header()
def place_bomb():
global bomb_state
field[cell_idx(player_col, player_row)] = BOMB_T
bomb_state = {"col": player_col, "row": player_row, "t0": time.ticks_ms()}
redraw_cell(player_col, player_row)
redraw_header()
beep(6, 30)
def explode(ccol, crow):
global score
now = time.ticks_ms()
newly = [(ccol, crow)]
field[cell_idx(ccol, crow)] = FLOOR
for dcol, drow in DIRS4:
for step in range(1, BLAST_RADIUS + 1):
c = ccol + dcol * step
r = crow + drow * step
if not (0 <= c < GRID_COLS and 0 <= r < GRID_ROWS):
break
t = field[cell_idx(c, r)]
if t == WALL:
break
if t == SOFT:
field[cell_idx(c, r)] = FLOOR
newly.append((c, r))
score += 1
break
newly.append((c, r))
for cell in newly:
fire_cells[cell] = now + FIRE_MS
beep(25, 60)
for cell in newly:
redraw_cell(cell[0], cell[1])
check_fire_collision_player()
check_fire_collision_enemy()
redraw_header()
def update_fire():
now = time.ticks_ms()
expired = []
for cell, exp in fire_cells.items():
if time.ticks_diff(now, exp) >= 0:
expired.append(cell)
for cell in expired:
del fire_cells[cell]
redraw_cell(cell[0], cell[1])
def try_move_player(dcol, drow):
global player_col, player_row
nc, nr = player_col + dcol, player_row + drow
if not is_walkable(nc, nr):
return
old = (player_col, player_row)
player_col, player_row = nc, nr
redraw_cell(old[0], old[1])
redraw_cell(player_col, player_row)
check_fire_collision_player()
check_player_enemy_contact()
def update_enemy():
global enemy_col, enemy_row, enemy_dir, enemy_last_move_ts
global enemy_alive, enemy_respawn_at
now = time.ticks_ms()
if not enemy_alive:
if time.ticks_diff(now, enemy_respawn_at) >= 0 and is_walkable(ENEMY_SPAWN_COL, ENEMY_SPAWN_ROW):
enemy_col, enemy_row = ENEMY_SPAWN_COL, ENEMY_SPAWN_ROW
enemy_alive = True
redraw_cell(enemy_col, enemy_row)
return
if time.ticks_diff(now, enemy_last_move_ts) < ENEMY_MOVE_MS:
return
dcol, drow = DIRS4[enemy_dir]
nc, nr = enemy_col + dcol, enemy_row + drow
if not is_walkable(nc, nr):
enemy_dir = random.getrandbits(2) % 4
dcol, drow = DIRS4[enemy_dir]
nc, nr = enemy_col + dcol, enemy_row + drow
if not is_walkable(nc, nr):
enemy_last_move_ts = now
return
old = (enemy_col, enemy_row)
enemy_col, enemy_row = nc, nr
enemy_last_move_ts = now
redraw_cell(old[0], old[1])
redraw_cell(enemy_col, enemy_row)
check_fire_collision_enemy()
check_player_enemy_contact()
# =====================================================================
# 9) JOYSTICK
# =====================================================================
adc_v = ADC(Pin(25)) # VERT
adc_h = ADC(Pin(33)) # HORZ
sel_pin = Pin(32, Pin.IN, Pin.PULL_UP)
adc_v.atten(ADC.ATTN_11DB)
adc_h.atten(ADC.ATTN_11DB)
DEADZONE = 12000
# calibra o centro assumindo o manche solto na inicializacao
center_v = adc_v.read_u16()
center_h = adc_h.read_u16()
def read_joystick_dir():
vv = adc_v.read_u16() - center_v
vh = adc_h.read_u16() - center_h
if abs(vv) < DEADZONE and abs(vh) < DEADZONE:
return (0, 0)
if abs(vh) > abs(vv):
return (1, 0) if vh > 0 else (-1, 0)
else:
return (0, 1) if vv > 0 else (0, -1)
# OBS: se os eixos saírem trocados/invertidos no seu hardware,
# e' so trocar os sinais/posicoes aqui (nao precisa mexer no resto).
# =====================================================================
# 10) 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("Bomberman ESP32 - iniciando...")
generate_map()
redraw_all_cells()
redraw_header()
FRAME_MS = 33 # ~30 fps
next_move_ts = 0
sel_was_pressed = False
while True:
frame_start = time.ticks_ms()
dcol, drow = read_joystick_dir()
if (dcol or drow) and time.ticks_diff(frame_start, next_move_ts) >= 0:
try_move_player(dcol, drow)
next_move_ts = frame_start + MOVE_REPEAT_MS
sel_pressed = (sel_pin.value() == 0)
if sel_pressed and not sel_was_pressed and bomb_state is None:
place_bomb()
sel_was_pressed = sel_pressed
if bomb_state is not None and time.ticks_diff(frame_start, bomb_state["t0"]) >= BOMB_FUSE_MS:
explode(bomb_state["col"], bomb_state["row"])
bomb_state = None
update_fire()
update_enemy()
elapsed = time.ticks_diff(time.ticks_ms(), frame_start)
if elapsed < FRAME_MS:
time.sleep_ms(FRAME_MS - elapsed)