"""
TETRIX ARCADE ENGINE: Sovereign Game Arcade Worlds
════════════════════════════════════════════════════════════════════════════════
Target MCU : Raspberry Pi Pico (RP2040 @ 133 MHz, MicroPython v1.24.1)
Display Matrix : 1x wokwi-max7219-matrix (chain=4, rotate=270°)
Orientation: VERTICAL (DOUT = TOP, DIN = BOTTOM)
LCD Interface : wokwi-lcd2004 via PCF8574 I2C (GP4=SDA, GP5=SCL)
Audio Subsystem : Passive Buzzer PWM on GP6 — Arcade tone sequencer
Input Mechanism : 4x Tactile Pushbutton (active-LOW, GP0-GP3 pull-up)
Wiring Topology (per diagram.json):
SPI0 MOSI → GP19 · SCK → GP18 · CS → GP17
matrix1:DIN ← BOTTOM (Physical Point)
I2C0 SDA → GP4 · SCL → GP5
BUZZER → GP6
BTN_WHITE → GP0 (Rotate 90° CCW Loop)
BTN_GREEN → GP1 (Hard-drop to DIN)
BTN_YELLOW → GP2 (Shift Left)
BTN_BLUE → GP3 (Shift Right)
Game Architecture:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Input FSM │───▶│ Physics │───▶│ Renderer │
│ (Debounce) │ │ (Gravity+ │ │ (Matrix │
│ │ │ Collision) │ │ Framebuf) │
└──────────────┘ └──────┬───────┘ └──────────────┘
│
┌────────▼────────┐
│ Audio FSM │
│ (PWM Sequencer)│
└─────────────────┘
Scoring Algorithm (non-linear combinatorial reward curve):
1 line → +4 DP │ 2 lines → +13 DP
3 lines → +25 DP │ 4 lines → +50 DP
5 lines → (score + 100) × 2 DP [multiplicative combo ceiling]
Operational Lifecycle:
POWER-ON → INTRO CINEMATIC (4 phases × 4s) → GAME SESSION →
[GAME OVER cinematic + High Score check] → AUTO-RESTART → ∞
════════════════════════════════════════════════════════════════════════════════
"""
# ── Standard Library Imports ──────────────────────────────────────────────────
import utime
import urandom
import gc
# ── MicroPython Hardware Abstraction ─────────────────────────────────────────
from machine import SPI, I2C, Pin, PWM
# ── Peripheral Driver Imports ─────────────────────────────────────────────────
from max7219 import Max7219
from i2c_lcd import I2cLcd
# Seed based on CPU ticks for true session randomness
urandom.seed(utime.ticks_cpu())
# ════════════════════════════════════════════════════════════════════════════
# SECTION 1 · PERIPHERAL INSTANTIATION
# ════════════════════════════════════════════════════════════════════════════
spi = SPI(0, baudrate=10000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19))
cs = Pin(17, Pin.OUT, value=1)
matrix = Max7219(spi, cs, brightness=7)
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
lcd = I2cLcd(i2c, 0x27, 4, 20)
buz = PWM(Pin(6))
buz.duty_u16(0)
BTN_W = Pin(0, Pin.IN, Pin.PULL_UP) # WHITE — Rotate 90 CCW
BTN_G = Pin(1, Pin.IN, Pin.PULL_UP) # GREEN — Hard-drop
BTN_Y = Pin(2, Pin.IN, Pin.PULL_UP) # YELLOW — Move Left
BTN_B = Pin(3, Pin.IN, Pin.PULL_UP) # BLUE — Move Right
HIGH_SCORE = 0
# ════════════════════════════════════════════════════════════════════════════
# SECTION 2 · AUDIO ENGINE
# ════════════════════════════════════════════════════════════════════════════
def tone(f, d):
if f > 0:
buz.freq(f)
buz.duty_u16(32768)
else:
buz.duty_u16(0)
utime.sleep_ms(d)
buz.duty_u16(0)
def play(seq):
for f, d in seq:
tone(f, d)
SEQ_I1 = [(523,60),(0,20),(659,60),(0,20),(784,80),(1047,100)]
SEQ_I2 = [(262,50),(330,50),(392,50),(523,50),(659,70),(784,70),(1047,150)]
SEQ_I3 = [(880,40),(880,40),(880,40),(1047,70),(1319,70),(1568,70),(2093,180)]
SEQ_I4 = [(523,50),(659,50),(784,50),(1047,70),(1175,35),(1568,180)]
SEQ_ROTATE = [(1760,35)]
SEQ_DROP = [(1047,25),(784,25),(523,25)]
SEQ_LEFT = [(880,28)]
SEQ_RIGHT = [(880,28)]
SEQ_LAND = [(330,35),(262,40)]
SEQ_OVER = [(880,90),(784,90),(698,90),(523,145),(440,185),(262,370)]
# ════════════════════════════════════════════════════════════════════════════
# SECTION 3 · TETROMINO CATALOGUE (FULL 360 ROTATION)
# ════════════════════════════════════════════════════════════════════════════
BLOCKS = {
'I': [[(0,1),(1,1),(2,1),(3,1)], [(2,0),(2,1),(2,2),(2,3)]],
'O': [[(0,0),(1,0),(0,1),(1,1)]],
'T': [[(1,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(1,2),(2,1)], [(0,1),(1,1),(2,1),(1,2)], [(0,1),(1,0),(1,1),(1,2)]],
'S': [[(1,0),(2,0),(0,1),(1,1)], [(1,0),(1,1),(2,1),(2,2)]],
'Z': [[(0,0),(1,0),(1,1),(2,1)], [(2,0),(1,1),(2,1),(1,2)]],
'J': [[(0,0),(0,1),(1,1),(2,1)], [(1,0),(2,0),(1,1),(1,2)], [(0,1),(1,1),(2,1),(2,2)], [(1,0),(1,1),(0,2),(1,2)]],
'L': [[(2,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(1,2),(2,2)], [(0,1),(1,1),(2,1),(0,2)], [(0,0),(1,0),(1,1),(1,2)]],
'I5': [[(0,2),(1,2),(2,2),(3,2),(4,2)], [(2,0),(2,1),(2,2),(2,3),(2,4)]]
}
BLOCK_KEYS = list(BLOCKS.keys())
BW, BH = 8, 32
GRAVITY_MS = 50 # High-speed gravity (0.05s)
DEBOUNCE_MS = 110
# ════════════════════════════════════════════════════════════════════════════
# SECTION 4 · GEOMETRY ENGINE
# ════════════════════════════════════════════════════════════════════════════
def spawn_piece():
key = BLOCK_KEYS[urandom.getrandbits(10) % len(BLOCK_KEYS)]
return key, 0, 2, 0
def get_cells(key, rot_idx, bx, by):
variants = BLOCKS[key]
cells = variants[rot_idx % len(variants)]
return [(bx + dx, by + dy) for dx, dy in cells]
def is_valid(board, key, rot_idx, bx, by):
for x, y in get_cells(key, rot_idx, bx, by):
if not (0 <= x < BW and y < BH):
return False
if y >= 0 and board[y][x]:
return False
return True
# ════════════════════════════════════════════════════════════════════════════
# SECTION 5 · LCD RENDERING UTILITIES
# ════════════════════════════════════════════════════════════════════════════
def scroll_lcd(line, text, speed=0.1):
padded = " " * 20 + text + " " * 20
for i in range(len(padded) - 19):
lcd.move_to(0, line)
lcd.putstr(padded[i:i+20])
utime.sleep(speed)
# ════════════════════════════════════════════════════════════════════════════
# SECTION 6 · RENDERER SYSTEM
# ════════════════════════════════════════════════════════════════════════════
def render(board, key, rot_idx, bx, by):
matrix.fill(0)
for y in range(BH):
for x in range(BW):
if board[y][x]: matrix.pixel(x, y, 1)
for cx, cy in get_cells(key, rot_idx, bx, by):
if 0 <= cy < BH: matrix.pixel(cx, cy, 1)
matrix.show()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 7 · INTRODUCTION ARCADE
# ════════════════════════════════════════════════════════════════════════════
def run_intro():
matrix.fill(0); matrix.show()
lcd.clear(); lcd.print_line(1, " Hello, PLAYER! ")
play(SEQ_I1); utime.sleep(2)
lcd.clear(); lcd.print_line(0, " Welcome to The")
lcd.print_line(1, " Tetrix Game in")
lcd.print_line(2, " Raspberry Pi Pico!")
play(SEQ_I2); utime.sleep(2)
lcd.clear(); lcd.print_line(1, " Ready & Lets Play")
lcd.print_line(2, " !!!!!!!!!!!!!")
play(SEQ_I3); utime.sleep(2)
lcd.clear(); lcd.print_line(0, " Get as many DOTS ")
lcd.print_line(1, " Poin as You Can!!!!!")
lcd.print_line(2, " Good Luck!")
lcd.print_line(3, " :D ")
play(SEQ_I4); utime.sleep(2)
lcd.clear()
# ════════════════════════════════════════════════════════════════════════════
# SECTION 8 · CORE GAME LOOP
# ════════════════════════════════════════════════════════════════════════════
def run_game():
global HIGH_SCORE
board = [[0] * BW for _ in range(BH)]
score = 0
key, rot, bx, by = spawn_piece()
g_timer = utime.ticks_ms()
_tw = _tg = _ty = _tb = 0
lcd.clear()
lcd.print_line(0, "===TETRIX ARCADES===")
lcd.print_line(1, " W:Rotate G:Drop")
lcd.print_line(2, " Y:Left B:Right")
utime.sleep(2)
lcd.clear()
while True:
now = utime.ticks_ms()
needs_render = False
# Bug Fix 1: Score selalu di line 0 selama main
lcd.move_to(0, 0)
lcd.putstr("Score: {:<10} DP".format(score))
# --- WHITE: Rotate 90 CCW (360 Consistent) ---
if BTN_W.value() == 0 and utime.ticks_diff(now, _tw) > DEBOUNCE_MS:
if is_valid(board, key, rot + 1, bx, by):
rot += 1; play(SEQ_ROTATE); needs_render = True
_tw = now
# --- YELLOW: Move Left ---
if BTN_Y.value() == 0 and utime.ticks_diff(now, _ty) > DEBOUNCE_MS:
if is_valid(board, key, rot, bx - 1, by):
bx -= 1; play(SEQ_LEFT); needs_render = True
_ty = now
# --- BLUE: Move Right ---
if BTN_B.value() == 0 and utime.ticks_diff(now, _tb) > DEBOUNCE_MS:
if is_valid(board, key, rot, bx + 1, by):
bx += 1; play(SEQ_RIGHT); needs_render = True
_tb = now
# --- GREEN: Hard-drop ---
if BTN_G.value() == 0 and utime.ticks_diff(now, _tg) > DEBOUNCE_MS:
while is_valid(board, key, rot, bx, by + 1): by += 1
play(SEQ_DROP); needs_render = True
g_timer = now - (GRAVITY_MS + 1)
_tg = now
# --- GRAVITY SUBSYSTEM ---
if utime.ticks_diff(now, g_timer) >= GRAVITY_MS:
if is_valid(board, key, rot, bx, by + 1):
by += 1
needs_render = True
else:
play(SEQ_LAND)
for cx, cy in get_cells(key, rot, bx, by):
if 0 <= cy < BH: board[cy][cx] = 1
# Line clear & scoring
full_rows = [y for y in range(BH) if all(board[y])]
n = len(full_rows)
if n > 0:
for y in sorted(full_rows):
board.pop(y); board.insert(0, [0] * BW)
if n == 1: score += 4
elif n == 2: score += 13
elif n == 3: score += 25
elif n == 4: score += 50
elif n >= 5: score = (score + 100) * 2
play([(1500, 50), (1800, 50)])
key, rot, bx, by = spawn_piece()
if not is_valid(board, key, rot, bx, by): return score
g_timer = now
if needs_render: render(board, key, rot, bx, by)
utime.sleep_ms(16)
# ════════════════════════════════════════════════════════════════════════════
# SECTION 9 · MISSION CONTROL
# ════════════════════════════════════════════════════════════════════════════
def main():
global HIGH_SCORE
while True:
run_intro()
final_score = run_game()
# Update High Score session
if final_score > HIGH_SCORE: HIGH_SCORE = final_score
play(SEQ_OVER)
lcd.clear()
lcd.print_line(0, "HAHAHAHA! You Lose!")
# Scroll score Best Score
scroll_lcd(2, "LAST SCORE: {} DP".format(final_score), 0.08)
lcd.clear()
lcd.print_line(0, " BEST SCORE: ")
lcd.print_line(1, " {} DP".format(HIGH_SCORE))
lcd.print_line(2, " RESTARTING... ")
utime.sleep(5)
gc.collect()
if __name__ == "__main__":
main()FPS: 0
Power: 0.00W
Power: 0.00W
Yellow: to LEFT
Blue: to Right
TETRIX
ROTATION
DROPING