tsb_game_page.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. --[[
  2. @module game_page
  3. @summary 俄罗斯方块游戏演示页面
  4. @version 1.0
  5. @date 2026.02.05
  6. @author 江访
  7. @usage
  8. 本文件是俄罗斯方块游戏的演示页面。
  9. ]]
  10. local game_page = {}
  11. -- 屏幕与棋盘参数
  12. local SCREEN_W, SCREEN_H = 480, 320 -- 横屏:480宽,320高
  13. local common_ui = require("tsb_common_page")
  14. local GRID_W, GRID_H = 15, 10
  15. local CELL_SIZE = 20 -- 每格像素大小(20px)
  16. local BOARD_W = GRID_W * CELL_SIZE
  17. local BOARD_H = GRID_H * CELL_SIZE
  18. local BOARD_X = (SCREEN_W - BOARD_W) // 2 -- 水平居中
  19. local BOARD_Y = 100 -- 棋盘左上角Y坐标
  20. -- 7 种俄罗斯方块颜色(RGB565格式)
  21. local COLORS = {
  22. 0x07FF, -- I (青色)
  23. 0x001F, -- J (蓝色)
  24. 0xFD20, -- L (橙色)
  25. 0xFFE0, -- O (黄色)
  26. 0x07E0, -- S (绿色)
  27. 0x8010, -- T (紫色)
  28. 0xF800, -- Z (红色)
  29. }
  30. -- 7 种形状(1 表示方块)
  31. local SHAPES = {
  32. { { 1, 1, 1, 1 } }, -- I
  33. { { 1, 0, 0 }, { 1, 1, 1 } }, -- J
  34. { { 0, 0, 1 }, { 1, 1, 1 } }, -- L
  35. { { 1, 1 }, { 1, 1 } }, -- O
  36. { { 0, 1, 1 }, { 1, 1, 0 } }, -- S
  37. { { 0, 1, 0 }, { 1, 1, 1 } }, -- T
  38. { { 1, 1, 0 }, { 0, 1, 1 } }, -- Z
  39. }
  40. -- 游戏状态
  41. local grid -- grid[y][x] = nil 或 颜色下标
  42. local curPiece -- {x, y, shape, color}
  43. local score = 0
  44. local gameOver = false
  45. local gameTimer = nil
  46. -- UI 控件
  47. local main_container
  48. local scoreLabel
  49. local statusLabel
  50. local leftBtn, rightBtn, rotateBtn, downBtn, restartBtn
  51. ----------------------------------------------------------------
  52. -- 工具函数:网格与方块
  53. ----------------------------------------------------------------
  54. local function initGrid()
  55. grid = {}
  56. for y = 1, GRID_H do
  57. grid[y] = {}
  58. for x = 1, GRID_W do
  59. grid[y][x] = nil
  60. end
  61. end
  62. end
  63. local function rotateShape(shape)
  64. local h, w = #shape, #shape[1]
  65. local res = {}
  66. for x = 1, w do
  67. res[x] = {}
  68. for y = 1, h do
  69. res[x][y] = shape[h - y + 1][x]
  70. end
  71. end
  72. return res
  73. end
  74. local function newPiece()
  75. local idx = math.random(1, #SHAPES)
  76. local shp = SHAPES[idx]
  77. local pw = #shp[1]
  78. local px = math.floor(GRID_W / 2) - math.floor(pw / 2) + 1
  79. curPiece = {
  80. x = px,
  81. y = 1,
  82. shape = shp,
  83. color = idx,
  84. }
  85. end
  86. local function eachBlock(piece, cb)
  87. local shape = piece.shape
  88. for j = 1, #shape do
  89. for i = 1, #shape[j] do
  90. if shape[j][i] == 1 then
  91. cb(piece.x + i - 1, piece.y + j - 1)
  92. end
  93. end
  94. end
  95. end
  96. local function validPosition(piece)
  97. local ok = true
  98. eachBlock(piece, function(x, y)
  99. if x < 1 or x > GRID_W or y < 1 or y > GRID_H then
  100. ok = false
  101. return
  102. end
  103. if grid[y][x] ~= nil then
  104. ok = false
  105. return
  106. end
  107. end)
  108. return ok
  109. end
  110. local function mergePiece()
  111. if not curPiece then return end
  112. eachBlock(curPiece, function(x, y)
  113. if y >= 1 and y <= GRID_H and x >= 1 and x <= GRID_W then
  114. grid[y][x] = curPiece.color
  115. end
  116. end)
  117. end
  118. -- 返回本次消掉的行数
  119. local function clearLines()
  120. local cleared = 0
  121. for y = GRID_H, 1, -1 do
  122. local full = true
  123. for x = 1, GRID_W do
  124. if grid[y][x] == nil then
  125. full = false
  126. break
  127. end
  128. end
  129. if full then
  130. cleared = cleared + 1
  131. -- 下移
  132. for yy = y, 2, -1 do
  133. grid[yy] = grid[yy - 1]
  134. end
  135. local row = {}
  136. for x = 1, GRID_W do row[x] = nil end
  137. grid[1] = row
  138. y = y + 1
  139. end
  140. end
  141. if cleared > 0 then
  142. score = score + cleared * 100
  143. end
  144. return cleared
  145. end
  146. ----------------------------------------------------------------
  147. -- 绘制函数:使用lcd.fill绘制棋盘
  148. ----------------------------------------------------------------
  149. local function drawCell(x, y, color)
  150. local cx = BOARD_X + (x - 1) * CELL_SIZE
  151. local cy = BOARD_Y + (y - 1) * CELL_SIZE
  152. lcd.fill(cx, cy, cx + CELL_SIZE - 2, cy + CELL_SIZE - 2, color)
  153. end
  154. local function drawBoard()
  155. -- 绘制棋盘背景
  156. lcd.fill(BOARD_X, BOARD_Y, BOARD_X + BOARD_W - 1, BOARD_Y + BOARD_H - 1, 0x1082) -- 深灰色背景
  157. -- 绘制固定的方块
  158. for y = 1, GRID_H do
  159. for x = 1, GRID_W do
  160. local fixed = grid[y][x]
  161. if fixed then
  162. drawCell(x, y, COLORS[fixed])
  163. else
  164. -- 绘制空单元格(带边框效果)
  165. local cx = BOARD_X + (x - 1) * CELL_SIZE
  166. local cy = BOARD_Y + (y - 1) * CELL_SIZE
  167. lcd.fill(cx, cy, cx + CELL_SIZE - 2, cy + CELL_SIZE - 2, 0x0841) -- 更深的灰色
  168. lcd.fill(cx + 1, cy + 1, cx + CELL_SIZE - 3, cy + CELL_SIZE - 3, 0x18C3) -- 浅灰色内框
  169. end
  170. end
  171. end
  172. -- 绘制当前方块
  173. if curPiece then
  174. eachBlock(curPiece, function(x, y)
  175. if y >= 1 and y <= GRID_H and x >= 1 and x <= GRID_W then
  176. drawCell(x, y, COLORS[curPiece.color])
  177. end
  178. end)
  179. end
  180. -- 绘制棋盘边框
  181. lcd.fill(BOARD_X - 2, BOARD_Y - 2, BOARD_X + BOARD_W + 1, BOARD_Y - 1, 0xFFFF) -- 上边框
  182. lcd.fill(BOARD_X - 2, BOARD_Y + BOARD_H, BOARD_X + BOARD_W + 1, BOARD_Y + BOARD_H + 1, 0xFFFF) -- 下边框
  183. lcd.fill(BOARD_X - 2, BOARD_Y - 2, BOARD_X - 1, BOARD_Y + BOARD_H + 1, 0xFFFF) -- 左边框
  184. lcd.fill(BOARD_X + BOARD_W, BOARD_Y - 2, BOARD_X + BOARD_W + 1, BOARD_Y + BOARD_H + 1, 0xFFFF) -- 右边框
  185. end
  186. ----------------------------------------------------------------
  187. -- UI更新函数
  188. ----------------------------------------------------------------
  189. local function updateScoreLabel()
  190. if scoreLabel then
  191. scoreLabel:set_text("分数: " .. tostring(score))
  192. end
  193. end
  194. local function updateStatusLabel(extra)
  195. if not statusLabel then return end
  196. if gameOver then
  197. statusLabel:set_text("游戏结束! 点击重新开始")
  198. else
  199. if extra and extra ~= "" then
  200. statusLabel:set_text("俄罗斯方块 | " .. extra)
  201. else
  202. statusLabel:set_text("俄罗斯方块 | 使用按钮游玩")
  203. end
  204. end
  205. end
  206. local function redrawAll(extraStatus)
  207. updateScoreLabel()
  208. updateStatusLabel(extraStatus)
  209. drawBoard()
  210. end
  211. ----------------------------------------------------------------
  212. -- 控制逻辑
  213. ----------------------------------------------------------------
  214. local function stepDown()
  215. if gameOver or not curPiece then return end
  216. local test = {
  217. x = curPiece.x,
  218. y = curPiece.y + 1,
  219. shape = curPiece.shape,
  220. color = curPiece.color,
  221. }
  222. if validPosition(test) then
  223. curPiece = test
  224. redrawAll()
  225. return
  226. end
  227. -- 碰到底或碰到已固定方块
  228. mergePiece()
  229. local cleared = clearLines()
  230. newPiece()
  231. if not validPosition(curPiece) then
  232. gameOver = true
  233. redrawAll("游戏结束!")
  234. if gameTimer then
  235. sys.timerStop(gameTimer)
  236. gameTimer = nil
  237. end
  238. else
  239. if cleared > 0 then
  240. redrawAll("消除了 " .. cleared .. " 行!")
  241. else
  242. redrawAll()
  243. end
  244. end
  245. end
  246. local function moveLeft()
  247. if gameOver or not curPiece then return end
  248. local test = {
  249. x = curPiece.x - 1,
  250. y = curPiece.y,
  251. shape = curPiece.shape,
  252. color = curPiece.color,
  253. }
  254. if validPosition(test) then
  255. curPiece = test
  256. redrawAll()
  257. end
  258. end
  259. local function moveRight()
  260. if gameOver or not curPiece then return end
  261. local test = {
  262. x = curPiece.x + 1,
  263. y = curPiece.y,
  264. shape = curPiece.shape,
  265. color = curPiece.color,
  266. }
  267. if validPosition(test) then
  268. curPiece = test
  269. redrawAll()
  270. end
  271. end
  272. local function softDrop()
  273. if gameOver or not curPiece then return end
  274. local test = {
  275. x = curPiece.x,
  276. y = curPiece.y + 1,
  277. shape = curPiece.shape,
  278. color = curPiece.color,
  279. }
  280. if validPosition(test) then
  281. curPiece = test
  282. redrawAll()
  283. end
  284. end
  285. local function rotatePiece()
  286. if gameOver or not curPiece then return end
  287. local newShape = rotateShape(curPiece.shape)
  288. local test = {
  289. x = curPiece.x,
  290. y = curPiece.y,
  291. shape = newShape,
  292. color = curPiece.color,
  293. }
  294. if validPosition(test) then
  295. curPiece = test
  296. redrawAll("旋转")
  297. end
  298. end
  299. local function restartGame()
  300. if gameTimer then
  301. sys.timerStop(gameTimer)
  302. end
  303. score = 0
  304. gameOver = false
  305. initGrid()
  306. newPiece()
  307. redrawAll("重新开始,分数: 0")
  308. -- 重新启动定时器
  309. gameTimer = sys.timerLoopStart(stepDown, 400)
  310. end
  311. ----------------------------------------------------------------
  312. -- 创建游戏UI
  313. ----------------------------------------------------------------
  314. function game_page.create_ui()
  315. -- 创建主容器
  316. main_container = airui.container({
  317. x = 0,
  318. y = 0,
  319. w = SCREEN_W,
  320. h = SCREEN_H,
  321. color = 0x0000, -- 黑色背景
  322. })
  323. -- 标题栏
  324. local title_bar = airui.container({
  325. parent = main_container,
  326. x = 0,
  327. y = 0,
  328. w = SCREEN_W,
  329. h = 50,
  330. color = 0x1E3A8A, -- 深蓝色
  331. })
  332. -- 游戏标题
  333. airui.label({
  334. parent = title_bar,
  335. text = "俄罗斯方块",
  336. x = 80,
  337. y = 15,
  338. w = 160,
  339. h = 20,
  340. })
  341. local battery_label = common_ui.add_battery_display(title_bar)
  342. -- 返回按钮
  343. common_ui.create_back_button(title_bar, game_page.cleanup)
  344. -- 分数标签
  345. scoreLabel = airui.label({
  346. parent = main_container,
  347. text = "分数: 0",
  348. x = 220,
  349. y = 70,
  350. w = 150,
  351. h = 20,
  352. })
  353. -- 状态标签
  354. statusLabel = airui.label({
  355. parent = main_container,
  356. text = "俄罗斯方块 | 使用按钮游玩",
  357. x = 10,
  358. y = 70,
  359. w = SCREEN_W - 20,
  360. h = 20,
  361. })
  362. -- 控制按钮区域
  363. local btnY = BOARD_Y + BOARD_H + 10
  364. local btnW, btnH, gap = 70, 30, 10
  365. -- 计算按钮起始X坐标(居中显示)
  366. local totalBtnWidth = btnW * 3 + gap * 2 -- 第一行3个按钮
  367. local btnStartX = (SCREEN_W - totalBtnWidth) // 2
  368. -- 第一行按钮
  369. leftBtn = airui.button({
  370. parent = main_container,
  371. x = btnStartX,
  372. y = btnY,
  373. w = btnW,
  374. h = btnH,
  375. text = "左移",
  376. on_click = function() moveLeft() end,
  377. })
  378. rightBtn = airui.button({
  379. parent = main_container,
  380. x = btnStartX + btnW + gap,
  381. y = btnY,
  382. w = btnW,
  383. h = btnH,
  384. text = "右移",
  385. on_click = function() moveRight() end,
  386. })
  387. rotateBtn = airui.button({
  388. parent = main_container,
  389. x = btnStartX + 2 * (btnW + gap),
  390. y = btnY,
  391. w = btnW,
  392. h = btnH,
  393. text = "旋转",
  394. on_click = function() rotatePiece() end,
  395. })
  396. -- 第二行按钮
  397. btnY = btnY + btnH + gap
  398. downBtn = airui.button({
  399. parent = main_container,
  400. x = btnStartX,
  401. y = btnY,
  402. w = btnW * 2 + gap, -- 稍宽一点
  403. h = btnH,
  404. text = "下落",
  405. on_click = function() softDrop() end,
  406. })
  407. restartBtn = airui.button({
  408. parent = main_container,
  409. x = btnStartX + btnW * 2 + gap * 2,
  410. y = btnY,
  411. w = btnW,
  412. h = btnH,
  413. text = "重新开始",
  414. on_click = function() restartGame() end,
  415. })
  416. end
  417. ----------------------------------------------------------------
  418. -- 页面生命周期函数
  419. ----------------------------------------------------------------
  420. function game_page.init(params)
  421. math.randomseed(os.time())
  422. -- 初始化游戏状态
  423. initGrid()
  424. newPiece()
  425. -- 创建UI
  426. game_page.create_ui()
  427. -- 初始绘制
  428. redrawAll("准备开始!")
  429. -- 启动定时器
  430. gameTimer = sys.timerLoopStart(stepDown, 400)
  431. end
  432. function game_page.cleanup()
  433. -- 停止定时器
  434. if gameTimer then
  435. sys.timerStop(gameTimer)
  436. gameTimer = nil
  437. end
  438. -- 清理UI
  439. if main_container then
  440. main_container:destroy()
  441. main_container = nil
  442. end
  443. -- 重置游戏状态
  444. grid = nil
  445. curPiece = nil
  446. score = 0
  447. gameOver = false
  448. leftBtn = nil
  449. rightBtn = nil
  450. rotateBtn = nil
  451. downBtn = nil
  452. restartBtn = nil
  453. scoreLabel = nil
  454. statusLabel = nil
  455. end
  456. return game_page