Community project

K10 趣味小游戏

panjh

Published August 22, 2026

ESP32
Photo of K10 趣味小游戏Generated with AI

This project brings a classic block-stacking game to the ESP32 and K10 display. Players spawn falling pieces, move and rotate them to complete rows, and rack up points as lines clear from the board.

The guide includes a complete parts list, wiring diagram for connecting the K10 to the ESP32, and ready-to-flash firmware. Assembly covers powering up the K10, launching the game, and mastering the controls: move blocks left and right, rotate pieces, pause mid-game, and restart when needed.

Wiring diagram

Interactive · read-only

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Assembly

3 steps
  1. 准备 K10

    把 UNIHIKER K10 放在平整桌面上;这个小游戏只使用它自带的屏幕、A/B 按键、倾斜感应器和彩灯,不需要接任何外部零件。

    • Tip: 开始前让屏幕朝上平放一秒,倾斜操作会更自然。
    • 不要把 K10 放在潮湿或导电的桌面上,以免 USB 供电时发生短路。
  2. 开始并移动方块

    用 USB 线接通 K10 后,屏幕出现标题时短按 B 键开始。轻轻向左或向右倾斜 K10,让正在下落的彩色方块左右移动;填满一整条横线会把那一行消掉并加分。

    • Tip: 轻微倾斜就够了;方块到达边缘时会停住。
    • 不要大力甩动设备;轻微倾斜更容易控制,也能避免拉扯 USB 线。
  3. 旋转、暂停和重开

    游戏中每短按一次 A 键,正在下落的方块就旋转一次。短按 B 键暂停,再按一次 B 键继续;游戏结束后短按 B 键即可开始新的一局。

    • Tip: 机身彩灯提示状态:蓝色表示游戏中,黄色表示暂停,红色表示游戏结束。
    • 按键只需短按一次;连续快速按 A 键时,方块只有在旋转后不碰到边界或其他方块时才会转动。

Firmware

ESP32
main.cppDeploy to device
#include <Arduino.h>
#include "unihiker_k10.h"
#include "lvgl.h"


// Hoisted type definitions
enum GameState { TITLE, PLAYING, PAUSED, GAME_OVER };

struct Piece {
  int type;
  int rotation;
  int x;
  int y;
};


// Forward declarations
bool occupied(const Piece &p, int localX, int localY);
bool fits(const Piece &p);
void updateHud();
void renderBoard();
void showOverlay(const char *title, const char *help);
void spawnPiece();
void clearLines();
void gameOver();
void lockPiece();
void moveDown();
void startGame();
void togglePause();
void buildScreen();

UNIHIKER_K10 k10;

static const int SCREEN_W = 320;
static const int SCREEN_H = 240;
static const int COLS = 10;
static const int ROWS = 16;
static const int CELL = 13;
static const int BOARD_X = 95;
static const int BOARD_Y = 18;
static const uint32_t FRAME_MS = 50;
static const uint32_t DROP_MS = 550;





// Each piece has four rotations; each row is a 4-bit miniature grid.
const uint8_t SHAPES[7][4][4] = {
  {{0x0, 0xF, 0x0, 0x0}, {0x2, 0x2, 0x2, 0x2}, {0x0, 0xF, 0x0, 0x0}, {0x2, 0x2, 0x2, 0x2}},
  {{0x6, 0x6, 0x0, 0x0}, {0x6, 0x6, 0x0, 0x0}, {0x6, 0x6, 0x0, 0x0}, {0x6, 0x6, 0x0, 0x0}},
  {{0x2, 0x7, 0x0, 0x0}, {0x2, 0x6, 0x2, 0x0}, {0x0, 0x7, 0x2, 0x0}, {0x2, 0x3, 0x2, 0x0}},
  {{0x6, 0x3, 0x0, 0x0}, {0x1, 0x3, 0x2, 0x0}, {0x6, 0x3, 0x0, 0x0}, {0x1, 0x3, 0x2, 0x0}},
  {{0x3, 0x6, 0x0, 0x0}, {0x2, 0x3, 0x1, 0x0}, {0x3, 0x6, 0x0, 0x0}, {0x2, 0x3, 0x1, 0x0}},
  {{0x4, 0x7, 0x0, 0x0}, {0x3, 0x2, 0x2, 0x0}, {0x0, 0x7, 0x1, 0x0}, {0x2, 0x2, 0x6, 0x0}},
  {{0x1, 0x7, 0x0, 0x0}, {0x2, 0x2, 0x3, 0x0}, {0x0, 0x7, 0x4, 0x0}, {0x6, 0x2, 0x2, 0x0}}
};

const uint32_t COLORS[7] = {0x34D8FF, 0xFFD34D, 0xBB7BFF, 0x48E58B, 0xFF5C78, 0xFF9B4A, 0x4F8DFF};

GameState gameState = TITLE;
uint8_t board[ROWS][COLS];
lv_obj_t *cells[ROWS][COLS];
lv_obj_t *scoreLabel;
lv_obj_t *messageLabel;
lv_obj_t *helpLabel;
Piece current;
uint32_t score = 0;
uint32_t lastFrame = 0;
uint32_t lastDrop = 0;
bool previousA = false;
bool previousB = false;
int tiltRepeat = 0;

bool occupied(const Piece &p, int localX, int localY) {
  return (SHAPES[p.type][p.rotation][localY] & (1 << (3 - localX))) != 0;
}

bool fits(const Piece &p) {
  for (int py = 0; py < 4; py++) {
    for (int px = 0; px < 4; px++) {
      if (!occupied(p, px, py)) continue;
      int bx = p.x + px;
      int by = p.y + py;
      if (bx < 0 || bx >= COLS || by >= ROWS) return false;
      if (by >= 0 && board[by][bx] != 0) return false;
    }
  }
  return true;
}

void updateHud() {
  char text[28];
  snprintf(text, sizeof(text), "SCORE %lu", (unsigned long)score);
  lv_label_set_text(scoreLabel, text);
}

void renderBoard() {
  for (int y = 0; y < ROWS; y++) {
    for (int x = 0; x < COLS; x++) {
      uint8_t colorIndex = board[y][x];
      if (gameState == PLAYING || gameState == PAUSED) {
        for (int py = 0; py < 4; py++) {
          for (int px = 0; px < 4; px++) {
            if (occupied(current, px, py) && current.x + px == x && current.y + py == y) {
              colorIndex = current.type + 1;
            }
          }
        }
      }
      lv_obj_set_style_bg_color(cells[y][x], lv_color_hex(colorIndex ? COLORS[colorIndex - 1] : 0x10213A), 0);
    }
  }
}

void showOverlay(const char *title, const char *help) {
  lv_label_set_text(messageLabel, title);
  lv_obj_align(messageLabel, LV_ALIGN_CENTER, 0, -15);
  lv_obj_clear_flag(messageLabel, LV_OBJ_FLAG_HIDDEN);
  lv_label_set_text(helpLabel, help);
  lv_obj_align(helpLabel, LV_ALIGN_CENTER, 0, 18);
  lv_obj_clear_flag(helpLabel, LV_OBJ_FLAG_HIDDEN);
}

void spawnPiece() {
  current.type = random(0, 7);
  current.rotation = 0;
  current.x = 3;
  current.y = -1;
}

void clearLines() {
  int cleared = 0;
  for (int y = ROWS - 1; y >= 0; y--) {
    bool full = true;
    for (int x = 0; x < COLS; x++) if (board[y][x] == 0) full = false;
    if (!full) continue;
    cleared++;
    for (int pull = y; pull > 0; pull--) {
      for (int x = 0; x < COLS; x++) board[pull][x] = board[pull - 1][x];
    }
    for (int x = 0; x < COLS; x++) board[0][x] = 0;
    y++;
  }
  if (cleared) {
    score += cleared * cleared * 100;
    updateHud();
  }
}

void gameOver() {
  gameState = GAME_OVER;
  showOverlay("GAME OVER", "Press B for a new game");
  k10.rgb->write(-1, 70, 0, 0);
}

void lockPiece() {
  for (int py = 0; py < 4; py++) {
    for (int px = 0; px < 4; px++) {
      if (!occupied(current, px, py)) continue;
      int bx = current.x + px;
      int by = current.y + py;
      if (by >= 0 && by < ROWS && bx >= 0 && bx < COLS) board[by][bx] = current.type + 1;
    }
  }
  clearLines();
  spawnPiece();
  if (!fits(current)) gameOver();
}

void moveDown() {
  Piece test = current;
  test.y++;
  if (fits(test)) current = test;
  else lockPiece();
}

void startGame() {
  memset(board, 0, sizeof(board));
  score = 0;
  updateHud();
  spawnPiece();
  gameState = PLAYING;
  lv_obj_add_flag(messageLabel, LV_OBJ_FLAG_HIDDEN);
  lv_obj_add_flag(helpLabel, LV_OBJ_FLAG_HIDDEN);
  lastDrop = millis();
  k10.rgb->write(-1, 0, 25, 70);
  renderBoard();
}

void togglePause() {
  if (gameState == PLAYING) {
    gameState = PAUSED;
    showOverlay("PAUSED", "Press B to continue");
    k10.rgb->write(-1, 50, 30, 0);
  } else if (gameState == PAUSED) {
    gameState = PLAYING;
    lv_obj_add_flag(messageLabel, LV_OBJ_FLAG_HIDDEN);
    lv_obj_add_flag(helpLabel, LV_OBJ_FLAG_HIDDEN);
    lastDrop = millis();
    k10.rgb->write(-1, 0, 25, 70);
  }
}

void buildScreen() {
  k10.setScreenBackground(0x07111F);
  lv_obj_t *screen = lv_scr_act();
  lv_obj_t *frame = lv_obj_create(screen);
  lv_obj_set_pos(frame, BOARD_X - 3, BOARD_Y - 3);
  lv_obj_set_size(frame, COLS * CELL + 6, ROWS * CELL + 6);
  lv_obj_set_style_bg_opa(frame, LV_OPA_TRANSP, 0);
  lv_obj_set_style_border_width(frame, 2, 0);
  lv_obj_set_style_border_color(frame, lv_color_hex(0x61CFFF), 0);
  lv_obj_set_style_radius(frame, 4, 0);
  lv_obj_clear_flag(frame, LV_OBJ_FLAG_SCROLLABLE);

  for (int y = 0; y < ROWS; y++) {
    for (int x = 0; x < COLS; x++) {
      cells[y][x] = lv_obj_create(screen);
      lv_obj_set_pos(cells[y][x], BOARD_X + x * CELL, BOARD_Y + y * CELL);
      lv_obj_set_size(cells[y][x], CELL - 1, CELL - 1);
      lv_obj_set_style_radius(cells[y][x], 1, 0);
      lv_obj_set_style_border_width(cells[y][x], 0, 0);
      lv_obj_set_style_pad_all(cells[y][x], 0, 0);
      lv_obj_clear_flag(cells[y][x], LV_OBJ_FLAG_SCROLLABLE);
    }
  }

  scoreLabel = lv_label_create(screen);
  lv_obj_set_style_text_color(scoreLabel, lv_color_hex(0x9EEBFF), 0);
  lv_obj_set_style_text_font(scoreLabel, &lv_font_montserrat_14, 0);
  lv_obj_align(scoreLabel, LV_ALIGN_TOP_MID, 0, 1);

  messageLabel = lv_label_create(screen);
  lv_obj_set_style_text_color(messageLabel, lv_color_hex(0xFFFFFF), 0);
  lv_obj_set_style_text_font(messageLabel, &lv_font_montserrat_14, 0);
  lv_obj_set_style_bg_color(messageLabel, lv_color_hex(0x172A47), 0);
  lv_obj_set_style_bg_opa(messageLabel, LV_OPA_95, 0);
  lv_obj_set_style_pad_all(messageLabel, 8, 0);

  helpLabel = lv_label_create(screen);
  lv_obj_set_style_text_color(helpLabel, lv_color_hex(0xB8DCFF), 0);
  lv_obj_set_style_text_font(helpLabel, &lv_font_montserrat_14, 0);
  lv_obj_set_style_bg_color(helpLabel, lv_color_hex(0x172A47), 0);
  lv_obj_set_style_bg_opa(helpLabel, LV_OPA_95, 0);
  lv_obj_set_style_pad_all(helpLabel, 6, 0);

  updateHud();
  renderBoard();
  showOverlay("TILT TETRIS", "Tilt: move | A: rotate | B: start");
}

void setup() {
  k10.begin();
  k10.initScreen(2);
  randomSeed(micros());
  buildScreen();
}

void loop() {
  lv_timer_handler();
  bool a = k10.buttonA->isPressed();
  bool b = k10.buttonB->isPressed();
  bool aPressed = a && !previousA;
  bool bPressed = b && !previousB;
  previousA = a;
  previousB = b;

  if ((gameState == TITLE || gameState == GAME_OVER) && bPressed) startGame();
  else if ((gameState == PLAYING || gameState == PAUSED) && bPressed) togglePause();

  if (gameState == PLAYING && aPressed) {
    Piece test = current;
    test.rotation = (test.rotation + 1) % 4;
    if (fits(test)) current = test;
    renderBoard();
  }

  uint32_t now = millis();
  if (gameState != PLAYING || now - lastFrame < FRAME_MS) return;
  lastFrame = now;

  float tilt = k10.getAccelerometerY();
  if (tilt > 0.22f || tilt < -0.22f) {
    tiltRepeat++;
    if (tiltRepeat >= 4) {
      Piece test = current;
      test.x += tilt > 0 ? 1 : -1;
      if (fits(test)) current = test;
      tiltRepeat = 0;
    }
  } else {
    tiltRepeat = 3;
  }

  if (now - lastDrop >= DROP_MS) {
    lastDrop = now;
    moveDown();
  }
  renderBoard();
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik